1 Introduction
This article provides a technical analysis of how many Windows kernel mode drivers can be interacted with from user mode without the hardware they were developed for. This work was motivated by driver-oriented vulnerability research and the need to evaluate the exploitability of individual findings, which frequently affect code whose reachability is hardware-gated. The methodology presented here should help anyone determine whether a particular Windows kernel mode driver vulnerability remains reachable - and thus potentially exploitable - even in the absence of the hardware the driver was developed for.
The reader is expected to have basic Windows driver knowledge, especially regarding device objects. The rest of this article is written with the assumption that the reader is already familiar with the concepts described in the introduction article: Anatomy of Access: Windows Device Objects from a Security Perspective.
Just like the introduction article, this resource is not focused on any specific bug class, but rather the attack surface and, to an extent, the Windows Plug and Play architecture.
All the tests demonstrated here were conducted on Windows 11 23H2 (winver 10.0.22631.3007).
For more such latest threat research and vulnerability advisory, please subscribe to Atos Cyber Shield blogs.
2 The offensive value of kernel mode drivers
In addition to the obvious Local Privilege Escalation potential, vulnerable drivers are often abused in BYOVD attacks - a post-exploitation technique leveraged by attackers to disrupt system defenses such as EDR components.
Two main criteria determine whether a driver vulnerability is a strong candidate for BYOVD attacks: 1. Exploitation allows meaningful disruption of an otherwise tamper-resistant security component. Examples include kernel-level vulnerabilities granting arbitrary memory read/write access, arbitrary code execution, or arbitrary resource abuse (e.g., overwriting files, closing handles, or terminating processes). 2. Its exploitability is independent of rare system conditions, such as the presence of specific hardware.
Although BYOVD-style attacks have been well documented for years, with numerous public reports and research papers on the topic (e.g. https://www.ndss-symposium.org/wp-content/uploads/2026-s1491-paper.pdf, https://blackpointcyber.com/blog/qilin-ransomware-and-the-hidden-dangers-of-byovd/, https://www.sophos.com/en-us/blog/itll-be-back-attackers-still-abusing-terminator-tool-and-variants), none of them specifically examines the role of hardware-gating in driver vulnerability reachability.
3 Device object creation and maintenance - common patterns
The analysis provided in this resource is structured around device objects, because they are the most viable attack vector. However, the techniques demonstrated here practically impact driver code reachability from userland in general, not just via IRP.
The most common obstacles in attacking a driver via its device object are: 1. The device object is not created. 2. The driver's internal state does not allow the exercise of the vulnerable behavior despite the device object being accessible.
Both scenarios are very common when dealing with a device driver deployed on a system without the corresponding physical hardware.
In the rest of the article I am often referring to device stacks and device nodes. I have covered device stacks quite broadly in my introduction article. While a device node and a device stack are not the same thing, the terms are often used interchangeably, because every device node has exactly one device stack.
3.1 Unconditional creation upon driver load
Many drivers, especially non-PnP drivers, create their device objects either directly from within their DriverEntry function, or from some other function that gets invoked in the direct call chain originating from DriverEntry.
Multidev_WDM demo driver exemplifies this pattern. We can see the device creation invoked right away in DriverEntry:
CDO creation invoked directly from DriverEntry
The driver also removes the device object by calling IoDeleteDevice, but that happens only when DriverUnload is called (when the driver is being unloaded):
CDO cleanup from DriverUnload
Drivers built this way can be interacted with after simple deployment consisting of just two steps:
Create the driver's service entry: sc.exe create SampleDrv type= kernel start= demand binPath= System32\drivers\SampleDrv.sys
sc.exe create SampleDrv type= kernel start= demand binPath= System32\drivers\SampleDrv.sys
Start the service (driver will load): sc.exe start SampleDrv
If we look at a randomly picked driver from https://loldrivers.io/, we will see that its deployment command matches this pattern:
LOL drivers - zam64.sys deployment
But most device drivers do not fall into this category, as we will see in the following sections of this article.
3.2 Conditional device creation and maintenance
Oftentimes driver initialization routines perform additional checks. For example, kernel mode components of security software (EDR, anti-virus, monitoring, enhanced authentication etc.) tend to check for product-specific registry keys and entries, which are created and initialized during normal product deployment.
Actual device drivers (created to drive physical hardware) tend to only create their device objects in the presence of that hardware. Without it they either: - do not attempt to create any device objects at all, - they remove any device objects shortly after their creation, by calling IoDeleteDevice.
Let's focus on how that logic is implemented and evaluate whether and how it can be worked around, especially from the BYOVD perspective - by solely operating from userland (with no physical/hypervisor access).
By the way, the second scenario, in which a device object is first created and then deleted shortly after, creates a situation that could be considered a race condition, because there is a short time window in which the device object exists.
3.3 PnP-specific callbacks as the main location of PnP driver initialization logic
In PnP-compatible drivers (which make up most of device drivers), initialization logic extends beyond DriverEntry into the following PnP-specific routines: AddDevice and the IRP_MJ_PNP handler.
This section explores both of them and explains why most PnP-compatible drivers need to be set up in a way that ensures these functions are called if we want to interact with the driver.
3.3.1 AddDevice
All PnP-compatible drivers must define this routine. It is responsible for creating functional device objects (FDO) and filter device objects (filter DO) for devices enumerated by the PnP manager. This explains why AddDevice is where most of the initialization logic resides. That includes: - creation of device objects (IoCreateDevice), - initialization of various internal variables that are later required to reach the vulnerable code, - I/O queue management in WDF (KMDF) drivers.
The MSDN page about managing I/O queues in WDF drivers says: > Drivers typically call WdfIoQueueCreate from within an EvtDriverDeviceAdd callback function. The framework can begin delivering I/O requests to the driver after the driver's EvtDriverDeviceAdd callback function returns.
In the context of WDF (KMDF) drivers, AddDevice is referred to as EvtDriverDeviceAdd (different name, same application).
AddDevice is not called from within the DriverEntry routine, which means it does not automatically execute upon driver load. Instead, the PnP manager invokes it only after it discovers a new device node and determines that this driver should either control the device directly or serve as a filter in the device stack.
Let's look at some code. Note: all structure-specific offsets are for the 64-bit architecture.
Both in DriverEntry and in AddDevice, the first parameter the function receives is a pointer to the DRIVER_OBJECT structure. As we can read on the MSDN page, the stru