CVE-2023-28229 + CVE-2023-36906 — CNG Key Isolation UAF + OOB Read (AppContainer Escape)
Last updated: 2026-04-11
Severity: High
Component: CNG Key Isolation service (keyiso, lsass.exe subprocess)
Bug Class: Use-After-Free (CVE-2023-28229) + Out-of-Bounds Read / Info Leak (CVE-2023-36906)
Privilege Escalation: AppContainer (restricted/sandbox) → SYSTEM (via lsass DLL injection)
Patch: April 2023 + August 2023 Patch Tuesday
Related: Use After Free, Race Conditions, Heap Grooming, Mitigations
Tags:uaf,race-condition,oob-read,info-leak,user-mode,rpc,cfg
Vulnerability Summary
CVE-2023-28229 is a race-condition Use-After-Free in the CNG Key Isolation service (lsass.exe subprocess). The service is accessible via RPC from AppContainer processes (Adobe Reader, Firefox renderer), making it an AppContainer sandbox escape vector. The race occurs between key/provider/secret/memory-buffer object reference count initialization and the object’s addition to the context list — a predictable handle index makes timing the race reliable.
CVE-2023-36906 is a companion OOB read: the “Use Context” property in SPCryptGetProviderProperty uses wcslen to determine the copy length rather than the stored allocation size, leaking adjacent heap data — specifically, the provider object address stored at offset +0x0 of an adjacent memory buffer object.
Discovered and exploited by k0shl of Cyber Kunlun (blog: whereisk0shl.top).
Affected Service
CNG Key Isolation (keyiso): provides isolated key storage to protect private keys. Runs as a subprocess of lsass. Accessible via RPC from processes at AppContainer integrity level — this is the attack path from browser/PDF renderer sandboxes.
Object Structure (Common to Key/Provider/Secret)
Offset Field
+0x00 Magic value (DWORD): 0x44444447 = key, 0x44444446 = provider, 0x44444449 = secret
(when freed, magic is overwritten to another value)
+0x04 (padding)
+0x08 Reference count (DWORD, interlocked)
+0x20 VTable pointer → virtual function called on free
+0x30 Handle/index (QWORD): sequential from 0, incremented by 1 per allocation
Key insight: the handle at +0x30 is predictable — it starts at 0 and increments. The attacker can compute the handle of any not-yet-allocated object, enabling the race to be won reliably.
Root Cause — CVE-2023-28229 (UAF Race)
In SrvCryptCreatePersistedKey / SrvCryptFreeKey:
// Allocation path:
keyobject->refcount = 1; // [a] refcount initialized to 1
SrvAddKeyToList(context, keyobject); // [b] list-add increments refcount (→2)
// Free path (SrvCryptFreeKey):
if (InterlockedExchangeAdd(obj+2, -1) == 1) // [c] decrement: if count was 1, free
SrvFreeKey(obj); // [d] object freed here
if (InterlockedExchangeAdd(obj+2, -1) == 1) // [e] decrement again: if count was 1...
vtable_call(obj+4, 0x80)(obj+4+0x118, obj+5); // [f] USE AFTER FREE — vtable dispatch
Race window: between [a] (refcount = 1) and [b] (list-add, refcount → 2), call SrvCryptFreeKey with the predicted handle. The check at [c] sees refcount=1 → frees the object at [d]. Then [b] completes, refcount incremented on freed memory. Check [e] may see 1 again, triggering the vtable call [f] on freed memory.
No lock protects the window between [a] and [b] — this is the root cause. Three threads are required:
- Thread A: allocates the key object (triggers [a] and [b])
- Thread B: calls SrvFreeKey with the predicted handle (hits [c]-[d] between [a] and [b])
- Thread C: allocates a property object of the same size to occupy the freed slot
Root Cause — CVE-2023-36906 (OOB Read)
In SPCryptGetProviderProperty for the "Use Context" property:
// SET: attacker writes non-null-terminated content into "Use Context" buffer
// GET: wcslen loop walks buffer until null — reads PAST the allocation end
while (*(_WORD *)(v17 + 2 * v13)) // [c] — no bounds check, reads until null
++v13;
v16 = 2 * v13 + 2;
memcpy(output, v25, v16); // [d] — copies over-sized result to caller
Exploit: Place a memory buffer object adjacent to the “Use Context” property object in the heap. Memory buffer objects store the provider object pointer at +0x0. Setting the “Use Context” buffer without a null terminator causes the wcslen walk to read into the adjacent memory buffer, leaking the provider object address.
Exploitation Chain
Objects Used
| Object | Purpose | Size |
|---|---|---|
| Provider object | Info-leak target and fake vtable | medium |
| Memory buffer object | Adjacent to property — leaks provider ptr | same as property |
| “Use Context” property | OOB read via wcslen; also fake key object | size matches key object |
| Key object | UAF trigger target | 0x38 bytes |
Four-Stage Chain
Stage 1 — Spray and layout:
- Spray provider objects and memory buffer objects
- Provider objects: final exploit stage target; memory buffers: info-leak intermediary
Stage 2 — Info leak (CVE-2023-36906):
- Free some memory buffer objects to create holes
- Allocate a “Use Context” property of the same size to occupy a hole → now adjacent to another memory buffer
- Write non-null-terminated content to the “Use Context” property
- Query the “Use Context” property → OOB read leaks the adjacent memory buffer’s first 8 bytes = provider object address
Stage 3 — Fake provider object:
- Free enough provider objects to ensure the leaked address is free
- Spray property objects of the same size as the provider object, each containing:
- At offset +0x80 from
freebuffer[4]: LoadLibraryW function pointer - At offset +0x118 from
freebuffer[4]: address of the DLL path string (elsewhere in the property buffer)
- At offset +0x80 from
- One of these property objects now occupies the leaked provider object address
Stage 4 — Trigger UAF (three-thread race):
- Thread A: allocate key object →
refcount = 1[a] →SrvAddKeyToList[b] - Thread B: call
SrvFreeKeywith predicted handle → fires between [a] and [b] → frees key object - Thread C: allocate property object of the same size (0x38) → occupies freed key slot
The vtable dispatch at [f] becomes:
vtable_ptr = *(obj + 0x20) = controlled property content
call [vtable_ptr + 0x80] = LoadLibraryW
arg1 = [vtable_ptr + 0x118] = DLL path in property buffer
Result: LoadLibraryW loads the attacker DLL in lsass — AppContainer → lsass DLL injection → SYSTEM.
XFG / CFG Context
lsass enables XFG mitigation. The vtable dispatch is XFG-checked. However, the exploit does not need ROP — it achieves direct LoadLibraryW call by placing LoadLibraryW’s address at vtable_ptr+0x80, which is a valid XFG-checked call target (an actual function export, not a ROP gadget). The DLL path is passed as a valid pointer in the first argument, satisfying XFG type constraints.
Patch
Microsoft added RtlEnterCriticalSection / RtlLeaveCriticalSection around the window between refcount initialization and list-add in the free path:
// After patch:
RtlEnterCriticalSection(lock);
// ... validate list linkage (fast-fail 3 if corrupt) ...
if (InterlockedExchangeAdd64(obj+1, -1) == 1)
SrvFreeKey(obj);
RtlLeaveCriticalSection(lock);
// second check (vtable call path) now outside the race window
Proof-of-Concept Notes
- Race requires high-frequency scheduling of 3 threads; no timing tricks needed because handle prediction makes trigger reliable
- DLL must be readable by lsass (null DACL or lsass-readable ACL)
- Works from AppContainer (Adobe Reader / Firefox renderer) — confirmed by author
References
- k0shl (Cyber Kunlun), “Isolate me from sandbox — Explore elevation of privilege of CNG Key Isolation”, whereisk0shl.top
- @chompie1337, @DannyOdler, @cplearns2h4ck — discussion on patch analysis
