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 的数据处理核心,移位和移动指令负责数据变换和搬运,而 syscall 则是用户态和内核态之间的桥梁。在 CTF 里,你写的 shellcode 基本就是这几类指令的组合。
我的建议: 刚开始学 MIPS 汇编时,别急着背指令格式。先拿一个简单的 CTF 题目,用 objdump -d 反汇编出来,对照着本章的指令表一条条看。看多了,自然就记住了。我当年就是这么过来的。