Everything a Windows system does beyond raw computation — talking to disks, network cards, GPUs, USB devices, filesystems, and antivirus engines — ultimately runs through drivers. A driver is a kernel-mode module loaded into the address space of ntoskrnl.exe, running in ring 0 with the same privileges as the kernel itself. Because a buggy or malicious driver has unrestricted access to physical memory and every kernel structure, the rules that govern how drivers are written, loaded, and invoked are among the most important — and most security-relevant — parts of Windows internals. This article walks through the driver model: how a driver initializes, the two major frameworks used to write one, how drivers are loaded and signed, the IRQL rules they must obey, and why third-party drivers are a favorite attacker foothold.
DriverEntry and Initialization
Every driver has an entry point named DriverEntry, the kernel equivalent of main. When the driver is loaded, the kernel calls DriverEntry and passes it a pointer to a freshly created DRIVER_OBJECT — the kernel structure that represents the driver instance. The job of DriverEntry is to make that object usable:
- Fill in the dispatch table: the
MajorFunctionarray inside theDRIVER_OBJECT, indexed by IRP major function codes (IRP_MJ_CREATE,IRP_MJ_READ,IRP_MJ_WRITE,IRP_MJ_DEVICE_CONTROL, and so on). Each slot is a function pointer to the routine that handles that kind of request. - Register an unload routine (
DriverUnload) so the driver can be cleanly removed. - For a Plug and Play driver, set an
AddDeviceroutine, which the PnP manager calls when a matching device appears so the driver can create and attach a device object to the device stack. - Create one or more device objects with
IoCreateDevice, optionally naming them and exposing a symbolic link so user mode can open them.
Once DriverEntry returns success, the driver is live: requests aimed at its devices are packaged as I/O Request Packets (IRPs) and delivered to the handler functions the driver registered.
WDM (Windows Driver Model)
The classic way to write a driver is the Windows Driver Model. WDM is fundamentally IRP-based: the driver receives IRPs, inspects the current stack location to see what is being asked, does the work (or passes the IRP down to a lower driver in the stack), and completes the request. Beyond ordinary reads and writes, a WDM driver must also handle IRP_MJ_PNP for Plug and Play events (start device, query remove, remove device) and IRP_MJ_POWER for power transitions.
The catch is that WDM makes the developer responsible for an enormous amount of fiddly, concurrency-sensitive bookkeeping: correct reference counting on device objects, proper IRP cancellation logic, managing the PnP state machine by hand, and obeying strict IRQL rules on every call. Getting any of this subtly wrong produces race conditions, use-after-free bugs, and blue screens. A large fraction of historical kernel vulnerabilities trace back to mistakes in exactly this manual plumbing.
KMDF (Kernel-Mode Driver Framework)
To tame that complexity, Microsoft introduced the Windows Driver Frameworks (WDF), whose kernel-mode half is the Kernel-Mode Driver Framework (KMDF). KMDF is an object-based abstraction layered on top of WDM. Instead of hand-processing raw IRPs, the driver works with framework objects, each with a defined lifetime and event callbacks:
WDFDRIVER— represents the driver itself.WDFDEVICE— represents a device the driver manages.WDFQUEUE— a framework-managed queue that delivers requests to the driver and handles synchronization and cancellation.WDFREQUEST— a wrapper around an IRP that the driver reads parameters from and completes.
Crucially, KMDF handles the PnP and power state machines, IRP cancellation, and much of the reference counting for the driver, so the developer writes far less error-prone boilerplate. This eliminates entire classes of bugs and is the recommended model for most new hardware drivers. A parallel framework, UMDF (User-Mode Driver Framework), lets certain device classes be driven from a user-mode host process, so a crash takes down only that process rather than the whole machine — a big reliability and security win where it applies.
Driver Loading and Signing
Drivers are loaded through the same service database as ordinary services. Each driver has a key under HKLM\SYSTEM\CurrentControlSet\Services\<name> with a Type value marking it as a kernel driver and a Start value that controls when it loads: 0 (boot-start, loaded by winload before the kernel initializes devices), 1 (system-start, loaded during kernel init), 2/3 (auto/demand, loaded later by the Service Control Manager), or 4 (disabled).
Because a driver is ring-0 code, loading one is gated by Driver Signature Enforcement (DSE): on 64-bit Windows the kernel refuses to load an unsigned driver. Legitimate drivers are signed through Microsoft’s attestation or WHQL process; developers can enable test-signing mode to run their own test certificates during development (a state defenders watch for, since it weakens the guarantee). On systems with Hypervisor-Protected Code Integrity (HVCI), code-integrity checks are enforced from within the hypervisor, raising the bar further.
IRQL and Driver Constraints
Kernel code runs at an Interrupt Request Level (IRQL), and a driver must constantly be aware of which one it is at. The common levels are PASSIVE_LEVEL (0, ordinary code, can block and touch pageable memory), APC_LEVEL (1), and DISPATCH_LEVEL (2, where the scheduler and DPCs run), with device interrupt levels above that. The rules that follow from IRQL are strict and non-negotiable:
- Code running at
DISPATCH_LEVELor above cannot access pageable memory — touching a paged-out page there causes a fatal page fault (IRQL_NOT_LESS_OR_EQUAL, one of the most familiar bugchecks). Such code must use non-paged pool. - Code at
DISPATCH_LEVELor above cannot block or wait on dispatcher objects, because the scheduler itself runs at that level. - Many kernel APIs document a maximum IRQL at which they may be called; violating it corrupts state or crashes.
This is why driver code is peppered with distinctions between paged and non-paged allocations and why so much care goes into which routine runs at which level.
Inspecting Drivers
Loaded drivers and their objects are directly observable from both user mode and a kernel debugger:
# User mode: list kernel drivers registered as services
sc query type=driver
# Detailed inventory of installed drivers (paths, states)
driverquery /v
# WinDbg (kernel): list loaded modules, including drivers
lm
# Dump a driver object: its dispatch table, devices, and routines
!drvobj mydriver 7
# Enable Driver Verifier for a driver to catch pool/IRQL/leak bugs
verifier /flags 0x1 /driver mydriver.sys
!drvobj <name> 7 is the key debugger command: the 7 flag prints the driver’s device objects, the populated entries of its MajorFunction dispatch table, and its other registered routines — an instant map of what the driver actually handles. Driver Verifier (verifier.exe) is the essential development and triage tool: it subjects a driver to aggressive checks (special pool to catch overruns, IRQL validation, leak detection) so latent bugs crash immediately and visibly instead of silently corrupting the kernel.
Security Relevance
Because a driver is unrestricted ring-0 code, the driver model is one of the most consequential security boundaries on the system. The dominant modern abuse is Bring Your Own Vulnerable Driver (BYOVD): rather than write and sign malicious kernel code, an attacker loads a legitimately signed but buggy third-party driver that exposes an arbitrary kernel read/write primitive through its IRP_MJ_DEVICE_CONTROL handler. With that primitive, the attacker can disable protections from the kernel — clearing a process’s Protected Process Light flag to open LSASS, unregistering endpoint-security callbacks, or patching code-integrity state — all while every loaded module is validly signed.
The defenses map directly onto the concepts above. Loading a driver in the first place requires administrative rights and SeLoadDriverPrivilege, so the boundary starts at not letting untrusted code reach that point. Microsoft’s vulnerable-driver blocklist, enforced by HVCI where available, refuses to load known-bad drivers by hash. Driver Verifier and attested signing raise the cost of shipping a flawed driver. And for defenders hunting an active BYOVD attack, the signals are concrete: an unexpected service of driver type appearing, a rarely seen signed driver being loaded from a user-writable path, or test-signing suddenly enabled. Everything here is described for defensive understanding and authorized testing only.
Conclusion
The driver model is where third-party code meets the kernel: DriverEntry wires up a DRIVER_OBJECT, WDM or KMDF structures how requests are handled, the service database and signing rules govern loading, and IRQL constraints discipline what a driver may do at any moment. Understanding it explains both how legitimate hardware support works and why signed-but-vulnerable drivers are such a potent attack surface. Having covered how kernel modules load, the next article in the series turns to how user-mode code is loaded and laid out: the Windows image loader and the PE file format.
You Might Also Like
This article is part of the Windows Internals series. These related deep-dives cover adjacent parts of the system:



Comments