Windows driver IOCTL — source audit
TL;DR: Windows drivers handle requests through IRPs (I/O Request Packets). The
IRP_MJ_DEVICE_CONTROLdispatcher receives user-supplied IOCTL codes and buffers. Bugs cluster aroundMETHOD_NEITHER(raw user pointers, no probe), missingProbeForRead/ProbeForWrite, integer overflow in length math, and arbitrary writes via SystemBuffer or Type3InputBuffer. This is the OSEE-adjacent kernel-audit floor. Companion to fuzzing-windows-drivers and kernel-objects-and-irps.
Where the bug lives
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NTSTATUS DriverDeviceControl(PDEVICE_OBJECT dev, PIRP irp) {
PIO_STACK_LOCATION sp = IoGetCurrentIrpStackLocation(irp);
ULONG ioctl = sp->Parameters.DeviceIoControl.IoControlCode;
ULONG inLen = sp->Parameters.DeviceIoControl.InputBufferLength;
ULONG outLen = sp->Parameters.DeviceIoControl.OutputBufferLength;
PVOID buf;
switch (ioctl) {
case IOCTL_DO_THING: {
buf = irp->AssociatedIrp.SystemBuffer; // METHOD_BUFFERED → kernel-owned
...
}
case IOCTL_DO_OTHER: {
buf = sp->Parameters.DeviceIoControl.Type3InputBuffer; // METHOD_NEITHER → user pointer!
...
}
}
}
The four IOCTL methods:
| Method | Input buffer location | Output buffer location |
|---|---|---|
METHOD_BUFFERED | Irp->AssociatedIrp.SystemBuffer (kernel) | same buffer |
METHOD_IN_DIRECT | MDL (locked user memory, kernel-mapped) | same |
METHOD_OUT_DIRECT | as above | as above |
METHOD_NEITHER | Parameters.DeviceIoControl.Type3InputBuffer (raw user pointer) | Irp->UserBuffer (raw user pointer) |
METHOD_NEITHER is the dangerous one. The driver gets user-mode pointers and is responsible for ProbeForRead/ProbeForWrite inside an exception handler before touching them.
Bug class 1 — missing probe on METHOD_NEITHER
1
2
3
4
5
case IOCTL_NEITHER_BAD: {
PUCHAR userPtr = sp->Parameters.DeviceIoControl.Type3InputBuffer;
RtlCopyMemory(kernelDst, userPtr, inLen); // BAD: no Probe, no try/except
break;
}
The user can pass a kernel address as userPtr (after dereferencing some kernel struct), turning the IOCTL into an arbitrary kernel-read primitive. Or pass a malformed pointer that crashes the kernel.
Correct pattern:
1
2
3
4
5
6
__try {
ProbeForRead(userPtr, inLen, sizeof(ULONG));
RtlCopyMemory(kernelDst, userPtr, inLen);
} __except (EXCEPTION_EXECUTE_HANDLER) {
return GetExceptionCode();
}
Bug class 2 — integer overflow in length
1
2
3
4
ULONG count = *(ULONG *)userPtr;
ULONG total = count * sizeof(MY_ITEM); // overflow → small total
PVOID buf = ExAllocatePoolWithTag(NonPagedPool, total, 'tagX');
for (i = 0; i < count; ++i) buf[i] = ...; // BOF in kernel
Same pattern as user-mode but with kernel-pool consequences. The bug results in pool corruption — an exploitable primitive in modern Windows.
Audit:
- Every
ExAllocatePool*size is the result of safe arithmetic. RtlULongMult,RtlSIZETMult, or explicit overflow checks.
Bug class 3 — output buffer too small
1
2
3
4
5
6
case IOCTL_GETDATA: {
PVOID out = irp->AssociatedIrp.SystemBuffer;
RtlCopyMemory(out, kernelData, dataLen); // BAD: dataLen may > outLen
irp->IoStatus.Information = dataLen;
break;
}
If dataLen > outLen, the kernel writes past the user-mode buffer (METHOD_BUFFERED: writes into the system buffer that’s bigger than the user wanted — but IoStatus.Information signals more bytes than the user asked for, possibly leaking adjacent pool memory back).
Correct:
1
2
3
ULONG copy = min(dataLen, outLen);
RtlCopyMemory(out, kernelData, copy);
irp->IoStatus.Information = copy;
Bug class 4 — uninitialised pool
1
2
3
PVOID buf = ExAllocatePoolWithTag(NonPagedPool, 256, 'tagX');
RtlCopyMemory(buf, src, copyLen); // copyLen < 256
RtlCopyMemory(out, buf, 256); // leaks 256 - copyLen bytes of pool
Pool memory is not zeroed by default. A copy-out that exceeds the populated region leaks adjacent kernel memory to user space — kernel info-disclosure.
Use ExAllocatePool2(POOL_FLAG_NON_PAGED, ..., 'tagX') with the zero-init flag (modern Windows), or RtlZeroMemory after alloc.
Bug class 5 — race on IRP completion
If a worker thread completes the IRP after the dispatcher returned, races become possible. Audit:
IoMarkIrpPendingpaired with deferred completion.- The completion path doesn’t touch user buffers after the IRP completed.
Bug class 6 — IOCTL routing weakness
1
2
3
switch (ioctl & 0x3FFF) { // BAD: stripping access bits
case 0x100: ...
}
Some drivers strip the access bits (FILE_READ_ACCESS / FILE_WRITE_ACCESS) from the IOCTL code before dispatching. The intent: simplify the switch. The bug: a “read” IOCTL becomes reachable through a write-handle path, bypassing the access enforcement built into the IOCTL code.
Bug class 7 — handle/object misuse
1
2
3
4
HANDLE h = ...;
ObReferenceObjectByHandle(h, ..., ..., UserMode, &obj, NULL);
// use obj
ObDereferenceObject(obj);
Bugs:
KernelModepassed for a user-supplied handle (skips access checks).- Missing
ObDereferenceObject(leak; bug-class but rarely exploitable). - Acting on
objafter dereferencing (UAF in kernel).
Bug class 8 — DOS device name and security
Drivers expose \\.\MyDriver via IoCreateDevice + IoCreateSymbolicLink. The DACL on the device decides who can CreateFile it. A driver creating a device with a permissive default DACL exposes IOCTLs to all users — turning local privesc surface into network-reachable surface in some cases.
Audit:
IoCreateDeviceSecurewith a tight SDDL string.- No default-everyone-allowed DACL.
Practical audit workflow
- Enumerate IOCTLs the driver handles (the switch in
DispatchDeviceControl). - For each, identify the method (
CTL_CODEmacro bits or sniffIoControlCode & 3). - For METHOD_NEITHER, verify probes exist.
- For all, verify input/output length math.
- Trace the dispatched path to the actual operation; find sinks (pool writes, file ops, registry, calls into hardware).
- Reachability: who can open the device? What permissions?
Tools
- IDA + Hex-Rays decompilation of the driver
.sys. IoctlBF,HEVDtest driver,Driver Verifierto surface kernel bugs during fuzzing.- WinDbg + symbol server for kernel debugging — see kernel-debugging-with-windbg.
OSEE relevance
OSEE / advanced Windows exploitation expects you to read kernel C/decompiled C, find the bug, and chain it through SMEP/SMAP/KCFG/HVCI. The audit is the gateway to the exploit.
Source-audit checklist
- Every METHOD_NEITHER handler has ProbeForRead/Write inside try/except.
- Length math uses overflow-checked helpers.
- Output writes bounded by
outLen. - Pool allocations zeroed before copy-out.
- Access bits in IOCTL codes preserved through the switch.
- Handle resolution uses UserMode for user-supplied handles.
- Devices created with tight SDDL.
References
- Microsoft — IRP processing in driver dispatch routines
- Microsoft — Buffer descriptions for I/O control codes
- HackSys Extreme Vulnerable Driver
- Bruce Dang et al. — Practical Reverse Engineering (kernel chapter)
- See also: kernel-objects-and-irps, fuzzing-windows-drivers, kernel-debugging-with-windbg, decompiler-driven-source-review, type-confusion-kernel