查看原文
其他

CVE-2020-1350 SigRedDNS漏洞分析

Adventure 看雪学苑 2022-07-01

本文为看雪论坛优秀文章

看雪论坛作者ID:Adventure



一、漏洞介绍




CVE-2020-1350是DNS.exe在处理畸形DNS Sig消息时,由于对数据包的字段校验不严格,导致了整型溢出。
 
DNS SIG 消息中包含对DNS记录集合的数字签名,DNS记录集合就是一种名字相同,或者类型相同的DNS消息集合。
 
攻击者可以配置一个恶意的域名TestDnsRce.com,该域名的DNS指向的DNS服务器作为本次攻击的目标。
 
* 当客户端查询evildomain.com的DNS记录;

* 目标DNS服务器向根域名服务器查询evildomain.com的NS记录;

* 根域名服务器告诉目标DNS,evildomain.com的权威DNS服务器是3.3.3.3,并该记录缓存起来;

* 客户端向目标服务器查询evildomain.com的Sig记录;

* 目标服务器将请求转发给权威DNS服务器;

* 权威DNS服务器返回畸形Sig查询结果;
 
目标DNS服务器处理SigQuery的畸形消息时触发漏洞第一现场;
 
参考下图:


漏洞在dns!SigWireRead函数中,该函数用来缓存从另一台DNS服务器返回的DNS Sig记录。


根据上图漏洞函数中的第11行,RR_AllocateEx传入了一个16位无符号整型,经过计算后作为申请内存的大小参数。
 
假如传入的Sig查询记录消息包,经过计算后使大小超出0xFFFF,就会触发整型溢出漏洞;


二、触发漏洞




由于整个消息包大小是有限制的,不能通过类似响应一个数据很大Signature的消息包来触发该漏洞;
 
也不能通过UDP来触发该漏洞,因为DNS消息通过UDP传输的大小也是有限制的,上限为512或4096字节,这取决于目标服务器是否支持DNS扩展机制(EDNSO);
 
如果使用DNS截断,就会先发送一个UDP请求,如果响应消息被截断,TC字段被设置,就会尝试用TCP协议重新请求,TCP协议传输DNS消息最大限制是64KB(0xFFFF),仍然不够触发漏洞;

1. DNS名称压缩


在使用TCP协议传输时,通过修改DNS消息中被压缩的名称,可以导致SigName.Length变大, 而且不改变整个DNS消息包的大小;
 
下图为DNS 消息中使用名称压缩的数据包:


红色框 - DNS消息开始至偏移0xC的数据;
 
绿色框 - Flag标记,0xC0 代表DNS名称位置相对偏移在DNS消息中;
 
蓝色框 - DNS名称的偏移,0x0C,表示从DNS消息头开始,偏移0x0C字节才是DNS名称的开始位置;
 
黄色框 - 简单编码后的DNS名称;
 
DNS名称被使用以下方式进行编码,域名中的每个“点”作为分隔符,替换成了每个点后面的字符个数,并以NULL结尾;
 
例如:www.google.com


编码后:3www6google3com
 
在上面捕获的DNS流量数据包中,因为请求的域名是以 “9.xxx"开头的,所以偏移指向0x1;
 
因此,如果我们将偏移0xC改成0xD,域名开始的偏移就会指向0x39,表示域名的下一部分有0x39个字节;从而导致下一部分名称数据会扩展到数据包中的签名部分;
 
如之前在dns!SigWireRead函数中所见,传入RR_AllocateEx参数的大小是经过计算的, (signatureLength + SIgName.Length +0x14);
 
DNS名称最大长度为0xFF字节,从伪造的DNS名称中多出来的大小足以触发整型溢出漏洞,并且整包数据大小不会超过65KB的限制;


三、漏洞利用




该漏洞利用最耗时的是了解如何正确操作堆布局的方法;下面部分重点介绍如何利用堆布局避免程序Crash并控制堆内存的释放和分配;

1. WinDNS堆管理器


首先需要了解一下WinDNS服务是如何管理堆内存的;
 
WinDNS服务有它自己的堆内存池,如果申请的Buffer大小超出0xA0字节,WinDNS将使用Windows自带堆管理器(HeapAlloc);
 
否则,WinDNS使用自己的内存池,按照申请的内存大小分类,分别是:0x50,0x68,0x88,0xA0;每种大小,使用单项链表关联起来;
 
