一、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 32位内存布局 内核空间 (kseg2/kseg3) 0x80000000 - 0xFFFFFFFF 栈 (Stack) 高地址 → 低地址增长 堆 (Heap) 低地址 → 高地址增长 数据段 (.data/.bss) 代码段 (.text) 0x00400000 起始

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,函数返回时就会跳转到你指定的地址。这就是栈溢出漏洞利用的基础。