介绍
MS-RPC 是 Windows 网络中使用率较高的协议,许多服务和应用程序都依赖它。因此,MS-RPC 中的漏洞可能会导致严重后果。Akamai 安全情报小组在过去一年中一直致力于 MS-RPC 研究。我们发现并利用了漏洞,构建了研究工具,并记录了该协议的一些未记录的内部细节。
虽然之前的博客文章重点关注服务中的漏洞,但这篇文章将研究 RPC 运行时(MS-RPC 的“引擎”)中的漏洞。这些漏洞类似于我们在 2022 年 5 月发现的漏洞。
整数溢出模式
这三个新漏洞有一个共同点——它们的存在都是因为在三个数据结构中插入时出现了整数溢出:
-
SIMPLE_DICT(仅保存值的字典) -
SIMPLE_DICT2(同时保存键和值的字典) -
队列
所有这些数据结构都是使用动态数组实现的,每次数组满了之后,数组就会增大。这是通过分配当前数组所分配内存的两倍来实现的。这种分配很容易受到整数溢出的影响。
图 1 显示了 RPC 运行时的反编译代码。它显示了插入 SIMPLE_DICT 结构的过程以及可能触发整数溢出的易受攻击的代码行(突出显示)。图 1:SIMPLE_DICT 结构扩展中的整数溢出 探索漏洞 要触发漏洞,我们需要了解其根本原因,确定是否存在流向漏洞函数的流程,以及触发漏洞需要多长时间。
为了简洁起见,我们将描述三个漏洞中的一个:Queue 数据结构中的漏洞。由于其他整数溢出的性质相似,因此以下部分中的分析可以互换进行。
理解整数溢出 队列是一个简单的 FIFO(先进先出)数据结构。RPC 运行时中的队列是使用一个结构体实现的,该结构体包含队列条目数组、当前容量以及队列中最后一个项目的位置。
当新条目添加到队列时(假设有可用插槽),所有项目都会在数组中向前移动,新项目会添加到数组的开头。然后,队列中最后一个项目的位置会增加。
当发生出队时,最后一个项目被拉出,并且最后一个项目的位置减少(图 2)。
图 2:入队和出队操作期间的队列结构
如前所述,该漏洞发生在插入新条目时。如果动态数组已满,则代码会执行以下操作:
-
分配一个具有以下大小的新数组: CurrentCapacity * 2 * sizeof(QueueEntry) -
将旧项复制到新数组 -
释放旧项目数组 -
容量加倍
对于 32 位系统,在计算新数组大小时会发生溢出:
-
我们用 0x10000000 (!) 个项目填充队列。 -
发生扩展。计算新分配的大小:0x10000000 * 16。 由于溢出,新分配大小为 0。 -
分配一个零长度数组。 -
代码将旧的 items 数组复制到新的小数组中。这将导致野复制(线性大复制)。
在 64 位系统上,此漏洞不可利用,因为存在大量分配失败的情况。这会导致代码正常退出,而不会触发任何越界写入。尽管 64 位系统不受此问题影响,但它们容易受到其他整数溢出的影响(在 SIMPLE_DICT 和 SIMPLE_DICT2 中)。
代码流
RPC 连接使用 OSF_SCONNECTION 类表示。每个连接可以处理多个客户端调用 (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 及其所呈现的攻击面。我们感谢微软迅速做出反应并修复了这些问题。
原文始发于微信公众号(红云谈安全):探索 RPC 运行时中的三个远程代码执行漏洞
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论