Kernel objects and IRPs

Kernel objects and IRPs

TL;DR: Drivers expose functionality through DEVICE_OBJECTs; user mode sends them I/O Request Packets (IRPs). The IRP_MJ_DEVICE_CONTROL major function backed by DeviceIoControl is the dominant attack surface.

What it is

The Windows I/O Manager translates every CreateFile/ReadFile/WriteFile/DeviceIoControl call against a device into an IRP — a heap-allocated structure describing the operation, the caller’s buffers, and a stack of per-driver locations. Each driver’s DriverObject->MajorFunction[] array points to handlers for each IRP major code. Vulnerability research lives almost entirely inside those handlers, particularly IRP_MJ_DEVICE_CONTROL, where IOCTL codes carry a method (METHOD_BUFFERED, METHOD_IN_DIRECT, METHOD_OUT_DIRECT, METHOD_NEITHER) that dictates how buffers are passed.

Preconditions / where it applies

  • Driver IoCreateDevice + IoCreateSymbolicLink exposing a \\.\Name accessible to the caller’s integrity level
  • DACL on the device permits the caller’s token; many drivers default to World/Everyone which is the LPE jackpot
  • IOCTL codes enumerable via static analysis (CTL_CODE macro pattern) or runtime hooks

Technique

IOCTL code structure (CTL_CODE(DeviceType, Function, Method, Access)):

1
#define IOCTL_FOO  CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

Method behaviour and bug-class implications:

  • METHOD_BUFFERED — I/O manager copies user buffer into a kernel pool; driver reads/writes through Irp->AssociatedIrp.SystemBuffer. Safest, but pool overflow risk if size is mis-handled.
  • METHOD_IN_DIRECT / OUT_DIRECT — user buffer locked into an MDL; driver gets Irp->MdlAddress. Race/pin issues.
  • METHOD_NEITHER — raw user pointers in Type3InputBuffer and UserBuffer. Every read/write must use ProbeForRead/ProbeForWrite and SEH; missing probes give arbitrary kernel R/W. Major source of LPE bugs.

Attacker workflow:

  1. Open the device: CreateFileW(L"\\\\.\\HEVD", GENERIC_ALL, ...)
  2. Discover IOCTL codes via reverse engineering or fuzzing — see fuzzing-windows-drivers
  3. Send DeviceIoControl(hDev, IOCTL, inbuf, inlen, outbuf, outlen, ...)
  4. Observe bug-class: pool overflow, type-confusion-kernel, use-after-free-kernel, double-fetch, uninitialised-memory-disclosures, kernel-stack-overflow

Key structures researchers learn cold:

1
2
3
4
5
DRIVER_OBJECT.MajorFunction[]  — dispatch table
DEVICE_OBJECT                  — links to driver, attached stack
IRP                            — request packet
IO_STACK_LOCATION              — per-driver state slot inside IRP
IRP_MJ_*                       — major codes (CREATE, CLOSE, DEVICE_CONTROL, READ, WRITE, CLEANUP, ...)

!devstack, !drvobj /a, !irp in WinDbg make these inspectable (see kernel-debugging-with-windbg).

Detection and defence

  • Probe every user buffer once into a local kernel copy; never re-read user memory across operations — see double-fetch
  • Restrict device DACLs (IoCreateDeviceSecure, SDDL_DEVOBJ_SYS_ALL_ADM_RWX_WORLD_R); deny non-admin where possible
  • Validate InputBufferLength/OutputBufferLength against your struct sizes and array bounds
  • Driver Verifier with IRP/IOCTL options and Microsoft Pool Quotas catches many bug classes before shipping

References

Related: fuzzing-windows-drivers, double-fetch, type-confusion-kernel, kernel-debugging-with-windbg