你越界了——打扰了主人的休息
执行摘要
- Akamai 研究人员 Ben Barnea 在 Microsoft Windows RPC 服务中发现了两个重要漏洞,这些漏洞被指定为 CVE-2022-37998 和 CVE-2022-37973 ,基本评分为 7.7。
- 这两个漏洞主要利用本地会话管理器 RPC 接口中的一些 bug。
- 这些漏洞可引发拒绝服务攻击,从而造成容器和会话服务(例如 Microsoft Defender 应用程序防护、沙盒、Docker 和 Windows 终端服务器)无法正常工作。
- 未安装补丁的 Windows 10、Windows 11 和 Windows Server 2022 计算机中存在该漏洞。
- 漏洞已披露给 Microsoft,并在 2022 年 10 月 Patch Tuesday 中得到了解决。
- 我们在研究存储库中 提供了这一概念验证。
前言
在过去一年中,Akamai 安全情报组对 MS-RPC 开展了深入探究。MS-RPC 是一种功能繁多的协议,但其研究尚且存在较多不足,这可产生许多切实影响。其中一个影响便是暴露了 RPC 接口中的 漏洞。这便是本篇博客文章重点介绍的内容:本地会话管理器 (LSM) RPC 接口中的漏洞。
LSM 是会话管理器子系统中的一项服务。LSM 负责管理与 Windows 计算机上的终端服务器会话相关的本地会话。它能与 Winlogon、Csrss 及其他 Windows 相关组件进行通信。
LSM 以 lsm.dll 形式实现, 并且包含客户端和服务器逻辑。LSM 公开了若干 RPC 接口,其中一个较为值得关注的接口与 Hyper-V 虚拟机中运行的容器会话管理相关。这两个漏洞便位于该接口内。
这个接口是什么?
新 RPC 接口的指定 UUID 为 c938b419-5092-4385-8360-7cdc9625976a。该接口恰好公开了两个函数: ContainerCom_AskForSession 和 ContainerCom_SessionLoggedOff。此外,该接口注册了安全回调,该回调始终返回 RPC_S_OK,从而允许所有人访问。LSM 服务器注册了一个 Hyper-V 套接字 (hvsocket) 端点,仅可通过 Hyper-V 容器进行访问。
图 1:在客户端通过 hvsocket 建立 RPC 连接与在服务器端建立 hvsocket 端点
在容器内创建会话之后(例如,因 RDP 连接而创建),LSM 客户端会首先调用 容器 LSM 中的 RpcGetRequestForWinlogon。该函数负责仲裁会话创建,它在容器内运行时,首先会向主机请求权限。为此,它会使用父项的一个 hvsocket,执行针对主机的 ContainerCom_AskForSession RPC 调用。RPC 接口对连接到容器的会话数设有限制。为实施这一限制,它会跟踪新创建的会话。
LSM 如何跟踪会话?
答案很简单。有一个名为 ContainerSessionServer 的全局对象,其包含两个用于跟踪会话的变量:
- 统计已创建会话总数的计数器。 其值仅限为 1,也就是说,在任意给定时刻,仅允许创建一个会话。
- 容器的 GUID 与其容器会话计数之间的映射。 对于每个容器,其值仅限为 2。
在一个容器每次请求会话时, ContainerSessionServer::AskForSession 首先都会检查会话总数计数器值是否小于 1。如果小于 1,则增加会话总数计数,也会增加映射中的容器会话计数器值。
当调用 ContainerSessionServer::OnSessionLoggedOff 时(在容器退出时调用,或者直接作为 RPC 调用),该函数将会话总数计数以及容器会话计数均减 1。
详细审视 RPC 函数中的漏洞
虽然该接口听起来非常简单,实现也很简单,但我们发现了四个 bug,并将这些 bug 串联为两个漏洞。
串联 #1:利用关键部分发起 DoS——CVE-2022-37998
Bug #1——无法退出关键部分
ContainerSessionServer::AskForSession 利用一个关键部分来同步对全局对象 ContainerSessionServer的访问。
图 2:漏洞的反编译代码。该函数未释放关键部分便退出
如上图所示,从第 112 行开始进入该关键部分。随后,在第 114-116 行中,它会检查容器会话计数器是否达到了限值 (2)。如果已达限值,LSM 将不再跟踪该会话,并会立即退出函数(第 125 行)。遗憾的是,代码并未退出先前进入的关键部分。因此,再次调用该接口时系统会卡住,一直在等待该关键部分释放。
但不要忘记,会话总数计数器的限值是 1,那怎样才能让容器的会话计数器值达到 2 呢?从逻辑上来说,这根本不可能实现。不过,这里还有第二个 bug!
Bug #2——计数器跟踪错误
注销与容器的会话后,将对主机中的 ContainerSessionServer::OnSessionLoggedOff 进行 PRC 调用。该函数首先通过调用 DecreaseTotalSessionCount 减少会话总数计数器的值。无论是否在跟踪容器,该函数都会执行此操作。如果该函数发现并未跟踪容器,则会退出,但不会把之前减去的会话总数计数器值加回来。
这会导致会话总数计数器值为负(因为该计数器的值类型是有符号整数)。我们只需先发送多个 OnSessionLoggedOff 请求,然后再发送任何 AskForSession 请求,从而不断将会话总数计数器值减少至任意负值。
第一个和第二个 bug 的串联
我们可使用 bug #2 将会话总数计数器值减少数次,直至其变为负值。然后通过向 AskForSession发送两个请求来利用 bug #1。第二次调用该函数时,函数会检查会话总数计数器的值是否小于 1——由于第二个 bug,这个值确实小于 1。随后在该函数检测到容器会话计数器值为 2 时,它就会返回,但不会退出关键部分。
图 3:漏洞利用过程概况图
DoS 取决于新传入的 RPC 调用是否会分配至创建关键部分死锁的同一线程。如果 RPC 运行时将新调用分配至同一线程,则不会发生 DoS,因为 EnterCriticalSection 支持嵌套所有权,即同一线程可调用 EnterCriticalSection 两次。如果 RPC 调用被分配至其他任何线程,而非造成关键部分死锁的那个线程,则该调用将永远处于等待状态。
串联 #2:利用内存泄漏发起 DoS——CVE-2022-37973
Bug #3——内存泄漏
ContainerSessionServer::AskForSession 还会跟踪容器的事件,例如容器退出/暂停/恢复等。其实现方法是使用容器的 GUID 调用 HcsOpenComputeSystem ,然后使用 HcsRegisterComputeSystemCallback来注册回调。
已注册的回调接收上下文对象。上下文在 ContainerSessionServer::AskForSession内分配。但如果发生错误,许多情况下函数退出时不会释放为上下文分配的内存。这就会造成内存泄漏,攻击者可多次触发该内存泄漏。在调用数量达到一定程度后,LSM 进程将由于内存耗尽而崩溃。
我们在测试中发现,通过无限循环发送 RPC 请求时,每秒可分配 3 MB 的内存。在本测试中,在分配的内存量达到 24 GB 后,LSM 服务便崩溃了。耗尽 24 GB 的内存大约需要两小时。该服务不会自动重新生成。
Bug #4——远程访问
MS-RPC 中的端点是多路复用的。如果服务器注册了多个接口和多个端点,则可通过每个端点访问每个接口。端点和接口之间 未相互绑定。
该接口本应只能通过容器的 hvsocket 访问。但在本测试中,LSM 注册了可远程访问的命名管道端点“\pipe\LSM_API_service”。由于端点是多路复用的,远程攻击者可连接命名管道端点,然后向容器接口发送请求。修复方法很简单——安全回调应检查客户端使用的是哪个端点,如果不是 hvsocket,则拒绝访问。
第三个和第四个 bug 的串联
容器跟踪功能基于客户端标识符属性。也就是说,对于 hvsocket 而言,其客户端标识符应该是容器的 GUID。对于命名管道,其客户端标识符应该是客户端的机器名称。
要触发第一项漏洞利用攻击,客户端必须拥有客户端标识符,该标识符是正在运行的容器的实际 GUID。因此,远程客户端无法触发这些 bug,除非攻击者能成功查明正在运行的容器的 GUID,并更改其机器名称,但这基本行不通。
可惜由于第三个 bug(内存泄漏)的存在,系统会直接分配对象,而不会对所请求的容器执行任何检查。这意味着远程攻击者(利用 bug #4)可远程触发内存泄漏。通过多次调用,攻击者就能造成内存耗尽,进而导致进程崩溃。
影响
虽然这些漏洞被归为拒绝服务 (DoS) 类漏洞,但攻击者可利用它们绕过安全功能,因此它们确实会造成安全性方面的影响。
利用第一个漏洞(关键部分),攻击者可对特定新接口发起 DoS 攻击。该问题会阻止创建新的沙盒实例。
利用第二个漏洞发起的攻击可从远程或容器内触发,并导致整个进程崩溃。这会造成所有依赖 LSM 的功能失效,Microsoft Defender 应用程序防护和沙盒等安全功能也不例外。此外,RDP 和 Docker 将无法正常运行。
图 4:Microsoft Edge 中显示的 MDAG 错误