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的特点:
- 寄存器寻址: 操作数在寄存器里,比如
add $t0, $t1, $t2 - 立即数寻址: 操作数是指令里的常数,比如
addi $t0, $t1, 100 - 基址寻址: 寄存器+偏移量,比如
lw $t0, 0x10($sp) - PC相对寻址: 分支指令用,比如
beq $t0, $t1, label - 伪直接寻址: 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架构拆成了四个核心模块:历史哲学、寄存器、指令格式、寻址模式。底部是CTF里的实际落脚点。我个人习惯在调试前先过一遍这张图,确认自己当前在哪个环节——是寄存器没搞对,还是指令解码出了问题。
最后提醒一句:MIPS的延迟槽是个大坑。分支指令后面的那条指令,无论分支是否跳转,都会被执行。我见过太多人在写ROP链时,把gadget地址放在分支后面,结果CPU先执行了延迟槽里的指令,整个链就歪了。嗯,这个后面章节我们会专门讲。