如果链表中没有可用的WinDNS内存,则使用Windows原生堆来申请一个大内存块(memory chunk);然后按照大小分别划分给WinDNS不同的单项链表中;
 
例如,要分配的内存大小是:0x50,0x68, 0x88,0xA0;则对应的内存块大小是:0xFF0, 0xFD8, 0xFF0, 0xFA0,
 
每个内存块可分配的WinDNS内存数量:
 

下图为dns!Mem_Alloc伪代码:


当WinDNS的一块内存被释放时,并不会直接释放内存,而是将内存块再次添加对大小的链表中;减少实际内存分配消耗;
 
WinDNS内存的分配和释放遵循最基本的Last In First Out (LIFO)规则, 最后一个释放的,将会再下一次申请时被使用;
 
下图为dns!Mem_Free伪代码:


2. WinDNS Buffer 结构



了解WinDNS内存管理机制,让我们在后面构造堆布局时更方便;

3.如何避免堆拷贝溢出时程序Crash


编写POC时,遇到的第一个问题就是通过memcpy拷贝Signature数据到申请的堆内存时,发生了访问违规;
 
我们必须报证拷贝溢出部分的内存被拷贝到有效的内存地址上;
 
在观察堆布局时发现,Windows把我们使用原始堆申请的WinDNS内存块,放在名为"Internal"的堆Segments上;
 
Internal Heap Sigments的大小为:0x41fd0-0x41ff0,经过观察,这些Heap Segments,只会被WinDNS用来申请内存块;


如果我们能报证被覆盖的内存大小低于0xA0,就能报证被覆盖的内存块一定是Heap Segments这里的其中一个内存块;

4. 构造堆坑


如果我们可以释放这些连续Heap Segments中的一块内存,然后重新申请到刚刚被释放的内存,然后再进行溢出我们可以使溢出的数据覆盖到有效的内存地址,这也是最常用的堆溢出利用技术;
 
在这种场景下,我们可以让客户端发送一个Sig请求给DNS服务器,服务器可以在返回的数据包中设置短TTL,来控制内存释放;
 
同样的,如果我们想让一块内存长时间存在,则可以修改为长TTL;每两分钟就会释放一次TTL过期的内存。
 
TTL(Time To Live)通常用秒来表示,用来表示DNS查询缓存记录的有效时间,不可能一直有效,当过期后,就会丢弃缓存数据,并向上层权威名称服务器获取最新数据;


构造堆坑的经典步骤如下:
 
(1) 使VictimDNS服务器向EvilDNS服务器发送大量subdomain查询请求;
 
(2) VictimDNS服务器得到EvilDNS服务器响应后,会将数据缓存在堆内存中(Heap Spray);
 
(3) EvilDNS服务器响应subdomain查询请求时,设置一个为短TTL,剩余全部为长TTL;
 
(4) 等待被设置短TTL的DNS响应包内存被释放,WinDNS每两分钟释放一次过期的数据包;
 
(5) 再次请求subdomain查询,EvilDNS服务器会响应一个畸形DNS包,触发整型溢出;
 
(6) 因为内存是LIFO分配规则,上一步请求的DNS缓存记录,将会使用第4步释放的堆坑内存;
 
下图为使用堆喷避免crash:


现在可以稳定避免在拷贝数据时发生Crash了,但由于覆盖了堆上一些其它对象导致不定时Crash;
 
下图为覆盖了CacheTreeNode后发生Crash。


通过在Windbg中观察什么到底是什么类型内存,挨着被我们覆盖的内存;
 
下图为在Windbg中跟踪内存申请。


可以看到在我们溢出的Buffer附近,两块新的WinDNS内存块被申请了,大小分别是0xFF0和0xFA0,(后者是由于堆喷,记录大小是0xA0导致的)
 
那0xFF0 大小的内存块是干啥的?这个内存块的划分大小是0x88,用来保存DNS记录缓存的二进制Tree对象;被我们覆盖的正是缓存树对象;
 
当遍历树时就会发生Crash;
 
此时思路已经很清楚了,还记得被覆盖的内存是在Heap Segments中的,该内存只能被WinDNS用来管理内存块;
 
也就是被覆盖的对象大小<= 0xA0 , 并且是WinDNS内存块管理的可用大小0x55、0x68、0x88、0xA0 这些大小;
 
我们知道WinDNS管理的内存被释放后,不会还给Windows,而是添加到相应大小的链表中;
 
我们可以强制分配大量大小为0x88的内存,并释放这些内存,一旦释放后,这些内存就会被添加到WinDNS的FreeList链表;
 
