客户至上、服务为根、勇于拼搏、务实创新

< 返回上层

【安全公告】CVE-2020-1350 Windows DNS服务器蠕虫严重漏洞

2020-07-17 16:41:03 来源:蓝队云

image.jpeg

介绍

DNS,通常被称为互联网电话簿,是一种将人类友好的计算机主机名转换为IP地址的网络协议。由于它是Internet的核心组成部分,因此存在许多DNS服务器的解决方案和实现,但是只有少数几种被广泛使用。

“ Windows DNS服务器Microsoft的实现,是Windows域环境的必要组成部分和要求。

SIGRedCVE-2020-1350)是Windows DNS服务器中的一个可蠕虫,严重漏洞(CVSS基本评分为10.0),影响Windows Server 20032019版,并且可能由恶意DNS响应触发。由于该服务以提升的特权(SYSTEM)运行,因此,如果成功利用该服务,则会向攻击者授予域管理员权限,从而有效地损害了整个公司的基础结构。

动机

我们的主要目标是找到一个漏洞,使攻击者可以破坏Windows Domain环境,最好是未经身份验证的环境。各种独立安全研究人员以及民族国家赞助的研究都有很多相关研究。大多数公开和可公开获得的资料和漏洞利用都集中在MicrosoftSMBEternalBlue)和RDPBlueKeep)协议的实现上,因为这些目标同时影响服务器和端点。要获得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.8Google)或1.1.1.1Cloudflare),或者至少是不受攻击者控制的服务器。

这意味着即使我们在解析DNS响应时发现问题,也需要建立一个中间人来加以利用。显然,这还不够。

NS记录救援

NS代表名称服务器,该记录指示哪个DNS服务器是该域的权限(哪个服务器包含实际的DNS记录)。NS记录通常负责解析给定域的子域。一个域通常具有多个NS记录,这些记录可以指示该域的主要和备用名称服务器。

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

1.    将我们域的(deadbeef.funNS记录配置为指向我们的恶意DNS服务器(ns1.41414141.club)。

2.    查询受害Windows DNS服务器的NS记录deadbeef.fun

3.    受害DNS尚不知道该查询的答案,将查询转发到位于其上方的DNS服务器(8.8.8.8)。

4.    权威服务器(8.8.8.8)知道答案,并响应的NameServer deadbeef.funns1.41414141.club

5.    受害Windows DNS服务器处理并缓存此响应。

6.    下次我们查询的子域时deadbeef.fun,目标Windows DNS服务器也会查询ns1.41414141.club其响应,因为它是该域的NameServer

image.png

1:查询我们的恶意服务器的受害DNS服务器的数据包捕获。

漏洞– CVE-2020-1350

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

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

image.png

2 Wire_CreateRecordFromWireRRWireReadTable被传递给RR_DispatchFunctionForType确定的处理功能。

image.png

3RRWireReadTable及其一些受支持的响应类型。

支持的响应类型之一是SIG查询。根据Wikipedia的说法,SIG查询是SIG0)(RFC 2931)和TKEYRFC 2930)中使用“ 签名记录 ” RFC 3755指定RRSIG替代DNSSEC内部使用的SIG

让我们检查一下Cutterdns.exe!SigWireReadSIG响应类型的处理函数生成的反汇编:

image.png

4dns.exe!SigWireReadCutter中看到的拆卸图。

RR_AllocateEx通过以下公式计算传递给第一个参数(负责为资源记录分配内存的函数):

