# BYOVD EDRKiller ## Disclaimer ⚠️ This project is provided exclusively for educational purposes and is intended to be used only in authorized environments. You may only run or deploy this project on systems you own or have explicit, documented permission to test. Any unauthorized use of this project against systems without consent is strictly prohibited and may be illegal. 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) # General 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](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/design/microsoft-recommended-driver-block-rules#vulnerable-driver-blocklist-xml) 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](https://www.loldrivers.io/drivers/e0e93453-1007-4799-ad02-9b461b7e0398/)), is already blocked by Microsoft and will trigger an error explicitly stating that the driver is vulnerable when attempting to load it: Pasted image 20250727172458 From [@d1rkmtr](https://x.com/d1rkmtr/status/1947664686897365177)'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. Pasted image 20250727172100 Interestingly, the `wsftprm.sys` driver, listed on [LOLDDrivers](https://www.loldrivers.io/drivers/30e8d598-2c60-49e4-953b-a6f620da1371/), 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. Pasted image 20250727172206 The vulnerability in this driver was discovered by [Northwave](https://northwave-cybersecurity.com/vulnerability-notice-topaz-antifraud), 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. # Reversing the driver 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](https://northwave-cybersecurity.com/vulnerability-notice-topaz-antifraud). The first step is to inspect the driver's import table and check whether it imports the following native APIs: - `ZwOpenProcess` - `ZwTerminateProcess` Pasted image 20250727143504 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`. Pasted image 20250727144806 Pasted image 20250727145950 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](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nc-wdm-driver_dispatch), which means its prototype should conform to: ```c NTSTATUS DriverDispatch( _In_ PDEVICE_OBJECT DeviceObject, _Inout_ PIRP Irp ); ``` So we can change the input parameters; Pasted image 20250727150140 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: Pasted image 20250727150317 Pasted image 20250727150852 Next, I revisited the `ZwTerminateProcess` import and found that it is called within the function `sub_140002848`. Inside this function, the flow is straightforward: - `ZwOpenProcess` is first called to obtain a handle to the target process. - If the handle is valid, `ZwTerminateProcess` is invoked to kill that process. Pasted image 20250727151547 Pasted image 20250727151631 Pasted image 20250727151712 The function takes `a1` as input, which is passed into the fourth parameter of `ZwOpenProcess`. According to the [ZwOpenProcess](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/nf-ntddk-zwopenprocess) prototype, the fourth argument is a pointer to a `_CLIENT_ID` structure, which specifies the process ```c 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` Pasted image 20250727163908 `TerminateProcessByID` is called by `sub_14000264C` with a supplied buffer. I renamed the function to `BeforeTerminateProcessById` Pasted image 20250727163949 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. Pasted image 20250727164333 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 ``` Pasted image 20250727165443 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`. Pasted image 20250727164956 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`. Pasted image 20250727170526 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. Pasted image 20250727170610 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. ## Summary of Key Details - **Device Name:** `\\DosDevices\\Warsaw_PM` - Accessed from user-mode as `\\\\.\\Warsaw_PM`) - **IOCTL Code:** `0x22201C` (Triggers the vulnerable process termination routine) - **Expected Input Buffer:** - **Size:** `1036` bytes - **Format:** The first 4 bytes represent the target PID as a `DWORD` ``` typedef struct _wsftprmKillBuffer { DWORD dwPID; BYTE bPadding[1032]; } wsftprmKillBuffer; ``` # Proof of Concept 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. ### EDR's Currently, the targeted EDR solutions and associated processes are: - Microsoft Defender Antivirus - Microsoft Defender for Endpoint - Elastic EDR - Sysmon ### What does it do - Writes vulnerable driver to `C:\Windows\System32\Drivers\` and loads the driver (Configurable in `settings.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\` ### Test The testing environment has Secure Boot, Virtualization-Based Security (VBS), and Hypervisor-Protected Code Integrity (HVCI) enabled. These mitigations were verified using my [EnumMitigations](https://github.com/0xJs/EnumMitigations) tool. Pasted image 20250727190301 Pasted image 20250727190532 ### Cleanup - 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 ``` ## Credits I got inspired to expand upon the tools provided in the Evasion Lab (CETP from [Altered Security](https://www.alteredsecurity.com/evasionlab)), taught by [Saad Ahla](https://www.linkedin.com/in/saad-ahla/). To NorthWave for finding the vulnerable driver