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 面向芯片级控制,读内存、下断点、跟踪指令流。两者结合,才能在固件启动失败或服务异常时定位到具体位置。
三、沙箱执行:把风险关进笼子
固件中的服务往往以 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 用户态快速验证单个二进制,再用系统模式观察启动与服务;若仿真失败或行为异常,转用 UART 看真实启动日志,必要时用 JTAG 下断点;沙箱与依赖库模拟则贯穿始终,保证执行可控、符号可解。环境搭好之后,下一步才是把动态行为与静态扫描结果对照,形成可复现的验证用例。