第一章: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指令的典型执行流程:
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开发。