1. MIPS架构概述:从历史到CTF实战

MIPS架构,说实话,是我个人最早接触的RISC处理器之一。记得2015年我第一次在CTF里碰到MIPS的逆向题,当时一脸懵——这指令格式怎么跟x86完全不一样?后来在IoT设备固件分析中,MIPS几乎成了家常便饭。今天我们就从根上把它捋清楚。

1.1 MIPS的历史与设计哲学

MIPS诞生于1984年,由斯坦福的John Hennessy教授团队开发。名字全称是Microprocessor without Interlocked Pipeline Stages——说白了就是“无互锁流水线级微处理器”。

设计哲学很纯粹:简单、规整、高效。MIPS的每条指令都是32位,固定长度。你想想看,这跟x86那种变长指令比起来,解码器能省多少晶体管?

核心设计原则:

  • 所有指令等长(32位)——硬件解码简单
  • 只有Load/Store指令访问内存——其他操作都在寄存器里完成
  • 流水线友好——每条指令的各个阶段清晰分离
  • 延迟槽设计——分支指令后的指令总是被执行

我在逆向一个家用路由器固件时,发现厂商用了MIPS架构。当时我就在想,为什么不是ARM?后来明白了——MIPS的授权费更低,而且对网络包处理这种场景,MIPS的流水线效率其实很高。

1.2 寄存器约定:你得记住这32个

MIPS有32个通用寄存器,每个32位(MIPS32)或64位(MIPS64)。CTF里最常见的是MIPS32小端模式。寄存器命名和用途是硬规矩,搞错了调试时会一头雾水。

寄存器编号 名称 用途 CTF中要注意的
$0 $zero 恒为0 写它没效果,读它永远是0
$1 $at 汇编器临时寄存器 伪指令展开时会用到
$2-$3 $v0-$v1 函数返回值 系统调用结果在这里
$4-$7 $a0-$a3 函数参数 前4个参数,多了用栈
$8-$15 $t0-$t7 临时寄存器 调用者保存,函数内随便用
$16-$23 $s0-$s7 保存寄存器 被调用者保存,得push/pop
$24-$25 $t8-$t9 临时寄存器 $t9常用于跳转表
$26-$27 $k0-$k1 内核保留 用户态别碰
$28 $gp 全局指针 访问静态数据用
$29 $sp 栈指针 栈溢出攻击的核心目标
$30 $fp 帧指针 有些编译器当$s8用
$31 $ra 返回地址 ROP攻击的关键寄存器

个人经验:在CTF的MIPS Pwn题里,我最常盯着$ra和$sp看。$ra被覆盖意味着你能控制程序流,$sp被覆盖意味着栈布局被你掌控。曾经有一道题,我花了3小时没找到溢出点,后来发现是忽略了$gp的初始化——嗯,这种坑踩过一次就记住了。

1.3 指令格式: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位     5位    5位    5位    5位    6位

I型(Immediate): 带立即数的指令,比如加载、分支。

31:26  25:21  20:16  15:0
opcode  rs     rt     immediate
6位     5位    5位    16位

J型(Jump): 跳转指令,比如j、jal。

31:26  25:0
opcode  target
6位     26位

避坑指南:我曾经在逆向一个MIPS的shellcode时,发现j指令的target是26位,但实际地址需要左移2位再拼接PC的高4位。这意味着你只能跳到256MB对齐的地址范围内。很多新手写ROP链时,跳转地址算错了,就是因为没理解这个拼接规则。

1.4 寻址模式:其实就五种

MIPS的寻址模式比x86少得多,这也是RISC的特点:

  1. 寄存器寻址: 操作数在寄存器里,比如 add $t0, $t1, $t2
  2. 立即数寻址: 操作数是指令里的常数,比如 addi $t0, $t1, 100
  3. 基址寻址: 寄存器+偏移量,比如 lw $t0, 0x10($sp)
  4. PC相对寻址: 分支指令用,比如 beq $t0, $t1, label
  5. 伪直接寻址: J型指令用,26位地址+PC高4位

你可能会问:怎么没有x86那种复杂的基址变址寻址?说白了,MIPS的设计者认为复杂寻址对性能提升有限,反而让硬件变复杂。实际项目中,我99%的时间只用基址寻址和寄存器寻址。

1.5 字节序:大小端之争

MIPS既支持大端也支持小端,但CTF里90%的题目是小端。为什么?因为大部分家用路由器(比如Linksys、TP-Link)用的都是小端MIPS。

大小端影响什么?影响你读内存时的字节顺序。举个例子:

内存地址:  0x1000  0x1001  0x1002  0x1003
小端存储:  0x78    0x56    0x34    0x12   → 实际值 0x12345678
大端存储:  0x12    0x34    0x56    0x78   → 实际值 0x12345678

CTF实战要点:

  • 拿到固件先看ELF头里的e_flags,确认大小端
  • IDA Pro里设置处理器为"MIPSEL"或"MIPSB"
  • 写shellcode时注意立即数的字节序——我见过有人把大端shellcode直接扔小端MIPS上跑,结果全崩了

1.6 CTF中的常见MIPS设备

CTF里MIPS题目几乎都来自IoT设备。我整理了一下常见的:

设备类型 常见型号 处理器 CTF出现频率
家用路由器 TP-Link WR841N AR9341 (MIPS 24Kc) 极高
企业AP Ubiquiti NanoStation AR7240 (MIPS 24Kc) 高
网络摄像头 D-Link DCS-930L RTL8186 (MIPS) 中
NAS设备 Synology DS218j Marvell Armada (MIPS) 低
机顶盒 中兴B860A BCM7252 (MIPS) 中

这些设备有个共同点:内存小、没ASLR、没NX。你想想看,一个只有32MB内存的路由器,跑个Linux内核都费劲,哪还有空间做内存保护?所以MIPS的Pwn题往往比x86简单——ROP链好写,shellcode直接扔栈上就能跑。

我的建议:刚开始学MIPS Pwn,先拿TP-Link WR841N的固件练手。网上有现成的漏洞分析文章,固件也好提取。我记得第一次成功在MIPS上跑通shellcode时,那种感觉——嗯,比x86有成就感多了。

1.7 MIPS架构知识体系总览

下面这张图把MIPS架构的核心知识点串起来了。我建议你把它当索引用,哪个点不清楚就回来看。

MIPS架构知识体系 MIPS架构 历史与设计哲学 寄存器约定 指令格式 寻址模式 1984年诞生 RISC理念 流水线优化 32个通用寄存器 $ra/$sp/$gp 调用约定 R型(寄存器) I型(立即数) J型(跳转) 寄存器寻址 基址+偏移 PC相对寻址 CTF应用:路由器/IoT设备固件逆向与Pwn 小端MIPS最常见(TP-Link、D-Link等家用路由器) ROP攻击重点关注$ra(返回地址)和$sp(栈指针)

这张图把MIPS架构拆成了四个核心模块:历史哲学、寄存器、指令格式、寻址模式。底部是CTF里的实际落脚点。我个人习惯在调试前先过一遍这张图,确认自己当前在哪个环节——是寄存器没搞对,还是指令解码出了问题。

最后提醒一句:MIPS的延迟槽是个大坑。分支指令后面的那条指令,无论分支是否跳转,都会被执行。我见过太多人在写ROP链时,把gadget地址放在分支后面,结果CPU先执行了延迟槽里的指令,整个链就歪了。嗯,这个后面章节我们会专门讲。