第一章:ARM架构基础与逆向环境搭建

做ARM逆向Pwn,说白了就是跟一种完全不同的处理器架构打交道。我刚开始从x86转ARM时,最大的感受就是——这玩意儿怎么跟我想象的不一样?寄存器命名不同、指令集风格迥异、调用约定也变了。但别担心,这一章我会带你把这些基础打牢。

1.1 ARM vs x86:核心差异

先看一张对比表,把两个架构的关键区别理清楚:

对比维度 x86 (IA-32/x86-64) ARM (AArch32/AArch64)
指令集风格 CISC,变长指令(1-15字节) RISC,定长指令(4字节/2字节Thumb)
通用寄存器 8个(32位)/16个(64位) 16个(32位)/31个(64位)
栈操作 PUSH/POP专用指令 LDM/STM或LDR/STR实现
条件执行 主要通过分支指令 大多数指令可带条件码
内存访问 支持非对齐访问 对齐要求严格(早期版本)
调用约定 参数通过栈传递(32位) 参数通过寄存器传递(R0-R3)

我的经验: 在x86上做栈溢出,你直接覆盖返回地址就行。但在ARM上,你得小心——很多函数用栈帧指针(FP/R11)来恢复现场,覆盖错了直接崩。我曾经在调试一个IoT设备固件时,就因为没注意ARM的栈布局,浪费了整整两天。

1.2 ARM寄存器体系

ARM的寄存器命名很直观。以32位ARM(AArch32)为例:

  • R0-R3:参数传递和返回值。函数调用时,前4个参数依次放入R0-R3。
  • R4-R11:通用寄存器,被调用者保存。你写函数时,如果要用这些寄存器,得自己压栈保护。
  • R12 (IP):内部过程调用寄存器,常用于链接器生成的跳转代码。
  • R13 (SP):栈指针,指向当前栈顶。
  • R14 (LR):链接寄存器,保存函数返回地址。
  • R15 (PC):程序计数器,指向当前指令+8(流水线效应)。

避坑指南: 我曾经在逆向一个ARM固件时,看到BL指令后直接去LR指向的地址找返回点,结果发现不对。后来才想起来——ARM的PC是当前指令地址+8,所以BL指令存入LR的值是PC-4。这个细节在调试时特别容易踩坑。

1.3 ARM指令集速览

ARM指令集有几个特点,你写exploit时必须记住:

  • 条件执行:每条指令都可以带条件码后缀,比如MOVEQ R0, #1表示“如果相等才执行”。这能减少分支指令,但也让逆向分析更烧脑。
  • 多寄存器加载/存储:LDMIA R0!, {R1-R4}一次性加载4个寄存器,常用于函数入口/出口的栈操作。
  • 桶形移位器:很多数据处理指令可以在第二个操作数上做移位,比如ADD R0, R1, R2, LSL #2。

下面这张图展示了ARM指令的典型执行流程:

ARM指令执行流程与寄存器交互 取指 (Fetch) PC → 内存 → 指令 译码 (Decode) 条件码检查 执行 (Execute) ALU/移位器 ARM 通用寄存器组 (AArch32) R0-R3 参数/返回值 R4-R11 被调用者保存 R12 (IP) 内部调用 R13 (SP) 栈指针 R14 (LR) 链接寄存器 R15 (PC) 程序计数器 注意:ARM的PC指向当前指令地址+8(流水线效应),这与x86完全不同 条件执行让每条指令都可能被跳过,逆向时务必检查指令前缀

1.4 QEMU用户态模拟

你手头没有ARM开发板?没关系,QEMU用户态模拟能帮你跑ARM二进制文件。我个人习惯用这个组合:

# 安装QEMU用户态模拟器
sudo apt install qemu-user qemu-user-static

# 运行ARM 32位程序
qemu-arm -L /usr/arm-linux-gnueabihf ./vuln_program

# 运行ARM 64位程序
qemu-aarch64 -L /usr/aarch64-linux-gnu ./vuln_program64

注意: -L参数指定动态链接器的路径。如果你没有安装对应的交叉编译工具链,QEMU会报找不到加载器的错误。我建议直接装gcc-arm-linux-gnueabihf和gcc-aarch64-linux-gnu,省心。

1.5 GDB-multiarch调试

调试ARM程序,GDB-multiarch是标配。你想想看,一个调试器能同时处理x86、ARM、MIPS,多方便。

# 启动QEMU的GDB stub
qemu-arm -g 1234 ./vuln_program

# 另一个终端启动GDB
gdb-multiarch -q
(gdb) set architecture arm
(gdb) target remote localhost:1234
(gdb) file ./vuln_program
(gdb) b main
(gdb) c

这里有个小技巧:set architecture arm告诉GDB你调试的是ARM代码。如果你忘了这步,GDB会默认用x86的解析方式,反汇编出来的东西完全不对。我刚开始就犯过这个错,盯着屏幕看了半小时没看懂。

1.6 pwntools与ARM交互

pwntools对ARM的支持已经很成熟了。写exploit时,你只需要指定架构和操作系统:

from pwn import *

# 设置ARM上下文
context.arch = 'arm'
context.os = 'linux'
context.endian = 'little'

# 或者用更精确的设置
context.update(arch='aarch64', os='linux', bits=64)

# 生成ARM shellcode
shellcode = asm(shellcraft.sh())
print(shellcode.hex())

# 连接远程ARM服务
io = remote('target.com', 1337)
io.send(asm('add r0, r0, #1'))

我的习惯: 写ARM exploit时,我总会在脚本开头加一句context.log_level = 'debug'。这样pwntools会打印出所有发送和接收的原始数据,方便你对照GDB里的内存布局。特别是做堆利用时,这一步能省下大量排查时间。

1.7 本章小结

ARM架构和x86的差异,说白了就是RISC和CISC哲学的不同。寄存器更多、指令更规整、条件执行更灵活——这些特点既是优势,也是你在逆向和Pwn时需要注意的地方。QEMU+GDB+pwntools这套组合拳,能让你在PC上就完成ARM程序的调试和exploit开发。