第四章:动态分析环境搭建
做固件逆向,静态分析只能看到「死」的代码。
你得让固件跑起来,才能看到它真正的行为。
这一章,我带你搭一套完整的动态分析环境。从 QEMU 仿真到 JTAG 硬件调试,一步步来。
4.1 QEMU 系统模式与用户模式仿真
QEMU 这东西,我用了快十年了。它有两种工作模式,很多人一开始搞混。
4.1.1 用户模式仿真
用户模式只模拟一个进程。说白了,就是把你编译好的 ARM/MIPS 二进制文件,直接当本地程序跑。
好处是快,启动几乎没延迟。坏处是——它不模拟硬件外设。
我遇到过有人拿用户模式跑一个依赖 UART 输出的固件,结果程序直接崩了。嗯,这很正常。
# 运行一个 ARM 架构的 ELF 文件
qemu-arm ./my_firmware.elf
# 指定库路径(常见坑)
qemu-arm -L /usr/arm-linux-gnueabihf ./my_firmware.elf
4.1.2 系统模式仿真
系统模式模拟一整台机器。CPU、内存、外设,全给你模拟出来。
我习惯用系统模式跑完整的固件镜像。比如一个路由器固件,你把它解包后,用 QEMU 启动整个系统。
# 启动一个 ARM 架构的 Debian 系统
qemu-system-arm -M virt -kernel vmlinuz -initrd initrd.img \
-drive file=rootfs.qcow2,format=qcow2 -append "root=/dev/vda" \
-netdev user,id=net0 -device virtio-net,netdev=net0
这里有个关键点:-M 参数指定机器类型。不同的固件需要不同的机器模型。我踩过最大的坑是选了错误的机器类型,导致网卡驱动加载失败,折腾了两天。
virt 或 realview-pb-a8。这两个兼容性最好。
4.2 Unicorn 引擎基础
Unicorn 是 QEMU 的精简版。它只模拟 CPU 指令,不模拟外设。
你想想看,有时候我们只想跑一段 shellcode,或者测试一个函数的行为。开一个完整的 QEMU 虚拟机,太笨重了。
Unicorn 就是干这个的。
from unicorn import *
from unicorn.arm_const import *
# 初始化 ARM 架构的 Unicorn 引擎
mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
# 映射内存
ADDRESS = 0x10000
SIZE = 0x1000
mu.mem_map(ADDRESS, SIZE)
# 写入要模拟的代码
CODE = b"\x01\x00\xa0\xe3" # MOV R0, #1
mu.mem_write(ADDRESS, CODE)
# 设置起始地址
mu.emu_start(ADDRESS, ADDRESS + len(CODE))
# 读取结果
r0 = mu.reg_read(UC_ARM_REG_R0)
print(f"R0 = {r0}")
我在做漏洞利用开发时,经常用 Unicorn 来测试 ROP 链。不用反复烧录固件,效率高很多。
4.3 GDB 与 GDB-Multiarch 调试
GDB 是调试界的瑞士军刀。但做嵌入式调试,你得用 gdb-multiarch。
4.3.1 为什么用 GDB-Multiarch?
普通的 GDB 只支持本机架构。你在一台 x86 电脑上调试 ARM 固件,普通 GDB 根本看不懂 ARM 指令。
gdb-multiarch 支持多种架构。装一个就够了。
# 安装
sudo apt install gdb-multiarch
# 启动并连接远程目标
gdb-multiarch -q
(gdb) target remote localhost:1234
(gdb) set architecture arm
(gdb) file ./firmware.elf
(gdb) break *0x10000
(gdb) continue
我个人习惯在调试前先 set architecture。否则 GDB 可能解析错指令长度,导致断点偏移。
4.3.2 与 QEMU 配合调试
QEMU 内置了 GDB 服务器。启动时加个 -s 参数就行。
qemu-system-arm -M virt -kernel vmlinuz -s -S
-s 表示在 1234 端口开启 GDB 服务。-S 表示启动时暂停 CPU,等你连上 GDB 后再继续。
我曾经调试一个 U-Boot 启动流程,就是用这个组合。在 GDB 里一步步看代码怎么从汇编跳到 C 语言的 start_armboot 函数。那种感觉,很爽。
-R 0x0 来禁用随机化。
4.4 OpenOCD + JTAG 调试环境
软件仿真再强,也比不上真机调试。JTAG 就是干这个的。
4.4.1 OpenOCD 是什么?
OpenOCD(Open On-Chip Debugger)是一个开源调试工具。它通过 JTAG/SWD 接口连接目标芯片,提供 GDB 服务器功能。
说白了,它就是个翻译官。把 GDB 的调试命令翻译成 JTAG 协议,让 PC 能控制嵌入式芯片。
4.4.2 硬件准备
你需要一个 JTAG 调试器。常见的有:
- J-Link(SEGGER 出品,稳定但贵)
- ST-Link(ST 官方,便宜但只支持 STM32)
- FT2232H(开源方案,通用性强)
我手头常备一个 J-Link 和一个 FT2232H。J-Link 用于正式项目,FT2232H 用于折腾各种冷门芯片。
4.4.3 配置与启动
OpenOCD 需要配置文件。告诉它用什么调试器、连什么芯片。
# openocd.cfg
source [find interface/jlink.cfg]
source [find target/stm32f4x.cfg]
# 启动
openocd -f openocd.cfg
启动后,OpenOCD 会在 3333 端口开启 GDB 服务。然后你就可以用 GDB 连上去了。
gdb-multiarch -q
(gdb) target remote localhost:3333
(gdb) monitor reset halt
(gdb) flash write_image erase ./firmware.bin 0x08000000
(gdb) continue
monitor reset halt 可以让芯片停在复位向量表处。这是分析启动流程的最佳起点。
4.4.4 JTAG 调试的常见问题
- 连接不上:检查接线。TCK、TMS、TDI、TDO 四根线必须正确。我见过有人把 TDI 和 TDO 接反了,折腾半天。
- 时钟太快:降低 JTAG 时钟频率。有些老芯片扛不住 10MHz。
- 供电不足:目标板需要独立供电。JTAG 接口不提供电源。
知识体系总览
下面这张图,是我梳理的动态分析环境核心逻辑。你可以把它当作路线图。
这张图你看懂了吗?核心就一句话:不管用 QEMU 还是 JTAG,最终都通过 GDB 来统一调试。
我个人建议初学者先从 QEMU 用户模式入手。成本最低,一台电脑就够了。等熟悉了 GDB 的操作,再上 OpenOCD 和 JTAG。
嗯,环境搭好了,下一步就是真正动手分析了。