3. MIPS 汇编基础(上):从算术到分支,打好漏洞利用的地基

各位同学,欢迎来到第三章。前两章我们搭好了环境,今天开始真正碰 MIPS 的指令了。

说实话,我第一次接触 MIPS 汇编时,心里是有点抵触的。毕竟之前一直玩 x86,寄存器多、指令花哨,突然切到 MIPS,感觉像从自动挡换成了手动挡。但后来我发现,正是这种「简洁」,让漏洞利用变得异常清晰——你几乎一眼就能看出数据流怎么走,栈怎么布局。

这一章我们只讲四类最基础的指令:算术运算、逻辑运算、数据传输、条件分支。别小看它们,我敢说,90% 的 MIPS 漏洞利用,核心逻辑都绕不开这几条指令。

核心观点:MIPS 是典型的 RISC 架构,指令格式规整、操作数固定。你只要掌握了这四类指令,就能读懂大部分 Pwn 题的反汇编代码。

MIPS 汇编基础(上)知识体系 MIPS 指令核心 算术运算 逻辑运算 数据传输 条件分支 add/sub addi and/or sll/srl lw/sw la beq/bne slt 每条指令都是 32 位定长,操作数固定为 3 个寄存器

3.1 算术运算指令:add、sub、addi

MIPS 的算术运算,说白了就是三个操作数:两个源,一个目的。格式永远是 op rd, rs, rt,意思是 rd = rs op rt。

举个例子:

add $t0, $t1, $t2   # $t0 = $t1 + $t2
sub $t3, $t4, $t5   # $t3 = $t4 - $t5
addi $t6, $t7, 100  # $t6 = $t7 + 100

看到没?addi 是立即数加法,第三个操作数是常数。这在漏洞利用里太常用了——比如你算偏移量、算地址差值,基本都靠它。

我的习惯:在写 shellcode 或 ROP gadget 时,我一般优先用 addi 而不是 add。因为立即数可以直接写,少占一个寄存器,代码也更紧凑。但要注意,addi 的立即数只有 16 位有符号,范围是 -32768 到 32767。超过这个范围?那就得拆成两条指令了。

另外,MIPS 没有 x86 那种 inc、dec 指令。想加 1?老老实实写 addi $t0, $t0, 1。我第一次写 MIPS shellcode 时就踩过这个坑——习惯性想找 inc,结果发现根本没有。

3.2 逻辑运算指令:and、or、xor、nor、sll、srl

逻辑运算在漏洞利用里主要干两件事:清位和移位。

先看基本逻辑:

and $t0, $t1, $t2   # $t0 = $t1 & $t2
or  $t0, $t1, $t2   # $t0 = $t1 | $t2
xor $t0, $t1, $t2   # $t0 = $t1 ^ $t2
nor $t0, $t1, $t2   # $t0 = ~($t1 | $t2)

这里有个小技巧:nor 配合 $zero 寄存器可以实现取反。$zero 是 MIPS 里一个特殊的寄存器,永远为 0。所以 nor $t0, $t1, $zero 就等于 $t0 = ~$t1。

移位指令更常用:

sll $t0, $t1, 2    # $t0 = $t1 << 2
srl $t0, $t1, 2    # $t0 = $t1 >> 2

为什么说移位在 Pwn 里重要?你想想看,计算数组索引、对齐栈帧、提取地址中的某些位,全得靠它。我记得有一次调试一个堆溢出,偏移量算来算去不对,最后发现是忘了做 4 字节对齐的移位——白白浪费了半天。

注意:MIPS 的移位指令 sll/srl 是逻辑移位,不是算术移位。右移时高位补 0,不会保留符号位。如果你需要算术右移(保留符号),得用 sra 指令。这个区别在写有符号数处理时特别重要。

3.3 数据传输指令:lw、sw、la

数据传输是漏洞利用的命脉。没有它,你连栈上的数据都读不到,更别提改返回地址了。

三条核心指令:

指令 格式 含义 我的用法
lw lw $rt, offset($rs) 从内存读 4 字节到寄存器 读栈上数据、读 GOT 表
sw sw $rt, offset($rs) 将寄存器值写入内存 写返回地址、写 shellcode
la la $rt, label 将标签地址加载到寄存器 获取函数地址、全局变量地址

看个实际例子:

lw $t0, 0($sp)      # 从栈顶读 4 字节到 $t0
sw $t1, 4($sp)      # 将 $t1 写入 sp+4 的位置
la $t2, main        # 将 main 函数的地址加载到 $t2

这里有个关键点:lw 和 sw 的地址必须是 4 字节对齐的。MIPS 对非对齐访问会触发异常。我在做 IoT 设备漏洞时遇到过这种情况——明明偏移算对了,但程序就是崩,最后发现是地址没对齐。

实战经验:在构造 ROP 链时,lw 和 sw 是最常用的 gadget。比如 lw $ra, 0($sp); jr $ra 这种组合,几乎每个 MIPS ROP 利用里都有。你如果能熟练找到这类 gadget,漏洞利用就成功了一半。

至于 la,它其实是一条伪指令。汇编器会把它展开成 lui + ori 两条真实指令。为什么?因为 MIPS 的指令长度固定为 32 位,没法直接塞下一个 32 位的地址,只能拆成高 16 位和低 16 位分别加载。

3.4 条件分支指令:beq、bne、slt

条件分支是控制流的灵魂。没有它,程序就只能顺序执行,漏洞利用也无从谈起。

先看最简单的两条:

beq $t0, $t1, label   # if ($t0 == $t1) goto label
bne $t0, $t1, label   # if ($t0 != $t1) goto label

但 MIPS 没有 x86 那种直接的 cmp 指令。那怎么比较大小呢?答案是 slt(set on less than):

slt $t2, $t0, $t1     # if ($t0 < $t1) $t2 = 1; else $t2 = 0
bne $t2, $zero, label  # if ($t2 != 0) goto label

这两条组合起来,就实现了 if ($t0 < $t1) goto label。你想想看,是不是很巧妙?

类似的,slti 是比较立即数:

slti $t2, $t0, 100    # if ($t0 < 100) $t2 = 1; else $t2 = 0

避坑指南:我曾经在写一个栈溢出利用时,想通过 bne 跳过一段代码,结果忘记把比较结果存到寄存器里,直接用了 bne $t0, $t1, label。看起来没问题对吧?但实际跑起来发现跳转总是不对。后来才意识到,bne 比较的是两个寄存器的值,而我当时 $t1 里存的是个垃圾值。所以,用 slt 时一定要确保比较结果写到了正确的寄存器。

另外,MIPS 的分支指令有一个延迟槽的特性——分支后面的那条指令无论如何都会执行。这个我们后面会专门讲,现在先记住:分支后面那条指令,一定会被执行。这个特性在写 shellcode 时经常被利用。

3.5 本章小结

好了,四类指令讲完了。我们来快速回顾一下:

  • 算术运算:add/sub/addi,三操作数格式,没有 inc/dec
  • 逻辑运算:and/or/xor/nor/sll/srl,配合 $zero 实现取反
  • 数据传输:lw/sw/la,注意 4 字节对齐,la 是伪指令
  • 条件分支:beq/bne/slt,比较大小需要组合使用

这些指令看着简单,但它们是 MIPS 漏洞利用的基石。下一章我们会继续讲剩下的指令——跳转、栈操作、系统调用。到时候你会发现,今天学的这些,全都会用上。

嗯,今天就到这里。去写几条指令练练手吧,光看是记不住的。