最近在整理以前的笔记,看到了刚开始学习IoT安全时候写的一篇流程文章,当时是跟着《解密家用路由器0day漏洞挖掘技术》一步步走的。那个时候对MIPS下的ROP、一些基础安全知识掌握得都不是很熟悉,如今重新看到了当时的笔记,决定独立重新再做一遍。
漏洞的原理很简单,就是由于sprintf函数导致的缓冲区溢出。数据源来自网络包的cookie,未做长度的校验就送入到了sprintf函数中拼接字符串。漏洞的利用难点在于如何通过网络数据包发送带有00截断的system函数地址,解决方案是先将system函数地址进行数学运算保存在栈上,然后从栈上恢复后再运算恢复system函数地址。
环境搭建
FirmAE是真滴好用,这篇文章我的重心也是要放在ROP利用上,因此手动搭建环境的流程就直接略过了。
固件下载地址在参考链接中,FirmAE是使用debug模式启动的,可以连接到仿真虚拟机的终端中,方便调试。
这里可以看到FirmAE对该虚拟机的编号IID等于3,就可以直接在images目录中去得到压缩的文件系统,不用再次使用binwalk解压出来一堆东西。
漏洞分析
漏洞是发生在程序/htdocs/cgibin中,该程序通过web服务程序httpd调用,httpd根据前端的请求将数据通过环境变量和标准输入传递给cgibin,cgibin执行完毕后将结果通过标准输出返回给httpd。
漏洞触发的流程如下:
httpd:
main->httpd_main->sub_407130->sub_406DB4->process_request->process_cgi->spawn 创建CGI进程
CGI:
main->hedwigcgi_main
对该漏洞调试涉及到了gdb的多进程调试,具体可以参考我之前写的这篇文章:调试httpd调用的cgibin程序,此处不再赘述,就简单说一下即可:
首先在FirmAE中找到httpd服务的进程,然后通过gdbserver attach上去:
在宿主机中执行如下的gdb调试命令,我是选择将它们全部保存到一个调试文件中,然后gdb-multiarch使用-x参数启动时运行调试命令:
1 | target remote 192.168.0.1:1234 |
在此处对路由器进行发包,格式在本文后续中有。然后就会断在执行函数fork处,执行如下命令准备调试子进程cgibin:
1 | set follow-fork-mode child # 调试cgibin子进程 |
执行到cgibin中,进行调试:
1 | b *0x00409660 |
存在溢出的缓冲区起始地址为sp+0xC0,hedwigcgi_main函数返回地址保存在sp+0x4E4,因此需要填充0x424个字节才能开始覆盖返回地址。v27的格式前17个字节是固定的字符串/runtime/session,随后的数据都是可控的,因此控制uid=
后填充0x424-0x11=0x413个字节开始覆盖返回地址。
1 | int hedwigcgi_main() |
因此简单构造如下的数据包就可以控制函数的返回地址为0xdeadbeef:
1 | POST /hedwig.cgi HTTP/1.1 |
漏洞利用
查看system函数的地址,system函数位于哪一个动态库中,以及动态库的加载地址:
1 | pwndbg> info address system |
可以看到函数system地址的最后一个字节是00,如果直接使用的话,在数据包的处理阶段有许多字符串操作函数可能直接就截断了,也可能在格式化字符串函数sprintf处被截断。因此在ROP的时候需要进行一些运算使得payload中不包含00,且可以恢复到函数system地址。
常用的运算方式例如使用xor计算、先将地址进行加减运算然后恢复等,此处采用的是后者。
补充一个吧,一开始自己走了弯路,在cgibin中去找了大半天的gadget,然后才想到cgibin的加载地址前两位肯定是00,最后才恍然大悟,应该在libc.so.0->libuClibc-0.9.30.1.so中去寻找。
由于system函数的地址是经过计算后才能跳转执行,因此通常是选择将运算结果保存到t9,然后jr指令跳转执行。通过执行
mipsrop.find("move $t9")
发现了不少可以使用的指令,都是从其他寄存器中赋值给t9。
如下的这条gadget品相就极好,不仅从s0恢复了system函数的地址,还顺带设置好了栈上的字符串地址到s5再传递给a0。这条地址也可以通过mipsrop.stackfinder()
找到。1
2
3
4
5
6
7# gadget2
.text:000159CC addiu $s5, $sp, 0x14C+var_13C
.text:000159D0 move $a1, $s3
.text:000159D4 move $a2, $s1
.text:000159D8 move $t9, $s0
.text:000159DC jalr $t9 ;
.text:000159E0 move $a0, $s5以s0为例作为中间保存值的寄存器,执行
mipsrop.find("addiu $s0")
。
如下的gadget对s0执行了加法操作,恢复了system函数地址,还从栈上恢复了s5寄存器,用于上一条gadget传参到a0。1
2
3
4
5# gadget1
.text:0002D194 addiu $s0, 1
.text:0002D198 move $t9, $s5
.text:0002D19C jalr $t9
.text:0002D1A0 nop
ROP构造
可以被控制的缓冲区地址是sp+0xC0+0x11=sp+0xD1。s0寄存器从sp+0x4C0开始恢复,因此填充0x3EF个字节开始覆盖;s5寄存器保存在sp+0x4C0+0x14=sp+0x4D4,因此填充0x403个字节开始覆盖;ra寄存器在之前计算过,是填充0x413个字节开始覆盖。
1 | rop = b"A" * 0x3EF |
验证如下,成功开始执行gadget:
但是此时还没有将要执行的命令布局到栈上。根据gadget2,命令在sp+0x10处,因此:
1 | rop += b"A" * 0x10 |
验证如下,要执行的命令和缓冲区中的数据结合了,但也证明了成功利用:
EXP
修改完毕的一个exp如下,这个exp在我的环境中调试是可以输入命令到system函数的,但是执行命令似乎失败了。不管了。。。ROP已经调试好了。
1 | from pwn import * |
小结
整个复现过程用了大概一天的时间,主要耗费在了找gadget上。我一开始是直接在cgibin程序中去找的gadget,等找了一大串又多又臭的ROP链才想到,cgibin程序本身的加载地址就是以00开头的。后面才醒悟过来应该在libc库中去寻找gadget。