
Proof-of-concept exploit for CVE-2026-29923, a BYOVD privilege escalation in pstrip64.sys. Demonstrates physical memory read/write via IOCTL to steal SYSTEM token and spawn elevated shell.
Disclaimer: This code is provided for educational and defensive research purposes only. It was written to further the understanding of kernel exploitation and help defenders protect against similar vulnerabilities. Any unauthorized, illegal, or malicious use of this project is strictly prohibited.
Hash: ab01485bb7c8bc1a9c86096eeea6d31d8fad557bf4d44072b46373d2203faa6e
Driver Name: pstrip64.sys
CVE: CVE-2026-29923
A "Bring Your Own Vulnerable Driver" (BYOVD) attack is an old but highly effective way for attackers to bypass modern Windows security protections by using a legacy driver that the operating system still officially trusts. Once the driver is loaded, the attacker weaponizes its flaws to bridge the gap between a standard, unprivileged process and total system-level control.
Earlier this week, a new vulnerability was disclosed in the pstrip64.sys driver, tracked as CVE-2026-29923. This blog post breaks down the entire lifecycle of the exploit: from my initial vulnerability research and the development of the Proof of Concept (PoC) to actionable mitigation strategies for defenders securing their environments.
The pstrip64.sys driver is a legacy kernel-mode component tied to EnTech Taiwan PowerStrip (up to version 3.90.736). While its legitimate purpose is to enable advanced graphics card display tweaking, its deep system privileges make it a highly attractive target for attackers.
When the vulnerability was first disclosed, I began by analyzing its DriverEntry function. This serves as the main initialization routine for the kernel driver, creating the \Device\PSTRIP64 device object and exposing it to user-mode applications via the \DosDevices\PSTRIP64 symbolic link. More importantly, it configures the driver's dispatch table. The entry that immediately caught my eye was at index 14 (IRP_MJ_DEVICE_CONTROL), which routes all user-supplied IOCTL requests directly into the sub_11340 handler function, our primary area of interest.
The sub_11340 function serves as the primary IOCTL dispatcher, interpreting requests from user-mode.
Out of all the exposed IOCTLs, 0x80002008 is undoubtedly the most interesting. While the default cases handle minor I/O port interactions, 0x80002008 acts as a gateway to sub_11000. By passing the SystemBuffer directly into this function.
This sub_11000 routine is the smoking gun. First, it uses HalTranslateBusAddress to take our user-supplied address and translate it into a valid system physical address. Then, it opens \Device\PhysicalMemory and maps it using ZwMapViewOfSection. By hardcoding the target process handle to (HANDLE)0xFFFFFFFFFFFFFFFFLL (which represents ZwCurrentProcess()), the driver maps this physical memory directly into our calling process's virtual address space. Crucially, it then writes this newly mapped virtual address back into the SystemBuffer to return to the user, officially handing our application a direct pointer to read and write physical memory.
With the vulnerability fully understood and a physical read/write primitive established, I have all the puzzle pieces required. Now, it’s time to start writing the Proof of Concept.
Note: This PoC was specifically developed and tested on a Windows 10 22H2 environment. Because the exploit relies on raw physical memory manipulation, the kernel structure offsets and physical memory boundaries are currently hardcoded for my setup. To test this on your own machine, you must update the Windows kernel offsets and adjust the physical address scanning ranges to match your specific OS build and RAM configuration.
The first step in my exploit is establishing communication with the driver. I did this by calling CreateFileA on the driver's symbolic link (\\.\PSTRIP64). Once I had a valid handle, I needed a clean way to abuse the 0x80002008 IOCTL I analyzed earlier. I created a wrapper function called MapPhysicalMemory(). This function populates my custom PSTRIP_MAP_REQUEST struct with the target physical address and the length of the memory chunk I want to read.
I then send this struct directly to the driver via DeviceIoControl. If successful, the driver maps that physical memory directly into my user-mode application and returns the virtual base address in the OutputResult field. I can now cast this returned address to a standard C++ pointer, giving me raw, unprivileged access to the system's physical RAM.
With my physical read/write primitive fully operational, my goal was to find the kernel data structures that contain process privileges. In Windows, every running process is represented by an EPROCESS structure.
Windows allocates EPROCESS structures in the kernel pool using a specific 4-byte identifier called a Pool Tag. For processes, this tag is the string Proc (which translates to 0x636F7250 in hex). By scanning the system’s physical RAM, I could search for this exact string.
My exploit loops through the physical memory space from 0x10000000 to 0x140000000, mapping memory in 2MB chunks (STEP_SIZE = 0x200000). I cast each mapped chunk to a raw byte array and scan it in 16-byte chunks (sizeof(_POOL_HEADER)).
However, simply finding the Proc tag in physical memory is not enough. Memory is messy, that tag could be a leftover artifact from a terminated process, or just random data that happens to match the hex value. If I blindly assumed that every Proc tag was a valid EPROCESS structure and started modifying memory, I would immediately cause a BSOD.
To ensure stability, I had to validate the structure using heuristics. First, I calculate the start of the EPROCESS structure (which sits slightly offset from the pool tag). From there, I check a few known constants for a running process:
0x2 (Normal Priority).0x0.If all these heuristics pass, I can be highly confident I am looking at a valid, active process. I then read its Unique Process ID (PID). If the PID matches my own exploit process, I save the physical address of its token pointer. If the PID is 4 (the Windows System process), I extract and save the actual value of its highly privileged token.
Finally, I align the saved physical address of my process's token pointer to the nearest 4KB boundary and use MapPhysicalMemory() one last time to map just that specific page.
Next, I navigate to the exact offset and overwrite my token with the System token value. Instantly, the Windows kernel treats my exploit process as NT AUTHORITY\SYSTEM.
After unmapping the page to ensure system stability, I simply call CreateProcessA to spawn cmd.exe. Because my current process is elevated, the new command prompt inherits these top-tier privileges, successfully completing the attack!
Note: A critical detail I discovered during my early debugging phase is how the driver handles the mapped pointer. By executing SystemBuffer->LowPart = (unsigned int)BaseAddress;, the driver casts the 64-bit virtual base address down to a 32-bit value before returning it. This truncation loses the high bits of the address, which resulted in immediate access violations when I tried to dereference it in my 64-bit exploit. To cleanly bypass this issue, I simply compiled my user-mode PoC as a 32-bit application, ensuring the returned pointer remained perfectly valid.
Note: During my initial testing, I encountered a fascinating edge case: my PoC successfully located my exploit process in memory, but it failed to find the System process (PID 4).
To understand why, I needed to inspect physical memory directly. I attached a kernel debugger (WinDbg) and used commands to retrieve the virtual address and the directory base of the System process. I then used !vtop to translate that virtual address into its exact physical address in RAM.
I switched back to my user-mode debugger attached to my PoC. I set a conditional breakpoint on my memory scanning loop, instructing it to pause execution the moment my MapPhysicalMemory() function grabbed the 2MB chunk containing the System process's physical address.
Once the breakpoint was hit, I began manually inspecting the raw bytes of the mapped memory. This is where I discovered a crucial detail about Windows kernel pool allocations.
When Windows allocates memory for a process, it starts with a _POOL_HEADER (containing our Proc tag), followed by an _OBJECT_HEADER, and finally the EPROCESS structure itself. For standard user-mode applications, these headers contain additional tracking data, meaning the actual EPROCESS structure starts 0x80 bytes after the pool tag.
However, inspecting the System process's memory revealed a different layout. System process lacks some of these standard tracking headers. The offset from the Proc tag to the start of the EPROCESS structure was only 0x40 bytes!
The fix was straightforward. I updated my PoC to handle both pool header sizes by looping through an array of possible offsets (0x40 and 0x80) whenever it encounters a Proc tag.
Cybersecurity is an endless game of cat-and-mouse between attackers and defenders. While attackers constantly hunt for vulnerable drivers, modern security products and blue teams have several robust ways to detect and block this exact operation.
The most effective way to stop a BYOVD (Bring Your Own Vulnerable Driver) attack is to prevent the driver from loading in the first place.
If the driver is already loaded, security products can still detect the exploit during the token manipulation phase.
NT AUTHORITY\SYSTEM without a legitimate authentication chain is a massive red flag.cmd.exe), especially when the parent process has no business running as SYSTEM.