DNS通常被描述为“互联网的电话簿”,它是一种用于将人类友好的计算机主机名翻译成IP地址的网络协议。因为它是互联网的核心组件,所以有很多DNS服务器的解决方案和实现,但被广泛使用的只有几个。
“Windows DNS服务器”是Microsoft的实施,是Windows域环境的重要组成部分和要求。
SIGRed(CVE-2020-1350)是Windows DNS服务器中的一个易受攻击的严重漏洞(CVSS基本分数为10.0),它会影响Windows Server 2003至2019版,并可由恶意DNS响应触发。由于该服务在提升的权限(系统)下运行,如果攻击成功,攻击者将被授予域管理员权限,从而有效地危害整个公司基础架构。
我们的主要目标是找到一个漏洞,该漏洞会让攻击者危害Windows域环境,最好是未经身份验证。各种独立的安全研究人员以及由民族国家发起的研究都有大量的相关研究。大多数公开发布的材料和漏洞都集中在Microsoft对SMB(EternalBlue)和RDP(BlueKeep)协议的实现上,因为这些目标同时影响服务器和端点。要获得域管理员权限,一种直接的方法是直接利用域控制器。因此,我们决定将研究重点放在主要存在于Windows Server和域控制器上的较少公开探索的攻击面上。输入WinDNS。
“域名系统(DNS)是组成TCP/IP的行业标准协议组之一,DNS客户端和DNS服务器一起向计算机和用户提供计算机名称到IP地址映射名称解析服务。”-Microsoft。
DNS主要使用端口53上的用户数据报协议(UDP)来服务请求。DNS查询包括来自客户端的单个UDP请求,后跟来自服务器的单个UDP回复。
除了将名称转换为IP地址外,DNS还具有其他用途。例如,邮件传输代理使用DNS查找传递电子邮件的最佳邮件服务器:MX记录提供域和邮件交换器之间的映射,这可以提供额外的容错和负载分布层。可在维基百科上找到可用的DNS记录类型及其相应用途的列表。
但这篇博客文章的重点不是介绍DNS功能和历史的冗长论述,所以我们鼓励您在这里阅读更多关于DNS的内容。
单个DNS消息(响应/查询)在UDP中限制为512字节,在TCP中限制为65,535字节。
DNS本质上是层次化和分散化的。这意味着当DNS服务器不知道其收到的查询的答案时,该查询将被转发到层次结构中位于其上方的DNS服务器。在层次结构的顶部有全球13个根DNS服务器。
DNS服务器-dns.exe负责在安装了DNS角色的Windows服务器上回答DNS查询。
DNS服务器解析转发查询的响应(应答)的方式中存在错误。
由于DNS查询没有复杂的结构,因此在第一个场景中发现解析问题的可能性较低,因此我们决定以解析转发查询的传入响应的函数为目标。
如前所述,转发查询是利用DNS体系结构能够将其不知道答案的查询转发到层次结构中其上方的DNS服务器。
但是,大多数环境将其转发器配置为知名的、受人尊敬的DNS服务器,如8.8.8.8(Google)或1.1.1.1(Cloudflare),或者至少是不受攻击者控制的服务器。
这意味着即使我们在解析DNS响应时发现问题,我们也需要建立中间人来利用它。显然,这还不够好。
NS代表“名称服务器”,该记录指示哪个DNS服务器是该域的授权机构(哪个服务器包含实际的DNS记录)。NS记录通常负责解析给定域的子域。一个域通常具有多个NS记录,这些NS记录可以指示该域的主和备份名称服务器。
要让目标Windows DNS服务器解析来自我们的恶意DNS名称服务器的响应,我们执行以下操作:
配置我们域的(deadbeef.un)NS记录以指向我们的恶意DNS服务器(ns1.41414141.Club)。
受害者DNS(尚不知道此查询的答案)将查询转发到其上方的DNS服务器(8.8.8.8)。
权威服务器(8.8.8.8)知道答案,并响应Deadbeef.un的NameServer是ns1.41414141.Club。
下次我们查询deadbeef.un的子域时,目标Windows DNS服务器还将查询ns1.41414141.Club以获得响应,因为它是该域的NameServer。
支持的响应类型之一是用于SIG查询。根据维基百科,SIG查询是“SIG(0)(RFC2931)和TKEY(RFC2930)中使用的签名记录。RFC 3755指定RRSIG作为SIG的替代品,在DNSSEC内使用。“。
让我们检查一下Cutter为dns.exe!SigWireRead(SIG响应类型的处理程序函数)生成的反汇编:
传递给RR_AllocateEx(负责为资源记录分配内存的函数)的第一个参数按以下公式计算:
签名字段大小可以改变,因为它是SIG响应的主要有效载荷。
如下图所示,RR_AllocateEx希望其参数在16位寄存器中传递,因为它只使用RDX的DX部分和RCX的CX部分。
这意味着,如果我们可以使上面的公式输出大于65,535字节(16位整数的最大值)的结果,我们就会有一个整数溢出,这会导致比预期小得多的分配,这可能会导致基于堆的缓冲区覆盖。
非常方便的是,此分配的内存地址随后作为memcpy的目标缓冲区传递,从而导致基于堆的缓冲区溢出。
总而言之,通过发送包含较大(大于64KB)SIG记录的DNS响应,我们可以在较小的已分配缓冲区上导致约64KB的受控基于堆的缓冲区溢出。
现在我们可以让受害的DNS服务器向我们的DNS服务器查询各种问题,我们已经有效地将其转换为客户端。我们可以让受害的DNS服务器向我们的恶意DNS服务器询问特定类型的查询,并分别以匹配的恶意响应进行回答。
我们认为触发此漏洞所需的全部操作就是让受攻击的DNS服务器向我们查询SIG记录,并使用长签名(长度&>=64KB)对其作出SIG响应。我们失望地发现,UDP上的DNS的大小限制为512字节(如果服务器支持EDNS0,则为4096字节)。在任何情况下,这都不足以触发漏洞。
但是,如果服务器有合法的理由发送大于4096字节的响应,会发生什么情况呢?例如,冗长的TXT响应或可解析为多个IP地址的主机名。
根据DNS RFC 5966:“在没有EDNS0(DNS 0的扩展机制)的情况下,任何需要发送超过512字节限制的UDP响应的DNS服务器的正常行为都是服务器截断响应,使其符合该限制,然后在响应头中设置TC标志。”当客户端接收到这样的响应时,它将TC标志作为应该通过TCP重试的指示。“。
太棒了!因此,我们可以在响应中设置TC(截断)标志,这会导致目标Windows DNS服务器启动到我们的恶意NameServer的新TCP连接,并且我们可以传递大于4,096字节的消息。但是有多大呢?
根据DNS RFC7766:“DNS客户端和服务器应该同时(例如,在单个”WRITE“系统调用中)将两个八位字节的长度字段和由该长度字段描述的消息传递到TCP层,以使所有数据更有可能在单个TCP段中传输。“。
由于消息的前两个字节表示其长度,因此TCP上的DNS中消息的最大大小表示为16位,因此限制为64KB。
但是,即使是长度为65,535的消息也不足以触发漏洞,因为消息长度包括报头和原始查询。计算传递给RR_AllocateEx的大小时不考虑此开销。
让我们再看一看合法的DNS响应(为方便起见,我们选择了类型A的响应)。
您可以看到,Wireshark将答案名称字段中的字节0xc00c计算为research.checkpoint.com。问题是,为什么?
根据对DNS的热烈欢迎,powerdns.org说:“为了将尽可能多的信息压缩到512个字节中,DNS名称可以(而且通常必须)压缩为…。在本例中,应答的DNS名称编码为0xc0 0x0c。c0部分设置了两个最高有效位,表明后面的6+8位是指向消息中较早位置的指针。在这种情况下,这指向数据包中的位置12(=0x0c),紧跟在DNS报头之后。“。
距离数据包开头的偏移量0x0c(12)处是什么?这是research.checkpoint.com!
在这种压缩形式中,指针指向编码字符串的起始处。在DNS中,字符串编码为(<;size>;<;value>;)链。
因此,我们可以使用“魔术”字节0xc0来引用数据包中的字符串。让我们再次检查计算传递给RR_AllocateEx的大小的公式:
反转Name_PacketNameToCountNameEx确认了我们上面描述的行为。Name_PacketNameToCountNameEx的目的是在考虑指针压缩的情况下计算Name字段的大小。拥有一个原语,它允许我们在仅用两个字节表示分配的情况下将分配的大小大幅增加,这正是我们所需要的。
因此,我们可以在SIG签名者的姓名字段中使用指针压缩。然而,简单地指定0xc00c作为签名者的名字不会导致溢出,因为查询的域名已经存在于查询中,并且从分配的值中减去开销大小。但是0xc00d呢?我们必须满足的唯一约束是我们的编码字符串有效(以0x0000结尾),而且我们可以很容易地做到这一点,因为我们有一个没有任何字符约束的字段-签名值。对于域414141.un,0xc00d指向域的第一个字符(‘4’)。然后将该字符的序数值用作未压缩字符串的大小(‘4’表示值0x34(52))。将这个未压缩字符串的大小与我们可以在签名字段中容纳的最大数据量(最多65,535,取决于原始查询)聚合在一起,得到的值大于65,535个字节,从而导致溢出!
虽然看起来我们崩溃是因为我们试图将值写入未映射的内存,但是可以通过允许我们覆盖一些有意义的值的方式来调整堆的形状。
以前对dns.exe的攻击尝试在线可用。例如:更深入地查看MS11-058。
我们知道,LAN环境中的恶意攻击者可能会触发此漏洞。然而,我们认为看看这个错误是否可以在没有局域网接入的情况下远程触发会很有趣。
到目前为止,您应该知道DNS可以通过TCP传输,并且Windows DNS服务器支持此连接类型。您还应该熟悉TCP上的DNS的结构,但以防万一,这里快速回顾一下:
00000 50 4f 53 54 20 2f 70 77 6e 20 48 54 54 50 2f 31 POST/pwn HTTP/10010 2E 31 0d 0A 41 63 63 65 70 74 3a 20 2a 2f 2a 0d 1..接受:*/*.00200 0A 52 65 66 65 72 65 3a 20 68 74 74 70 3a 2f。
即使这是HTTP有效负载,将其发送到端口53上的目标DNS服务器也会导致Windows DNS服务器将此有效负载解释为DNS查询。它使用以下结构完成此操作:
0000 50 4f 53 54 20 2f 70 77 6e 20 48 54 54 50 2f 31 POST/pwn HTTP/10010 2E 31 0d 0A 41 63 63 65 70 74 3a 20 2a 2f 2a 0d 1..接受:*/*.0020 0A 52 65 66 65 72 65 3a 20 68 74 74 70 3a 2f。参考:http://Message Length:20559(0x504f)事务ID:0x5354标志:0x202f问题:28791。
幸运的是,Windows DNS服务器同时支持RFC 7766的“连接重用”和“流水线”,这意味着我们可以在单个TCP会话上发出多个查询,并且无需等待回复。
当受害者访问我们控制的网站时,我们可以使用基本JavaScript从浏览器向DNS服务器发出POST请求。但如上所述,POST请求以我们无法真正控制的方式进行解释。
但是,我们可能会滥用“连接重用”和“管道”功能,方法是向目标DNS服务器(https://target-dns:53/))发送HTTP post请求,其中包含要单独查询的POST数据中包含另一个“走私”DNS查询的二进制数据。
“填充”,以便第一个DNS查询在POST数据内具有适当的长度(0x504f)。
实际上,大多数流行的浏览器(如Google Chrome和Mozilla Firefox)都不允许对端口53的HTTP请求,因此此缺陷只能在有限的Web浏览器中利用,包括Internet Explorer和Microsoft Edge(不基于Chromium)。
此错误存在的主要原因是RR_AllocateEx API需要16位的大小参数。通常可以安全地假设单个DNS消息的大小不超过64KB,因此此行为应该不会造成问题。但是,正如我们刚才所看到的,当在计算缓冲区大小时考虑Name_PacketNameToCountNameEx的结果时,这个假设是错误的。这是因为Name_PacketNameToCountNameEx函数计算的是未压缩名称的有效大小,而不是在数据包中表示它所用的字节数。
要查找此错误的其他变体,我们需要找到满足以下条件的函数:
调用Name_PacketNameToCountNameEx,其结果用于计算传递给RR_AllocateEx的大小。
传递给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;pResourceRecRecord。=0xFFFFFFFF//<;--检查#2-相同的边界检查(?)&;&;wRecordDataLength>;=(Unsign Int)(pCurrentPos-pRecordData)//<;--检查#3-边界检查{dwRemainingDataLength=wRecordDataLength-(pCurrentPos-pRecordData);dwBytesRead=countN。--检查#4-整数溢出检查(32位)&;&;dwAllocationSize<;=0xFFFF)//<;--检查#5-整数溢出检查(16位){pResourceRecord=RR_AllocateEx(dwAllocationSize,0,0);IF(PResourceRecord){Name_CopyCountName(&;pResourceRecord->;data,&;CountName);memcpy(&;pResourceRecord-&G}
如您所见,此函数包含许多安全检查。其中之一(检查5)是16位溢出检查,可防止此函数中漏洞的变体。我们还想提一下,此函数比dns.exe中的平均函数有更多的安全检查,这让我们怀疑是否已经注意到并修复了此错误,但仅在该特定函数中。
如前所述,Microsoft在两个不同的模块中实现了DNS客户端和DNS服务器。虽然我们的漏洞肯定存在于DNS服务器中,但我们想看看它是否也存在于DNS客户端中。
与dns.exe!SigWireRead不同,dnsapi.dll!Sig_RecordRead确实在Sig_RecordRead+D0处验证传递给dnsani.dll!DNS_AllocateRecordEx的值小于0xFFFF字节,从而防止溢出。
dnsapi.dll中不存在此漏洞,并且两个模块之间的命名约定不同,这一事实使我们相信Microsoft为DNS服务器和DNS客户端管理两个完全不同的代码库,并且不同步它们之间的错误修补程序。
根据微软的要求,我们决定保留有关攻击原语的信息,以便让用户有足够的时间修补他们的DNS服务器。相反,我们将讨论我们的开发计划,因为它适用于Windows Server 2012R2。但是,我们确实认为该计划也应该适用于其他版本的Windows Server。
dns.exe二进制文件是使用Control Flow Guard(CFG)编译的,这意味着覆盖内存中的函数指针的传统方法不足以利用此缺陷。如果此二进制文件不是使用CFG编译的,利用此错误将非常简单,因为在相当早的时候我们遇到了以下崩溃:
如您所见,我们在ntdll!LdrpValidateUserCallTarget崩溃。这是负责将函数指针目标作为CFG的一部分进行验证的函数。我们可以看到,要验证的指针(RCX)是完全可控的,这意味着我们成功地覆盖了某个函数指针。我们看到崩溃的原因是函数指针被用作全局位图表的索引,每个地址有“允许”/“不允许”位,而我们的任意地址导致从表本身的未映射页读取。
要在击败CFG的同时利用此缺陷进行完全远程代码执行,我们需要找到具有以下功能的原语:Write-What-Where(精确覆盖堆栈上的返回地址)和Infoleak(泄漏内存地址,如堆栈)。
为了实现Infoleak原语,我们使用溢出损坏了仍在缓存中的DNS资源记录的元数据。然后,当再次从缓存查询时,我们能够泄漏相邻的堆内存。
WinDNS使用函数Mem_alloc动态分配内存。此函数管理其自己的内存池,以用作高效缓存。有4个内存池存储桶,用于不同的分配大小(最高可达0x50、0x68、0x88、0xA0)。如果请求的分配大小大于0xA0字节,则默认为使用本机Windows堆的Heapalloc。堆管理器为内存池头分配额外的0x10字节,其中包含元数据,包括缓冲区类型(已分配/空闲)、指向下一个可用内存块的指针、用于调试检查的cookie等。堆管理器以单链接列表的方式实现其分配列表,Me。
.