4. 固件动态分析环境搭建

静态扫描能发现字符串、硬编码密钥、危险函数调用等问题,但固件真正“跑起来”之后才会暴露运行时行为:服务如何监听端口、认证逻辑是否可绕过、崩溃是否可被利用、启动脚本如何拉起进程。动态分析环境的目标,就是让固件在受控条件下执行,并保留足够的观测与干预能力。本节围绕四条主线展开:QEMU 仿真、JTAG/UART 硬件调试接口、沙箱执行、依赖库模拟。

一、QEMU 仿真:从整机到用户态

QEMU 提供两种与固件分析高度相关的模式。系统模式(qemu-system-*)模拟完整 SoC,可加载内核与根文件系统,适合分析启动流程、init 脚本、内核模块与网络服务;用户态模式(qemu-<arch>)只模拟 CPU 指令集与系统调用,直接运行单个二进制,适合快速验证某个 CGI、守护进程或解密函数。

以常见的 MIPS 大端路由器固件为例,先识别架构:

binwalk -Me firmware.bin
file squashfs-root/bin/busybox
# 输出示例:ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV)

若使用用户态仿真,需要把目标根目录作为 -L 参数传入,让动态链接器与依赖库可被找到:

sudo apt install qemu-user-static
cp /usr/bin/qemu-mips-static squashfs-root/usr/bin/
sudo chroot squashfs-root /usr/bin/qemu-mips-static /bin/busybox ls

系统模式则需要内核镜像、设备树与根文件系统三件套。若厂商未提供内核,可尝试用同架构的 OpenWrt 内核启动,并在启动参数中指定 root=/dev/mtdblock0 或 root=/dev/sda。启动后通过串口重定向观察内核日志:

qemu-system-mips -M malta -kernel vmlinux -hda rootfs.img \
  -append "root=/dev/sda console=ttyS0" -nographic -net nic -net user

仿真成功与否的判断依据很直接:能否进入 shell、能否看到 init 拉起服务、目标端口是否监听。若卡在 Uncompressing Linux...,通常是内核与机器类型不匹配;若根文件系统挂载失败,检查 root= 与镜像格式。

二、JTAG 与 UART:硬件调试接口

UART 是最容易获得的调试通道,通常为 3.3V TTL,四针排列为 VCC、GND、TX、RX。用 USB-TTL 适配器连接时,适配器 RX 接目标 TX,适配器 TX 接目标 RX,GND 共地。串口参数常见为 115200 8N1。上电后若看到 U-Boot 倒计时,按任意键即可进入引导命令行,此时可读取环境变量、修改启动参数,甚至进入单用户模式:

setenv bootargs 'console=ttyS0,115200 root=/dev/mtdblock2 init=/bin/sh'
saveenv
boot

JTAG 提供更底层的控制能力,可读写内存、设置断点、单步执行,适合分析启动早期代码或绕过 UART 被禁用的设备。典型链路为 JTAG 适配器(如 FT2232)连接目标 TCK、TMS、TDI、TDO、GND,配合 OpenOCD 使用:

openocd -f interface/ftdi/ft2232.cfg -f target/mips_m4k.cfg
# 另开终端
telnet localhost 4444
> halt
> mdw 0x80000000 16
> bp 0x80001234 4
> resume

UART 与 JTAG 的分工可以这样理解:UART 面向系统级交互,看日志、进 shell、改启动参数;JTAG 面向芯片级控制,读内存、下断点、跟踪指令流。两者结合,才能在固件启动失败或服务异常时定位到具体位置。

固件动态分析环境:四条并行路径与观测点 固件镜像 firmware.bin / rootfs QEMU 仿真 系统模式 / 用户态模式 硬件调试接口 JTAG / UART 沙箱执行 chroot / namespaces / seccomp 依赖库模拟 NVRAM / 硬件抽象 / stub 观测与干预点 串口日志 / 内核 dmesg GDB 断点 / 内存读写 strace / 网络抓包 库函数返回值注入 判断依据:进入 shell / 服务监听 / 崩溃点可复现 否则回退检查架构、内核、根文件系统与库依赖

三、沙箱执行:把风险关进笼子

固件中的服务往往以 root 运行,直接在本机执行可能修改系统文件、开放端口或连接外部 C2。沙箱的目标是限制可见文件系统、网络与系统调用。最轻量的做法是 chroot 加 unshare:

sudo unshare -m -n -p -f chroot squashfs-root /bin/sh
# 在沙箱内挂载 proc 与 tmpfs
mount -t proc proc /proc
mount -t tmpfs tmpfs /tmp

更严格的做法是使用 seccomp 过滤危险系统调用,或用 firejail、bubblewrap 限制网络与文件访问。分析网络服务时,建议把目标网卡放入独立命名空间,仅保留一个 veth 对用于抓包,避免固件主动外联。判断沙箱是否有效,可以观察:固件是否仍能写宿主机文件、是否能解析外网域名、是否在 /proc 中看到宿主机进程。

四、依赖库模拟:让缺失的符号跑起来

固件二进制常依赖厂商私有库、NVRAM 读写接口或硬件抽象层。缺少这些库时,程序会在启动阶段报 undefined symbol 或直接段错误。处理思路分三步:先定位缺失符号,再决定是补库还是打桩。

readelf -d ./bin/httpd | grep NEEDED
# 输出示例:libnvram.so, libshared.so, libc.so.0
ldd ./bin/httpd
# 若提示 not found,则需补齐或替换

对于 NVRAM 这类接口,可以编写 stub 库,导出同名函数并返回可控值,再用 LD_PRELOAD 注入:

// nvram_stub.c
char *nvram_get(const char *key) {
    if (strcmp(key, "lan_ipaddr") == 0) return "192.168.1.1";
    return "";
}
int nvram_set(const char *key, const char *val) { return 0; }
gcc -shared -fPIC -o nvram_stub.so nvram_stub.c
LD_PRELOAD=./nvram_stub.so ./bin/httpd

若缺失的是硬件寄存器访问,可在 QEMU 中把对应地址映射为普通内存,或在 stub 中直接返回固定值。关键判断是:程序是否越过初始化阶段、是否开始监听端口、是否进入可交互状态。只要服务能稳定运行并接受请求,后续的漏洞验证才有意义。

建议把 QEMU 启动命令、串口参数、stub 库编译命令与沙箱挂载步骤记录成脚本,每次分析新固件时只需替换镜像路径与架构参数,可显著减少重复劳动。
JTAG 与 UART 操作涉及硬件电平与引脚定义,接错 VCC 可能损坏设备。上电前务必确认电压与引脚顺序,必要时先用万用表测量 GND 与 TX 的对地电压。

四条路径并非互斥:典型流程是先用 QEMU 用户态快速验证单个二进制,再用系统模式观察启动与服务;若仿真失败或行为异常,转用 UART 看真实启动日志,必要时用 JTAG 下断点;沙箱与依赖库模拟则贯穿始终,保证执行可控、符号可解。环境搭好之后,下一步才是把动态行为与静态扫描结果对照,形成可复现的验证用例。