第三章 MIPS架构逆向分析:延迟槽、ROP与GOT覆写

MIPS架构在CTF里不算最常见,但一旦出现,往往就是硬骨头。我早年打比赛时,第一次碰到MIPS的题目,差点被延迟槽搞到心态爆炸。后来做嵌入式安全项目,天天跟MIPS路由器固件打交道,才慢慢摸透了这套体系的脾气。

今天咱们就聊聊MIPS逆向的三个核心难点:延迟槽特性、ROP链构造、GOT覆写技巧。这些东西搞明白了,MIPS的题基本就拿下大半。

3.1 MIPS延迟槽——反直觉的指令执行

延迟槽是MIPS最反人类的设计之一。说白了,就是分支指令后面的那条指令,无论分支是否跳转,都会先执行完。

核心规则:分支指令(beq、bne、j、jal等)后的那条指令,在分支跳转前一定会被执行。

看个例子就明白了:

// 原始代码
if (x == 0) {
    y = 1;
} else {
    y = 2;
}
z = 3;

MIPS编译后大概长这样:

beq $t0, $zero, equal   # 如果x==0,跳转到equal
addi $t1, $zero, 2      # 延迟槽:y = 2(无论是否跳转都执行!)
j done                   # 跳转到结束
addi $t1, $zero, 1      # 延迟槽:y = 1(但这条永远不会被执行到)

equal:
addi $t1, $zero, 1      # 这里才是真正的y=1
nop                      # 延迟槽:空操作

done:
addi $t2, $zero, 3      # z = 3

嗯,这里要注意:addi $t1, $zero, 2这条指令,不管x等不等于0,它都会执行。所以上面的代码逻辑其实是错的——正确的写法需要把延迟槽考虑进去。

常见陷阱:我在逆向MIPS固件时,经常看到新手把延迟槽指令当成普通指令来理解,结果反编译出来的逻辑完全不对。记住:延迟槽里的指令,是分支指令的"影子",它和分支指令是绑定的。

3.2 延迟槽对漏洞利用的影响

延迟槽不只是逆向时要注意,写exploit时更得小心。我举个例子:

假设你控制了某个函数的返回地址,想跳转到ROP gadget。在MIPS上,jr $ra(跳转到$ra寄存器)后面那条指令,会在跳转前执行。这意味着:

  • 延迟槽指令必须有效——不能是非法指令,否则CPU会异常
  • 延迟槽指令会影响寄存器状态——可能破坏你精心布置的gadget链
  • 延迟槽不能是分支指令——MIPS禁止在延迟槽里放分支指令(硬件会触发异常)

实战技巧:我一般会在ROP链里,把延迟槽位置填上nop(0x00000000)或者sll $zero, $zero, 0(也是0x00000000)。这样既不会崩溃,也不会影响寄存器状态。

3.3 MIPS ROP链构造——从零开始

MIPS的ROP和x86不太一样。x86有ret指令自动跳转,MIPS没有。MIPS的ROP核心是通过jr $ra或jalr $ra来串联gadget。

构造ROP链的基本步骤:

  1. 找gadget——用ROPgadget或手动搜索,找jr $ra结尾的代码片段
  2. 布置栈——把gadget地址按顺序放在栈上,每个gadget后面跟它的延迟槽指令
  3. 控制$ra——通过栈溢出或其他漏洞,把$ra指向第一个gadget
  4. 串联执行——每个gadget执行完后,通过jr $ra跳到下一个

来看一个实际例子。假设我们有这样的gadget:

// Gadget 1: 把栈上的值加载到$t0,然后跳转
0x00401234: lw $t0, 0($sp)    # 从栈顶加载到$t0
0x00401238: jr $ra            # 跳转到$ra
0x0040123c: addiu $sp, $sp, 4 # 延迟槽:sp+=4

// Gadget 2: 把$t0写入某个地址
0x00401300: sw $t0, 0($a0)    # 把$t0写入$a0指向的地址
0x00401304: jr $ra            # 跳转到$ra
0x00401308: addiu $ra, $ra, 8 # 延迟槽:ra+=8(跳过下一个gadget的延迟槽)

ROP链的栈布局:

栈地址     | 内容
-----------|------------------
sp+0x00    | 0x00401234  (Gadget 1地址)
sp+0x04    | 0xDEADBEEF  (Gadget 1的延迟槽,这里填nop)
sp+0x08    | 0x00401300  (Gadget 2地址)
sp+0x0C    | 0x00000000  (Gadget 2的延迟槽,nop)
sp+0x10    | 目标地址    (要写入的值)
sp+0x14    | 0x00401300  (Gadget 2再次执行)
sp+0x18    | 0x00000000  (延迟槽)
sp+0x1C    | 目标地址+4  (下一个要写入的值)
...

