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_TX
IOCTL_FRAMESERVER_CONSUME_TX
IOCTL_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.sys
which 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
NtQuerySystemInformation
technique. - Create a kernel pool layout so that future
FSContextReg
allocations land in holes surrounded by attacker controlled data. - Allocate an
FSContextReg
object (via anIOCTL_FRAMESERVER_INIT_CONTEXT
IOCTL). - Perform an out of bounds action on the
FSContextReg
object (via anIOCTL_FRAMESERVER_PUBLISH_RX
) which will do some interesting write and ultimately set thePreviousMode
of theKTHREAD
to 0. - Use
NtReadVirtualMemory
andNtWriteVirtualMemory
on kernel addresses to copy the system token to theEPROCESS
of 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::PublishRx
code is slightly different, ie recentmskssrv.sys
versions contain a call toObfDereferenceObject
on one of the fields of theFsStreamReg
object, while older versions don't - the structure layout of the
FsStreamReg
object 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
mskssrv
device via a call toKsOpenDefaultDevice
. - Leak some required kernel addresses via
NtQuerySystemInformation(SystemExtendedHandleInformation, ...)
:- the current
KTHREAD
address (and hence currentPreviousMode
address) EPROCESS
address of System process and current process (and hence their respectiveToken
address)FILE_OBJECT
address of themskssrv
device 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
NtFsControlFile
with fsctl code 0x119ff8. - Close some pipes to create holes in the pool spray.
- Call the
IOCTL_FRAMESERVER_INIT_CONTEXT
ioctl:FsInitializeContextRendezVous()
callsFSRendezvousServer::InitializeContext()
FSRendezvousServer::InitializeContext()
allocates anFsContextReg
object, which has a size of 0x78 bytes, and sets theFsContext2
field in theFILE_OBJECT
to point to this object. TheFsContextReg
object will take the spot of a previously created hole.
- Refill the remaining holes.
- Call the
IOCTL_FRAMESERVER_PUBLISH_RX
ioctl, in a separate thread:- This will call into
FSRendezvousServer::PublishRx()
and subsequentlyFSStreamReg::PublishRx()
. FSStreamReg::PublishRx()
will expect aFsStreamReg
object in theFsContext2
field, which is 0x1d8 bytes large, while in fact a smallerFsContextReg
object 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 thePreviousMode
address of the current thread there. This will decrement thePreviousMode
of the main thread to 0. At this point the attacker can callNtReadVirtualMemory
andNtWriteVirtualMemory
on kernel addresses from the main thread.- If not taken care of,
FSStreamReg::PublishRx()
will now callKeSetEvent
on another out of bounds fields, which coincides with theProcessBilled
field 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::MoveNext
will 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
NtReadVirtualMemory
in a loop until it succeeds. AfterPreviousMode
gets overwritten in the other thread, this will succeed. - Write the System token to the
EPROCESS
of the current process usingNtWriteVirtualMemory
. - Read the value of the
FsContext2
field out of theFILE_OBJECT
to get the address of theFsContextReg
object. - Read (for later restore) and overwrite the
ProcessBilled
in one of the neighboring pool chunk headers to NULL, soKeSetEvent
will 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 callKeSetEvent
since the field is NULL. - Wait for
FSStreamReg::PublishRx()
to finish in the other thread. - Restore the corrupted
ProcessBilled
field to its original value. - Increment the refcount in the current
EPROCESS
. - Reset the
PreviousMode
to 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
nt
kernel gadgets:PoFxProcessorNotification
,IoSizeofWorkItem
andRtlClearBit
- Forge a fake
CClfsContainer
object at 0x1000000 with a fake vtable at 0x1000800. - Forge a fake
BitMapHeader
at 0x1000400 with a bitmap pointer pointing to thePreviousMode
address. - 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
FsContextReg
object 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
CreateLogFile
which will end up callingCClfsBaseFilePersisted::CheckSecureAccess
.ClfsBaseFilePersisted::CheckSecureAccess
will use the DWORD at offset 0x398 as an offset from the end of the log block header (of size 0x70) to find aCLFS_CONTAINER_CONTEXT
object (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_CONTEXT
object, which is attacker controlled data. At offset 0x18 into this object, a pointer to aCClfsContainer
object is dereferenced. The exploit places the address 0x1000000 as the pointer there, where it previously prepared a fakeCClfsContainer
object.CLFS!CClfsBaseFilePersisted::CheckSecureAccess
will 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!PoFxProcessorNotification
function 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!PoFxProcessorNotification
will 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!RtlClearBit
as the function to be called.nt!RtlClearBit
expects 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 thePreviousMode
address. The second argument - the bit number to clear - is not controlled butrdx
is conveniently set to 0.nt!RtlClearBit
will hence set thePreviousMode
field to 0.- For stability, the exploit prepared a second vtable entry at
nt!IoSizeofWorkItem
- which does nothing but seteax
- because later onCClfsBaseFilePersisted::CheckSecureAccess
will 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
PreviousMode
to 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.sys
versions (in conjunction with a CLFS technique), but the "decrement where" primitive provided by theObfDereferenceObject()
call in newermskssrv.sys
versions. - Valentina goes through some more complicated pool grooming in combination with an infoleak in
FSStreamReg::GetStats
to prevent a laterKeSetEvent
call from crashing on an invalid pointer. The exploit analyzed here uses a different solution: it triggers theFSStreamReg::PublishRx
function from a separate thread and keeps it in an locked while loop (using a self-referencing linked list entry in usermode), which prevents theKeSetEvent
call from being reached. Once it has kernel R/W, it changes the field that triggers theKeSetEvent
to NULL and then changes the self-referencing linked list entry so the while loop breaks. This means theKeSetEvent
call will be skipped. (Afterwards it restores the field that would have triggered theKeSetEvent
call 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::PublishRx
when dereferencing the fakeFSFrameMdl
pointer in the CLFS-less exploit flow. In the CLFS exploit flow, SMAP would catch the fakeCClfsContainer
object vtable access in usermode. - Remove kernel address leaks via
NtQuerySystemInformation
calls.
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
NtQuerySystemInformation
calls using theSystemExtendedHandleInformation
parameter.
Other References
- Critically close to zero(day): Exploiting Microsoft Kernel streaming service by Valentina Palmiotti