DNS Server远程代码执行漏洞分析-SIGRed(CVE-2020-1350)

 

简介

SIGRed(CVE-2020-1350)是Windows DNS服务器中一个可修复的严重漏洞,影响Windows server 2003至2019版本,并且可以由恶意DNS响应触发。由于服务在提升的特权(系统)中运行,如果成功利用,攻击者将被赋予域管理员权限,从而威胁整个公司基础设施。

 

起因

我们的主要目标是找到一个可以让攻击者破坏Windows域环境的漏洞,最好是不需要经过身份验证的漏洞。大多数已发布的公开资料和漏洞利用都集中在微软的SMB(EternalBlue)和RDP(BlueKeep)协议实现上,因为这些目标都影响服务器,要获得域管理员权限,一种直接的方法是直接利用域控制器(Domain Controller)。因此,我们决定将我们的研究重点放在一个较少的攻击面上,它主要存在于Windows服务器和域控制器上。DNS客户端和DNS服务器一起为计算机和用户提供计域名解析服务。

 

Windows DNS 概述

DNS主要在53端口上使用 UDP 协议来处理请求。DNS查询由来自客户端的单个UDP请求和来自服务器的单个UDP响应组成。

除了将名称转换为IP地址外,DNS还有其他用途,例如,邮件传输代理使用DNS找到发送电子邮件的最佳邮件服务器:MX记录提供了域和exchanger邮件之间的映射,这可以提供额外的容错和负载,在Wikipedia上可以找到可用的DNS记录类型及其相应用途的列表。

但是,本文的目的不是要对DNS功能和历史进行长篇大论,因此我们建议您在此处阅读有关DNS的更多信息。

您需要了解的内容:

  • DNS通过UDP/TCP 53端口运行。
  • 一条DNS消息(响应/查询)在UDP中限制为512字节,在TCP中限制为65,535字节。
  • DNS本质上是分层的和分散的,这意味着,当DNS服务器不知道它接收到的查询的答复时,该查询将被转发到它上层的DNS服务器,在架构的顶部,全世界有13个根DNS服务器。

在Windows中,DNS客户端和DNS服务器在两个不同的模块中实现:

  • DNS客户端 – dnsapi.dll 负责DNS解析。
  • DNS服务器 – dns.exe负责在安装了DNS角色的Windows 服务器上响应DNS查询。

我们的研究围绕dns.exe模块进行。

 

准备环境

我们的攻击面主要有两种情况:

  • 1.DNS服务器解析传入查询的方式中的漏洞。
  • 2.DNS服务器解析转发查询时响应的方法的漏洞。

由于DNS查询没有复杂的结构,因此在第一种情况下发现解析问题的机会较小,因此我们决定以解析转发查询的传入响应的函数为目标。因此我们决定将目标定位于解析转发查询响应的函数。

如上所述,转发查询是利用DNS来将不知道答复的查询转发到上层DNS服务器中。但是,大多数环境将其转发器配置为知名的DNS服务器,例如8.8.8.8(Google)或1.1.1.1(Cloudflare)或者至少是不受攻击者控制的服务器。这意味着即使我们在解析DNS响应时发现问题,也需要建立一个中间人来加以利用。显然,这无法达到要求。

NS记录重用

NS代表“名称服务器”,该记录表明哪个DNS服务器是该域的授权,NS记录通常负责解析指定域的域名,一个域通常有多个NS记录,这些记录可以表明该域的主名称服务器和备份名称服务器。

若要使目标Windows DNS服务器解析来自恶意DNS名称服务器的响应,请执行以下操作:

  • 1.将我们域的(deadbeef.fun)NS记录配置为指向我们的恶意DNS服务器(ns1.41414141.club)。
  • 2.查询受害Windows DNS服务器的NS记录deadbeef.fun。
  • 3.受害DNS目前不知道该查询的答复,将查询转发到其上层的DNS服务器(8.8.8.8)。
  • 4.权威服务器(8.8.8.8)知道答复,并响应的NameServer deadbeef.fun为ns1.41414141.club。
  • 5.受害Windows DNS服务器处理并缓存此响应。
  • 6.下次我们查询域名deadbeef.fun,目标Windows DNS服务器也会查询ns1.41414141.club并响应,因为它是该域的NameServer。

 

漏洞– CVE-2020-1350

函数:dns.exe!SigWireRead
漏洞类型:整数溢出导致基于堆的缓冲区溢出

