SIGRed漏洞(CVE-2020–1350)在Windows Server 2012/2016/2019上的远程代码执行利用技术

0 阅读8分钟

by Worawit Wangwarunyoo, DATAFARM Research Team, Datafarm Company Limited

本文描述了在 Windows Server 2012 R2 到 Windows Server 2019 上对 SIGRed (CVE-2020–1350) 的远程代码执行利用技术。有关漏洞详细信息,请参阅 Checkpoint 的研究文章。

准备名称服务器

为了减少设置域名的步骤,我在目标 Windows DNS 服务器上配置了“条件转发器”,如下图所示。同时我使用 dnslib 为“evildns.com”域创建了恶意的 DNS 客户端和服务器。

[图片占位符]

触发漏洞

如果我们简单地按照 Checkpoint 的文章触发漏洞,最终很可能会导致 dns!SigWireRead 调用的 memcpy 内部发生崩溃。

[图片占位符]

原因是 SIG 资源记录被分配在进程堆的末尾附近。触发漏洞需要覆盖大约 64KB 的超大 SIG 记录缓冲区,因此 memcpy 会尝试写入超出当前堆空间的范围,这是一个无效的内存区域。如果我们转储导致崩溃的地址周围的内存,会发现该地址无效。

[图片占位符]

要利用堆溢出漏洞,通常我们需要了解堆的内部结构以及分配在堆上的某些对象结构。

WinDNS 堆管理器

WinDNS 管理自己的内存池。有 4 个内存池桶(dns!StandardAllocLists),对应不同的分配大小(0x50、0x68、0x88、0xa0)。如果所需分配大小大于 0xa0,WinDNS 将使用原生 Windows 堆(通过 HeapAllocHeapFree 函数)。

以下是用于 dns 中动态分配内存的 dns!Mem_Alloc 函数的伪代码:

[图片占位符]

接下来是用于释放 Mem_Alloc 分配的内存的 dns!Mem_Free 函数的伪代码:

[图片占位符]

逆向 Mem_AllocMem_Free 函数后,我们可以看到一些有助于利用漏洞的问题:

  • WinDNS 堆永远不会将内存归还给原生 Windows 堆:如果内存大小小于或等于 0xa0,WinDNS 只是将其放在空闲列表的头部。原生 Windows 堆将其视为已使用内存。因此我们可以自由破坏原生 Windows 堆块元数据,因为在分配和释放时会检查堆元数据。
  • 所有 WinDNS 堆头值都是已知的:WinDNS 堆头包含元数据,如缓冲区类型、大小、桶索引、cookie(固定值)。所有元数据值都是已知的,无需信息泄露。因为我们必须通过覆盖堆中的许多对象来开始利用,这个条件非常有助于在没有其他信息泄露漏洞的情况下利用此漏洞。
  • 空闲块以单向链表形式保存:这种数据结构意味着块以相反的顺序分配和释放(后进先出)。存在已知技术可以在内存损坏后滥用单向链表中的空闲块,例如伪造空闲列表来控制下一次分配的位置,这可能导致块重叠、任意写入(我认为在这种情况下任意写入很困难,因为在 Mem_Alloc 返回地址之前会检查 8 字节的 cookie);通过反向顺序释放来控制块分配顺序。

监控堆中的对象

为了了解在处理用户查询时堆中分配了哪些对象,我在 Mem_Alloc 中添加了一个断点来打印堆栈跟踪,以监控对象和代码路径。然后我向服务器发送各种 DNS 请求。下面是示例。

[图片占位符]

我发现了一些有趣的对象。首先,每当 Windows DNS 服务器缓存来自权威名称服务器的响应时,都会创建 DNS 资源记录对象(触发漏洞时分配的 SIG 记录也是 RR 对象)。RR 头有一个数据大小字段,该字段可以被覆盖,稍后用于信息泄露。另一个是超时对象,它包含一个带 1 个参数的函数指针。我们可以覆盖它们,以便稍后控制 rip(程序计数器)寄存器。

无崩溃的堆缓冲区溢出

在深入研究了 WinDNS 堆之后,这一步现在应该很容易了。我所做的就是让服务器分配许多 RR 对象,这些对象在利用过程中永远不会被释放。然后释放一个后面跟着许多 RR 对象的对象(总大小必须 > 64KB),释放的块将在触发漏洞时被 SIG RR 对象分配。

信息泄露