Name_PacketNameToCountNameEx结果] + [0x14] + [签名字段的长度(rdi– rax]

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

image.png

5:根据RFC 2535SIG资源记录的结构。

正如你可以在下面的图片中看到,RR_AllocateEx预计其参数在传递16位寄存器,因为它仅使用dx部分rdxcx部分rcx

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

image.png

6RR_AllocateEx将其参数转换为其16位值。

方便地,此分配的内存地址随后作为的目标缓冲区传递memcpy,从而导致基于堆的缓冲区溢出。

image.png

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

总而言之,通过发送包含大(大于64KBSIG记录的DNS响应,我们可以在小的分配缓冲区上引起大约64KB的基于堆的受控缓冲区溢出。

触发漏洞

现在我们可以使受害DNS服务器查询我们的DNS服务器以解决各种问题,我们已经有效地将其转变为客户端。我们可以使受害DNS服务器询问我们的恶意DNS服务器特定类型的查询,并分别以匹配的恶意响应进行回答。

我们认为触发此漏洞所需要做的只是使受害DNS服务器向我们查询SIG记录,并为其回答带有长签名(长度> = 64KB)的SIG响应。我们很失望地发现基于UDPDNS的大小限制为512字节(如果服务器支持EDNS0,则为4,096字节)。无论如何,这不足以触发漏洞。

但是,如果服务器出于正当理由发送大于4,096字节的响应会怎样?例如,冗长的TXT响应或可以解析为多个IP地址的主机名。

DNS截断但是,还有更多!

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

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

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

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

image.png

8DNS over TCP消息的前两个字节代表消息的长度。

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

DNS指针压缩少即是多

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

image.png

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

您可以看到Wireshark 0xc00c将答案的名称字段中的字节评估为research.checkpoint.com。问题是,为什么?

根据对DNS的热烈欢迎,powerdns.org表示
为了将尽可能多的信息压缩到512字节中,可以(通常必须)压缩DNS名称……在这种情况下,答案的DNS名称编码为0xc0 0x0cc0部分设置了两个最高有效位,表示接下来的6 + 8位是指向消息中较早位置的指针。在这种情况下,这指向数据包内的位置12= 0x0c),紧随DNS头之后。

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

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

image.png

10<size> <value>链的示意图。

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

Name_PacketNameToCountNameEx结果] + [0x14] + [签名字段的长度(rdi– rax]

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

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

让我们用连接到的WinDBG进行测试dns.exe

image.png

我们坠毁了!

尽管似乎由于试图将值写入未映射的内存而使我们崩溃,但是可以以允许我们覆盖一些有意义的值的方式来调整堆的形状。

还值得一提的是,由于SIG记录和RRSIG记录具有相同的结构,因此Microsoft使用相同的函数(SigWireRead)来解析这两种记录类型。在检查时可以看到RRWireReadTable:索引0x1824)和460x2e)都指向该函数SigWireRead
这意味着记录类型SIGRRSIG均可用于触发此漏洞,因为它们是由相同的易受攻击的函数-解析的SigWireRead

dns.exe可以在线获取以前的利用尝试。例如:更深入地了解ms11-058

从浏览器触发

我们知道此错误可能是由LAN环境中存在的恶意参与者触发的。但是,我们认为看看是否可以在没有LAN访问权限的情况下远程触发此错误会很有趣。

HTTP中走私DNS

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

image.png

11DNS 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://target-dns:53/带有二进制数据的HTTP POST请求发送到目标DNS服务器()来滥用连接重用管道传递功能,该二进制数据在POST数据中包含另一个走私的” DNS查询,需要分别进行查询。

我们的HTTP有效负载包括以下内容:

·       HTTP请求头,我们不控制(User-AgentReferer,等)。

·       填充,以便第一个DNS查询在POST数据内具有适当的长度(0x504f)。

·       POST数据中的走私” DNS查询。

image.png

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

实际上,大多数流行的浏览器(例如Google ChromeMozilla Firefox)都不允许HTTP请求访问端口53,因此只能在有限的一组Web浏览器中利用此bug,包括Internet ExplorerMicrosoft 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客户端中。

image.png

13Sig_RecordReadfrom的反汇编片段dnsapi.dll

看起来,与不同dns.exe!SigWireReaddnsapi.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 GuardCFG)编译的,这意味着覆盖内存中函数指针的传统方法不足以利用此bug。如果此二进制文件不是使用CFG编译的,那么利用此错误将非常简单,因为很早以前我们就遇到了以下崩溃:

image.png

14:在崩溃ntdll!LdrpValidateUserCallTarget

如您所见,我们在坠毁ntdll!LdrpValidateUserCallTarget。这是负责验证作为CFG一部分的函数指针目标的函数。我们可以看到,待验证的指针(rcx)是完全可控的,这意味着我们在此过程中的某处成功重写了函数指针。我们看到崩溃的原因是,函数指针被用作每个地址具有允许” /“不允许位的全局位图表的索引,而我们的任意地址导致对该表本身中未映射页面的读取。

为了在克服CFG的同时将此漏洞利用到完整的远程代码执行中,我们需要找到具有以下功能的原语:在哪里写(精确地覆盖堆栈上的返回地址)和信息泄漏(泄漏内存地址) ,例如堆栈)。

信息泄漏

为了实现Infoleak原语,我们使用溢出来破坏仍在缓存中的DNS资源记录的元数据。然后,当再次从缓存中查询时,我们能够泄漏相邻的堆内存。

WinDNS的堆管理器

WinDNS使用该功能Mem_Alloc动态分配内存。此功能管理自己的内存池,以用作有效的缓存。有4个内存池存储区,用于不同的分配大小(最大为0x500x680x880xA0)。如果请求的分配大小大于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已经可以抵御这种威胁

披露时间表

·       2020519Microsoft提交的初次报告。

·       2020618– Microsoft发布了此漏洞的CVE-2020-1350

·       202079– Microsoft承认此问题为CVSS评分为10.0的可蠕虫,严重漏洞。

·       2020714微软发布了修复程序(星期二修补程序)。

参考资料

非常感谢我的同事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/