介绍
DNS,通常被称为“互联网电话簿”,是一种将人类友好的计算机主机名转换为IP地址的网络协议。由于它是Internet的核心组成部分,因此存在许多DNS服务器的解决方案和实现,但是只有少数几种被广泛使用。
“ Windows DNS服务器”是Microsoft的实现,是Windows域环境的必要组成部分和要求。
SIGRed(CVE-2020-1350)是Windows DNS服务器中的一个可蠕虫,严重漏洞(CVSS基本评分为10.0),影响Windows Server 2003至2019版,并且可能由恶意DNS响应触发。由于该服务以提升的特权(SYSTEM)运行,因此,如果成功利用该服务,则会向攻击者授予域管理员权限,从而有效地损害了整个公司的基础结构。
动机
我们的主要目标是找到一个漏洞,使攻击者可以破坏Windows Domain环境,最好是未经身份验证的环境。各种独立安全研究人员以及民族国家赞助的研究都有很多相关研究。大多数公开和可公开获得的资料和漏洞利用都集中在Microsoft对SMB(EternalBlue)和RDP(BlueKeep)协议的实现上,因为这些目标同时影响服务器和端点。要获得Domain Admin特权,一种直接的方法是直接利用Domain Controller。因此,我们决定将研究重点放在主要存在于Windows Server和域控制器上的,鲜为人知的攻击面上。输入WinDNS。
Windows DNS概述
“ 域名系统(DNS)是构成TCP / IP的行业标准协议套件之一,并且DNS客户端和DNS服务器共同为计算机和用户提供计算机名称到IP地址的映射名称解析服务。” – 微软。
DNS主要在端口53上使用用户数据报协议(UDP)来服务请求。DNS查询包括来自客户端的单个UDP请求和来自服务器的单个UDP响应。
除了将名称转换为IP地址外,DNS还具有其他用途。例如,邮件传输代理使用DNS查找最佳的邮件服务器来传递电子邮件:MX记录提供域和邮件交换器之间的映射,这可以提供附加的容错和负载分配层。可用DNS记录类型及其对应用途的列表可在Wikipedia上找到。
但是,本博客文章的目的不是要对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 Server上回答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服务器是该域的权限(哪个服务器包含实际的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。
图1:查询我们的恶意服务器的受害DNS服务器的数据包捕获。
漏洞– CVE-2020-1350
函数:dns.exe!SigWireRead
漏洞类型:整数溢出导致基于堆的缓冲区溢出
dns.exe 为每种受支持的响应类型实现解析功能。
图2: Wire_CreateRecordFromWire:RRWireReadTable被传递给RR_DispatchFunctionForType确定的处理功能。
图3:RRWireReadTable及其一些受支持的响应类型。
支持的响应类型之一是SIG查询。根据Wikipedia的说法,SIG查询是SIG(0)(RFC 2931)和TKEY(RFC 2930)中使用的“ 签名记录 ” 。RFC 3755指定RRSIG替代DNSSEC内部使用的SIG。”
让我们检查一下Cutter为dns.exe!SigWireReadSIG响应类型的处理函数生成的反汇编:
图4:dns.exe!SigWireRead在Cutter中看到的拆卸图。
RR_AllocateEx通过以下公式计算传递给第一个参数(负责为“资源记录”分配内存的函数):
[ Name_PacketNameToCountNameEx结果] + [0x14] + [签名字段的长度(rdi– rax)]
签名字段的大小可能会有所不同,因为它是SIG响应的主要有效负载。
图5:根据RFC 2535的SIG资源记录的结构。
正如你可以在下面的图片中看到,RR_AllocateEx预计其参数在传递16位寄存器,因为它仅使用dx部分rdx和cx部分rcx。
这意味着,如果我们可以使上面的公式输出的结果大于65,535字节(16位整数的最大值),则整数溢出会导致分配的分配比预期的要小得多,这有望导致基于堆的分配。缓冲区覆盖。
图6:RR_AllocateEx将其参数转换为其16位值。
方便地,此分配的内存地址随后作为的目标缓冲区传递memcpy,从而导致基于堆的缓冲区溢出。
图7:从中分配的缓冲区RR_AllocateEx被传递到中memcpy。
总而言之,通过发送包含大(大于64KB)SIG记录的DNS响应,我们可以在小的分配缓冲区上引起大约64KB的基于堆的受控缓冲区溢出。
触发漏洞
现在我们可以使受害DNS服务器查询我们的DNS服务器以解决各种问题,我们已经有效地将其转变为客户端。我们可以使受害DNS服务器询问我们的恶意DNS服务器特定类型的查询,并分别以匹配的恶意响应进行回答。
我们认为触发此漏洞所需要做的只是使受害DNS服务器向我们查询SIG记录,并为其回答带有长签名(长度> = 64KB)的SIG响应。我们很失望地发现基于UDP的DNS的大小限制为512字节(如果服务器支持EDNS0,则为4,096字节)。无论如何,这不足以触发漏洞。
但是,如果服务器出于正当理由发送大于4,096字节的响应会怎样?例如,冗长的TXT响应或可以解析为多个IP地址的主机名。
DNS截断–但是,还有更多!
根据DNS RFC 5966:
“在没有EDNS0(DNS 0的扩展机制)的情况下,任何DNS服务器需要发送超过512字节限制的UDP响应的正常行为是服务器截断响应使其符合该限制,然后在响应标头中设置TC标志。当客户端收到这样的响应时,它将TC标志作为指示它应改为通过TCP重试。”
大!因此,我们可以TC在响应中设置(截断)标志,这将导致目标Windows DNS服务器启动与恶意NameServer的新TCP连接,并且我们可以传递大于4,096字节的消息。但是要大多少?
根据DNS RFC 7766:
“ DNS客户端和服务器应同时(例如,在单个“写入”系统调用中)将两个八位字节的长度字段以及该长度字段描述的消息传递到TCP层,以使得所有数据更有可能在单个TCP段中传输。”
由于邮件的前两个字节表示其长度,因此TCP上DNS中邮件的最大大小表示为16位,因此限制为64KB。
图8:DNS over TCP消息的前两个字节代表消息的长度。
但是,即使长度为65,535的消息也不足以触发漏洞,因为消息长度包括标头和原始查询。计算传递给的大小时,不会考虑此开销RR_AllocateEx。
DNS指针压缩–少即是多
让我们再来看一个合法的DNS响应(为方便起见,我们选择了类型A的响应)。
图9:的DNS响应dig research.checkpoint.com A @8.8.8.8,如Wireshark所示。
您可以看到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>)链。
图10:<size> <value>链的示意图。
因此,我们可以使用“魔术”字节0xc0从数据包中引用字符串。让我们再次检查计算传递到的大小的公式RR_AllocateEx:
[ Name_PacketNameToCountNameEx结果] + [0x14] + [签名字段的长度(rdi– rax)]
反向Name_PacketNameToCountNameEx确认我们上面描述的行为。目的Name_PacketNameToCountNameEx是计算名称字段的大小,并考虑指针压缩。当仅用两个字节表示分配时,拥有一个允许我们大量增加分配大小的基元正是我们所需要的。
因此,我们可以在SIG签名者的“名称”字段中使用指针压缩。但是,仅指定0xc00c为签名者的名称不会引起溢出,因为查询的域名已经存在于查询中,并且从分配的值中减去开销大小。但是呢0xc00d?我们唯一需要满足的约束是编码的字符串是有效的(以结尾0x0000),并且我们可以轻松做到这一点,因为我们有一个没有任何字符约束的字段-签名值。对于域41414141.fun,0xc00d指向域的第一个字符('4')。然后,将此字符的序数值用作未压缩字符串的大小(“ 4”表示值0x34(52))。该未压缩字符串的大小加上我们可以在Signature字段中容纳的最大数据量(最多65,535,具体取决于原始查询)的汇总将导致大于65,535字节的值,从而导致溢出!
让我们用连接到的WinDBG进行测试dns.exe:
我们坠毁了!
尽管似乎由于试图将值写入未映射的内存而使我们崩溃,但是可以以允许我们覆盖一些有意义的值的方式来调整堆的形状。
还值得一提的是,由于SIG记录和RRSIG记录具有相同的结构,因此Microsoft使用相同的函数(SigWireRead)来解析这两种记录类型。在检查时可以看到RRWireReadTable:索引0x18(24)和46(0x2e)都指向该函数SigWireRead。
这意味着记录类型SIG和RRSIG均可用于触发此漏洞,因为它们是由相同的易受攻击的函数-解析的SigWireRead。
dns.exe可以在线获取以前的利用尝试。例如:更深入地了解ms11-058。
从浏览器触发
我们知道此错误可能是由LAN环境中存在的恶意参与者触发的。但是,我们认为看看是否可以在没有LAN访问权限的情况下远程触发此错误会很有趣。
在HTTP中走私DNS
到现在为止,您应该知道DNS可以通过TCP传输,并且Windows DNS Server支持此连接类型。您还应该熟悉基于TCP的DNS的结构,以防万一,这里有个快速的回顾:
图11:DNS over TCP消息格式。
考虑以下标准HTTP有效负载:
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有效负载,将其发送到端口53上的目标DNS服务器也会导致Windows DNS Server将此有效负载解释为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的 “连接重用”和“管道重用” ,这意味着我们可以在单个TCP会话上发出多个查询,而我们无需等待答复就可以这样做。
为什么这很重要?
当受害者访问我们控制的网站时,我们可以使用基本的JavaScript从浏览器向DNS服务器发出POST请求。但是,如上所示,POST请求以我们无法控制的方式进行解释。
但是,我们可以通过将https://www.landui.com:53/带有二进制数据的HTTP POST请求发送到目标DNS服务器()来滥用“连接重用”和“管道传递”功能,该二进制数据在POST数据中包含另一个“走私的” DNS查询,需要分别进行查询。
我们的HTTP有效负载包括以下内容:
· HTTP请求头,我们不控制(User-Agent,Referer,等)。
· “填充”,以便第一个DNS查询在POST数据内具有适当的长度(0x504f)。
· POST数据中的“走私” DNS查询。
图12:在单个TCP会话中的多个查询,如Wireshark所示。
实际上,大多数流行的浏览器(例如Google Chrome和Mozilla Firefox)都不允许HTTP请求访问端口53,因此只能在有限的一组Web浏览器中利用此bug,包括Internet Explorer和Microsoft Edge(基于非Chromium) )。
变异分析
出现此错误的主要原因是因为RR_AllocateExAPI期望size参数为16位。通常可以安全地假设单个DNS消息的大小不超过64KB,因此此行为应该不会引起问题。但是,正如我们刚刚看到的那样,当Name_PacketNameToCountNameEx在计算缓冲区大小时考虑到结果时,这种假设是错误的。发生这种情况是因为该Name_PacketNameToCountNameEx函数计算的是未压缩名称的有效大小,而不是其在数据包中表示该字节所花费的字节数。
要查找此错误的其他变体,我们需要找到一个满足以下条件的函数:
· RR_AllocateEx 以可变大小(而不是恒定值)调用。
· 调用了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; 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; }
如您所见,此功能包含许多安全检查。其中一项(检查#5)是16位溢出检查,可防止此功能的漏洞变型。我们还要提及的是,此功能比中的普通功能具有更多的安全性检查dns.exe,这使我们想知道是否已经注意到并修复了该错误,但仅在该特定功能中。
如前所述,Microsoft在两个不同的模块中实现了DNS客户端和DNS服务器。虽然我们的漏洞确实存在于DNS服务器中,但我们想看看它是否也存在于DNS客户端中。
图13:Sig_RecordReadfrom的反汇编片段dnsapi.dll。
看起来,与不同dns.exe!SigWireRead,dnsapi.dll!Sig_RecordRead 它确实验证了Sig_RecordRead+D0传递给其的值dnsapi.dll!Dns_AllocateRecordEx小于0xFFFF字节,从而防止了溢出。
此漏洞不存在dnsapi.dll,并且两个模块之间的命名约定不同,这一事实使我们相信Microsoft管理DNS服务器和DNS客户端的两个完全不同的代码库,并且不同步错误补丁他们。
开发计划
根据Microsoft的要求,我们决定保留有关漏洞利用原语的信息,以便为用户提供足够的时间修补其DNS服务器。相反,我们讨论了适用于Windows Server 2012R2的开发计划。但是,我们确实认为该计划也应适用于其他版本的Windows Server。
该dns.exe二进制文件是使用Control Flow Guard(CFG)编译的,这意味着覆盖内存中函数指针的传统方法不足以利用此bug。如果此二进制文件不是使用CFG编译的,那么利用此错误将非常简单,因为很早以前我们就遇到了以下崩溃:
图14:在崩溃ntdll!LdrpValidateUserCallTarget。
如您所见,我们在坠毁ntdll!LdrpValidateUserCallTarget。这是负责验证作为CFG一部分的函数指针目标的函数。我们可以看到,待验证的指针(rcx)是完全可控的,这意味着我们在此过程中的某处成功重写了函数指针。我们看到崩溃的原因是,函数指针被用作每个地址具有“允许” /“不允许”位的全局位图表的索引,而我们的任意地址导致对该表本身中未映射页面的读取。
为了在克服CFG的同时将此漏洞利用到完整的远程代码执行中,我们需要找到具有以下功能的原语:在哪里写(精确地覆盖堆栈上的返回地址)和信息泄漏(泄漏内存地址) ,例如堆栈)。
信息泄漏
为了实现Infoleak原语,我们使用溢出来破坏仍在缓存中的DNS资源记录的元数据。然后,当再次从缓存中查询时,我们能够泄漏相邻的堆内存。
WinDNS的堆管理器
WinDNS使用该功能Mem_Alloc动态分配内存。此功能管理自己的内存池,以用作有效的缓存。有4个内存池存储区,用于不同的分配大小(最大为0x50、0x68、0x88、0xA0)。如果请求的分配大小大于0xA0字节,则默认为HeapAlloc,使用本地Windows堆。堆管理器为内存池头分配额外的0x10字节,其中包含元数据,包括缓冲区的类型(已分配/空闲),指向下一个可用内存块的指针,用于调试检查的cookie等。堆管理器以单链接列表的方式实现了其分配列表,这意味着将按照释放时的相反顺序分配块(LIFO)。
写在哪里
为了实现“在哪里写”原语,我们通过破坏块的标头(元数据),事实上破坏了空闲列表来攻击WinDNS堆管理器。
在空闲列表损坏之后,下次我们尝试分配大小合适的任何内容时,内存分配器都会为我们分配我们选择的内存区域作为可写分配–“ Malloc-Where”利用原语。
要绕过CFG,我们希望该内存区域位于堆栈上(由于信息泄漏,我们希望知道其位置)。一旦在堆栈上具有写功能,就可以将返回地址覆盖到要执行的地址,从而有效地劫持了执行流程。
值得一提的是,默认情况下,DNS服务会在前3次崩溃中重新启动,从而增加了成功利用的机会。
结论
Microsoft已确认此高严重性漏洞,并将其分配给CVE-2020-1350。
我们相信,利用此漏洞的可能性很高,因为我们在内部发现了利用此漏洞所需的所有原语。由于时间限制,我们没有继续追求该漏洞的利用(包括将所有利用原语链接在一起),但我们确实相信,坚定的攻击者将能够利用它。成功利用此漏洞将产生严重影响,因为您经常会发现未打补丁的Windows域环境,尤其是域控制器。此外,某些Internet服务提供商(ISP)甚至可能已将其公共DNS服务器设置为WinDNS。
强烈建议用户修补受影响的Windows DNS服务器,以防止利用此漏洞。
作为临时的解决方法,在应用补丁之前,建议将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服务器远程执行代码(CVE-2020-1350)”
Check Point SandBlast Agent E83.11已经可以抵御这种威胁
披露时间表
· 2020年5月19日–向Microsoft提交的初次报告。
· 2020年6月18日– Microsoft发布了此漏洞的CVE-2020-1350。
· 2020年7月9日– Microsoft承认此问题为CVSS评分为10.0的可蠕虫,严重漏洞。
· 2020年7月14日–微软发布了修复程序(星期二修补程序)。
参考资料
非常感谢我的同事Eyal Itkin(@EyalItkin)和Omri Herscovici(@omriher)在这项研究中的帮助。
文章转自
售前咨询
售后咨询
备案咨询
二维码
TOP