CVE-2023-21537 漏洞复现
CVE-2023-21537 是一个 Windows 消息队列(MSMQ)驱动程序 mqac.sys
中的漏洞。该漏洞于 2023 年 1 月披露,并已被微软修复。漏洞并没有公开的 PoC 程序,漏洞发现者只通过文章 Racing bugs in Windows kernel 透露了部分信息。笔者在其基础上深入分析了相关代码,成功地复现了此漏洞。本文就是对漏洞研究的总结。
漏洞复现环境搭建
由于此漏洞已经在较新的系统中被修复,复现漏洞需要在旧版本的系统中进行。笔者在 Hyper-V 虚拟机中安装了 Windows 10 21H1 版本,内部版本号为 19043.928。此外,由于消息队列是 Windows 的可选功能,需要在控制面板中手动启用。方法是:打开控制面板,选择「程序」,点击「启用或关闭 Windows 功能」,并开启「Microsoft 消息队列(MSMQ)服务器」。如下图所示。
之后,打开计算机管理,在侧栏中选择「服务和应用程序」,点开「消息队列」下面的「专用队列」,并右键新建专用队列。队列名称需要记下来,之后的 PoC 程序里面需要用到。完成后,情况如下图所示。
为了方便调试 PoC 程序,在虚拟机中还可以开启内核调试,这样就可以通过 Host 上的 WinDbg 给虚拟机的 Windows 内核下断点进行调试了。方法是开启管理员权限的 Powershell,执行如下命令1
2bcdedit /debug on
bcdedit /dbgsettings net hostip:192.168.1.2 port:50001 key:1.2.3.4
把 hostip
换成 Host 的 ip 地址,port
和 key
可以修改。完成后,重启虚拟机,之后在 WinDbg 里面用这些参数就可以对虚拟机的内核进行 debug 了。
漏洞分析
根据漏洞发现者的文章,该漏洞的成因是 mqac.sys
中的 ACSendMessage
函数会两次读取一个来自用户的输入参数,第一次该参数用于控制数组长度,第二次则是在释放堆内存时,根据该长度进行释放。然而,这段逻辑并未考虑参数会被用户修改的可能,因而构成一个 Double fetch 漏洞,可能导致错误的内存被释放。
mqac.sys
提供了 IoControl 调用的处理函数,名称为 ACDeviceControl
,该函数将会解析用户传入的参数,并调用不同的派发函数。通过逆向分析 ACDeviceControl
函数,发现当 IoControl 调用号为 0x19658107
且输出缓冲区的总长度为 0x2C0
时,它会进一步调用 ACSendMessage
这一派发函数。IDA 反编译得到的关键代码如下(省略了部分上下文):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20__int64 __fastcall ACDeviceControl(struct _DEVICE_OBJECT *DeviceObject, struct _IRP *Irp)
{
struct _IO_STACK_LOCATION *CurrentStackLocation Irp->Tail.Overlay.CurrentStackLocation;
unsigned int LowPart = CurrentStackLocation->Parameters.Read.ByteOffset.LowPart;
__int64 Length = CurrentStackLocation->Parameters.Read.Length;
const struct CQueueBase *FsContext = CurrentStackLocation->FileObject->FsContext;
NTSTATUS Information;
// ...
switch ( LowPart )
{
case 0x19658107u:
if ( (_DWORD)Length == 704 )
{
Information = ACSendMessage(DeviceObject, Irp, Options, FsContext, (struct CACSendParameters *)UserBuffer);
goto LABEL_246;
}
// ...
}
// ...
}
ACSendMessage
函数首先将用户态缓冲区复制到内核态栈上的缓冲区,之后,将执行核心的业务逻辑,调用 CQueue::PutNewPacket
来发送用户请求的数据,完成后再调用 ACFreeDeepCopyQueueFormat
进行堆内存的释放。此处便存在漏洞:进行内存释放操作传入的第二个参数直接读取自用户态缓冲区中。下面是 IDA 反编译得到的关键结果,参数 UserBuffer
就是指向用户缓冲区的指针。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26__int64 __fastcall ACSendMessage(
struct _DEVICE_OBJECT *DeviceObject,
struct _IRP *Irp,
unsigned int Options,
const struct CQueueBase *FsContext,
struct CACSendParameters *UserBuffer)
{
const struct BOID *v15;
PVOID Contents[36];
memset(Contents, 0, 0x118ui64);
_DWORD *DeviceExtension = DeviceObject->DeviceExtension;
int v9 = CQueueBase::Validate(FsContext);
if ( v9 >= 0 )
{
if ( DeviceExtension[246] )
{
ACDeepProbeSendParams(DeviceObject, UserBuffer, (struct ACSendParametersPointerContents *)Contents);
v15 = (const struct BOID *)*((_QWORD *)UserBuffer + 45);
}
// ...
if ( Contents[23] )
ACFreeDeepCopyQueueFormat((char *)Contents[23], *((_DWORD *)UserBuffer + 148));
// ...
}
// ...
}
这样问题看上去就很明确了:我们首先获得指向 MQAC 虚拟设备的句柄,然后向其发送号码为 0x19658107
的 IoControl 调用,并通过调整参数使 ACFreeDeepCopyQueueFormat
函数能够执行。这又要满足 Contents[23]
不为零的约束条件。观察之后发现 Contents
先被初始化为了全 0,然后传入了 ACDeepProbeSendParams
这个函数里面,所以还要设法让这个函数帮我们修改 Contents[23]
,才能触发漏洞。