CVE-2023-36802: Microsoft Streaming Service Proxy Elevation of Privilege Vulnerability
BenoƮt Sevens
The Basics
Disclosure or Patch Date: September 12, 2023
Product: Windows
Advisory: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-36802
Affected Versions:
- Windows 10 without KB5030211 or KB5030214
- Windows 11 without KB5030219 or KB5030217
- Windows Server 2019 without KB5030214
- Windows Server 2022 without KB5030216 or KB503025
First Patched Version:
- Windows 10 with KB5030211 or KB5030214
- Windows 11 with KB5030219 or KB5030217
- Windows Server 2019 with KB5030214
- Windows Server 2022 with KB5030216 or KB503025
Issue/Bug Report: N/A
Patch CL: N/A
Bug-Introducing CL: N/A
Reporter(s):
- Guanghui Xia (@ze0r) with Hebei HuaCe
- Quan Jin (@jq0904) & ze0r with DBAPPSecurity WeBin Lab
- Valentina Palmiotti with IBM X-Force
- Microsoft Threat Intelligence
- Microsoft Security Response Center
The Code
Proof-of-concept:
HANDLE h;
HRESULT hr;
DWORD bytesReturned;
char buf[0x100] = {0};
hr = KsOpenDefaultDevice(KSNAME_Server, GENERIC_READ | GENERIC_WRITE, &h);
memset(buf, 'A', sizeof(buf));
*(int32_t *)buf = 1;
*((int64_t *)buf + 3) = 0;
BOOL status = DeviceIoControl(h, IOCTL_FRAMESERVER_INIT_CONTEXT, &buf, sizeof(buf), &buf, sizeof(buf), &bytesReturned, 0);
memset(buf, 'A', sizeof(buf));
*((DWORD*)buf + 8) = 1;
*((DWORD*)buf + 9) = 1;
status = DeviceIoControl(h, IOCTL_FRAMESERVER_PUBLISH_RX, &buf, sizeof(buf), &buf, sizeof(buf), &bytesReturned, 0);
Exploit sample: Not public
Did you have access to the exploit sample when doing the analysis? Yes
The Vulnerability
Bug class: Type confusion
Vulnerability details:
When a IOCTL_FRAMESERVER_PUBLISH_RX IOCTL is performed on the mskssrv device driver, a call is performed to FSRendezvousServer::PublishRx() which in its turn calls FSStreamReg::PublishRx(). FSStreamReg::PublishRx() will take whatever is in the FsContext2 field of the FILE_OBJECT and use it as a FsStreamReg object further on, as long as it is a valid object.
An attacker can precede the IOCTL_FRAMESERVER_PUBLISH_RX with a IOCTL_FRAMESERVER_INIT_CONTEXT IOCTL on the same device handle, which will initialize the FsContext2 field to an object of a different type, i.e. of type FsContextReg.
FSStreamReg::PublishRx() performs several operations on what it thinks is a FsStreamReg object, some of which lead to exploitable scenarios, like writing out of bounds.
A similar vulnerability was present in 3 other IOCTL's:
IOCTL_FRAMESERVER_PUBLISH_TXIOCTL_FRAMESERVER_CONSUME_TXIOCTL_FRAMESERVER_CONSUME_RX
All of these are referred to with the CVE-2023-36802 identifier.
Patch analysis:
Before the patch, FSRendezvousServer::PublishRx() would call FSRendezvousServer::FindObject() before calling into FSStreamReg::PublishRx(). Only if FSRendezvousServer::FindObject() returns TRUE, FSStreamReg::PublishRx() is called. However, FSRendezvousServer::FindObject() would not check the type of the object.
The patch changed the FSRendezvousServer::FindObject() to check if the type field of the object is specifically set to 2, otherwise it returns FALSE. The type field is set to 2 when constructing a FsStreamReg object (in FSStreamReg::FSStreamReg()). Additionally, FSRendezvousServer::FindObject() was renamed to the more appropriate name FSRendezvousServer::FindStreamObject().
Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.):
The bug could have been found via manual code auditing or fuzzing. Looking at the proof of concept above, a combination of two IOCTL's with relatively simple input buffers triggers a crash. Valentina Palmiotti found this vulnerability via manual code auditing as described in her blog post.
(Historical/present/future) context of bug:
- June 16, 2023: CVE-2023-29360, another privilege escalation vulnerability inside
mskssrv.syswhich was used by Synacktiv at Pwn2Own 2023, is patched and publicly disclosed. Although within the same driver, CVE-2023-29360 is quite distinct from CVE-2023-36802. - September 12, 2023: CVE-2023-36802 is patched and publicly disclosed, and reported to have been seen in the wild.
- October 10, 2023: Valentina Palmiotti publishes details of CVE-2023-36802 and its exploitation.
The Exploit
(The terms exploit primitive, exploit strategy, exploit technique, and exploit flow are defined here.)
Exploit strategy (or strategies):
- Leak the addresses of relevant kernel objects via the well known
NtQuerySystemInformationtechnique. - Create a kernel pool layout so that future
FSContextRegallocations land in holes surrounded by attacker controlled data. - Allocate an
FSContextRegobject (via anIOCTL_FRAMESERVER_INIT_CONTEXTIOCTL). - Perform an out of bounds action on the
FSContextRegobject (via anIOCTL_FRAMESERVER_PUBLISH_RX) which will do some interesting write and ultimately set thePreviousModeof theKTHREADto 0. - Use
NtReadVirtualMemoryandNtWriteVirtualMemoryon kernel addresses to copy the system token to theEPROCESSof our current process. - Clean up.
- Spawn a command of interest, such as
cmd.exe.
Exploit flow:
Depending on the Windows version, two different exploit flows exist in the analyzed in the wild sample. This is due to the fact that depending on the Windows version:
- the
FSStreamReg::PublishRxcode is slightly different, ie recentmskssrv.sysversions contain a call toObfDereferenceObjecton one of the fields of theFsStreamRegobject, while older versions don't - the structure layout of the
FsStreamRegobject is slightly different. This is important for side effects caused in the exploit flow.
Since newer mskssrv.sys versions contain a convenient call to ObfDereferenceObject, it will use that to decrement the PreviousMode field directly to 0. We will look at this exploit flow first.
Since older mskssrv.sys versions do not contain the convenient ObfDereferenceObject call, the exploit flow is a bit more convoluted. It corrupts a CLFS-specific object to call kernel gadgets via a vtable call.
CLFS-less exploit flow
- Open the
mskssrvdevice via a call toKsOpenDefaultDevice. - Leak some required kernel addresses via
NtQuerySystemInformation(SystemExtendedHandleInformation, ...):- the current
KTHREADaddress (and hence currentPreviousModeaddress) EPROCESSaddress of System process and current process (and hence their respectiveTokenaddress)FILE_OBJECTaddress of themskssrvdevice file handle
- the current
- Spray the pool with objects of size 0x80 (excluding the 0x10 byte pool header), using a well known named pipe technique of calling
NtFsControlFilewith fsctl code 0x119ff8. - Close some pipes to create holes in the pool spray.
- Call the
IOCTL_FRAMESERVER_INIT_CONTEXTioctl:FsInitializeContextRendezVous()callsFSRendezvousServer::InitializeContext()FSRendezvousServer::InitializeContext()allocates anFsContextRegobject, which has a size of 0x78 bytes, and sets theFsContext2field in theFILE_OBJECTto point to this object. TheFsContextRegobject will take the spot of a previously created hole.
- Refill the remaining holes.
- Call the
IOCTL_FRAMESERVER_PUBLISH_RXioctl, in a separate thread:- This will call into
FSRendezvousServer::PublishRx()and subsequentlyFSStreamReg::PublishRx(). FSStreamReg::PublishRx()will expect aFsStreamRegobject in theFsContext2field, which is 0x1d8 bytes large, while in fact a smallerFsContextRegobject is present, neighbored with controlled data.FSStreamReg::PublishRx()will then callObfDereferenceObject()on a field taken out of bounds from a neighboring object. The attacker placed thePreviousModeaddress of the current thread there. This will decrement thePreviousModeof the main thread to 0. At this point the attacker can callNtReadVirtualMemoryandNtWriteVirtualMemoryon kernel addresses from the main thread.- If not taken care of,
FSStreamReg::PublishRx()will now callKeSetEventon another out of bounds fields, which coincides with theProcessBilledfield from a pool header of a neighboring object (which is not attacker controlled). Since this is an invalid pointer, it would bugcheck the system. To prevent this, the attacker keepsFSStreamReg::PublishRx()locked in a while loop. This is achieved by forging a self-referencing linked list entry.FSFrameMdlList::MoveNextwill keep returning the same list entry. The list entry is placed in usermode, so the exploit can break this while loop on demand (see below).
- This will call into
- Meanwhile in the main thread:
- Try to read the System process token using
NtReadVirtualMemoryin a loop until it succeeds. AfterPreviousModegets overwritten in the other thread, this will succeed. - Write the System token to the
EPROCESSof the current process usingNtWriteVirtualMemory. - Read the value of the
FsContext2field out of theFILE_OBJECTto get the address of theFsContextRegobject. - Read (for later restore) and overwrite the
ProcessBilledin one of the neighboring pool chunk headers to NULL, soKeSetEventwill not crash the system. - Break the currently locked while loop in
FSStreamReg::PublishRx()by changing the self-referencing list entry.FSStreamReg::PublishRx()will now proceed but not callKeSetEventsince the field is NULL. - Wait for
FSStreamReg::PublishRx()to finish in the other thread. - Restore the corrupted
ProcessBilledfield to its original value. - Increment the refcount in the current
EPROCESS. - Reset the
PreviousModeto 1. - Launch a command of interest, such as
cmd.exe.
- Try to read the System process token using
Exploit flow with CLFS
On older systems, FSStreamReg::PublishRx does not contain a call to ObfDereferenceObject. This is unfortunate, as ObfDereferenceObject was a very easy primitive to decrement the PreviousMode.
However, FSStreamReg::PublishRx() will still call FSFrameMdl::UnmapPages() on an address taken out of bounds from a neighboring (sprayed) object, which will do a few useful writes at an address taken out of bounds:
- write QWORD 0 at offset 0xc8 from that address
- write DWORD 2 at offset 0x10 from that address
How does the exploit leverage this?
- Leak the required kernel addresses in the same way as in the CLFS-less exploit flow.
- Create a CLFS log file, open it and leak its kernel address (using
NtQuerySystemInformation(SystemExtendedHandleInformation, ...)). - Resolve some
ntkernel gadgets:PoFxProcessorNotification,IoSizeofWorkItemandRtlClearBit - Forge a fake
CClfsContainerobject at 0x1000000 with a fake vtable at 0x1000800. - Forge a fake
BitMapHeaderat 0x1000400 with a bitmap pointer pointing to thePreviousModeaddress. - Spray the pool (again using
NtFsControlFile), but the crafted objects now contain the kernel address of the CLFS log file + 0x2c9. - Create holes, allocate the
FsContextRegobject in a hole and refill the remaining holes, just as before. - Trigger
FSStreamReg::PublishRx().FSFrameMdl::UnmapPages()will be called on that address read out of bounds and hence write:- QWORD 0 at offset 0x2c9+0xc8=0x391 from the start of the CLFS log file in memory
- DWORD 2 at offset 0x2c9+0x10=0x2d9 from the start of the CLFS log file in memory
- Call
CreateLogFilewhich will end up callingCClfsBaseFilePersisted::CheckSecureAccess.ClfsBaseFilePersisted::CheckSecureAccesswill use the DWORD at offset 0x398 as an offset from the end of the log block header (of size 0x70) to find aCLFS_CONTAINER_CONTEXTobject (via a call toCClfsBaseFile::GetSymbol()). But the QWORD write corrupted the least significant byte of that DWORD, which makes the offset change from 0x1460 to 0x1400. So now whatever is at CLFS log file+0x70+0x1400, will be interpreted as aCLFS_CONTAINER_CONTEXTobject, which is attacker controlled data. At offset 0x18 into this object, a pointer to aCClfsContainerobject is dereferenced. The exploit places the address 0x1000000 as the pointer there, where it previously prepared a fakeCClfsContainerobject.CLFS!CClfsBaseFilePersisted::CheckSecureAccesswill then call the first function in the vtable of that object (which is under normal circumstancesCClfsContainer::AddRef), passing the object itself as the first argument. The exploit placed thent!PoFxProcessorNotificationfunction in the fake vtable. (Note that there is CFG in the CLFS driver, so the exploit has to use allowed function addresses as gadgets.)nt!PoFxProcessorNotificationwill dereference a few addresses of its argument and call one of these addresses with another one of these addresses as an argument. So now the exploit controls both the function that is called and its first argument. The exploit choosesnt!RtlClearBitas the function to be called.nt!RtlClearBitexpects 2 parameters: a pointer to a bitmap header (containing a pointer to the bitmap itself) and a bit number to clear. Recall the exploit prepared a fake bitmap header which has a bitmap pointer pointing to thePreviousModeaddress. The second argument - the bit number to clear - is not controlled butrdxis conveniently set to 0.nt!RtlClearBitwill hence set thePreviousModefield to 0.- For stability, the exploit prepared a second vtable entry at
nt!IoSizeofWorkItem- which does nothing but seteax- because later onCClfsBaseFilePersisted::CheckSecureAccesswill call what it thinks isCClfsContainer::Release.
- Copy the System process token to the current
EPROCESS. - Restore the original 0x1460 offset in the CLFS log file.
- Restore the
PreviousModeto 1. - Launch a command of interest, such as
cmd.exe.
Note that in this version of mskssrv.sys and this exploit flow, the exploit didn't have to take care of the awkward KeSetEvent call. The reason is that the offset to the field passed to KeSetEvent in older versions of mskssrv.sys does not fall within a pool chunk header, but rather in controlled sprayed data and was set to NULL this way.
Notable differences with Valentina Palmiotti's exploit
It is an interesting exercise to understand the differences between the in the wild exploit described above and the exploit of Valentina Palmiotti as described in her blog post.
Valentina's exploit is based on the recent mskssrv.sys versions. The can be deduced from the presence of an ObfDereferenceObject call in the screenshots. So let's compare her exploit with the "CLFS-less exploit flow".
- Valentina uses the "write 2 where" primitive provided by the
FSFrameMdl::UnmapPages()call, "in conjunction with the I/O Ring technique". The exploit we analyzed used that primitive for oldermskssrv.sysversions (in conjunction with a CLFS technique), but the "decrement where" primitive provided by theObfDereferenceObject()call in newermskssrv.sysversions. - Valentina goes through some more complicated pool grooming in combination with an infoleak in
FSStreamReg::GetStatsto prevent a laterKeSetEventcall from crashing on an invalid pointer. The exploit analyzed here uses a different solution: it triggers theFSStreamReg::PublishRxfunction from a separate thread and keeps it in an locked while loop (using a self-referencing linked list entry in usermode), which prevents theKeSetEventcall from being reached. Once it has kernel R/W, it changes the field that triggers theKeSetEventto NULL and then changes the self-referencing linked list entry so the while loop breaks. This means theKeSetEventcall will be skipped. (Afterwards it restores the field that would have triggered theKeSetEventcall because it's the owning process EPROCESS in a pool chunk).
Known cases of the same exploit flow:
The in the wild exploited CVE-2022-37969 CLFS vulnerability followed a very similar exploit flow, as descibed by Zscaler's blogpost.
Part of an exploit chain? This exploit was likely used as a standalone local privilege escalation.
The Next Steps
Variant analysis
Areas/approach for variant analysis (and why): Given the relatively simple nature of this vulnerability, as well as CVE-2023-29360, more fuzzing and code auditing on the mskssrv.sys could yield more bugs.
Found variants: N/A
Structural improvements
What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.?
Ideas to kill the bug class:
CastGuard could have potentially caught this bug, since it seems (based on their vtables) that both the FsStreamReg and FsContextReg type derive from the same FsRegObject class.
Ideas to mitigate the exploit flow:
- MTE and CHERI would not catch the type confusion, but would catch the out of bounds access as a consequence of the type confusion.
- SMAP would catch the user mode access in
FSStreamReg::PublishRxwhen dereferencing the fakeFSFrameMdlpointer in the CLFS-less exploit flow. In the CLFS exploit flow, SMAP would catch the fakeCClfsContainerobject vtable access in usermode. - Remove kernel address leaks via
NtQuerySystemInformationcalls.
Other potential improvements:
N/A
0-day detection methods
What are potential detection methods for similar 0-days? Meaning are there any ideas of how this exploit or similar exploits could be detected as a 0-day?
- Static signaturing of used exploit techniques (e.g. using Yara).
- Analysing samples based on interesting dynamic signals, such as
NtQuerySystemInformationcalls using theSystemExtendedHandleInformationparameter.
Other References
- Critically close to zero(day): Exploiting Microsoft Kernel streaming service by Valentina Palmiotti