触发漏洞时,我们可以通过仅修改数据大小字段来覆盖一个有效的 RR 对象。然后,我们可以查询被覆盖的 RR 以泄露相邻块。泄露溢出的数据是无用的,所以我们应该先释放相邻块。WinDNS 堆将写入一个指向下一个空闲块的指针,然后我们可以泄露指向堆地址的指针。

[图片占位符]

通过精心构造溢出的数据和释放顺序,我们可以在溢出区域泄露堆地址。因为我们能够完全控制溢出数据,所以可以在其中创建一个伪造的空闲列表。然后新对象将被分配在我们控制的区域中。我们可以读取/写入它们。

然后我尝试通过查找包含可执行模块地址的堆对象来泄露 dns.exe 模块。我找到了两个对象(一个指向 BSS 节,另一个指向字符串节)。执行查询使服务器分配具有指向 dns.exe 指针的对象,然后读取其内容,接着计算 dns.exe 的基地址。

注意:泄露 DNS 地址时,我们可以从最低有效 12 位确定目标操作系统和版本,因为模块必须在内存页的起始位置加载。

控制程序计数器(rip)

如前所述,超时对象包含一个带 1 个参数的函数指针。我们可以使其在溢出区域中分配,然后覆盖它以控制 PC。超时对象中的函数指针在 dns!Timeout_CleanupDelayedFreeList 函数中使用。但是自 Windows Server 2012 以来的 dns.exe 是使用控制流防护编译的。启用 CFG 后,我们只能跳转到允许列表中的函数。如果我们试图跳转到函数结尾处(为了启动 ROP),最终会在 ntdll!LdprValidateUserCallTarget 中崩溃,如下图所示(来自 Windows Server 2012 R2)。

[图片占位符]

启用 CFG 时,执行代码的常见过程是修改栈中的返回地址,这需要任意写入能力。但我们现在不能进行任意写入。我们也不知道任何线程栈地址。现在我们只能在溢出区域控制堆。程序将栈地址存储在堆对象中的情况非常罕见。我甚至没有尝试在堆中寻找栈地址。

接下来,我尝试通过搜索包含数据指针的对象来实现任意读取。然后使其在我们控制的区域中分配并覆盖指针。我希望服务器会解引用指针并将数据复制给我。但我发现的条件太多,无法使用(可能存在可用于任意读取的对象,但我找不到)。

然后我检查了 dns.exe 中 CFG 允许的函数,希望能找到有用的东西。大多数函数可以被跳过,因为我们只能控制 1 个参数。我们可以完全忽略参数超过 1 个的函数。花费无数小时绕过 CFG 后,我找到了 dns!NsecDNSRecordConvert 函数。

[图片占位符]

从反编译的代码来看,param_1 显然是一个指向结构体的指针。在第 15 行,服务器找到从 param_1+0x20 处的字符串指针复制的缓冲区长度。在第 18 行,服务器分配内存来存储数据。在第 22 行,服务器将字符串复制到新分配的内存中。通过完全控制的函数参数,我们可以使该函数将数据(作为字符串)从任意地址复制到新分配的内存中。而且,我们可以使新分配的内存位于我们控制的区域中。然后读取数据。因此,我们可以通过 dns!NsecDNSRecordConvert 函数实现任意读取。

代码执行

具备了调用 CFG 允许列表中的函数(带 1 个参数)和任意读取的能力,我考虑使用 kernel32!WinExecmsvcrt!system 来执行代码。我选择 msvcrt!system,因为 kernel32.dll 更有可能被微软的月度补丁修改。msvcrt.dll 中的偏移量应该适用于任何补丁级别。

为了通过 msvcrt!system 执行代码,我从 dns.exe 的导入表中读取 msvcrt!memcpy,然后计算 msvcrt!system 的地址。最后,重复控制 PC 的步骤,但跳转到 msvcrt!system。成功执行代码。

在成功开发出针对 Windows Server 2012 R2 的利用程序后,我只是修改了 Windows Server 2016 和 Windows Server 2019 的 dns.exemsvcrt.dll 的偏移量,它就完美地工作了。

注意:Windows Server 2019 中的 DLL 是使用 CFG 导出抑制编译的(下图是 msvcrt.dll)。如果 dns.exe 启用了它,利用路径将会困难得多。

演示视频

以下是 Windows Server 2012 R2、Windows Server 2016 和 Windows Server 2019 的演示视频:

  • Windows Server 2012 R2 上的远程代码执行
  • Windows Server 2016 上的远程代码执行
  • Windows Server 2019 上的远程代码执行

参考