避免从Windows中分配新的堆块,然后向WinDNS中申请大量不会释放的内存;确保我们覆盖的内存位于新的HeapSegments中。
 
这样就不会覆盖到堆中一些重要对象;
 
下图为堆梳理,避免溢出时覆盖重要的对象。



四、覆盖堆上的对象




通过前面的布局,我们已经可以为我们溢出对象在堆上构造一个坑了,通过这个坑,我们可以覆盖被我们用来堆喷的DNS缓存记录对象;

1. 了解内存环境


因为我们使用堆喷申请了很多内存块,这些内存被连续添加FreeList中,代表它们以连续的顺序进行分配。
 
所以我们进行DNS SIG记录查询的顺序,就是它们在堆上出现的顺序,我们必须知道溢出时,我们会覆盖哪些内存;
 
下图为使用一个伪造的Record对象覆盖内存:


2. RR_Record 结构


首先来看看我们用来堆喷的对象结构,缓存的WinDNS Record:


了解这些结构和WINDNS_BUFF结构,将会更方便构造RR_Record对象;

3. 控制内存释放


前面,我们通过控制Record对象的TTL字段,等待TTL时间到期后,每过2分钟就会触发一轮释放;
 
每次都要等2分钟,不是我们没有耐心,而是这会影响到我们控制重新申请内存;如果可以立即释放内存就好了;
 
当一个RR_Record对象从缓存中响应给对应的NS查询时,首先会检查dwTTL 和 dwTimeStamp字段是否已经过期;
 
因为每2分钟才清理一次过期的缓存记录对象,可能会发生要查询的缓存记录对象已经过期了的场景;
 
我们可以在伪造一个RR_Record对象时,将dwTTL字段和dwTimeStamp字段设置为0,然后查询对应的subdomain就会导致这块伪造的对象内存被立即释放;

4. 控制内存申请


掌握了前面的控制WinDNS内存释放, 现在控制内存申请其实会方便了,因为WinDNS内存申请基于LIFO;
 
一旦我们释放了一块内存,我们下一次申请同样大小的内存时,一定是我们前面释放的那块内存;
 
因为我们同样可以控制WINDNS_BUFF结构,我们可以伪造原始Buffer大小,这样我们就可以控制WinDNS从内存块中返回给我们内存的大小了;
 

下图为控制申请不同大小的内存:




五、泄露内存




1. 泄露堆地址


我们可以通过以下步骤泄露堆地址:

* 构造2个连续的RR_Record对象,释放第2个对象的内存;

* 修改第1个伪造RR_Record对象,wRecordSize字段大小,这个代表返回的数据大小;

* 给Victim发送对应第1个RR_Record对象的SIG查询,加上wRecordSize的大小;

* Victim响应时会根据实Buffer的大小来读取数据,包括刚刚释放的Record对象中WINDNS_FREE_BUF结构,堆地址就在pNextFreeBuff字段中;

下图为使用伪造的RR_Record对象泄露堆指针:


现在我们知道了一个堆地址,并且这个地址的内存我们可以控制,后面会用到;

2. 泄露 dns.exe 地址


下一步我们需要泄露dns.exe中的一个地址,来绕过ASLR;我们可以申请特殊类型的对象,这里选择DNS_Timeout对象;
 
下图为DNS_Timeout对象结构:
 

当DNS记录过期时,dns!RR_Free被调用;
 
如果DNS记录中类型是以下类型时,内存不会立即释放,而是会调用dns!TimeoutFreeWithFunctionEx函数。

* DNS_TYPE_NS

* DNS_TYPE_SOA

* DNS_TYPE_WINS

* DNS_TYPE_WINSR

下图为dns!RR_Free伪代码:



在Timeout_FreeWithFunctionEx函数中,会使用WinDNS为DNS_Timeout对象申请内存;
 
在下图中第13行,初始化DNS_Timeout对象时,将RR_Free函数地址赋值给了pTimeoutObj->pFreeFunction字段;
 
以及一个字符串变量pszFile赋值给了pTimeoutObj->pszFile字段;
 
通过之前写了Heap地址的方法,将DNS_Timeout对象申请到我们控制的内存中,就可以获取RR_Free函数地址,
 
进而等于拿到了dns.exe的基址;
 
下图为dns!Timeout_FreeWithFunctionEx函数伪代码:


通过Free伪造的RR_Record对象和伪造的wSize=0x50,该大小其实是申请一个DNS_Timeout对象需要的大小;
 
