第一章:ARM与MIPS逆向基础——从架构差异到实战手感

做逆向,尤其是CTF里的Pwn和嵌入式固件分析,ARM和MIPS是绕不开的两座山。我早年刚接触嵌入式安全时,总觉得x86那套栈结构走天下,结果第一次拿到MIPS路由器固件,反汇编一看——寄存器名字全变了,调用约定也完全不同,整个人直接懵掉。

这一章,咱们就把ARM和MIPS的底裤扒干净。你不需要死记硬背,跟着我的思路走一遍,以后看到这两种架构的二进制,心里就有谱了。

1.1 ARM vs MIPS:宏观对比

先看一张总览表,后面再逐个拆解。

对比维度ARMMIPS
指令长度32位(ARM模式)/ 16位(Thumb模式)固定32位
寄存器数量16个通用寄存器(R0-R15)32个通用寄存器($0-$31)
寻址模式多种灵活寻址(偏移、变址、多寄存器)相对简单(基址+偏移为主)
条件执行几乎所有指令可条件执行仅分支指令有条件
延迟槽无分支延迟槽(经典设计)
大小端可配置(通常小端)可配置(通常大端,但小端也常见)
调用约定参数通过R0-R3传递,多余参数压栈参数通过$a0-$a3传递,多余参数压栈
典型应用手机、嵌入式、IoT路由器、网关、网络设备

核心差异一句话:ARM指令更灵活但更复杂,MIPS指令更规整但更“硬”。逆向时,ARM的Thumb模式经常让人头疼,MIPS的延迟槽则是个经典坑点。

1.2 寄存器模型——你的“手牌”

逆向分析说白了就是跟踪数据在寄存器里的流动。两种架构的寄存器模型,我建议你当成“手牌”来记。

ARM寄存器(32位模式)

  • R0-R3:参数传递和返回值。我习惯叫它们“临时工”,函数里随便用,调用者不负责保存。
  • R4-R11:局部变量寄存器。被调用者必须保存(callee-saved),逆向时看到这些寄存器被压栈,基本就是函数开头。
  • R12(IP):内部过程调用暂存器。常用于函数跳转时的临时中转。
  • R13(SP):栈指针。这个不用多说,Pwn里最常盯着的目标。
  • R14(LR):链接寄存器。保存函数返回地址。嗯,这里要注意——ARM没有单独的返回指令,返回就是 MOV PC, LR 或者 BX LR。
  • R15(PC):程序计数器。ARM里PC可以直接读写,这在MIPS里是做不到的。

我的小技巧:逆向ARM时,先定位LR的压栈和出栈位置,就能快速画出函数调用关系图。我曾经在分析一个IoT固件时,靠LR回溯找到了三个隐藏的后门函数。

MIPS寄存器

MIPS有32个寄存器,但常用的也就那么几个。我列个速查表:

寄存器名编号用途保存责任
$zero$0恒为0,写它无效—
$at$1汇编临时变量(编译器用)调用者
$v0-$v1$2-$3函数返回值调用者
$a0-$a3$4-$7函数参数调用者
$t0-$t9$8-$15, $24-$25临时寄存器调用者
$s0-$s7$16-$23保存寄存器(callee-saved)被调用者
$k0-$k1$26-$27内核保留—
$gp$28全局指针被调用者
$sp$29栈指针被调用者
$fp$30帧指针被调用者
$ra$31返回地址被调用者

避坑指南:我曾经在逆向一个MIPS路由器固件时,看到函数开头没有保存$ra,以为是个不返回的函数。后来才发现——MIPS有延迟槽,JALR指令后面的那条指令在跳转前已经执行了。如果你忽略延迟槽,反汇编出来的控制流图全是错的。

1.3 指令集特点——怎么读反汇编

两种架构的指令风格差异很大。我直接上对比,你感受一下。