dns.exe 为每种受支持的响应类型实现解析功能。

Wire_CreateRecordFromWire:RRWireReadTable被传递给RR_DispatchFunctionForType函数处理

RRWireReadTable支持响应的类型

支持的响应类型之一用于SIG查询。根据Wikipedia:”SIG查询是在SIG(0) (RFC 2931)和TKEY (RFC 2930)中使用的签名记录,RFC 3755指定RRSIG作为在DNSSEC内使用的SIG的替代品。”

让我们来看一下由Cutter生成的反汇编:dns.exe!SigWireRead SIG响应类型的处理函数:

RR_AllocateEx通过以下公式计算传递给第一个参数:

[Name_PacketNameToCountNameEx result] + [0x14] + [The Signature field’s length (rdi–rax)]

签名字段的大小可能会有所不同,因为它是SIG响应的主要payload。

R根据RFC 2535的SIG资源记录的结构

正如你在下面的图片中看到,RR_AllocateEx希望其参数在16位寄存器中传递,因为它只使用rdx的dx部分和rcx的cx部分。

这意味着,如果我们可以使上述公式输出的结果大于65,535字节(16位整数的最大值),则会出现整数溢出,从而导致分配比预期的小得多,这可能会导致基于堆的缓冲区溢出。

比较方便的是,这个分配的内存地址作为memcpy的目标缓冲区传递,从而导致基于堆的缓冲区溢出。

从中分配的缓冲区RR_AllocateEx被传递到中memcpy

总而言之,通过发送一个包含较大(大于64KB) SIG记录的DNS响应,我们可能会导致一个基于堆的缓冲区溢出,溢出大约64KB。

 

触发漏洞

现在我们能够让受害者DNS服务器查询我们的DNS服务器,我们已经有效地将其转换为客户端。我们可以让受害者DNS服务器询问我们的恶意DNS服务器特定类型的查询,并分别用匹配的恶意响应请求。

我们认为触发此漏洞所需要做的只是使受害DNS服务器向我们查询SIG记录,然后用一个很长的签名(length >= 64KB)回复它一个SIG响应。我们失望地发现DNS超过UDP的大小限制为512字节(如果服务器支持EDNS0,则为4,096字节)。无论如何,这都不足以触发漏洞。

但是,如果有合理的理由让服务器发送一个大于4,096字节的响应,会发生什么呢?例如,一个很长的TXT响应或可以解析为多个IP地址的主机名。

DNS截断-但等一下,还有更多!

根据DNS RFC 5966:“在没有EDNS0(DNS 0扩展机制)的情况下,任何需要发送超过512字节限制的UDP响应的DNS服务器的正常行为是截断响应,使其符合该限制,然后在响应头中设置TC标志。当客户端收到这样的响应时,它将TC标志作为一个指示,指示它应该通过TCP重试。”

因此,我们可以在响应中设置TC(truncation) 标志,这将导致目标Windows DNS服务器启动与恶意NameServer新的TCP连接,并且我们可以传递大于4,096字节的消息。但是要大多少?

根据DNS RFC 7766:“DNS客户端和服务器应同时2个八位字节长度字段和该长度字段描述的消息传递到TCP层”,(例如,在单个的“write”系统调用中),以使所有数据更有可能在单个TCP段中传输。

由于邮件的前两个字节表示其长度,因此TCP上DNS中邮件的最大大小表示为16位,因此限制为64KB。

但是,即使长度为65,535的消息也不足以触发漏洞,因为消息长度包括标头和原始查询。在计算传递给RR_AllocateEx的大小时,没有考虑这个开销。

DNS指针压缩–少即是多

让我们再来看一个合法的DNS响应(为方便起见,我们选择了A类型的响应)。

dig research.checkpoint.com A @8.8.8.8的DNS响应,如Wireshark所示

您可以看到Wireshark对research.checkpoint.com的答复名称字段中的0xc00c字节进行了计算。问题是,为什么?

为了尽可能多的将信息压缩到512字节中,可以(通常必须)压缩DNS名称……在这种情况下,答复的DNS名称编码为0xc0 0x0c。c0部分设置了两个最高有效位,表示下面的6+8位是指向消息前面某个地方的指针。在本例中,这指向数据包内的位置12(=0x0c)它紧跟在DNS header之后。