随后在RR_Free->Timeout_FreeWithFunctionEx函数中触发申请RRDNS_Timeout对象;
 
然后通过发送一些subdomain的NS查询请求到VictimDNS服务器,当查询记录过期时,就会为每个NS查询申请一个Timeout对象;
 
这里有必要多发送几个NS查询,如果等待NS查询记录过期时,已经被释放过0x50大小内存了;
 
只需要发送一个NS查询请求,Timeout对象上的伪造的RR_Record对象和伪造的wRecordSize,就可以泄露Timeout对象的内容了;
 
下图为通过申请Timeout对象泄露dns.exe地址:


现在我们有了一个dns.exe的基址,通过基址我们可以通过基址+固定偏移,来定位dns.exe内的任意函数位置;
 
甚至可以为各种dns.exe版本,做一个函数地址偏移的映射表;
 
最初,我以为可以简单的通过释放伪造的RR_Record对象,且对象的wRecordType= DNS_TYPE_NS 来触发申请Timeout对象的申请;
 
在做这些尝试时,对传入已修改wRecordType的伪造RR_Record对象,某些检查会阻止RR_Free的调用;


六、任意地址读




其实我们通过覆盖DNS_Timout对象的pFreeFunction指针,已经拥有任意代码执行的能力了!
 
在dns!Timeout_CleanDelayedFreeList函数中,会依次调用CoolingDelayedFreeList列表中每一个Tiemout对象的pFreeFunction指向的函数地址;
 
CoolingDelayedFreeList列表中的保存着即将被释放的DNS_Timeout对象;
 
幸运的是,Tiemout对象中包含一个可以传给pFreeFunction参数的字段;
 
下图是dns!Timeout_CleanDelayedFreeList函数伪代码:


我们可以等Timeout对象申请后通过覆盖Timtout对象的这些字段,来触发漏洞;
 
新版本的dns.exe编译时带了CFG,目前公开可以绕过CFG的方法:通过覆盖栈上的返回地址,然后执行ROP代码;
 
虽然目前我们没有找到稳定的方法来将数据写到栈上;但是可以在dns.exe中找一个对CFG有效的函数地址作为我们的读写能力;
 
dns!NsecDnsRecordConvert这个函数比较核实,它只有输入一个参数;
 
下图为NsecDnsRecordConvert函数可以接收的参数结构:


在函数内申请了一块内存,并且调用了Dns_StringCopy函数;这就是我们的任意地址读Primitive;
 
因为我们可以控制传入的函数参数和参数内容,我们可以将pDnsString指向我们想要读的地址;
 
在DNS_StringCopy函数内会申请一段内存,并将pDnsString指向的内存数据拷贝进去。
 
下图为NsecDnsRecordConvert函数伪代码:


因为我们可以控制wSize,我们可以控制上图中Rpc_AllocateRecord申请的内存大小;
 
我们可以控制大小,让它申请内存时,申请到我们布置好的堆坑上;等内存拷贝完,我们通过内存泄露来获取读取的数据;


我们读取的地址应该在dns.exe范围内,并且数据中包含一个msvcrt.dll的地址;
 
这里选择的是dns!_imp_exit 该地址范围内包含一个msvcrt!exit的函数地址,通过泄露该地址,我们就可以绕过msvcrt的ASLR;
 
拿到msvcrt的基址后,加固定偏移,就可以计算出msvcrt!system函数的地址;
 
需要注意的是:Dns_StringCopy希望拷贝源是一个NULL结尾的字符串;如果地址的最低有效字节是0x00,则计算出的字符串大小是1,并且该地址不会被复制;
 
在案例中的msvcrt文件,这个问题并不会影响,但是作者并没有测试所有版本的msvcrt;


七、远程代码执行




一切就绪,我们可以再次触发申请一个Timeout对象,然后覆盖pFreeFunction字段为msvcrt!system函数地址,以及pFreeFuncParam为一个堆内存地址,

该地址上保存着要执行的payload 命令;
 
为了获取反弹的shell,最简单的办法是使用mshta.exe来执行一个远程HTA文件。不过这个exp还有是存在更多扩展性和利用思路;


- End -




看雪ID:Adventure

https://bbs.pediy.com/user-home-749942.htm

  *本文由看雪论坛 Adventure  原创,转载请注明来自看雪社区。



《安卓高级研修班》2021年6月班火热招生中!



# 往期推荐





公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存