一、MIPS 架构基础:从历史到实战
各位同学,欢迎来到 MIPS 漏洞利用的第一课。在开始之前,我想先聊聊 MIPS 这个架构。说实话,我第一次接触 MIPS 是在做嵌入式路由器漏洞分析的时候。那时候我还在用 x86 的思路去调试,结果踩了不少坑。嗯,这节课我们就从最基础的东西讲起。
1.1 MIPS 的历史与设计哲学
MIPS 是 Microprocessor without Interlocked Pipeline Stages 的缩写。这个名字本身就透露了它的核心设计思想——精简指令集计算机(RISC)。
上世纪 80 年代,斯坦福大学的 John Hennessy 教授带领团队开发了 MIPS 架构。它的设计哲学可以概括为三点:
- 指令精简:每条指令完成一个简单操作,硬件实现更高效
- 流水线友好:指令格式规整,便于流水线执行
- 软件补位:硬件不做复杂操作,由编译器去优化
核心观点:MIPS 的设计理念是“让硬件做简单的事,让软件做复杂的事”。这和 x86 的 CISC 哲学正好相反。
我个人觉得,理解这个哲学很重要。你在做漏洞利用的时候,会发现 MIPS 的很多特性——比如延迟槽、没有硬件栈保护——都源于这种设计思路。
1.2 MIPS 寄存器体系
MIPS 有 32 个通用寄存器,每个 32 位(MIPS32)或 64 位(MIPS64)。我们做 32 位 MIPS 漏洞利用时,主要关注这 32 个寄存器。
| 寄存器编号 | 名称 | 用途 | 备注 |
|---|---|---|---|
| $0 | $zero | 恒为 0 | 写入无效,读取总是 0 |
| $1 | $at | 汇编临时变量 | 编译器使用,别乱动 |
| $2-$3 | $v0-$v1 | 函数返回值 | 漏洞利用时经常看 |
| $4-$7 | $a0-$a3 | 函数参数 | 前 4 个参数放这里 |
| $8-$15 | $t0-$t7 | 临时寄存器 | 调用者保存 |
| $16-$23 | $s0-$s7 | 保存寄存器 | 被调用者保存 |
| $24-$25 | $t8-$t9 | 临时寄存器 | 调用者保存 |
| $26-$27 | $k0-$k1 | 内核保留 | 别碰,会死机 |
| $28 | $gp | 全局指针 | 访问全局变量 |
| $29 | $sp | 栈指针 | 漏洞利用核心目标 |
| $30 | $fp | 帧指针 | 有些编译器当 $s8 用 |
| $31 | $ra | 返回地址 | 漏洞利用核心目标 |
实战提示:做 MIPS 漏洞利用时,$ra(返回地址)和 $sp(栈指针)是你最需要关注的寄存器。我曾经在分析一个路由器漏洞时,花了半天才发现是 $gp 被破坏了导致全局变量访问异常——这种坑你提前知道就好。
1.3 MIPS 指令格式:R/I/J 型
MIPS 指令长度固定为 32 位。这个特性让指令解码变得非常简单。指令分为三种基本格式:
R 型(Register)
用于寄存器之间的运算操作。
31 26 25 21 20 16 15 11 10 6 5 0
+--------+--------+--------+--------+--------+---------+
| opcode | rs | rt | rd | shamt | funct |
| 6 bit | 5 bit | 5 bit | 5 bit | 5 bit | 6 bit |
+--------+--------+--------+--------+--------+---------+
举个例子:add $t0, $t1, $t2 就是把 $t1 和 $t2 相加,结果存到 $t0。
I 型(Immediate)
用于带立即数的操作,以及分支跳转。
31 26 25 21 20 16 15 0
+--------+--------+--------+--------------------------------+
| opcode | rs | rt | immediate |
| 6 bit | 5 bit | 5 bit | 16 bit |
+--------+--------+--------+--------------------------------+
比如 addi $t0, $t1, 100 就是 $t0 = $t1 + 100。
J 型(Jump)
用于无条件跳转。
31 26 25 0
+--------+----------------------------------------------+
| opcode | target address |
| 6 bit | 26 bit |
+--------+----------------------------------------------+
注意:J 型指令的目标地址是 26 位,实际使用时需要左移 2 位并与 PC 高 4 位拼接。这意味着你只能在 256MB 范围内跳转。做漏洞利用时,如果目标地址跨了 256MB 边界,你就得用寄存器跳转(jr)了。
1.4 MIPS 寻址模式
MIPS 的寻址模式比 x86 简单得多。主要有以下几种:
- 寄存器寻址:操作数在寄存器中,比如
add $t0, $t1, $t2 - 立即数寻址:操作数是指令中的立即数,比如
addi $t0, $t1, 100 - 基址寻址:地址 = 基址寄存器 + 偏移量,比如
lw $t0, 0x10($sp) - PC 相对寻址:用于分支指令,目标地址 = PC + 4 + 偏移量 × 4
- 伪直接寻址:用于 J 型指令,目标地址 = (PC & 0xF0000000) | (target × 4)
你想想看,MIPS 没有 x86 那种复杂的变址寻址、基址变址寻址。这其实降低了硬件复杂度,但也意味着编译器需要做更多工作。
1.5 MIPS 内存布局与栈帧结构
这部分是漏洞利用的核心。我们先看内存布局:
MIPS 的栈帧结构和 x86 有些不同。我们来看一个典型的函数调用栈帧:
高地址
+------------------+
| 调用者栈帧 |
+------------------+
| 参数 5-8 (如果) | ← $sp (调用者视角)
+------------------+
| 保存的 $ra | 返回地址
+------------------+
| 保存的 $fp | 旧帧指针
+------------------+
| 保存的 $s0-$s7 | 被调用者保存寄存器
+------------------+
| 局部变量 |
+------------------+
| 临时空间 | ← $sp (被调用者视角)
+------------------+
低地址
关键点:MIPS 没有 x86 的 push/pop 指令。栈操作通过 addiu $sp, $sp, -N 和 sw/lw 实现。这意味着栈帧的分配和释放完全由编译器控制。
我记得第一次调试 MIPS 的栈溢出时,看到 addiu $sp, $sp, -32 这种指令,还以为是编译器写错了。后来才明白,MIPS 就是这么干的——一次性分配好栈空间,然后用 sw/lw 访问。
避坑指南:MIPS 的返回地址保存在栈上,而不是像 x86 那样用 call 指令自动压栈。这意味着如果你覆盖了栈上的 $ra,函数返回时就会跳转到你指定的地址。这就是栈溢出漏洞利用的基础。