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

ObjectPurposeSize
Provider objectInfo-leak target and fake vtablemedium
Memory buffer objectAdjacent to property — leaks provider ptrsame as property
“Use Context” propertyOOB read via wcslen; also fake key objectsize matches key object
Key objectUAF trigger target0x38 bytes

Four-Stage Chain

Stage 1 — Spray and layout:

  1. Spray provider objects and memory buffer objects
  2. Provider objects: final exploit stage target; memory buffers: info-leak intermediary

Stage 2 — Info leak (CVE-2023-36906):

  1. Free some memory buffer objects to create holes
  2. Allocate a “Use Context” property of the same size to occupy a hole → now adjacent to another memory buffer
  3. Write non-null-terminated content to the “Use Context” property
  4. Query the “Use Context” property → OOB read leaks the adjacent memory buffer’s first 8 bytes = provider object address

Stage 3 — Fake provider object:

  1. Free enough provider objects to ensure the leaked address is free
  2. 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)
  3. 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 SrvFreeKey with 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