与数据包开头的偏移量0x0c(12)是什么?是research.checkpoint.com啊!

在这种压缩形式中,指针指向编码字符串的开头。在DNS中,字符串被编码为(<size> <value>)链。

因此,我们可以使用“magic”字节0xc0从数据包中引用字符串。让我们再次检查计算传递给RR_AllocateEx的大小的公式:

[Name_PacketNameToCountNameEx result] + [0x14] + [The Signature field’s length (rdi–rax)]

逆向Name_PacketNameToCountNameEx确认我们上面描述的行为。Name_PacketNameToCountNameEx的目的是计算名称字段的大小,并将指针压缩考虑在内。当仅用两个字节表示分配时,拥有一个允许我们大量增加分配大小的原语正是我们所需要的。

因此,我们可以在SIG签名者的名称字段中使用指针压缩。但是,仅指定0xc00c为签名者的名称不会引起溢出,因为查询的域名已经存在查询中,并且从分配的值中减去开销大小。但是0xc00d呢?我们唯一需要满足的约束是编码字符串是有效的(以结尾0x0000),并且我们可以轻松做到这一点,因为我们有一个没有任何字符约束的字段-签名值(signature value)。对于域名41414141.fun,0xc00d指向域名的第一个字符(“4”)。然后将此字符的序号值用作未压缩字符串的大小(“4”表示值0x34(52))。该未压缩字符串的大小加上我们可以在Signature字段中容纳的最大数据量(最多65,535,具体取决于原始查询)的汇总将造成大于65,535字节的值,从而导致溢出!

让我们用WinDBG附加到dns.exe:

我们crashed了!

虽然我们似乎崩溃了,因为我们试图写值到未映射的内存,堆的形状可以允许我们重写一些有意义的值。

以前对dns.exe的利用可以在线获得。例如:A deeper look at ms11-058

 

从浏览器触发

我们知道这个bug可以由局域网环境中的恶意参与者触发。然而,我们想看看这个bug是否可以在没有局域网访问的情况下被远程触发。

在HTTP内私有DNS

到目前为止,您应该知道DNS可以通过TCP传输,并且Windows DNS Server支持此连接类型。您还应该熟悉DNS在TCP上的结构,但以防万一,这里是一个快速回顾:

考虑以下标准HTTP payload:

0000   50 4f 53 54 20 2f 70 77 6e 20 48 54 54 50 2f 31   POST /pwn HTTP/1
0010   2e 31 0d 0a 41 63 63 65 70 74 3a 20 2a 2f 2a 0d   .1..Accept: */*.
0020   0a 52 65 66 65 72 65 72 3a 20 68 74 74 70 3a 2f   .Referer: http:/

即使这是一个HTTP payload,将它发送到53端口上的目标DNS服务器会导致Windows DNS服务器将此payload解释为一个DNS查询。它使用以下结构来实现这一点:

0000   50 4f 53 54 20 2f 70 77 6e 20 48 54 54 50 2f 31   POST /pwn HTTP/1
0010   2e 31 0d 0a 41 63 63 65 70 74 3a 20 2a 2f 2a 0d   .1..Accept: */*.
0020   0a 52 65 66 65 72 65 72 3a 20 68 74 74 70 3a 2f   .Referer: http:/

Message Length: 20559 (0x504f)
Transaction ID: 0x5354
Flags: 0x202f
Questions: 28791 (0x7077)
Answer RRs: 28192 (0x6e20)
Authority RRs: 18516 (0x4854)
Additional RRs: 21584 (0x5450)
Queries: [...]

幸运的是,Windows DNS服务器同时支持RFC 7766的“连接重用(Connection Reuse)”和“Pipelining”,这意味着我们可以在一个TCP会话中发出多个查询,而无需等待应答。

为什么这很重要?

当受害者访问我们控制的网站时,我们可以使用基本的JavaScript从浏览器向DNS服务器发出POST请求。但是如上所示,POST请求的解释方式不是我们真正控制的。