关键点:MIPS的ROP链里,每个gadget后面必须跟一个有效的延迟槽指令。我习惯在每个gadget地址后面放一个nop(0x00000000),除非我需要利用延迟槽做点什么。

3.4 GOT覆写——MIPS上的PLT/GOT机制

MIPS的GOT(全局偏移表)和x86类似,但有个重要区别:MIPS的GOT条目是8字节对齐的,而且每个条目包含两个部分:函数地址和函数描述符(有些实现里是GOT[1]和GOT[2])。

GOT覆写的基本思路:

  • 找到目标函数的GOT条目地址
  • 通过漏洞(比如栈溢出、堆溢出、格式化字符串)修改GOT条目
  • 把GOT条目改成system、execve或其他有用函数的地址
  • 下次调用目标函数时,实际执行的是我们替换的函数

来看一个实际案例。假设程序里有这样一段代码:

// 程序调用puts输出字符串
lw $t9, %got(puts)($gp)   # 从GOT加载puts地址到$t9
jalr $t9                   # 调用puts
nop                        # 延迟槽

如果我们把puts的GOT条目改成system的地址,那么下次调用puts("/bin/sh")时,实际执行的是system("/bin/sh")。

实战经验:我遇到过一种情况——GOT条目被映射到只读页面。这时候就不能直接覆写GOT了。我的做法是:先通过mprotect或mmap把GOT所在页面改成可写,然后再覆写。当然,前提是你得先控制程序执行流。

3.5 综合案例:MIPS栈溢出+ROP+GOT覆写

把前面讲的东西串起来,看一个完整的利用流程:

// 漏洞程序(简化版)
void vulnerable() {
    char buf[64];
    read(0, buf, 256);  // 栈溢出!
    printf(buf);        // 格式化字符串漏洞(可选)
}

利用步骤:

  1. 信息收集:通过格式化字符串泄露libc地址、GOT地址、栈地址
  2. 计算偏移:算出system函数地址、"/bin/sh"字符串地址
  3. 构造ROP链:用gadget把system地址写入puts的GOT条目
  4. 触发调用:让程序再次调用puts("/bin/sh"),实际执行system("/bin/sh")

ROP链的栈布局(简化版):

sp+0x00: lw $t0, 0($sp) 的gadget地址  # 加载system地址到$t0
sp+0x04: nop                           # 延迟槽
sp+0x08: system地址                     # 被加载到$t0
sp+0x0C: sw $t0, 0($a0) 的gadget地址  # 把$t0写入GOT
sp+0x10: nop                           # 延迟槽
sp+0x14: puts的GOT地址                  # 写入目标
sp+0x18: 返回main的gadget              # 让程序重新执行
sp+0x1C: nop                           # 延迟槽

避坑指南:我曾经在MIPS的ROP链里踩过一个坑——jr $ra的延迟槽里放了addiu $sp, $sp, 4,结果$sp被改了,导致后续的gadget地址全部错位。后来我学乖了:延迟槽里尽量放nop,除非你非常确定自己在做什么。

3.6 知识结构总览

下面这张图把MIPS漏洞利用的核心知识点串起来了:

MIPS漏洞利用知识结构图 MIPS漏洞利用知识结构 延迟槽特性 ROP链构造 GOT覆写 分支后执行 禁止分支指令 jr $ra串联 延迟槽填充 8字节对齐 覆写为system 核心思路 利用延迟槽特性 → 构造ROP链 → 覆写GOT条目 → 获取shell 信息收集 构造ROP链 覆写GOT 获取shell 完整利用流程

3.7 常见问题与调试技巧

问题 原因 解决方法
ROP链执行到一半崩溃 延迟槽指令非法或破坏了寄存器 检查每个gadget的延迟槽,尽量用nop
GOT覆写后程序没反应 GOT页面只读,或者覆写地址不对 用cat /proc/pid/maps检查GOT权限
跳转地址不对齐 MIPS要求跳转地址4字节对齐 确保所有gadget地址是4的倍数
格式化字符串泄露失败 MIPS的参数传递规则和x86不同 MIPS前4个参数在$a0-$a3,其余在栈上

调试建议:我调试MIPS exploit时,喜欢用QEMU的用户态模拟模式(qemu-mips-static)。这样可以在x86机器上直接运行MIPS二进制文件,配合GDB调试,效率比真机高很多。命令大概是:qemu-mips-static -g 1234 ./vuln,然后用GDB远程连接。

MIPS的漏洞利用确实比x86繁琐一些,但核心思路是一样的:控制程序执行流,然后做你想做的事。延迟槽、ROP链、GOT覆写,这三板斧搞定了,MIPS的题基本就稳了。