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 2 bcdedit /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 0x19658107 u: 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 , 0x118 ui64); _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]
,才能触发漏洞。