- #_shellntel Blog
- Posts
- Finding an Undiscovered Process Termination Vulnerability in a 15 year old Antivirus Driver
Finding an Undiscovered Process Termination Vulnerability in a 15 year old Antivirus Driver
I recently got into Bring Your Own Vulnerable Driver (BYOVD) exploits and hunting for vulnerable drivers that could be used to terminate a process. These are powerful exploits that can be used to kill security products such as AV and EDR solutions.
These drivers are legitimate software and they are signed by the vendor, so they are trusted by the machine on which they are loaded. Problems arise when the driver does not validate IOCTL code handlers properly. This could allow for code execution in the kernel from user-mode.
 In order to find a process killing candidate, the driver needs to import ZwTerminateProcess and ZwOpenProcess. There are tons of vulnerable drivers out there and loldrivers is a great resource for exploring them, but many of these are already blocked my Microsoft making them difficult to load, or they are already flagged as malicious. So how do you find your own? 
The two software domains that come to mind for programs that are likely to implement these functions are antivirus (AV) products and anti-cheat software (commonly used by gaming engines). The barrier to entry is lower for the former, so that is where I focused my attention.
 I used ChatGPT to create a list of some lesser-known antivirus products that support process termination. I also asked it to find some that have not been updated for a while. Then, I would go through the list, download the software in a Windows 11 VM, and locate the drivers used by the antivirus. Using PE-bear, I would look at the driver’s Import Address Table (IAT) and see if it is importing ZwTerminateProcess and ZwOpenProcess. 

Not every antivirus driver imports these functions, and even when you find one, it doesn’t automatically mean it’s a vulnerable driver. It means it is a potential candidate to be a vulnerable driver, but further investigation is necessary.
Repeating this process for several AV’s is what eventually led me to find Prevx.
Prevx is a discontinued anti-malware utility. There are separate real-time and on-demand versions. It can remove low-risk adware for free, but the user has to purchase and enter a license key if it is more serious. Scanning can take anywhere from less than two minutes to five minutes.
Prevx got acquired by Webroot in 2010 and has since discontinued its Prevx AV product line. However, you can still find the software available for download.
 Prevx uses three drivers, pxscan.sys, pxkbf.sys, and pxrts.sys. The one that imports both ZwTerminateProcess and ZwOpenProcess is pxscan.sys so that is the winner. 
 Opening up pxscan.sys with IDA Free (too poor for IDA Pro 😭) to take a closer look, there are a couple of things we need to identify: 
- Device Name 
- MajorFunctions of the driver 
- How and where - ZwTerminateProcessis being called
- IOCTL code to get to - ZwTerminateProcess
 Fortunately, finding the Device Name on this driver is straightforward and we can see that it is pxscan in the DriverEntry. 

 The DriverObject passed in is a pointer to a DRIVER_OBJECT structure. This struct contains important info about the driver. The one we are most interested in is MajorFunction. This is an array of function pointers to dispatch routines that specify what operations the driver supports. Like Create, Read, Write, etc. The indices are prefixed with IRP_MJ_ . You can view the full list of IRP major function codes here. 
 In order to communicate with the driver, we need to call DeviceIoControl from user-mode. This corresponds to the IRP, IRP_MJ_DEVICE_CONTROL. The index number can be found online, or by checking the wdm.h file on your local machine. Usually somewhere in: 
C:\Program Files (x86)\Windows Kits\10\Include\ We can see the value is 0×0e which is 14. 

 Looking back in IDA Free, we can see the dispatch routine for MajorFunction[14] is sub_12670, which I’ll rename to DeviceControlDispatch. 

All MajorFunctions have the same function prototype, which looks like the following:
NTSTATUS SomeMajorFunction(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp); So I’ll rename the parameters in DeviceControlDispatch to match. 

The last thing I’ll change is the IOCTL code. Looking at the IO_STACK_LOCATION structure, we can see it’s a union, so we’ll need to select the correct one.

I’ll also rename the variable accordingly.

 Now, we can find ZwTerminateProcess in the imports section of IDA Free and see where that function is called by viewing cross references to it. 


 The function that calls ZwTerminateProcess is sub_11480. So now we view cross references where that function is called and so on and so on until we land in the DeviceControlDispatch dispatch routine. 
 Eventually, we bubble up from sub_12350 which is called in the else block of if (v10) in the DeviceControlDispatch function. 

 In order to land in this else block, the IOCTL code must be 0x22E044. 
 I will rename the identified function, sub_12350, to TargetFunc. 
 Now we know the ZwTerminateProcess function call bubbles up to the dispatch routine for IRP_MJ_DEVICE_CONTROL, and we see what IOCTL code to send to access it. Now we need to dig into TargetFunc to understand what function calls it’s making and how ZwTerminateProcess eventually gets called. 
TargetFunc eventually calls sub_11CB0 which performs some interesting operations. 

sub_12620 gets called twice. The function takes a registry path and a buffer, queries the Windows registry for the buffer's value, and returns a value. In the first call, it queries the value of c_rem at the path: 
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\pxscan\Files Since sub_12620 performs a Windows registry lookup, I will rename it to RegistryQuery. 
 The return value ( v3 ) returns a string representation of an integer, gets converted into an int, and is assigned to the variable Value. 

 Then, there is a variable v8 that gets initialized to 0 and a while loop that iterates Value times where the sub_12620 gets called a second time. My spidey senses told me that c_rem could be “files remaining” since the while loop iterates c_rem times and rem strongly aligns with “remaining”. 

 Looking at the contents of the while loop, we can see that the unicode string buffer passed to the RegistryQuery function is set to v8 which is 0. A new unicode string is created from the output value v5 and another function is called sub_11AF0 that takes in the unicode string as a parameter. 

To recap:
 The c_rem entry at the registry path KEY_LOCAL_MACHINE\System\CurrentControlSet\Services\pxscan\Files is a string representation of an integer value, likely indicating the number of files remaining. Aka, the number of processes to kill. 
 At that same registry path, there is another entry “0” that has a string value of v5. This is very likely a file path. The “0” entry increments based on the count of c_rem. E.g., if c_rem is “2”, you would expect to have registry entries “0” and “1”. 
 The function call sub_11AF0 does some string and path validation, and calls sub_11480 which is the function that terminates the process. This function takes the file path and the length as input. 

 Towards the bottom of the function, we can see the call to ZwTerminateProcess where the process gets terminated. 

Putting everything together, we’ve learned that the registry entries should look like the following to terminate Windows Defender, for example.

 Then, we can send IOCTL code 0×22E044 to the driver to terminate the processes. 
I created a PoC to automate the creation of the registry key and to send the IOCTL code to the driver.

This vulnerability has been tracked as CVE-2025-60349. Full code is available here:
Thanks for reading!
