Skip to content

Token Swapping

At the end of nearly every Windows kernel exploit chain, after the pool spray, after the information leak, after the I/O Ring corruption, there is a single write that matters more than all the others. The attacker overwrites the Token pointer in the current process's _EPROCESS with the token from the SYSTEM process (PID 4). One pointer swap. The process that was running as a normal user is now running as NT AUTHORITY\SYSTEM. Every access check, every privilege verification, every integrity level comparison now evaluates against the SYSTEM token. The exploit is complete.

Token swapping is the most common final step in Windows kernel privilege escalation because it is conceptually simple, mechanically reliable, and universally applicable. It works on every Windows version. It works with HVCI enabled. It works with every initial vulnerability type, as long as the exploit chain produces an arbitrary kernel R/W primitive. The only variables are structure offsets that change between Windows builds, and those are well-documented and can be looked up at runtime. This universality is why nearly every exploit chain documented in the KernelSight case studies ends with a token swap.

How the token pointer works

Every Windows process has an _EPROCESS structure in kernel memory. This structure contains a Token field that holds an EX_FAST_REF pointer to the process's _TOKEN object. The _TOKEN contains everything that defines the process's security identity: privilege bitmasks, SID arrays, integrity level, session information, and authentication identifiers.

_EPROCESS
  +0x4B8  Token              // EX_FAST_REF (Windows 10 21H2 / Windows 11 22H2)

EX_FAST_REF is a compressed pointer format used throughout the Windows kernel. On x64, the upper 60 bits hold the actual pointer, and the lower 4 bits hold a reference count. Reading the raw token pointer requires masking off the low 4 bits: token_address = raw_value & ~0xF. Writing a new token pointer requires setting the low bits to a valid non-zero reference count (typically 0x6 or 0x7), because a zero refcount triggers special handling in the kernel's fast reference code.

The Token offset within _EPROCESS changes between Windows builds. On Windows 10 21H2 and Windows 11 22H2/23H2, the offset is 0x4B8, but this is not guaranteed on other versions. Exploits maintain a lookup table mapping NtBuildNumber to the correct offset, or dynamically resolve it by scanning the PsGetProcessToken function for the offset constant.

The swap sequence

The token swap requires four steps, each dependent on having a stable kernel R/W primitive (typically from I/O Ring, pipe attributes, or PreviousMode manipulation).

Step 1: Locate the current process _EPROCESS. The attacker uses NtQuerySystemInformation(SystemHandleInformation) to find the kernel object address for the current process handle. Alternatively, the attacker traverses from KPCR (at gs:[0x188]) to the current KTHREAD, then to the owning _EPROCESS. Both methods are reliable and do not require an information leak beyond what the kernel R/W primitive itself provides.

Step 2: Locate the SYSTEM _EPROCESS. The SYSTEM process (PID 4) is always the head of the ActiveProcessLinks doubly-linked list. Starting from the current _EPROCESS, the attacker reads ActiveProcessLinks.Flink to walk the list, reading the UniqueProcessId field of each entry until PID 4 is found. The global variable PsInitialSystemProcess also points directly to the SYSTEM _EPROCESS, but its address requires KASLR bypass.

Step 3: Read the SYSTEM token. From the SYSTEM _EPROCESS, read the EX_FAST_REF at the Token offset. Mask off the low 4 bits to get the raw token pointer.

Step 4: Write the SYSTEM token to the current process. Overwrite the EX_FAST_REF at the current _EPROCESS + Token_offset with the SYSTEM token value, setting the low 4 reference count bits to a small non-zero value (e.g., 0x6 or 0x7). This single write completes the privilege escalation.

After the swap, the attacker verifies by calling OpenProcess on a SYSTEM-owned process like csrss.exe or winlogon.exe. Success confirms SYSTEM privileges.

The _TOKEN structure

Understanding the token's internal layout matters for the alternative approach (in-place privilege modification via token manipulation) and for understanding what changes during a swap.

_TOKEN (+0x000 to ~0x480)
  +0x040  TokenId                    // LUID
  +0x048  AuthenticationId           // LUID (SYSTEM = 0x3e7)
  +0x060  Privileges                 // _SEP_TOKEN_PRIVILEGES
    +0x000  Present                  // ULONG64 bitmask
    +0x008  Enabled                  // ULONG64 bitmask
    +0x010  EnabledByDefault         // ULONG64 bitmask
  +0x098  IntegrityLevelIndex
  +0x0D0  SessionId
  +0x0E8  UserAndGroupCount
  +0x4B8  UserAndGroups              // (varies by build)

When the token pointer is swapped, the current process inherits everything from the SYSTEM token: all privileges, all group memberships (including NT AUTHORITY\SYSTEM, BUILTIN\Administrators, and Everyone), the SYSTEM authentication ID (0x3e7), and the System integrity level. This is a complete identity change, not just a privilege addition.

Alternative: in-place privilege overwrite

Instead of swapping the entire token pointer, the attacker can modify fields within the existing _TOKEN. This approach overwrites _SEP_TOKEN_PRIVILEGES.Present and _SEP_TOKEN_PRIVILEGES.Enabled with 0xFFFFFFFFFFFFFFFF, enabling all 36 privileges simultaneously.

This takes two writes instead of one, but avoids several complications of the pointer swap approach. There is no reference counting to manage: the original token pointer remains valid, and no EX_FAST_REF low-bit arithmetic is needed. The process keeps its original group memberships and session binding, which may be less anomalous to detection tools that monitor token pointer changes.

