Following up on my previous post about the role of RFCs in vulnerability research, I thought it would be nice to explore IOCTLs, a crucial element in vulnerability research, exploit development, and reverse engineering Windows drivers. This post will be split into two parts. The first part (this one) will be more theoretical, featuring code samples to illustrate the structure of IOCTLs, a brief overview of IRPs, how Dispatch routines function, and how IRPs and IOCTLs interact. The second part will dive into practical examples, focusing on IOCTLs from the point of view of EoP and demonstrating how these concepts come into play.
Why this topic, you might ask? If you’ve ever considered reversing Windows drivers or have come across blogs discussing various EoP vulnerabilities either through third-party drivers or Windows itself (ily project zero), chances are you’ve encountered references to IOCTLs. While there are some resources out there, most only offer brief mentions of IOCTLs, without delving into the details (I’m not talking about MSDN). That’s why I thought of writing about this one (Honestly, I haven’t had much time to research into other interesting topics, but I’ve promised myself to publish a blog every month. So, to keep that, I’m writing about something I’m already pretty familiar with ^_^)
Input/Output Control (IOCTL) is a control code that allows user-mode applications to send specific commands to device drivers. These commands can trigger various operations, such as reading data, writing data, or performing hardware-specific tasks.
Windows drivers interact with these commands through the _DeviceIoControl_
API in user-mode and corresponding IRP (I/O Request Packet) handling functions in kernel-mode.
IOCTLs are represented as DWORDs but each of the 32 bits represent a detail about the request — Device Type, Required Access, Function Code, and Transfer Type. Microsoft created a visual diagram to break these fields down:
_METHOD_BUFFERED_
, _METHOD_IN_DIRECT_
, _METHOD_OUT_DIRECT_
, or _METHOD_NEITHER_
._FILE_DEVICE_UNKNOWN (0x22)_
. The Common bit is used for vendor-assigned values.The above description is taken from Microsoft’s documentation but Bit-Level Breakdown would make more sense as the description provided by Microsoft defines at a conceptual level.
But I prefer this one,
31 16 15 14 13 2 1 0
+-------------+-------------+---------+----------+
| Device Type | Access Type | Function | Method |
+-------------+-------------+---------+----------+
_FILE_DEVICE_DISK_
or _FILE_DEVICE_UNKNOWN_
.FILE_ANY_ACCESS (0x00)
for no specific access requirement, FILE_READ_ACCESS (0x01)
for read access, and FILE_WRITE_ACCESS (0x02)
for write access.0x800
to 0xFFF
for custom functions, with each code representing a specific action the driver is tasked with executing.METHOD_BUFFERED
, where data is copied to and from a buffer provided by the I/O manager; METHOD_IN_DIRECT
, used for large input buffers transferred via Direct Memory Access (DMA); METHOD_OUT_DIRECT
, for large output buffers transferred via DMA; and METHOD_NEITHER
, where pointers are passed directly and the driver is responsible for validating them.This second description is more technical, bit-level representation of how the IOCTL is actually constructed in memory. It’s more focused on the binary layout of the 32-bit IOCTL value rather than being just conceptual.
This is example of an IOCTL definition in a driver:
#define IOCTL_CUSTOM_OPERATION \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
/*
Device Type: FILE_DEVICE_UNKNOWN (0x22)
Function: 0x800
Method: METHOD_BUFFERED
Access: FILE_ANY_ACCESS
When you call DeviceIoControl with this code, the driver will execute the corresponding function if implemented.
*/
According to chatGPT “An I/O Request Packet (IRP) is a data structure used by the Windows I/O Manager to represent I/O requests. When a user-mode application calls _DeviceIoControl_
(or other I/O-related APIs), the I/O Manager creates an IRP to encapsulate the request details and passes it to the corresponding device driver.”
I/O Request Packets (IRPs) are essentially just an instruction for the driver. These packets allow the driver to act on the specific major function by providing the relevant information required by the function. There are many major function codes but the most common ones are IRP_MJ_CREATE
, IRP_MJ_CLOSE
, and IRP_MJ_DEVICE_CONTROL
. These correlate with user mode functions:
IRP_MJ_CREATE
→ CreateFile
IRP_MJ_CLOSE
→ CloseFile
IRP_MJ_DEVICE_CONTROL
→ DeviceIoControl
Definitions in DriverEntry
may look like this:
DriverObject->MyFunction[IRP_MJ_CREATE] = MyCreateCloseFunction;
DriverObject->MyFuntion[IRP_MJ_CLOSE] = MyCreateCloseFunction;
DriverObject->MyFunction[IRP_MJ_DEVICE_CONTROL] = MyDeviceControlFunction;
But what is a DriverEntry? Well,
DriverEntry
is the entry point for a Windows driver, similar tomain()
in C/C++. It is responsible for initializing key driver components, such as creating the device object and symbolic link for communication. The function typically callsIoCreateDevice()
orIoCreateDeviceSecure()
to create the device object, with the secure version applying access restrictions. It then sets up a symbolic link usingIoCreateSymbolicLink()
to allow user-mode processes to interact with the driver. Additionally,DriverEntry
defines essential functions like IRP handlers and unload routines. You can read more about them from here
When the following code in user mode is executed, the driver will receive an IRP with the major function code IRP_MJ_CREATE
and will execute the MyCreateCloseFunction
function:
hDevice = CreateFile(L"\\\\.\\MyDevice", GENERIC_WRITE|GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
The most important major function for us in almost all cases will be IRP_MJ_DEVICE_CONTROL
as it is used to send requests to perform a specific internal function from user mode. These requests include an IO Control Code which tells the driver exactly what to do, as well as a buffer to send data to and receive data from the driver.
The flow diagram of how IOCTLs are sent and processed can look something like this:
DeviceIoControl()
to send the necessary IOCTL and input/output buffers to the symlink.DeviceIoControl()
and passes it to the internal function MyCtlFunction()
.MyCtlFunction()
maps the function code 0x800
to the internal function SomeFunction()
, which is then executed.The simplified process would be like this,
DeviceIoControl
with an IOCTL code and input/output buffers.IRP_MJ_DEVICE_CONTROL
routine.IoStatus
field.BTW “dispatched” refers to the process of passing the IRP (I/O Request Packet) to the appropriate dispatch routine in the driver for processing. When the I/O Manager creates the IRP and adds the necessary details, it “dispatches” it to the driver’s corresponding function, like IRP_MJ_DEVICE_CONTROL, which handles that specific request type (in this case, an IOCTL request).
5. Completion: The driver completes the IRP, and the result is returned to the user-mode application.
I know its pretty boring reading theories like IOCTLs can be pretty dry, especially since once you dive into IOCTLs, you’ll also need to understand things like IRPs, dispatch routines etc. It’s a whole ongoing topic.My best advice is to try writing a simple driver as you can understand the stuffs better. Or, maybe check out some EoP blog posts (there are tons of them), and I’ll link a few at the end. You can also look at the HackSys Extreme Vulnerable Driver source code for more insights. Anyway, that’s all for now. The second part will come soon, and I promise it’ll be way more interesting, as we’ll dive into a real CVE to better understand these concepts!
Hope you guys find this post interesting and useful. Follow me on LinkedIn, Medium, X.
PEACE!
If you want to understand the concepts in practical manner or want to see how IOCTLs are actually used while developing a driver, go through this amazing post from Nikhil https://ghostbyt3.github.io/blog/Kernel_Exploitation_Primer_0x0
https://dl.acm.org/doi/pdf/10.1145/3564625.3564631 (Highly recommended, I read this paper a couple of times while I was learning about IOCTLs).
https://www.cyberark.com/resources/threat-research-blog/finding-bugs-in-windows-drivers-part-1-wdm
googleprojectzero.blogspot.com](https://googleprojectzero.blogspot.com/2015/10/windows-drivers-are-truely-tricky.html?source=post_page-----c49229b38d8d---------------------------------------)
blogs.vmware.com](https://blogs.vmware.com/security/2023/10/hunting-vulnerable-kernel-drivers.html?source=post_page-----c49229b38d8d---------------------------------------)
https://blog.talosintelligence.com/exploring-malicious-windows-drivers-part-1-introduction-to-the-kernel-and-drivers/ (Another great blog)
https://github.com/hacksysteam/HackSysExtremeVulnerableDriver