引言
MS-RPC 是 Windows 网络中广泛使用的协议,许多服务和应用程序都依赖它。 因此,MS-RPC 中的漏洞可能会导致严重后果。 Akamai 安全情报小组在过去一年中一直致力于 MS-RPC 研究。 我们发现并利用了漏洞,构建了研究工具,并编写了协议的一些未记录的内部结构。
Akamai 研究员 Ben Barnea 在 Microsoft Windows RPC 运行时中发现了三个重要漏洞,这些漏洞的基本评分均为 8.1:CVE-2023-24869、CVE-2023-24908 和 CVE-2023-23405。这些漏洞可能导致远程代码执行。由于 RPC 运行时库已加载到所有 RPC 服务器中,并且这些服务器通常由 Windows 服务使用,因此所有 Windows 版本(桌面版和服务器版)都会受到影响。产生这些漏洞的原因是 RPC 运行时使用的三种数据结构中的整数溢出。目前 Akamai 已负责任地向 Microsoft 披露这些漏洞,并且这些漏洞于 2023 年 3 月的补丁星期二得到解决。
虽然之前的一些文章主要关注服务中的漏洞,但我们将在本篇文章中探讨 RPC 运行时(MS-RPC 的 “引擎”)中的漏洞。这些漏洞与我们在 2022 年 5 月发现的漏洞类似。
整数溢出模式
这三个新漏洞有一个共同的主题 —— 它们都是由于插入三个数据结构时出现整数溢出而存在的:
- SIMPLE_DICT(只保存值的字典)
- SIMPLE_DICT2(同时保存键和值的字典)
- 队列
所有这些数据结构都是使用动态数组实现的,每次数组变满时该数组都会增长。 这是通过分配为当前数组分配的内存的两倍来实现的。 此分配容易受到整数溢出的影响。
图 1 显示了 RPC 运行时的反编译代码。它显示了 SIMPLE_DICT 结构的插入过程以及可以触发整数溢出的易受攻击的代码行(高亮显示)。
图 1:SIMPLE_DICT 结构扩展中的整数溢出
探索产生漏洞的原因
要触发漏洞,我们需要了解其根本原因,弄清楚是否存在指向漏洞函数的流程,以及触发需要多长时间。
为了简洁起见,我们将描述这三个漏洞之一:队列数据结构中的漏洞。由于其他整数溢出本质上是相似的,因此以下各节中进行的分析可以互换进行。
了解整数溢出
队列是一种简单的 FIFO(先进先出)数据结构。 RPC 运行时中的队列是使用一个结构体实现的,该结构体包含队列条目数组、当前容量以及队列中最后一项的位置。
当一个新条目添加到队列中时(假设有一个可用槽),所有项目都会在数组中向前移动,并且新项目将添加到数组的开头。 然后队列中最后一项的位置递增。
当发生出列时,最后一个项目将被拉出,并且最后一个项目的位置会递减(如图 2 所示)
图 2:排队和出队操作期间的队列结构
如前所述,该漏洞发生在插入新条目时。 如果动态数组已满,代码将执行以下操作:
- 分配具有以下大小的新数组:
当前容量 * 2 * sizeof (QueueEntry) - 将旧项目复制到新数组
- 释放旧项目数组
- 容量加倍
对于 32 位系统,在计算新数组大小时会发生溢出:
- 我们用 0x10000000 (!) 个项目填充队列。
- 发生扩展。新分配的大小计算为:0x10000000 * 16。由于溢出,新分配大小为 0。
- 分配了一个零长度数组。
- 代码将旧的 items 数组复制到新的小数组中。这将导致野生副本(线性大副本)。
在 64 位系统上,该漏洞无法被利用,因为会有大量的分配失败。这会导致代码正常退出,因而不会触发任何越界写入。尽管 64 位系统不会受到此问题的影响,但它们仍然很容易受到其他整数溢出的影响(在 SIMPLE_DICT 和 SIMPLE_DICT2 中)。
代码流
RPC 连接使用 OSF_CONNECTION 类表示。每个连接可以处理多个客户端调用 (OSF_SCALL),但在每个给定时间,仅允许在连接上运行一个调用,而其他调用则排队。
因此,一个使用队列的有趣函数是 OSF_SCONNECTION::MaybeQueueThisCall。 它作为调度已到达连接的新调用的一部分进行调用。在这种情况下,队列用于在处理另一个调用时 “保持” 传入调用。
因此,我们有一种用户控制的方式来填充队列(通过一个接一个地发送客户端调用),但此函数提出了一个要求:连接当前正在处理一个调用。这意味着如果我们想要填充队列,我们需要有一个需要时间才能完成的调用。在处理调用时,我们将发送多个新调用,这些调用将填满调度队列。
什么样的函数调用需要最长的时间才能完成?
- 最佳候选者是可以在其中引起无限循环的函数。
- 第二个最佳选择是身份验证强制漏洞,因为服务器会连接到我们 - 因此,我们可以控制响应时间。
- 最后的手段是具有复杂逻辑的复杂函数或处理大量数据并因此需要大量时间才能完成的函数。
我们决定使用我们自己的身份验证强制漏洞。
触发所需的时间
到目前为止,我们了解了填充队列需要什么以及如何完成。 但一个重要的问题出现了 —— 它是否是实用的?
我们对发生整数溢出的变量的控制程度最低。一次只能递增一个,类似于 refcount(引用计数)溢出。这种整数溢出比整数溢出略差,整数溢出是我们完全控制的两个变量相加或相乘,或者当添加的大小可以在一定程度上控制时(例如,数据包大小)。
正如前面所描述的,我们必须分配 0x10000000 (~268M) 项。 这是非常庞大的数量。
尝试在我的计算机上触发漏洞的速度约为每秒 15 到 20 个排队调用。这意味着在普通机器上大约需要 155 天才能触发它!我们预计每秒会产生更多的排队调用。RPC 运行时间这么慢是有什么原因吗?它不是多线程的吗?
我们的假设是多个线程同时处理同一连接的不同调用并将其排队。 经过一番逆向工作,我们发现实际的流程有点不同。
MS-RPC 数据包处理
在调度调用之前,代码会旋转到一个新的线程(如果需要的话)并调用 OSF_SCONNECTION::TransAsyncReceive。 TransAsyncReceive 尝试在同一连接上接收请求。 然后它将请求提交给新线程(通过调用 CO_SubmitRead)。
另一个线程从 TppWorkerThread 中选取请求,最终导致 ProcessReceiveComplete,后者调用 MaybeQueueThisCall 将 SCALL 排队到调度队列中。然后,它向上传播并尝试接收对此连接的新请求。
因此,虽然我们可能有多个线程在运行,但实际上只有一个线程用于连接。 这意味着我们无法同时将多个线程的调用添加到队列中。
数据包 “剩余”
我们试图找到每秒进行更多调用的方法,以最大限度地减少触发漏洞所需的时间。在逆向接收代码时,我们注意到,如果数据包的长度大于数据包中的实际 RPC 请求,则 RPC 运行时会保存剩余部分。稍后,当它检查新请求时,它不会立即使用套接字。它会首先检查是否有数据包 “剩余”,如果有,它会根据剩余数据提供新的请求。
这让我们能够发送更少的数据包,每个数据包都包含最大数量的请求。当我们尝试这样做时,每秒排队的调用数量保持相对不变,因此这似乎是没有任何帮助的。
总结
尽管预计利用这些漏洞的可能性很低,但我们已将它们添加到我们去年对 MS-RPC 研究中发现的重要漏洞列表中。 重要的是要记住,即使是难以利用的漏洞对于有能力(且有耐心)的攻击者来说也是一个机会。
虽然 MS-RPC 已经存在了几十年,但它仍然存在有待发现的漏洞。我们希望这项研究能够鼓励其他研究人员研究 MS-RPC 及其所呈现的攻击面。 我们要感谢 Microsoft 的快速响应并解决问题。
欢迎关注 Akamai ,第一时间了解高可用的 MySQL/MariaDB 参考架构,以及丰富的应用程序示例。