但是,我们可以利用“连接重用”和“Pipelining”特性,通过使用二进制数据向目标DNS服务器(https://target-dns:53/)发送HTTP POST请求,在POST数据中包含另一个“私藏(smuggled)”的DNS查询,分别进行查询。

我们的HTTP有payload包括以下内容:

  • HTTP请求头,我们不控制(User-Agent,Referer等)。
  • “Padding(填充)”,以便第一个DNS查询在POST数据内具有适当的长度(0x504f)
  • 我们“私藏”的DNS查询里面的POST数据。


在单个TCP会话中的多个查询,如Wireshark所示

实际上,大多数流行的浏览器(例如Google Chrome和Mozilla Firefox)都不允许HTTP请求访问53端口,因此只能在少数Web浏览器中利用此bug,包括Internet Explorer和Microsoft Edge(基于非Chromium)。

 

变体分析

出现这个漏洞的主要原因是RR_AllocateEx API期望size参数为16位。通常可以安全地假设单个DNS消息的大小不超过64KB,因此这种行为不应该出现问题。但是,正如我们刚才看到的,在计算缓冲区大小时考虑Name_PacketNameToCountNameEx的结果时,这种假设是错误的。这是因为Name_PacketNameToCountNameEx函数计算未压缩名称的有效大小,而不是它在包中表示该名称所需的字节数。

要查找此漏洞的其他变体,我们需要找到一个满足以下条件的函数:

  • 调用RR_AllocateEx时,大小是可变的(不是常数值)。
  • 有一个对Name_PacketNameToCountNameEx的调用,它的结果用于计算传递给RR_AllocateEx的大小。
  • 传递给RR_AllocateEx的值是使用16位或16位以上的值计算的。

dns.exe中满足这三个条件的唯一函数是NsecWireRead。让我们检查一下从函数反编译中得到的代码片段:

RESOURCE_RECORD* NsecWireRead(PARSED_WIRE_RECORD *pParsedWireRecord, DNS_PACKET *pPacket, BYTE *pRecordData, WORD wRecordDataLength)
{
 DNS_RESOURCE_RECORD *pResourceRecord;
 unsigned BYTE *pCurrentPos;
 unsigned int dwRemainingDataLength;
 unsigned int dwBytesRead;
 unsigned int dwAllocationSize;
 DNS_COUNT_NAME countName;
 pResourceRecord = NULL;
 pCurrentPos = Name_PacketNameToCountNameEx(&countName, pPacket, pRecordData, pRecordData + wRecordDataLength, 0);
 if (pCurrentPos)
 {
   if
    (pCurrentPos >= pRecordData                                         // <-- Check #1 - Bounds check
     && pCurrentPos - pRecordData <= 0xFFFFFFFF                         // <-- Check #2 - Same bounds check (?)
     && wRecordDataLength >= (unsigned int)(pCurrentPos - pRecordData)) // <-- Check #3 - Bounds check
    {
      dwRemainingDataLength = wRecordDataLength - (pCurrentPos - pRecordData);
      dwBytesRead = countName.bNameLength + 2;
      // size := len(countName) + 2 + len(payload)
      dwAllocationSize = dwBytesRead + dwRemainingDataLength;
      if (dwBytesRead + dwRemainingDataLength >= dwBytesRead            // <-- Check #4 - Integer Overflow check (32 bits)
          && dwAllocationSize <= 0xFFFF)                                // <-- Check #5 - Integer Overflow check (16 bits)
      {
       pResourceRecord = RR_AllocateEx(dwAllocationSize, 0, 0);
       if (pResourceRecord)
       {
         Name_CopyCountName(&pResourceRecord->data, &countName);
         memcpy(&pResourceRecord->data + pResourceRecord->data->bOffset + 2, pCurrentPos, dwRemainingDataLength);
       }
     }
   }
 }
 return pResourceRecord;
}

如您所见,这个函数包含许多安全检查。其中一项是16位溢出检查,它可以防止我们在该函数中的漏洞变体。我们还想指出,这个函数比dns中的函数有更多的安全检查。这让我们怀疑这个漏洞是否已经被注意到和修复,但只是在那个特定的函数。

如上所述,Microsoft在两个不同的模块中实现了DNS客户端和DNS服务器。虽然我们的漏洞确实存在于DNS服务器中,但我们想看看它是否也存在于DNS客户端中。

看来,不像dns.exe!SigWireRead, dnsapi.dll!Sig_RecordRead 它确实验证了Sig_RecordRead+D0传递给dnsapi.dll!Dns_AllocateRecordEx的值小于0xFFFF字节,从而防止了溢出。

这个漏洞在dnsapi.dll中不存在。并且两个模块之间的命名约定不同,这使我们相信Microsoft管理DNS服务器和DNS客户端的两个完全不同的代码库,并且不会同步它们之间的bug补丁。

 

Exploitation Plan

根据Microsoft的要求,我们决定保留有关漏洞利用原语的信息,以便给用户足够的时间来修补他们的DNS服务器。相反,我们将讨论应用于Windows Server 2012R2的利用方案。然而,我们相信这个方案也应该适用于其他版本的Windows Server。

该dns.exe二进制文件是用控制流保护(CFG)编译的,这意味着在内存中重写函数指针的传统方法不足以利用这个漏洞。如果这个二进制文件不是用CFG编译的,利用这个漏洞将是非常简单的,因为很早就我们遇到了以下崩溃:

如您所见,我们在ntdll!LdrpValidateUserCallTarget上崩溃了。这个函数负责作为CFG的一部分验证目标函数指针。我们可以看到被验证的指针(rcx)是完全可控的,这意味着我们成功地覆盖了某个函数指针。我们看到崩溃的原因是函数指针被用作全局位图表的索引,每个地址有“allowed” / “disallowed”位,而我们的任意地址导致从表本身的未映射page读取。

要利用这个漏洞在绕过CFG的同时实现远程代码执行,我们需要找到提供以下功能的原语:write- where(精确地覆盖栈上的返回地址)和infoleak(泄漏内存地址)。

信息泄漏

为了实现Infoleak(信息泄露)原语,我们利用溢出破坏了DNS资源记录的元数据,而它仍在缓存中。然后,当再次从缓存中查询时,我们可以泄漏相邻的堆内存。

WinDNS的堆管理器

WinDNS使用Mem_nualloc函数动态分配内存。此函数管理自己的内存池,以用作高效缓存。对于不同的分配大小,有4个内存池(0x50、0x68、0x88、0xA0)。如果请求的分配大小大于0xA0字节,则默认为HeapAlloc,它使用本地Windows堆。堆管理器为内存池header分配额外的0x10字节,其中包含元数据,包括缓冲区的类型(已分配/空闲)、指向下一个可用内存块的指针、用于调试检查的cookie等。堆管理器以单链表的方式实现它的分配列表,这意味着块的分配顺序与它们被释放的顺序相反(LIFO)。

Write-What-Where

为了实现“Write-What-Where”原语,我们通过破坏chunk的header(元数据)来攻击WinDNS堆管理器,实际上就是破坏freelist。

在freelist被破坏后,下次我们尝试分配大小合适的任何内容时,内存分配器都会为我们分配我们选择的内存区域作为”Malloc-Where”原语。

为了绕过CFG,我们希望该内存区域位于堆栈上,由于信息泄漏,我们希望知道它的位置。一旦我们在堆栈上有了写能力,我们就可以将返回地址改写为我们想要执行的地址,有效地劫持执行流程。

必须指出的是,默认情况下,DNS服务在前3次崩溃时重新启动,这增加了成功利用的机会。

 

总结

这个高度的漏洞已经被微软承认,并为其分配CVE-2020-1350。

我们相信这个漏洞被利用的可能性是很高的,因为我们在内部找到了利用这个漏洞所需的所有原语。由于时间限制,我们没有继续利用这个漏洞,但是我们相信其他的攻击者能够利用它。成功利用此漏洞将产生严重影响,因为通常可以找到未修补的Windows域环境,尤其是域控制器。此外,甚至一些互联网服务提供商(ISP)可能将其公共DNS服务器设置为WinDNS。

我们建议用户修补受影响的Windows DNS服务器,以防止利用此漏洞。

作为临时的解决方法,在打补丁之前,建议将DNS消息(通过TCP)的最大长度设置为0xFF00,这样可以消除此漏洞。您可以通过执行以下命令来实现:

reg add "HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesDNSParameters" /v "TcpReceivePacketSize" /t REG_DWORD /d 0xFF00 /f
net stop DNS && net start DNS

 

披露时间表

  • 2020年5月19日 – 向Microsoft提交的初步报告。
  • 2020年6月18日 – Microsoft发布了此漏洞的CVE-2020-1350。
  • 2020年7月9日 – Microsoft承认该问题是一个可修复的严重漏洞,CVSS评分为10.0。
  • 2020年7月14日 – Microsoft发布了补丁。

 

参考资料

非常感谢我的同事Eyal Itkin(@EyalItkin)和Omri Herscovici(@omriher)在这项研究中的帮助。

(完)