MIPS架构CTF程序分析与调试 - 第3节

MIPS 算术运算:加法与减法

MIPS 的算术运算,说白了就是三个操作数——两个源,一个目标。跟 x86 那种一个操作数又当爹又当妈的做法完全不同。

add / sub / addi

add 和 sub 都是三操作数格式:add $rd, $rs, $rt。结果存到第一个寄存器里。

add $t0, $t1, $t2   # $t0 = $t1 + $t2
sub $t3, $t4, $t5   # $t3 = $t4 - $t5

我刚开始做 MIPS 逆向时,最常犯的错误就是以为 add 可以像 x86 那样直接加立即数。不行,得用 addi。

addi $t0, $t1, 100  # $t0 = $t1 + 100

注意陷阱: addi 的立即数是 16 位有符号数,范围 -32768 到 32767。超过这个范围?你得拆成两步:先用 lui 加载高 16 位,再用 ori 拼低 16 位。

我曾经在调试一个路由器固件时,看到反汇编里连续三个 addi 操作同一个寄存器,当时就懵了。后来才发现是编译器在构造一个 32 位常量——嗯,MIPS 的立即数就是这么捉襟见肘。

逻辑运算:and / or / xori

逻辑运算在 CTF 里太常用了。尤其是当你需要构造 ROP 链时,or 和 and 简直就是你的左右手。

and / or / xori

and $t0, $t1, $t2   # $t0 = $t1 & $t2
or  $t3, $t4, $t5   # $t3 = $t4 | $t5
xori $t6, $t7, 0xFF # $t6 = $t7 ^ 0xFF

注意 xori 是带立即数的异或。为什么没有 andi 和 ori?其实有,但 xori 更常见于 CTF 题目——你想想看,异或一个固定值就能把数据"翻转"过来,在 shellcode 编码里特别好使。

个人习惯: 我在写 MIPS 漏洞利用时,经常用 or $zero, $t0, $zero 来把 $t0 的值"复制"到 $zero——虽然 $zero 是只读的,但这个操作其实是在做 move 的替代品。后面会讲。

移位指令:sll / srl

移位操作在 MIPS 里也是三操作数格式,但第三个操作数是移位量(立即数)。

sll $t0, $t1, 4    # $t0 = $t1 << 4
srl $t2, $t3, 2    # $t2 = $t3 >> 2

为什么移位这么重要?我举个例子。你在逆向一个加密算法时,看到 sll 和 srl 频繁出现,十有八九是在做位掩码或者乘法/除法的优化。MIPS 没有硬件乘法器(早期版本),编译器会用 sll 代替乘以 2 的幂。

# 乘以 10 的优化写法
sll $t0, $t1, 3    # $t0 = $t1 * 8
sll $t2, $t1, 1    # $t2 = $t1 * 2
add $t3, $t0, $t2  # $t3 = $t1 * 10

你看,一个乘法被拆成了两次移位加一次加法。这在 CTF 逆向里是个常见的识别点——看到这种模式,基本可以断定是编译器优化后的结果。

数据移动:move / li

MIPS 没有专门的 move 指令。你看到的 move $t0, $t1 其实是伪指令,汇编器会把它翻译成 add $t0, $t1, $zero。

move $t0, $t1      # 实际是 add $t0, $t1, $zero
li   $t0, 0x1234   # 加载立即数

li 也是伪指令。如果立即数在 16 位范围内,它变成 addi $t0, $zero, 0x1234。如果超过 16 位,汇编器会自动拆成 lui + ori 两条指令。

CTF 实战要点: 在逆向 MIPS 固件时,看到 add $t0, $zero, $t1 就是 move。看到 lui 后面紧跟着 ori 操作同一个寄存器,就是 li 大常数。记住这个模式,能让你快速读懂反汇编。

系统调用:syscall

syscall 是 MIPS 里最关键的指令之一。它触发一个异常,进入内核态。在 CTF 的 Pwn 题目里,你经常需要构造 syscall 来获取 shell 或者读写文件。

# MIPS Linux 系统调用示例
li   $v0, 4001      # sys_exit 的编号
li   $a0, 0         # 退出码
syscall

系统调用号放在 $v0 里,参数依次放在 $a0 到 $a3 里。返回值在 $v0 里。

系统调用 编号 参数 说明
sys_read 4003 $a0=fd, $a1=buf, $a2=count 从文件描述符读取
sys_write 4004 $a0=fd, $a1=buf, $a2=count 写入文件描述符
sys_open 4005 $a0=path, $a1=flags, $a2=mode 打开文件
sys_execve 4011 $a0=path, $a1=argv, $a2=envp 执行程序

我曾经踩过的坑: MIPS 的 syscall 指令后面必须紧跟一条 nop 或者有效指令,否则在某些实现里会触发延迟槽问题。嗯,这个细节在 CTF 里经常被用来设置陷阱——你构造的 ROP 链里如果 syscall 后面跟了无效指令,shell 就弹不出来了。

知识结构总览

下面这张图把本章的指令体系串起来了。我建议你把它存下来,做逆向时对照着看。

MIPS 汇编基础指令体系 算术运算 add sub addi 逻辑运算 and or xori 移位运算 sll srl 数据移动 move li 系统调用 syscall 💡 核心要点 • 所有算术/逻辑指令都是三操作数格式:目标寄存器 + 两个源 • 立即数指令(addi/xori)的立即数范围是 16 位有符号 • move 和 li 是伪指令,汇编器会翻译成真实指令 • syscall 触发系统调用,参数在 $a0-$a3,编号在 $v0

这张图把五类指令的关系理清楚了。你想想看,算术和逻辑运算构成了 MIPS 的数据处理核心,移位和移动指令负责数据变换和搬运,而 syscall 则是用户态和内核态之间的桥梁。在 CTF 里,你写的 shellcode 基本就是这几类指令的组合。

我的建议: 刚开始学 MIPS 汇编时,别急着背指令格式。先拿一个简单的 CTF 题目,用 objdump -d 反汇编出来,对照着本章的指令表一条条看。看多了,自然就记住了。我当年就是这么过来的。