第四章:动态分析环境搭建

做固件逆向,静态分析只能看到「死」的代码。

你得让固件跑起来,才能看到它真正的行为。

这一章,我带你搭一套完整的动态分析环境。从 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
注意:用户模式不支持 MMU 模拟。如果你的固件用了虚拟地址映射,老老实实用系统模式。

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 参数指定机器类型。不同的固件需要不同的机器模型。我踩过最大的坑是选了错误的机器类型,导致网卡驱动加载失败,折腾了两天。

我的经验:对于 IoT 固件,优先尝试 virtrealview-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 链。不用反复烧录固件,效率高很多。

核心优势:Unicorn 可以精确控制每条指令的执行。你可以在任意地址打断点,读取寄存器,甚至修改内存。这对分析混淆代码特别有用。

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 函数。那种感觉,很爽。

避坑指南:我曾经在调试时忘记关掉 ASLR(地址空间随机化),导致每次断点地址都不一样。记得在 QEMU 启动参数里加上 -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 接口不提供电源。
重要:JTAG 调试时,目标芯片的复位引脚必须拉高。否则 OpenOCD 无法识别芯片 IDCODE。

知识体系总览

下面这张图,是我梳理的动态分析环境核心逻辑。你可以把它当作路线图。

动态分析环境搭建 - 知识体系 固件动态分析目标 软件仿真(无硬件) 硬件调试(有硬件) QEMU 系统模式/用户模式 Unicorn 指令级模拟 OpenOCD JTAG/SWD 桥接 调试器硬件 J-Link/ST-Link GDB / GDB-Multiarch 固件行为分析 函数调用追踪 漏洞挖掘 模糊测试/崩溃分析 漏洞利用开发 ROP链/Shellcode测试 从软件仿真到硬件调试,GDB 是贯穿始终的统一调试接口

这张图你看懂了吗?核心就一句话:不管用 QEMU 还是 JTAG,最终都通过 GDB 来统一调试

我个人建议初学者先从 QEMU 用户模式入手。成本最低,一台电脑就够了。等熟悉了 GDB 的操作,再上 OpenOCD 和 JTAG。

嗯,环境搭好了,下一步就是真正动手分析了。

专注资料整理