Aspect Pointer Swap Privilege Overwrite
Write operations 1 (token pointer) 2 (Present + Enabled)
Inherited groups All SYSTEM groups Original groups only
Reference count risk Must manage EX_FAST_REF None
Detection surface Token pointer change visible Privilege fields change visible
Complexity Simpler single write Slightly more writes but safer cleanup

The choice between approaches depends on the exploit's constraints. Token pointer swap is simpler and provides a more complete elevation. Privilege overwrite is safer for cleanup and avoids reference counting issues. Both achieve the practical goal of unrestricted system access.

Finding the SYSTEM token

Several methods exist for locating the SYSTEM token, with different tradeoffs between reliability and prerequisites.

NtQuerySystemInformation (SystemHandleInformation) returns kernel object addresses for all open handles in the system. Scanning for handles belonging to PID 4 reveals the SYSTEM _EPROCESS address, from which the token can be read. This is the most common method in modern exploits and works on all builds, though newer Windows versions may restrict this information class (which can be re-enabled through ACL/SD manipulation of SepMediumDaclSd).

ActiveProcessLinks traversal walks the doubly-linked process list from any known _EPROCESS. PID 4 is typically the first entry, since the System process is created early in boot. This method requires kernel R/W but no additional information leak.

KPCR/KPRCB thread traversal starts from KPCR at gs:[0], which contains a pointer to the current KTHREAD. From the thread, navigate to _EPROCESS, then traverse ActiveProcessLinks. This provides a starting point for list traversal without requiring NtQuerySystemInformation.

Practical considerations

EX_FAST_REF encoding is the most common source of token swap failures. The low 4 bits of the token pointer are a reference count, not part of the address. Reading the token requires & ~0xF. Writing it requires setting the low bits to a valid value. Getting this wrong causes an immediate BSOD when the kernel dereferences the misaligned pointer.

Build-specific offsets require detection of the target Windows version. The Token offset in _EPROCESS moves between major builds. Exploits detect the version through NtQuerySystemInformation, PEB.OSBuildNumber, or by reading NtBuildNumber from KUSER_SHARED_DATA. A lookup table maps build numbers to offsets.

Token reference counting can cause BSOD during process teardown. When the original token is "orphaned" by the swap (its reference count is not decremented), the kernel may attempt to free it during process exit, leading to a double-free or refcount underflow. Robust exploits either increment the SYSTEM token's reference count, spawn a child process with the SYSTEM token before the parent exits, or use the in-place privilege overwrite approach to avoid the issue entirely.

Thread impersonation tokens persist after a process token swap. Each thread can have its own impersonation token that overrides the process token. After swapping the process token, the attacker should call NtSetInformationThread to clear thread-level impersonation tokens, ensuring the SYSTEM context applies uniformly.

PPL (Protected Process Light) has additional token integrity checks. Token swapping alone may not bypass PPL protections, which verify token integrity at a level beyond simple pointer validation. Attacking PPL-protected processes may require additional steps beyond token manipulation.

Mitigations and Limitations

Kernel Data Protection (KDP) on Windows 11 with VBS can protect critical structures in VBS-backed pages. If _EPROCESS.Token were protected by KDP, overwrites would fault through the hypervisor. This protection is not yet widely applied to token fields, but it represents the most likely future mitigation.

HVCI makes obtaining the initial R/W primitive harder by preventing code execution from corrupted function pointers, but it does not block data-only token manipulation once R/W is achieved.

Credential Guard isolates LSASS secrets in a VBS enclave but does not protect kernel token structures. It protects credentials from theft but not tokens from modification.

No direct mitigation for data-only attacks currently exists. Once kernel arbitrary R/W is available, no mainstream mitigation prevents token field modification. The entire defense model relies on preventing the initial R/W primitive from being established. Token swapping is the proof that defense-in-depth must start at the vulnerability level, because the exploitation endgame has no independent protection.

Verification

After a token swap, verify with:

  • OpenProcess on a SYSTEM-owned process (e.g., csrss.exe) -- success confirms SYSTEM privileges
  • Spawn cmd.exe and run whoami -- should return NT AUTHORITY\SYSTEM
  • Read back _EPROCESS.Token to confirm the write landed correctly
CVE Driver Role
CVE-2024-30085 cldflt.sys Token swap after obtaining kernel R/W via pool overflow
CVE-2024-30088 ntoskrnl.exe Token swap after TOCTOU race
CVE-2024-38193 afd.sys Privilege escalation via token after UAF
CVE-2023-28252 clfs.sys Token swap in CLFS exploit chain
CVE-2023-29336 win32kfull.sys Token swap after UAF
CVE-2025-3464 AsIO3.sys Token swap after PreviousMode flip via decrement-by-one

Post-Escalation: SYSTEM Shell

After elevation, two common approaches for spawning a SYSTEM process:

  • OpenProcess + DuplicateToken + CreateProcessAsUser: With SeDebugPrivilege, open a SYSTEM process (e.g., winlogon.exe), duplicate its token, spawn a child with that token.
  • Parent process spoofing via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS: Using InitializeProcThreadAttributeList and UpdateProcThreadAttribute, set the parent to a SYSTEM process. The child inherits the SYSTEM context. See XPN's parent process spoofing gist.

See Also