Microsoft 于 2023 年 4 月 11 日发布的安全更新,修复了 90 余个独立漏洞。其中尤为值得关注的是 CVE-2023-21554(代号 QueueJumper)——这是一个影响 Microsoft 消息队列 (MSMQ) 服务的远程代码执行漏洞。MSMQ 是 Windows 系统的可选组件,其核心功能是让应用程序能够通过本地和远程均可访问的消息队列交换消息。本分析报告由 Randori 团队与 X-Force 对抗性服务团队联合完成,作者包括Valentina Palmiotti、Fabius Watson 及 Aaron Portnoy。
CVE-2023-21554 之所以最初引发关注,主要源于以下特性:
通过 Think 时事通讯,了解有关 AI、自动化、数据等方面最重要且最有趣的行业趋势。请参阅 IBM 隐私声明。
为明确如何检测 MSMQ 的存在,我们在实验室环境中搭建了配置该组件的测试系统。当尝试连接 1801 端口上的 MSMQ 端点时,服务器未返回任何数据。由此可推测,若未接收到格式合规的数据,服务器不会作出响应。为掌握与该端点进行通信的方法,需进一步逆向分析其底层代码。
从 Microsoft 安全公告中,无法直接明确此次修复涉及哪个二进制文件。识别变更代码的第一步是下载对应更新文件。我们通过在 Microsoft 更新目录中检索 KB5025224,成功获取了该更新包。
KB5025224 的 Microsoft 更新目录搜索结果
具体而言,我们获取到的是适用于 x86 架构系统的 Windows 11 累积更新文件
windows10.0-kb5025224-x64_3522b1b562ed44bc949541dd1c7d08e1e967d925.msu
。
我们使用 7-Zip 解压工具对 MSU 文件及其包含的 CAB 文件进行解包处理。在解压后生成的所有文件夹中检索字符串“msmq”,最终匹配到容器索引文件
Windows11.0-KB5025239-x64/express.psf.cix.xml,
该文件包含以下条目:
<File id="21296" name="amd64_microsoft-windows-msmq-queuemanager-core_31bf3856ad364e35_10.0.22621.1555_none_5f975f8fdb349517\f\mqqm.dll" length="34811" time="133245266510000000" attr="128">
该条目引用了
为识别 MQQM.dll1.00
修复版与未修复版 MQQM.dll 文件的 BinDiff 对比结果
通过 Hex-Rays 反编译器对这些函数进行分析后发现,修复版文件的符号表中包含详细的函数名称,这些名称明确引用了 Microsoft 安全响应中心 (MSRC) 的漏洞标识,具体如下:
MQQM.dll 中的功能标志符号
经深入核查发现,包含 MSRC 案例编号的符号对应漏洞修复的功能标志。功能标志是 Windows 系统的核心组件,用于切换各类功能模块及实验性特性的启用状态。在此场景中,所发现的“功能”本质上是针对该漏洞的修复补丁:当功能标志处于启用状态时,系统会执行经过安全加固的修复版代码路径;若处于禁用状态,则会执行原始未修复的代码路径。
展示漏洞修复功能标志的反编译代码片段
针对构成 CVE-2023-21554 漏洞的每个子漏洞,其用户态组件与内核态组件中均存在对应的功能标志校验逻辑。对该安全更新包中其余二进制文件的分析显示,2023 年 4 月“补丁星期二”修复的绝大多数(甚至全部)漏洞,均引入了类似的功能标志机制。
通过交叉比对这些标志,我们得以迅速定位补丁代码的具体位置。具体而言, 中与 MSRC76146_MSMQ_OOBRWFixes 标志相关的变更看起来最有价值。
该函数内部共包含八处针对此功能标志的校验逻辑。其中一处校验示例如下:
展示漏洞修复逻辑的反编译代码片段
在上述反编译代码片段中,已修补的代码路径通过调用
GetNextSectionPtrSafe 函数的反编译片段
经核查该功能标志及其他相关功能标志的所有调用场景后,我们明确:CVE-2023-21554 漏洞由多个缺陷构成,这些缺陷均存在对各类 MSMQ 消息段落类型的大小校验不充分问题。此类漏洞允许攻击者通过任意 32 位偏移量篡改 MSMQ 消息末尾指针,使其超出原始分配缓冲区的边界范围。
在数据包处理完成后,
消息末尾的越界内存访问校验
.在评估漏洞可利用性时,于 MSMQ 服务对应的内核驱动程序 中发现了额外的内核漏洞。这些漏洞再次由功能标志符号所揭示,如同在 中所见,同样关联到 MSRC 案例 76146。
内核驱动程序 mqac.sys 中漏洞修复功能标志的反编译代码片段
如上方反编译代码片段所示,若该功能标志已启用,则会调用“safe”函数
大部分与协议解析相关的代码位于 库中,该库由mqsvc.exe 所引用的代码,必须从客户端发送一个有效的 MSMQ 请求。MSMQ 消息规范与协议体系庞大且复杂。最初,我们在 GitHubgist 中找到了扫描 MSMQ 服务器的公开代码,为更深入地探索数据包处理提供了基础。此外,Microsoft 官方文档对理解 MSMQ 消息格式提供了有益的帮助。
该 DLL 中的代码通过调用 WSARecv 函数在 1801 端口接收数据,相关调试过程如下所示:
0:020> bp ws2_32!wsarecv ; g
Breakpoint 0 hit
WS2_32!WSARecv:
00007ffd`651115c0 48895c2408 mov qword ptr [rsp+8],rbx ss:000000aa`f07fef30=0000000000000006
0:019> k
# Child-SP RetAddr Call Site
00 000000aa`f07fef28 00007ffd`57ba9cb0 WS2_32!WSARecv
01 000000aa`f07fef30 00007ffd`57b9683d
MQQM!NoReceivePartialBuffer+0x38
02 000000aa`f07fefb0 00007ffd`57b2078e MQQM!CWinsockConnection::ReceivePartialBuffer+0x7d
03 000000aa`f07ff010 00007ffd`57b25b28
MQQM!CSockTransport::BeginReceive+0xe6
04 000000aa`f07ff060 00007ffd`57b2d40b
MQQM!CSockTransport::NewSession+0x194
05 000000aa`f07ff3f0 00007ffd`57b2d367 MQQM!CSessionMgr::AcceptSockSession+0x6f
06 000000aa`f07ff430 00007ffd`645e26bd MQQM!AcceptIPThread+0x6a7
07 000000aa`f07ff7d0 00007ffd`6650a9f8
KERNEL32!BaseThreadInitThunk+0x1d
08 000000aa`f07ff800 00000000`00000000 ntdll!RtlUserThreadStart+0x28
0:019> r @$t0=@rdx
0:019> pt
WS2_32!WSARecv+0x19f:
00007ffd`6511175f c3 ret
0:019> dc poi(@$t0+8) LC
000001b5`880ff490 62f06110 524f494c 00000070 5a5a5a5a .a.bLIORp…ZZZZ
000001b5`880ff4a0 0073006e 00610074 0063006e 00200065 n.s.t.a.n.c.e. .
000001b5`880ff4b0 00440049 00000000 00000000 00000000 I.D………….
0:019> ba r1 poi(@$t0+8) ; g
Breakpoint 1 hit
MQQM!CBaseHeader::SectionIsValid+0x154:
00007ffd`57b53450 7462 je
MQQM!CBaseHeader::SectionIsValid+0x1b8 (00007ffd`57b534b4) [br=1]
函数负责解析新消息的开头部分,该部分起始于以下形式的 BaseHeader 结构:
BaseHeader 对象的结构
在服务器发送响应之前,传入消息字段由
MQQM.dll 中 SectionIsValid 的反汇编代码
完成初始验证后,大部分协议解析将在
反编译代码展示 EodHeader 结构处理逻辑中的漏洞
上述反编译代码显示,修复后的路径调用了名为
计算 EodHeader 段落总长度的汇编代码
在上述反汇编代码中,指向
WinDbg 调试会话展示向畸形 MSMQ 消息末尾写入数据时触发的内存访问冲突
为评估该漏洞所触发的越界写入漏洞的利用强度,我们首先对底层内存的分配机制与管理逻辑展开了调研。
在 MSMQ 中所分配的缓冲区里。该函数会通过以下方式调用 内核驱动程序NtDeviceIoControl.
数据包缓冲区分配函数的反编译
该内核组件返回一个缓冲区,该缓冲区是从磁盘上某文件的映射视图中划分出来的,该文件存储在
ProcessHacker 工具显示的 MQSVC 进程内存布局,表明数据包缓冲区内存为文件映射内存
处理完内存分配请求后,会通过 IOCTL 接口将数据包缓冲区地址,以及一个随机化的数据包句柄一同返回至用户态。驱动程序在处理来自用户态的请求时,会通过该句柄计算出数据包缓冲区的地址。
易受攻击缓冲区的后备内存位于映射文件内,因此不属于进程的堆空间。此外,受影响的内存不存储任何对象。该文件映射中仅包含 MSMQ 消息的内容,以及存储在 的元数据。
此外,若未预先掌握内存布局,则无法知晓文件映射与进程堆的相对位置,进而无法覆盖对象指针。因此,要实现可行的远程代码执行,需通过某种方法将内存构建为可预测的布局。
在尝试向目标发送合法 MSMQ 消息后,我们发现通过密集发送大量消息,可以强制在与
MQSVC 进程的内存布局示意图:文件映射相邻位置分配的新堆内存
然而,这要求攻击者能够向目标上的至少一个队列发送消息;否则,这些消息将被丢弃并覆盖,无法持久占用数据包内存。
在开展的研究测试中发现,默认情况下,非特权用户可创建一个允许任何人远程发送消息的队列。且该队列会永久保留在系统中。尽管现代环境中 MSMQ 多以可选组件和适配器的形式存在,但甲骨文、威睿和施乐等厂商提供的企业级软件,仍将 MSMQ 作为核心依赖项。因此,这种攻击场景并非不切实际。
越界内存访问漏洞引入了一种利用原语,它允许恶意客户端在分配给数据包的内存边界之外写入一个OnDiskExtensionHeader
在评估允许向偏移量写入数据的漏洞时
,必须考虑以下特性:
对于 QueueJumper 漏洞而言,分配的缓冲区相对于越界写入的偏移量是可控制的,因为该偏移量是通过攻击者可控的数据计算得出的。不过,越界写入的内容控制权限有限。
在此,Address 标头将被写入该位置。发生的一系列操作如下:
0x000000000000000C
被写入至Address0x00000000Address+0x2
0x0000000000000000Address+0x12
0x0000000000000000
被写入至 Address+0x1A
0x00000094
被写入至 Address+0xE
0x0000Address+0x220x0000
被写入至 Address+0x62
memcpy(Address+0xA6, Source, Source->AddressLength+0x08)
上方调用memcpySource 结构定义了特定类型(例如 NetBIOS)的单个传输地址。定义如下:
typedef struct _TA_ADDRESS {
USHORT AddressLength;;
USHORT AddressType; Info;
UCHAR Address[1];;
} TA_ADDRESS, *PTA_ADDRESS;
指定 AddressType 对应的地址所占字节数。
指定传输地址的类型。
指定一个包含传输地址的变长数组。
在以下调试会话中,TA_ADDRESS
rax=0000014e1d110180 rbx=0000014e1d110180 rcx=0000014e1d110184
rdx=0000014e1ce86b10 rsi=0000014e1d110001 rdi=0000000545d7fa50
rip=00007ff843208074 rsp=0000000545d7f940 rbp=0000000545d7f980
r8=000000000000000c r9=0000000000000002 r10=0000000000000000
r11=0000000545d7f938 r12=0000000000000002 r13=0000000000001000
r14=0000014e1ce86b10 r15=0000000000000000
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
MQQM!CQmPacket::CQmPacket+0x89c:
00007ff8`43208074 e8a5780a00 call MQQM!memcpy (00007ff8‘432af91e)
0:017> dc rcx
0000014e`1d110184 00010004 00000000 8620a8c0 00000000 ………. …..
在上方,TA_ADDRESS,类型为1。将每个字节转换为 IPv4 的八位组后,得到地址 ,这正是客户端的 IPv4 源地址。
该场景支持攻击者通过源地址,将可控数据写入内存中相对于数据包分配区域的指定偏移位置。尽管存在极强的限制性且可能不具备实操性,但理论上可通过以下方式精准控制写入内存的值:从不同源地址发起多次请求,并逐步递减数据包偏移量。
由于实现盲远程代码执行面临诸多限制,因此研究人员探究了信息泄露等替代攻击场景。
其中一种考虑的攻击方式是:篡改指向特定消息段的指针,使其指向越界内存,后续再通过读取该消息来获取相邻内存中的数据。
然而,每个消息段解析的起始阶段都会对当前段的位置进行校验,因此无法欺骗解析逻辑去解析越界的数据段。
另一种可行的攻击场景是:覆盖另一条消息的 OnDiskExtensionHeader,从而欺骗消息接收方,使其对接收消息的来源产生误判。同样,发送到队列的消息内容也有可能被覆盖,攻击者无法访问。若要覆盖消息的特定内容,攻击者需掌握该消息的相关信息(例如消息大小)。
存在一个名为 SoapHeader 的可选部分,该部分易受越界数据访问漏洞影响,其格式如下:
SoapHeader 数据包格式
经观察发现,存储 MSMQ 数据包数据的内存在缓冲区释放时未被正确清零。因此,当发送的 MSMQ 数据包尺寸小于之前发送的数据包时,内存布局会呈现为当前数据包数据 + 此前处理过的 MSMQ 数据包遗留数据。
研究发现,
写入内容的控制权限有限,且额外的非受控写入操作会破坏相邻内存,加之对消息布局的了解不足,这些因素共同构成了漏洞利用过程中的重大障碍。
在分析时,仅对 MSMQ 消息包初始处理过程中涉及的用户空间漏洞进行了可利用性评估。对内核变种漏洞的分析可能会揭示更多可利用的途径。例如,包含格式错误的标头可能会在 文件的内核内存映射中获得写入原语。在某些情况下,用户空间处理中的检查会阻止内核执行某些后处理操作。
此外,对于内核态变体漏洞而言,攻击者对消息内容的直接控制能力更弱,且操作的是与用户态共享的文件映射区域,而非内核池(堆)。同时,KASLR 等缓解措施,也为远程漏洞利用设置了难以突破的障碍。
由于补丁代码的实现方式,远程客户端可以安全地判断系统是否已应用更新。这可以通过发送一个触发漏洞但不会导致越界访问的 MSMQ 消息来实现,从而避免服务进程崩溃。如果消息的 UserHeader 中设置了 HTTP 标志,则预期会出现一个 SRMPEnvelopeHeader。在补丁中,对 字段乘以 2 的结果执行了整数溢出检查。该长度可以被设置,使得乘法运算的结果溢出为一个与实际数据大小相等的小数值。
反编译代码片段,展示对 SRMPEnvelopeHeader 结构体 DataLength 字段的整数溢出校验逻辑
若目标系统已安装补丁,该漏洞触发后会抛出异常,服务器将中止消息处理流程,因此不会向客户端返回任何响应。若未安装补丁,服务器会正常处理消息并返回响应。
由于易受攻击的缓冲区所对应的内存位置特性,对于能够向目标上任何队列发送消息的攻击者而言,实现远程代码执行似乎更为可行。在这种情况下,可以进行梳理,以便分配相邻的堆。对写入内容的控制极其有限,这为远程开发带来了挑战。由于存在 ASLR 等缓解措施,攻击者还必须获取信息泄露才能实现利用。将常量(0xC, 0x0
综上,我们得出结论:尽管 QueueJumper 漏洞的远程利用并非完全不可能,但上述各项要求使其实施难度极大。本文仅对这一极为复杂的消息规范、服务架构及协议体系进行了初步探讨。针对其用户态与内核态组件的运行机制,仍有必要开展进一步的漏洞研究。