By using this project, you agree to use it responsibly and ethically. The author assumes no liability for misuse or any consequences arising from the use of this project.
Tested on:
- Windows 11 (24H2)
- Windows Server 2022 (21H2)
To practice Bring Your Own Vulnerable Driver (BYOVD) techniques covered in the CETP course, I set out to build an EDR-killer using a vulnerable driver that is not currently blocked by Microsoft's recommended driver blocklist so I can load it on my latest W11 testing system with secure boot and HVCI enabled.
A well-known example, truesight.sys from Adlice (also listed on LOLDDrivers), is already blocked by Microsoft and will trigger an error explicitly stating that the driver is vulnerable when attempting to load it:
From @d1rkmtr's tweet, another potentially useful vulnerable driver was shared, but like the newly indexed BdApiUtil.sys on LOLDDrivers, it fails to load on the latest Windows 11 versions. This behavior is due to HVCI (Hypervisor-Protected Code Integrity) and Secure Boot, not the driver blocklist. These protections enforce stricter requirements for driver signing and integrity, which unsigned or improperly signed drivers can't meet.
Interestingly, the wsftprm.sys driver, listed on LOLDDrivers, is not on Microsoft's blocklist and loads successfully on fully patched Windows 11 with Secure Boot and HVCI enabled. Its digital signature is valid and accepted by the system.
The vulnerability in this driver was discovered by Northwave, indicating that it could be used for a EDR-killer. Making it a viable candidate for further exploration and development of a proof-of-concept.
Disclaimer: I am by no means an experienced reverse engineer. The vulnerability in this driver was already publicly known, and I did not discover it myself. Full credit for the original vulnerability research goes to Northwave.
The first step is to inspect the driver's import table and check whether it imports the following native APIs:
ZwOpenProcessZwTerminateProcess
If these functions are present, the next step is to cross-reference where they are called within the driver. This helps identify code paths that may open or terminate processes. While analyzing the references, I traced the usage of ZwOpenProcess and ZwTerminateProcess back to the driver's entry point, where they are ultimately invoked through the dispatch routine assigned to MajorFunction[14], which corresponds to IRP_MJ_DEVICE_CONTROL.
This dispatch routine is implemented in the function sub_140001540.
The function is currently misidentified as handling IRP_MJ_READ, but based on the driver setup, we know it is actually a IRP_MJ_DEVICE_CONTROL (MajorFunction[14]). This function is a driver dispatch routine, which means its prototype should conform to:
NTSTATUS DriverDispatch(
_In_ PDEVICE_OBJECT DeviceObject,
_Inout_ PIRP Irp
);So we can change the input parameters;
As we've determined that this function handles IRP_MJ_DEVICE_CONTROL requests (not IRP_MJ_READ), several structure references need to be corrected. For example:
Next, I revisited the ZwTerminateProcess import and found that it is called within the function sub_140002848. Inside this function, the flow is straightforward:
ZwOpenProcessis first called to obtain a handle to the target process.- If the handle is valid,
ZwTerminateProcessis invoked to kill that process.
The function takes a1 as input, which is passed into the fourth parameter of ZwOpenProcess. According to the ZwOpenProcess prototype, the fourth argument is a pointer to a _CLIENT_ID structure, which specifies the process
NTSYSAPI NTSTATUS ZwOpenProcess(
[out] PHANDLE ProcessHandle,
[in] ACCESS_MASK DesiredAccess,
[in] POBJECT_ATTRIBUTES ObjectAttributes,
[in, optional] PCLIENT_ID ClientId
);We can change the parameters and structures to reflect this, I also renamed the function to TerminateProcessByID
TerminateProcessByID is called by sub_14000264C with a supplied buffer. I renamed the function to BeforeTerminateProcessById
Next, I investigated how to trigger the functions BeforeTerminateProcessById and TerminateProcessByPID. With the help of ChatGPT, I discovered that it is invoked when a DeviceIoControl call is made with IOCTL 0x22201C and when the input buffer is 1036 bytes.
To determine the IOCTL code being handled when IOCTLCode2 == 4, we need to walk backward from that comparison and calculate the corresponding value for the IOCTL code. The logic is as follows:
v8 = IOCTLCode - 0x222000 // IOCTL 0x222000
v9 = v8 - 4 // IOCTL 0x222004
v10 = v9 - 4 // IOCTL 0x222008
v11 = v10 - 16 // IOCTL 0x222018
IOCTLCode2 == 4 // IOCTL 0x22201C
When we go back to TerminateProcessByID we can see that the first 4 bytes of the input buffer will be set within the _CLIENT_ID struct. Which is then passed into ZwOpenProcess.
This means that, of the 1036-byte input buffer expected by the driver when handling IOCTL 0x22201C, the first 4 bytes must contain the target Process ID. In C we can define this with the following example;
// Custom struct for the IOCT call
typedef struct _wsftprmKillBuffer {
DWORD dwPID;
BYTE bPadding[1032];
} wsftprmKillBuffer;
Back in the driver’s entry point, we can see that the symbolic link is created using the result of the sub_140001410 function. This symbolic link is important because it defines the user accessible name under \\DosDevices\\ that user mode applications will use to interact with the driver via CreateFileW.
These functions use XOR-encoded byte sequences to obfuscate the symbolic link name. In the case of this driver, the function sub_140001410 performs the decryption at runtime.
ChatGPT successfully decrypted the obfuscated symbolic link string as: \\DosDevices\\Warsaw_PM. This was the final missing piece needed to fully interact with the driver and begin building our EDRKiller.
- Device Name:
\\DosDevices\\Warsaw_PM- Accessed from user-mode as
\\\\.\\Warsaw_PM)
- Accessed from user-mode as
- IOCTL Code:
0x22201C(Triggers the vulnerable process termination routine) - Expected Input Buffer:
- Size:
1036bytes - Format: The first 4 bytes represent the target PID as a
DWORD
- Size:
typedef struct _wsftprmKillBuffer {
DWORD dwPID;
BYTE bPadding[1032];
} wsftprmKillBuffer;
This C project includes a proof-of-concept (POC) that enumerates EDR-related processes and repeatedly sends the vulnerable IOCTL to terminate them in a loop, continuing until the user presses q to exit.
Currently, the targeted EDR solutions and associated processes are:
- Microsoft Defender Antivirus
- Microsoft Defender for Endpoint
- Elastic EDR
- Sysmon
- Writes vulnerable driver to
C:\Windows\System32\Drivers\<FILE>and loads the driver (Configurable insettings.h) - Keeps looping and enumerating EDR Processes
- Kills EDR Process using the IOCTL of the vulnerable driver
- Exits when q is pressed
- Unloads the vulnerable driver and removes the file from
C:\Windows\System32\Drivers\<FILE>
The testing environment has Secure Boot, Virtualization-Based Security (VBS), and Hypervisor-Protected Code Integrity (HVCI) enabled. These mitigations were verified using my EnumMitigations tool.
- The payload should unload and remove the driver. If it didn't then manually remove it
sc stop wsftprm
sc delete wsftprm
del C:\Windows\System32\Drivers\wsftprm.sys
I got inspired to expand upon the tools provided in the Evasion Lab (CETP from Altered Security), taught by Saad Ahla.
To NorthWave for finding the vulnerable driver