ARM指令特点

  • 条件执行:几乎每条指令都可以加条件后缀。比如 ADDEQ R0, R1, R2 表示“如果上条指令结果为相等(EQ),才执行加法”。这在逆向时会让控制流变得很“平”——没有那么多分支跳转,但读起来要时刻留意条件码。
  • 多寄存器加载/存储:LDMIA、STMDB 这类指令可以一次操作多个寄存器。我刚开始看的时候总觉得是乱码,后来习惯了才发现,这其实是ARM高效压栈/出栈的方式。
  • 桶形移位器:ARM的第二个操作数可以经过移位再参与运算。比如 ADD R0, R1, R2, LSL #2 相当于 R0 = R1 + (R2 << 2)。逆向时看到这种指令,别慌,拆开算就行。

MIPS指令特点

  • 固定32位:每条指令都是4字节,对齐整齐。反汇编时地址永远是4的倍数,看着就舒服。
  • 三种指令格式:R型(寄存器)、I型(立即数)、J型(跳转)。每种格式的字段位置固定,解析起来很机械。
  • 分支延迟槽:这是MIPS最反人类的设计。分支指令后面的那条指令一定会被执行,不管分支是否跳转。逆向时,如果你看到跳转指令后面跟着一条看起来“多余”的指令,别删——那是延迟槽。

实战经验:我在分析一个MIPS的缓冲区溢出漏洞时,攻击者利用延迟槽的特性,把shellcode的一部分藏在了跳转指令后面。如果你按常规思路只看跳转目标地址,根本找不到完整的payload。

1.4 大小端模式——字节序的陷阱

大小端问题,说白了就是数据在内存里怎么排。ARM默认小端(低位在低地址),MIPS默认大端(高位在低地址),但两者都可以配置。

我遇到过最坑的一次:一个MIPS固件,厂商为了兼容性改成了小端模式,但反汇编工具默认按大端解析。结果我看到的指令全是乱码,折腾了两天才发现是字节序的问题。

架构常见默认如何判断
ARM小端(Little-Endian)看ELF头里的e_flags,或者直接看第一条指令的字节序
MIPS大端(Big-Endian)同样看ELF头,或者检查常见字符串的存储方式

快速判断法:在二进制里找一个明显的ASCII字符串,比如“ABCD”。如果内存里看到的是 41 42 43 44,那就是大端;如果是 44 43 42 41,那就是小端。这个方法我用了无数次,从没失手过。

1.5 调用约定——函数调用的“交通规则”

逆向的核心是理解函数调用。两种架构的调用约定有相似之处,但细节差异很大。

ARM调用约定(ATPCS)

  • 参数1-4:R0-R3
  • 多余参数:压栈,从右向左
  • 返回值:R0(32位),R0+R1(64位)
  • 返回地址:保存在LR(R14)
  • 栈对齐:8字节对齐

MIPS调用约定(O32)

  • 参数1-4:$a0-$a3
  • 多余参数:压栈,从右向左
  • 返回值:$v0(32位),$v0+$v1(64位)
  • 返回地址:保存在$ra($31)
  • 栈对齐:8字节对齐
  • 注意:MIPS调用者会预留16字节的“参数保存区”,即使参数少于4个

又一个坑:MIPS的调用者预留16字节这个特性,经常被CTF出题人利用。如果你在栈上看到函数参数之外还有16字节的“空洞”,别奇怪——那是调用者预留的,不是padding。

1.6 知识体系总览图

下面这张SVG图,把本章的核心知识点串起来了。你可以把它当成思维导图来用。

ARM与MIPS逆向基础 - 知识体系 ARM vs MIPS 逆向基础 架构对比 寄存器模型 指令集特点 大小端模式 指令长度差异 条件执行 vs 延迟槽 ARM: R0-R15 MIPS: $0-$31 ARM: 桶形移位器 MIPS: 三种指令格式 ARM: 默认小端 MIPS: 默认大端 核心:调用约定是逆向的“交通规则” ARM用R0-R3传参,MIPS用$a0-$a3传参

1.7 本章小结

ARM和MIPS的逆向基础,说白了就是三件事:认寄存器、读指令、懂调用。我个人的经验是,不要试图一次性记住所有细节——先记住寄存器用途和调用约定,然后在实际逆向中遇到不懂的指令再查。做得多了,自然就熟了。