Windows driver IOCTL — source audit

Windows driver IOCTL — source audit

TL;DR: Windows drivers handle requests through IRPs (I/O Request Packets). The IRP_MJ_DEVICE_CONTROL dispatcher receives user-supplied IOCTL codes and buffers. Bugs cluster around METHOD_NEITHER (raw user pointers, no probe), missing ProbeForRead/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:

  • IoMarkIrpPending paired 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:

  • KernelMode passed for a user-supplied handle (skips access checks).
  • Missing ObDereferenceObject (leak; bug-class but rarely exploitable).
  • Acting on obj after 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:

  • IoCreateDeviceSecure with a tight SDDL string.
  • No default-everyone-allowed DACL.

Practical audit workflow

  1. Enumerate IOCTLs the driver handles (the switch in DispatchDeviceControl).
  2. For each, identify the method (CTL_CODE macro bits or sniff IoControlCode & 3).
  3. For METHOD_NEITHER, verify probes exist.
  4. For all, verify input/output length math.
  5. Trace the dispatched path to the actual operation; find sinks (pool writes, file ops, registry, calls into hardware).
  6. Reachability: who can open the device? What permissions?

Tools

  • IDA + Hex-Rays decompilation of the driver .sys.
  • IoctlBF, HEVD test driver, Driver Verifier to 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