需要云计算吗? 即刻开始体验

在 RPC 运行时中发现三个远程代码执行漏洞

Akamai Wave Blue

寫於

Ben Barnea

May 26, 2023

Akamai Wave Blue

寫於

Ben Barnea

Ben Barnea 是 Akamai 的安全研究人员,他专注于 Windows、Linux、物联网及移动设备等各种架构方面的低级别安全研究和漏洞研究并拥有丰富的经验。他喜欢了解复杂机制的工作原理,尤其是它们是如何失效的。

请谨记,即便是难以利用的漏洞,对于有能力(并且有耐心)的攻击者来说也是机会。

执行摘要

  • Akamai 研究人员 Ben Barnea 在 Microsoft Windows RPC 运行时中发现了三个重要漏洞,这些漏洞的 CVE 编号分别为 CVE-2023-24869CVE-2023-24908CVE-2023-23405,它们的基本评分为 8.1。

  • 这些漏洞可以导致远程代码执行。由于 RPC 运行时库会被加载到所有 RPC 服务器中,而这些服务器通常由 Windows 服务使用,因此所有 Windows 版本(桌面版和服务器版)都会受到影响。

  • 这些漏洞是整数溢出漏洞,存在于 RPC 运行时所使用的三个数据结构中。

  • 这些漏洞已披露给 Microsoft,并在 2023 年 3 月 Patch Tuesday中得到修复。

简介

MS-RPC 是一种在 Windows 网络中广泛使用的协议,很多服务和应用程序都依赖该协议。因此,MS-RPC 中的漏洞会造成严重后果。在过去的一年里,Akamai 安全情报组一直在从事 MS-RPC 研究。我们发现并利用了这些漏洞,构建了研究工具,而且详细撰写了该协议的一些未公开的技术内幕。

虽然先前的博客文章专注于服务中的漏洞,但本文将探讨作为 MS-RPC“引擎”的 RPC 运行时中的这些漏洞。这些漏洞类似于 我们在 2022 年 5 月发现的一个漏洞

整数溢出的模式

这三个新的漏洞有一个共同特点,即它们都是因为对以下三个数据结构执行插入操作时发生整数溢出而产生的:

  1. SIMPLE_DICT(一个只保存值的字典)

  2. SIMPLE_DICT2(一个同时保存键和值的字典)

  3. 队列

所有这些数据结构都是使用一个每次会在变满时增大的动态数组实施的。分配的内存是分配给当前数组的内存的两倍时会出现这种情况。此分配容易受到整数溢出的影响。

图 1 展示了 RPC 运行时中的反编译代码。它显示了对 SIMPLE_DICT 数据结构执行的插入过程以及易受攻击的代码行(突出显示部分),攻击者可以在此代码行中触发整数溢出。

图 1 展示了 RPC 运行时中的反编译代码。 图 1:SIMPLE_DICT 结构扩展中的整数溢出

发现漏洞

要触发漏洞,我们需要了解其底层原因,确定是否存在流向易受攻击的函数的流量以及触发该漏洞所需的时间。

为了简洁起见,我们将详细说明这三个漏洞之一:队列数据结构中的漏洞。由于其他两个整数溢出漏洞的本质相似,因此以下各部分中完成的分析可以互换进行。

了解整数溢出

队列是一个简单的 FIFO(先进先出)数据结构。RPC 运行时中的队列是使用结构实施的,而结构包含一个队列条目数组、当前容量以及队列中最后一项的位置。

向队列中添加一个新条目(假定有一个可用槽位)时,数组中的所有项都会向前移动,而新项将添加到数组的开头部分。队列中最后一项的位置会随之递增。

发生出队操作时,最后一项将被取出,而最后一项的位置会随之递减(图 2)。

发生出队操作时的屏幕截图 图 2:排队和出队操作期间的队列结构

如前所述,该漏洞会在新条目插入时出现。如果动态数组已满,那么代码会执行下列操作:

  • 分配具有以下大小的新数组:
    CurrentCapacity * 2 * sizeof(QueueEntry)

  • 将旧项复制到新数组中

  • 释放包含旧项的数组

  • 将容量翻倍

对于 32 位系统,溢出将在计算新数组大小时发生:

  • 我们使用 0x10000000 (!) 个项填充队列。

  • 发生扩展。新分配的大小将按照以下方式进行计算:0x10000000 * 16。 由于这产生了溢出,因此新分配大小为 0

  • 分配了一个零长度数组。

  • 代码会将旧项数组复制到新的小数组中。这会导致失控复制(线性大量复制)。

在 64 位系统上,此漏洞无法被利用,因为存在失败的巨块分配。这会导致代码正常退出,而不会触发任何越界写入。尽管 64 位系统不会受到此问题的影响, 但它们容易受到另外两个整数溢出漏洞 (在 SIMPLE_DICT 和 SIMPLE_DICT2 中)的影响。

代码流

RPC 连接是使用 OSF_SCONNECTION 类表示的。每个连接都可以处理多个客户端调用 (OSF_SCALL),但是在 每个指定的时间段,只允许在 连接上运行一个调用,而其他调用将排队。

因此,一个使用队列的有趣函数是 OSF_SCONNECTION::MaybeQueueThisCall。在对已到达连接的新调用进行调度的过程中,它会被调用。在这种情况下,该队列用于在连接处理另一个调用时“挂起”传入调用。

这样,我们有一种受用户控制的方法来填充队列(通过依次发送客户端调用),但是此函数强加了一项要求:调用当前由连接处理。这意味着,如果我们要填充队列,那么需要一定的时间才能完成的调用。在连接处理该调用时,我们将发送多个新调用,这些调用将填满调度队列。

哪种类型的函数调用所需的完成时间最长?

  • 最优候选项是我们能够在其中产生无限循环的函数。

  • 次优选项是强制身份验证漏洞,因为这样服务器就会连接到我们,进而我们可以控制响应时间。

  • 最后的选项是一个具有复杂逻辑的复杂函数,或者一个处理大量数据并因此需要大量时间才能完成的函数。

我们决定使用 我们自己的强制身份验证漏洞

它所需的触发时间

到目前为止,我们了解了需要什么来填充队列以及如何才能完成此操作。但是,出现了一个重要的问题——这是否可行?

我们对发生整数溢出的变量的控制程度最低,因为我们一次只能让它递增一,这类似于 refcount(引用计数)溢出。与我们完全控制的两个变量在其中相加或相乘的整数溢出相比时,或者在能够对增加的大小(例如,数据包大小)进行一定程度的控制时,这种整数溢出的严重程度略低。

如前所述,我们必须分配 0x10000000(大约 2.68 亿)个项。这不是个小数目。

尝试在我的机器上触发此漏洞时大约每秒生成 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 作出快速响应并解决了这些问题。

我们的 GitHub 存储库 中提供了各种能够帮助您快速入门的工具和技术。



Akamai Wave Blue

寫於

Ben Barnea

May 26, 2023

Akamai Wave Blue

寫於

Ben Barnea

Ben Barnea 是 Akamai 的安全研究人员,他专注于 Windows、Linux、物联网及移动设备等各种架构方面的低级别安全研究和漏洞研究并拥有丰富的经验。他喜欢了解复杂机制的工作原理,尤其是它们是如何失效的。