0x00 概述
DNS,通常被称为是“互联网的电话簿”,是一种将计算机主机名转换为IP地址的网络协议。由于它是互联网的核心组成部分之一,因此存在许多DNS服务器的解决方案和实现,但只有其中的少数几种方案正在被广泛应用。
Windows DNS服务器是Microsoft的实现方案,也是Windows域环境的必要组成部分。
SIGRed(CVE-2020-1350)是Windows DNS服务器中的一个严重漏洞,CVSS基本评分为10分,影响Windows Server 2003至2019版本,并且可能由恶意DNS响应触发漏洞。由于该服务通常以特权帐户(SYSTEM)运行,因此,如果攻击者成功利用该服务,则同样可以拿到域管理员权限,从而可以攻陷整个企业基础结构。
0x01 目标
我们的主要目标是发现漏洞,可以使攻击者攻陷Windows域环境,最好是在未经身份验证的前提下。此前,有多位独立安全研究人员和具有民族国家背景的研究团队都做过相关研究。目前大多数公开的资料和漏洞利用,都集中在Microsoft对SMB(EternalBlue)和RDP(BlueKeep)协议的实现上,因为这些协议同时影响服务器和终端。要获得域管理员特权,一种直接的方法是利用域控。因此,我们决定将研究重点放在Windows Server和域控上,去寻找一些鲜为人知的攻击面。接下来,我们进入到Windows DNS。
0x02 Windows DNS概述
域名系统(DNS)是构成TCP/IP行业标准协议组件之一,DNS客户端和DNS服务器共同为计算机和用户提供由计算机名称映射到IP地址的解析服务。
DNS主要使用UDP/53端口来服务于请求。DNS查询过程包括来自客户端的单个UDP请求,和来自服务器的单个UDP响应。
除了将名称转换为IP地址外,DNS还有其他用途。例如,邮件传输代理通过DNS查找最优的邮件服务器以传送电子邮件。MX记录提供域名到邮件交换服务器的映射,这样就可以额外增加一层,以提供容错和负载均衡的功能。我们可以在Wikipedia上找到可用DNS记录类型及其对应用途的列表。
由于本文的重点是漏洞分析,因此我们在此不再对DNS进行过多的介绍,大家可以参考其他位置了解有关DNS的更多信息。
目前,我们需要掌握的前置知识包括:
1、DNS通过UDP/TCP 53端口运行。
2、一条DNS消息(响应或查询)在UDP中限制为512字节,在TCP中限制为65535字节。
3、DNS本质上是分层和分散的。这意味着,当DNS服务器不知道要查询的结果时,会将查询转发到上层的DNS服务器。在最顶部,分布着位于全球范围的13台根DNS服务器。
在Windows中,DNS客户端和DNS服务器是在两个不同的模块中实现:
1、DNS客户端 - dnsapi.dll负责DNS解析。
2、DNS服务器 - dns.exe负责在安装了DNS角色的Windows Server上答复DNS查询。
我们的研究过程围绕着dns.exe模块来进行。
0x03 准备环境
我们的攻击面主要有两个场景:
1、DNS服务器在解析传入查询的方式中存在漏洞;
2、DNS服务器在解析转发查询的响应(答复)的方式中存在漏洞。
由于DNS查询的结构并不复杂,因此在第一种情况下发现漏洞的概率较小,所以我们决定将目标聚焦在为了转发查询而解析传入响应的函数。
如前所述,转发查询是在DNS体系结构中将不知道答案的查询转发到上层的DNS服务器。
但是,大多数环境中都会设置一些知名的DNS服务器,例如Google的8.8.8.8或Cloudflare的1.1.1.1,而这些服务器无法被攻击者控制。
这意味着,即使我们发现了在解析DNS响应的过程中存在漏洞,也需要以中间人的方式实现漏洞利用。显然,这还不够。
NS记录
NS代表”Name Server”,该记录表明哪个DNS服务器是域的授权(哪个服务器包含实际的DNS记录)。NS记录通常负责解析特定域的子域名。一个域通常具有多个NS记录,这些记录可以指示该域的主要名称服务器和备用名称服务器。
如果想要让目标Windows DNS服务器解析来自恶意DNS名称服务器的响应,可以执行以下操作:
1、将我们的域名(deadbeef.fun)的NS记录配置为指向我们的恶意DNS服务器(ns1.41414141.club)。
2、在潜在受害Windows DNS服务器上查询deadbeef.fun的NS记录。
3、权威服务器(8.8.8.8)知道答案,并回答deadbeef.fun的名称服务器是ns1.41414141.club。
4、潜在受害Windows DNS服务器处理并缓存此响应。
5、下次我们在查询deadbeef.fun的子域名时,潜在受害Windows DNS服务器还会查询ns1.41414141.club的响应,因为它是该域名的名称服务器。
在查询恶意服务器的DNS服务器上捕获的数据包:
0x04 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响应类型的处理函数。
在Cutter中看到的dns.exe!SigWireRead的反汇编:
通过以下公式,计算传递给RR_AllocateEx(负责为“资源记录”分配内存的函数)的第一个参数:
[Name_PacketNameToCountNameEx result] + [0x14] + [The Signature field’s length (rdi–rax)]
签名字段的大小可能会有所不同,因为它是SIG响应中的主要Payload。
根据RFC 2535,查到SIG资源记录的结构:
如下图所示,RR_AllocateEx期望将其参数传递到16位寄存器中,因为它仅使用rdx的dx部分和rcx的cx部分。
这意味着,如果我们可以让上述公式计算的结果大于65535字节(16位整型的最大值),就将导致整型溢出,导致实际的分配比预期要小很多,有可能会导致基于堆的缓冲区覆盖。
RR_AllocateEx将其参数转换为16位值:
这个分配的内存地址随后会作为memcpy的目标缓冲区传递,从而导致基于堆的缓冲区溢出,这非常方便。
从RR_AllocateEx分配的缓冲区被传递给memcpy:
总而言之,通过发送包含较大SIG记录(大于64KB)的DNS响应,我们可以在较小的分配缓冲区上,引起一个大约64KB的受控堆缓冲区溢出。
0x05 触发漏洞
现在,我们已经可以让潜在受害DNS服务器查询我们的DNS服务器,从而将其转变为客户端。我们可以让潜在受害DNS服务器询问我们的恶意DNS服务器特定类型的查询,并以对应的恶意响应进行回答。
要触发该漏洞,只需让潜在受害DNS服务器向我们查询SIG记录,并回答包含较长签名(大于64KB)的SIG响应。但我们遗憾地发现,基于UDP的DNS大小限制为512字节(如果服务器支持EDNS0,则为4096字节)。无论如何,这不足以触发漏洞。
但是,如果服务器由于正当理由发送了大于4096字节的响应,会怎么样呢?例如,有一段比较长的TXT响应,或者可以解析为多个IP地址的主机名。
新的希望:DNS截断
根据DNS RFC 5966:
在没有使用EDNS0(DNS 0扩展机制)的情况下,如果需要发送超过512字节限制的UDP响应,服务器会截断响应,使其满足该大小限制,然后在响应头中设置TC标志。当客户端收到这样的响应时,TC标志会指示它使用TCP协议进行重试。
不错!所以说,我们可以在响应中设置TC(截断)标志,这将导致潜在受害Windows DNS服务器启动与恶意名称服务器的新TCP连接,并且可以传递大于4096字节的消息。但是,我们实际需要传递多大的消息?
根据DNS RFC 7766:
DNS客户端和服务器应该同时将两个8字节长度的字段以及该字段描述的消息传递到TCP层,以使所有数据能在单个TCP段中传输。
由于消息的前两个字节表示其长度,因此TCP上DNS消息的最大大小表示为16位,也就是限制在64KB以内。
通过TCP协议传递的DNS消息中,前两个字节表示消息的长度:
但是,即使一个长度为65535的消息,也不足以触发漏洞。因为消息中包含标头和原始查询。在计算传递给RR_AllocateEx的大小时,不会考虑这部分开销。
少即是多:DNS指针压缩
让我们再来看一个合法的DNS响应。为方便起见,我们选择了A类型的响应。
使用WireShark查看dig research.checkpoint.com A @8.8.8.8的响应:
我们可以看到,WireShark在响应的Name字段,将0xc00c字节直接转换为research.checkpoint.com。但这是为什么?
我们从powerdns.org找到了答案:
为了将尽可能多的信息压缩到512字节中,可以压缩DNS名称。因此,响应的DNS名称被编码为0xc0 0x0c,其中的c0部分设置了两个最高有效位,表示接下来的6+8位是指向消息中较早位置的指针。在这种情况下,它指向数据包中紧靠DNS标头的位置12(=0x0c)。
这样一来,在数据包开头处0x0c(12)偏移量的内容正是research.checkpoint.com。
在这种压缩形式中,指针指向编码字符串的开头。在DNS中,字符串被编码为一个(
(
因此,我们可以使用0x0c魔术字节从数据包中引用字符串。我们再次检查刚刚的公式:
[Name_PacketNameToCountNameEx result] + [0x14] + [The Signature field’s length (rdi–rax)]
通过对Name_PacketNameToCountNameEx进行逆向,可以确认我们上述描述的行为。Name_PacketNameToCountNameEx的作用是在考虑指针压缩的情况下计算名称字段的大小。当仅用两个字节表示分配时,如果能有一个允许我们大量增加分配大小的原语,就正是我们所需要的。
因此,我们可以在SIG Singer的Name字段中使用指针压缩。但是,只需要将0xc00c指定为签名者的名称就不会引起溢出,因为查询中已经存在要查询的域名,并且会从分配的值中减去这部分开销的大小。但是,0xc00d呢?我们唯一需要满足的限制条件是保证编码字符串有效(以0x0000结尾),这一点我们可以轻松实现,因为我们有一个没有任何限制的字段——签名值。对于域名41414141.fun,0xc00d指向域名的第一个字符(4)。然后,将该字符的值作为未压缩字符串的大小(4表示0x34(52))。这个未压缩字符串的大小再加上我们可以在签名字段包含的最大数据量(65535,具体取决于原始查询)之和可以大于65535字节,从而导致溢出。
我们使用WinDBG对dns.exe进行测试:
出现了崩溃!
在这里,似乎是由于试图将值写入未映射的内存而导致崩溃,但是我们可以尝试覆盖一些有意义的值。
有关于dns.exe的先前漏洞利用可以参考:https://blog.skullsecurity.org/2011/a-deeper-look-at-ms11-058 。
0x06 从浏览器触发
我们知道该漏洞可以由局域网内的恶意参与者触发。但是,我们还希望探究是否可以在没有局域网访问权限的情况下,远程触发该漏洞。
在HTTP中传输DNS
到现在为止,我们知道可以通过TCP协议传输DNS,并且Windows DNS服务器支持该连接类型。我们还熟悉基于TCP的DNS结构,以防万一,我们做一个简短的回顾。
考虑以下标准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,将其发送到目标DNS服务器的53端口,也会导致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请求会以我们无法控制的方式进行解析。
但是,我们可以通过将包含二进制数据的HTTP POST请求发送到目标DNS服务器(https://target-dns:53/),从而在POST数据中包含另一个“偷渡的”DNS查询,从而滥用“Connection Reuse”和“Pipelining”功能。
我们的HTTP Payload包含以下内容:
1、不受控制的HTTP请求标头(User-Agent、Referer等);
2、填充内容,以使POST数据中的第一个DNS查询具有适当的长度(0x504f);
3、POST数据中“偷渡的”DNS查询。
使用WireShark查看在单个TCP会话中的多个查询:
演示视频:https://youtu.be/PUlMmhD5it8
实际上,大多数流行的浏览器(例如Google Chrome和Mozilla Firefox)都不允许HTTP请求访问53端口,因此只能在有限的一组Web浏览器中利用该漏洞,例如Internet Explorer和Microsoft Edge(非基于Chromium的浏览器)
0x07 变体分析
出现此漏洞的主要原因是由于RR_AllocateEx需要16位的size参数。通常,我们可以假设单个DNS消息的大小不会超过64KB,因此这一行为通常不会出现问题。但是,正如我们刚刚所看到的,如果在计算缓冲区的大小时考虑了Name_PacketNameToCountNameEx的结果,那么这种假设就不再成立。发生这种情况是因为Name_PacketNameToCountNameEx函数计算的是未压缩名称的有效大小,而不是其在数据包中表示该名称所用的字节数。
要找到这个漏洞的其他变体,我们需要找到一个满足以下条件的函数:
1、使用可变大小调用RR_AllocateEx,而不是一个恒定值;
2、存在对Name_PacketNameToCountNameEx的调用,并且其结果用于计算传递给RR_AllocateEx的大小。
3、传递给RR_AllocateEx的值是使用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 // = (unsigned int)(pCurrentPos - pRecordData)) // = dwBytesRead // <-- Check #4 - Integer Overflow check (32 bits) && dwAllocationSize data + pResourceRecord->data->bOffset + 2, pCurrentPos, dwRemainingDataLength); } } } } return pResourceRecord; }
在这个函数中,包含许多安全检查。其中的一个检查(#5)是16位溢出检查,可以防止该函数的漏洞利用变体。在这个函数中,包含比dns.exe其他函数更多的安全检查,我们猜测Microsoft是否已经知道并且修复了这一漏洞,但他们的修复是不完全的,仅仅考虑了这一个函数。
正如之前所分析的,Microsoft在两个不同的模块中实现了DNS客户端和DNS服务器。尽管我们的漏洞影响DNS服务器,但还是想看看它是否同样也影响DNS客户端。
dnsapi.dll中Sig_RecordRead的反编译片段:
似乎与dns.exe!SigWireRead不同,dnsapi.dll!Sig_RecordRead确实在Sig_RecordRead+D0处验证了传递给dnsapi.dll!Dns_AllocateRecordEx的值是否小于0xFFFF字节,从而防止了溢出。
在dnsapi.dll中不存在这一漏洞,并且两个模块之间的命名约定不太相同,这样的事实让我们相信Microsoft维护了DNS服务器和DNS客户端这两个完全不同的代码库,并且对于漏洞补丁没有在两个代码库之间同步。
0x08 漏洞利用方法
根据Microsoft的要求,我们决定暂不发布有关漏洞利用原语的信息,以便为用户提供足够的时间来修复DNS服务器。但是,我们讨论了针对Windows Server 2012 R2的漏洞利用方法。我们认为这一方法也同样适用于其他版本的Windows Server。
dns.exe二进制文件是使用控制流防护(CFG)编译的,这意味着,利用覆盖内存中的函数指针这一传统方式将无法利用漏洞。如果这个二进制文件没有使用CFG进行编译,那么漏洞利用就会非常简单,因为我们很早就遇到了这样的崩溃。
ntdll!LdrpValidateUserCallTarget崩溃:
如我们所见,我们在ntdll!LdrpValidateUserCallTarget的位置发生了崩溃。这是负责验证函数指针的函数,作为CFG的一部分。我们看到要验证的指针(rcx)是完全可控的,这意味着我们成功重写了函数指针。这里产生崩溃的原因在于,函数指针被用作全局位图表的索引,每个地址都有一个“允许/不允许”的位,并且我们的任意地址导致从表本身的未映射页面中进行读取。
为了绕过CFG并实现完整的远程代码执行,我们需要找到以下原语:“在哪里写”(精确地覆盖栈上的返回地址)、“信息泄露”(泄露内存地址,例如栈的地址)。
信息泄露
为了实现信息泄露,我们使用溢出来破坏仍在缓存中的DNS资源记录元数据。然后,当再次从缓存中查询时,我们可以泄露相邻的堆内存。
WinDNS的堆管理器
WinDNS使用Mem_Alloc函数来动态分配内存。该函数管理自己的内存池,以作为有效的缓存。其中,有4个内存池存储区(Memory Pool Buckets),用于不同的分配大小(最大为0x50、0x68、0x88、0xA0)。如果请求的分配大小大于0xA0字节,则默认为HeapAlloc,它使用本地Windows堆。堆管理器会为内存池标头分配额外的0x10字节,其中的元数据包括缓冲区类型(已分配/可用)、指向下一个可用内存块的指针、用于调试检查的Cookie等。堆管理器以单链表的方式实现分配表,这意味着块将按照与释放时相反的顺序进行分配(LIFO)。
在哪里写
为了实现“在哪里写”的原语,我们通过破坏块的标头(元数据)的方式,来破坏freelist,从而实现对Windows堆管理器的攻击。
在freelist损坏后,我们下次在尝试分配合适大小的任何内容时,内存分配器都会为我们分配所选择的内存区域作为可写的区域,这也就实现了我们之前所说的利用原语。
要绕过CFG,我们希望该内存区域位于栈上。并且由于前面的信息泄露,我们已经知道了其位置。一旦在栈上具有写入的功能,就可以将返回地址覆盖为我们要执行的地址,从而有效地劫持了执行流。
值得一提的是,默认情况下,DNS服务会在前3次崩溃时重新启动,这样也可以增加成功实现漏洞利用的概率。
0x09 总结
目前,Microsoft已确认这一高危漏洞,并分配漏洞编号CVE-2020-1350。
我们认为,攻击者利用该漏洞的可能性非常高,因为我们在内部发现了利用该漏洞所需的所有原语。由于时间所限,我们没有对漏洞利用进行更深入的研究,包括探究如何将所有漏洞利用原语连接在一起等等,但我们相信,攻击者将有能力利用这个漏洞。而一旦成功利用该漏洞,将会产生严重影响,我们平时也经常发现未打补丁的Windows域环境,特别是域控。此外,一些互联网服务提供商(ISP)也可能已经在公共DNS服务器部署了WinDNS。
我们强烈建议用户修补受漏洞影响的WinDNS服务器,以防止攻击者利用此漏洞。
作为临时解决防范,在更新补丁前,可以将DNS消息(通过TCP协议)的最大长度设置为0xFF00,这样可以避免此漏洞风险。我们可以执行以下命令:
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\DNS\Parameters" /v "TcpReceivePacketSize" /t REG_DWORD /d 0xFF00 /f net stop DNS && net start DNS
目前,Check Point IPS可以防范此类威胁:“Microsoft Windows DNS Server Remote Code Execution (CVE-2020-1350)”。
0x10 时间节点
2020年5月19日 向Microsoft提交漏洞报告。
2020年6月18日 Microsoft为该漏洞分配CVE-2020-1350。
2020年7月9日 Microsoft将该漏洞评估为高危漏洞,CVSS评分为10。
2020年7月14日 Microsoft发布了修复补丁。
0x11 参考资料
https://en.wikipedia.org/wiki/Domain_Name_System
https://blog.skullsecurity.org/2011/a-deeper-look-at-ms11-058
https://know.bishopfox.com/blog/2017/10/a-bug-has-no-name-multiple-heap-buffer-overflows-in-the-windows-dns-client
https://powerdns.org/hello-dns/basic.md.html
https://www.cloudflare.com/learning/dns/what-is-dns/
https://tools.ietf.org/html/rfc7766
https://tools.ietf.org/html/rfc5966
https://tools.ietf.org/html/rfc2535
非常感谢我的同事Eyal Itkin(@EyalItkin)和Omri Herscovici(@omriher)在这项研究中对我提供的帮助。
本文翻译自:https://research.checkpoint.com/2020/resolving-your-way-into-domain-admin-exploiting-a-17-year-old-bug-in-windows-dns-servers/如若转载,请注明原文地址