mips_pwn
一、mips架构概述
1.寄存器
寄存器 | 别名 | 用途 |
---|---|---|
$0 | $zero | 常量0 |
$1 | $at | 保留给汇编器(Assembler Temporary)。在汇编过程中用于一些临时计算,程序员不应直接使用。 |
$2-$3 | \(v0-\)v1 | 用于存储函数的返回值。 |
$4-$7 | \(a0-\)a3 | 函数调用参数,用于传递最多 4 个函数参数。 |
$8-$15 | \(t0-\)t7 | 临时寄存器。用于函数内部的临时计算,不需要保存其值。 |
$16-$23 | \(s0-\)s7 | 保存寄存器。用于保存函数调用期间的值,调用函数时需要保留的值。 |
$24-$25 | \(t8-\)t9 | 临时寄存器。与 $t0-$t7
类似,但通常不需要在函数调用中保存其值。 |
$26-$27 | \(k0-\)k1 | 保留给操作系统内核。通常用于内核中进行系统调用或中断处理。 |
$28 | $gp | 全局指针。指向全局数据区域的基地址,便于访问全局变量。 |
$29 | $sp | 堆栈指针。指向当前堆栈的顶部,用于管理函数调用和局部变量。 |
$30 | \(fp(\)s8) | 帧指针。指向当前栈帧的基地址,通常用于访问局部变量和参数。 |
$31 | $ra | 返回地址。用于存储函数调用的返回地址,在函数调用时保存,并在函数返回时使用。 |
PC | PC | 保存当前正在执行的指令的地址,并在每次指令执行后自动递增,以指向下一条指令的地址。 |
mips架构中的fp寄存器相当于rbp,pc寄存器相当于rip。
2.特征
mips架构由于本身特性不支持nx,所以栈段具有执行权限
MIPS 处理器通常将指令缓存(I-cache)和数据缓存(D-cache)分开,这有助于提高访问效率和减少缓存冲突。
所有 MIPS 指令都具有固定的 32 位长度,这使得指令解码更加简单和高效。
MIPS 默认使用大端字节序,即最显著字节存储在最低地址。虽然 MIPS 也支持小端字节序,但大端字节序是 MIPS 的传统配置。
三种主要指令格式:
- R 型:用于寄存器间操作(算术、逻辑等),例如
add
、sub
。 - I 型:用于立即数操作、加载和存储、分支等,例如
addi
、lw
。 - J 型:用于跳转,例如
j
、jal
。
- R 型:用于寄存器间操作(算术、逻辑等),例如
流水线操作
MIPS架构采用了流水线技术来提高指令执行的效率。流水线允许处理器同时处理多条指令的不同部分,从而大幅提高吞吐量。
常见的MIPS芯片流水线操作分为五个阶段:
- IF(Instruction Fetch,指令提取):从内存中提取指令。
- ID(Instruction Decode,指令解码):对提取的指令进行解码,确定需要执行的操作。
- EX(Execute,执行):执行指令,包括算术运算、逻辑运算等。
- MEM(Memory Access,存储器访问):访问内存,读取或写入数据。
- WB(Write Back,寄存器写回):将执行结果写回寄存器。
在理想情况下,流水线中的每个阶段都会同时进行,使得处理器可以每个时钟周期执行一条新指令。然而,由于某些指令的执行需要更多的时间,可能会导致流水线暂停(称为“流水线停顿”),从而影响性能。
分支延迟槽
MIPS架构有一个特殊的概念叫分支延迟槽。当程序遇到分支指令(如跳转指令)时,程序会跳转到新的地址去执行新指令。然而,由于流水线的设计,紧接在分支指令之后的指令已经在流水线中开始执行了。为了避免浪费,MIPS架构规定,分支后的第一条指令(即位于分支延迟槽中的指令)会在跳转之前执行。
这意味着,在编写MIPS汇编代码或分析MIPS的二进制文件时,需要特别注意分支延迟槽的存在。例如,在以下MIPS汇编代码中:
1
2
3.text:0007F944 move $t9, $s0
.text:0007F948 jalr $t9
.text:0007F94C move $a0, $s1虽然
jalr
指令是一个跳转指令,但紧接在其后的move $a0, $s1
指令会在跳转之前执行。一般而言跳转指令的下一条指令会是nop,但这种行为在查找利用漏洞的gadgets以及构造payload时非常重要。
3.指令格式
op:指令基本操作,称为操作码。 rs:第一个源操作数寄存器。 rt:第二个源操作数寄存器。 rd:存放操作结果的目的操作数。 shamt:位移量; funct:函数,这个字段选择op操作的某个特定变体。
32位长度分配如下
R格式
6 | 5 | 5 | 5 | 5 | 6 |
---|---|---|---|---|---|
op | rs | rt | rd | shamt | funct |
用于寄存器间操作(算术、逻辑等),例如
add
、sub
。
I格式
6 | 5 | 5 | 16 |
---|---|---|---|
op | rs | rt | 立即数操作 |
用于立即数操作、加载和存储、分支等,例如
addi
、lw
。
J格式
6 | 26 |
---|---|
op | 跳转地址 |
用于跳转,例如 j
、jal
。
4.常用指令
指令 | 功能 | 语法 | 示例 | 解释 |
---|---|---|---|---|
add |
加法(有符号) | add $rd, $rs, $rt |
add $t0, $t1, $t2 |
将 $t1 和 $t2 的值相加,结果存储在
$t0 中。 |
addu |
加法(无符号) | addu $rd, $rs, $rt |
addu $t0, $t1, $t2 |
将 $t1 和 $t2
的值相加(无符号),结果存储在 $t0 中。 |
sub |
减法(有符号) | sub $rd, $rs, $rt |
sub $t0, $t1, $t2 |
将 $t1 的值减去 $t2 的值,结果存储在
$t0 中。 |
subu |
减法(无符号) | subu $rd, $rs, $rt |
subu $t0, $t1, $t2 |
将 $t1 的值减去 $t2
的值(无符号),结果存储在 $t0 中。 |
and |
按位与 | and $rd, $rs, $rt |
and $t0, $t1, $t2 |
将 $t1 和 $t2 的值按位与,结果存储在
$t0 中。 |
or |
按位或 | or $rd, $rs, $rt |
or $t0, $t1, $t2 |
将 $t1 和 $t2 的值按位或,结果存储在
$t0 中。 |
xor |
按位异或 | xor $rd, $rs, $rt |
xor $t0, $t1, $t2 |
将 $t1 和 $t2 的值按位异或,结果存储在
$t0 中。 |
nor |
按位与非 | nor $rd, $rs, $rt |
nor $t0, $t1, $t2 |
将 $t1 和 $t2 的值按位与非,结果存储在
$t0 中。 |
sll |
左移 | sll $rd, $rt, shamt |
sll $t0, $t1, 2 |
将 $t1 的值左移 2 位,结果存储在 $t0
中。 |
srl |
逻辑右移 | srl $rd, $rt, shamt |
srl $t0, $t1, 2 |
将 $t1 的值右移 2 位,结果存储在 $t0
中。 |
sra |
算术右移 | sra $rd, $rt, shamt |
sra $t0, $t1, 2 |
将 $t1 的值算术右移 2 位,结果存储在 $t0
中 |
lw |
加载字(32 位) | lw $rt, offset($rs) |
lw $t0, 4($a0) |
从地址 $a0 + 4 处加载 4 字节数据到
$t0 。 |
sw |
存储字(32 位) | sw $rt, offset($rs) |
sw $t0, 4($a0) |
将 $t0 中的数据存储到地址 $a0 + 4
处。 |
lb |
加载字节(8 位) | lb $rt, offset($rs) |
lb $t0, 0($a0) |
从地址 $a0 处加载 1 字节数据到 $t0 。 |
sb |
存储字节(8 位) | sb $rt, offset($rs) |
sb $t0, 0($a0) |
将 $t0 中的 1 字节数据存储到地址 $a0
处。 |
lui |
加载上半字(立即数) | lui $rt, imm |
lui $t0, 0x1234 |
将立即数 0x1234 加载到 $t0 的高 16 位(低
16 位为 0)。 |
ori |
立即数按位或 | ori $rt, $rs, imm |
ori $t0, $t1, 0xFF |
将 $t1 和立即数 0xFF 按位或,结果存储在
$t0 中。 |
beq |
等于分支 | beq $rs, $rt, offset |
beq $t0, $t1, label |
如果 $t0 等于 $t1 ,则跳转到
label 。 |
bne |
不等于分支 | bne $rs, $rt, offset |
bne $t0, $t1, label |
如果 $t0 不等于 $t1 ,则跳转到
label 。 |
j |
无条件跳转 | j target |
j label |
跳转到 label 处。 |
jal |
跳转并链接 | jal target |
jal subroutine |
跳转到 subroutine ,并将返回地址存储在 $ra
寄存器中。 |
jr |
跳转寄存器 | jr $rs |
jr $ra |
跳转到 $ra 寄存器中存储的地址。 |
nop |
空操作 | nop |
nop |
什么也不做,通常用于填充延迟槽。 |
1 | or s8,sp,zero #实现了x86架构中的mov功能,相当于mov s8,sp |
5.MIPS栈帧结构
典型的MIPS栈帧结构包括以下部分:
1 | +-------------------------+ <-- 栈顶(高地址) |
MIPS架构中的栈通常是向下增长的,这意味着随着栈的推进,栈顶指针($sp
)的值会递减。其中局部变量的寻址是通过\(sp或\)fp进行的。
mips函数调用基本格式,其中分为叶子函数和非叶子函数,一般pwn题中做的都是非叶子函数,因为main函数之前程序还会执行其他初始化函数。
叶子函数和非叶子函数的主要区别在于它们是否调用其他函数:
- 叶子函数:
- 定义:叶子函数是指在其内部不调用任何其他函数的函数。
- 特点
- 由于不调用其他函数,因此不需要保存和恢复返回地址(即
$ra
寄存器的值)。 - 叶子函数通常不需要额外的栈操作,因为它不需要保存其他函数的返回地址或其他寄存器的值。
- 返回时直接使用
jr $ra
指令跳转回调用者。
- 由于不调用其他函数,因此不需要保存和恢复返回地址(即
- 非叶子函数:
- 定义:非叶子函数是指在其内部会调用其他函数的函数。
- 特点
- 由于可能调用其他函数,需要保存当前函数的返回地址(存储在
$ra
寄存器中)到栈中,以防止被覆盖。 - 非叶子函数通常需要调整栈指针(
$sp
)并保存调用者的返回地址、寄存器状态等信息。 - 在返回时,需要从栈中恢复保存的返回地址和寄存器状态,然后使用
jr $ra
指令返回到调用者。
- 由于可能调用其他函数,需要保存当前函数的返回地址(存储在
简而言之,叶子函数不会调用其他函数,因此对栈的操作较少;而非叶子函数会调用其他函数,因此需要处理更多的栈操作来保存和恢复状态。
非叶子函数:
1 | #Prologue |
叶子函数:
1 | # 执行函数B的任务 |
二、mips环境搭建
1.安装qemu
1 | sudo apt install qemu |
2.安装gdb-multiarch
1 | sudo apt install gdb-multiarch |
3.安装ghidra
用于反编译mips指令,吾爱提供的有些IDA只包含x86和x64的Hex-Rays Decompiler插件
ghidra下载地址:https://github.com/NationalSecurityAgency/ghidra/releases
运行ghidra还需要JDK17及以上的环境
jdk下载地址:https://adoptium.net/zh-CN/
启动 Ghidra:
Windows
- 进入 Ghidra 的安装目录,双击
ghidraRun.bat
文件启动 Ghidra。
- 进入 Ghidra 的安装目录,双击
Linux/macOS
打开终端,导航到 Ghidra 的安装目录,然后运行以下命令启动 Ghidra:
1
./ghidraRun
初次运行设置:
- Ghidra 启动后会提示你设置用户目录,你可以选择默认路径或自定义路径。
- 阅读并接受用户协议后,Ghidra 会启动并显示主界面。
4.安装IDA插件mipsrop
这里我用的是吾爱的IDA_Pro_v8.3_Portable,其他版本情况可能会有不同。
1 | git clone https://github.com/devttys0/ida.git ida-plugins |
mipsrop.py在 ida-plugins/plugins/mipsrop目录下,将其复制进IDA的plugins目录即可
可能遇到的问题

在ida-plugins/plugins目录下还有个shims文件夹,将其也复制到IDA的plugins目录就行。
5.调试方法
1 | qemu-mipsel-static -g 6666 -L ./ ./program #开的端口是6666 |
之后用gdb连接
1 | gdb-multiarch program |
在python写pwn利用脚本过程中,可以在在process中指定打开的端口,然后附加到gdb时就可以连接。
1 | programe = 'your_program' |
三、mips的一些栈上漏洞利用
这里以32位的mips(o32 ABI)为例,其余原理相同。
1.栈溢出+syscall
如果一个函数是非叶子函数,则其返回地址也会出现在栈上,最后会读取该地址并返回,类似于x86架构,那我们就可以覆盖返回地址实现ROP,mips架构中比较麻烦的是寻找gadget,这里我们用的是IDA的mipsrop插件。
由于mips架构是没有NX保护的,其实我们可以把shellcode写到栈上后想办法跳转到shellcode处执行。
我们可以先用mipsrop.stackfinders()
这个方法来获取能把栈相关地址写到某个寄存器的gadget,然后定位到control
jump中为jalr \(fp的那个,因为\)fp也是一个栈相关的地址,正好位于返回地址向低地址偏移4字节处,如果能栈溢出的话也能进行控制$fp位置的内容。

然后既然能控制\(a2寄存器的值为一个栈上的可控地址,那么只要我们再找到一个能跳转到\)a2的gadget,将其写入$fp的位置处,就能实现ret2syscall。move $t9,reg
后面一般都会找到对应的跳转语句。

之后就可以手搓execve系统调用的shellcode了,系统调用号可以在https://syscalls.w3challs.com/?arch=mips_o32这查,v0存系统调用号,a0,a1,a2分别存三个参数,可以通过将字符串写到栈顶,在把参数指向$sp,就能实现字符参数的传递了。
1 | shellcode = """ |
这里需要注意的一点是execve的第一个参数最好是/bin/sh,如果图方便只传进去一个sh,因为后面的环境变量参数置零了,很可能会找不到sh报警告,继续向下执行。

四、题目复现
[xyctf2024]Ez1.0

非常简单粗暴的栈溢出,根据上述漏洞利用原理构造即可
1 | from pwn import * |
- 标题: mips_pwn
- 作者: collectcrop
- 创建于 : 2024-09-21 18:03:16
- 更新于 : 2024-09-21 18:53:29
- 链接: https://collectcrop.github.io/2024/09/21/mips-pwn/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。