第一章:ARM与MIPS逆向基础——从架构差异到实战手感
做逆向,尤其是CTF里的Pwn和嵌入式固件分析,ARM和MIPS是绕不开的两座山。我早年刚接触嵌入式安全时,总觉得x86那套栈结构走天下,结果第一次拿到MIPS路由器固件,反汇编一看——寄存器名字全变了,调用约定也完全不同,整个人直接懵掉。
这一章,咱们就把ARM和MIPS的底裤扒干净。你不需要死记硬背,跟着我的思路走一遍,以后看到这两种架构的二进制,心里就有谱了。
1.1 ARM vs MIPS:宏观对比
先看一张总览表,后面再逐个拆解。
| 对比维度 | ARM | MIPS |
|---|---|---|
| 指令长度 | 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图,把本章的核心知识点串起来了。你可以把它当成思维导图来用。
1.7 本章小结
ARM和MIPS的逆向基础,说白了就是三件事:认寄存器、读指令、懂调用。我个人的经验是,不要试图一次性记住所有细节——先记住寄存器用途和调用约定,然后在实际逆向中遇到不懂的指令再查。做得多了,自然就熟了。