Unsigned drivers loading on Windows 11 25H2 (fully patched) by exploiting Kernel R/W primitives

After having developed working exploits for physical r/w (ZwMapViewOfSection) and r/w through MSR (L_STAR) for Windows 11 25H2, I decided it was time to take it up a notch and try to load unsigned drivers using these primitives...

It took a a lot of debugging... and endless nights with long hours, but at the end it was worth it.

The exploit itself, without the kernel driver loading, is explained in more detail on my blog here: https://www.exploitpack.com/blogs/news/0-day-msr-kernel-exploit-for-windows-11-25h2 , 

Here is were the story of unsigned drivers begins..

Arbitrary Kernel Code Execution for this exploit:

First, R/W MSR_LSTAR were used to locate KiSystemCall64, abusing the IOCTLs from the vulnerable driver that lead to the WRMSR/RDMSR, and ended up calling MyKernelPayloadLoadDriver. This temporarily overwrites IA32_LSTAR with a ROP gadget address and issues a syscall. The kernel-side handler restores LSTAR and invokes the provided callback with krnl_base and get_system_routine. That callback goes into MyKernelPayloadLoadDriver, which then performs the mapping/patching steps below entirely in kernel context:

1. Resolving exports: I knew before the start that I had to resolve a large set of kernel exports, so this is were I began with MyKernelPayloadLoadDriver. This could potentially be done with get_kroutine as I used it on my other exploit, actually, thinking about get_kroutine was what gave me the idea, that loading unsigned drivers through this exploit could be possible DbgPrint (for debugging), pool allocators, Ps* functions, IoCreateDriver/Device/SymbolicLink,  MmIsAddressValid, had to be resolved.. etc.

 

2. Protections: There were protections that I needed to bypass if I wanted this to work. Windows callback verification paths should be taken care of: AlwaysAllowKernelCallbacks, PatchProcessCallbackVerifier, ForceProcessCallbackAPIsSuccess, so PsSetCreateProcessNotifyRoutine* calls from the unsigned driver won’t be rejected during runtime.

3. Mapping the driver: I used a function called MapDriver (easy to remember) to copy/relocate the PE into the executable pool, resolved imports via get_kroutine, and returned the entrypoint address. Then did some cleaning by freeing the temporary kernel copy to avoid a potential bugcheck. Of course first I had the bugcheck then I did this ;-D

4. Almost there... After IoCreateDriver returns, it tries to create/activate a control device IoCreateDevice using the device name, and creates a user-visible symbolic link. It deletes any existing links first for cleanliness. This ensures a handle can be opened later through the chosen symlink, meaning I can reload the exploit and re-execute it if I wanted to.

Back to blog