4. 固件动态分析:QEMU模拟运行、GDB调试固件、网络服务模拟与漏洞触发
静态分析能看到代码结构,但看不到代码的「脾气」。我见过太多人把固件反编译出来,盯着汇编看了三天,愣是没发现一个缓冲区溢出——结果一跑起来,当场崩溃。动态分析就是让固件真正「活过来」,看它怎么分配内存、怎么处理网络包、怎么在异常路径上翻车。
这一章我们聚焦三件事:用QEMU把固件跑起来、用GDB在关键函数下断点、模拟网络服务触发漏洞。说白了,就是给固件搭个手术台,边运行边解剖。
4.1 QEMU模拟运行:让固件在PC上「复活」
QEMU是个好东西。它不仅能模拟CPU指令集,还能模拟外设、中断控制器、串口。我最早用QEMU跑固件是在2016年,当时一个路由器固件死活跑不起来,后来发现是NAND Flash的ECC校验没模拟对——折腾了两天。
常见的固件运行场景分两种:
- 系统级模拟:模拟整个板子,包括CPU、内存、外设,适合跑完整的Linux内核+文件系统。
- 用户态模拟:只模拟CPU指令集,直接跑单个二进制文件,适合分析某个网络服务或命令行工具。
我个人习惯先用用户态模拟快速验证,确认目标二进制能正常执行后,再切到系统级模拟做完整测试。
4.1.1 用户态模拟:快速上手
假设你从固件里提取出一个MIPS架构的Web服务程序 httpd,想看看它启动后监听哪个端口。用QEMU用户态模拟,一条命令就够了:
# 安装MIPS用户态模拟器
sudo apt install qemu-user-static
# 模拟运行,指定CPU架构和库路径
qemu-mips-static -L ./squashfs-root ./squashfs-root/usr/sbin/httpd
这里 -L 参数指定根文件系统路径,QEMU会从那里加载动态链接库。如果缺库,它会报错——嗯,这时候你就得去固件里翻对应的 .so 文件了。
-L 都不用加。直接 qemu-mips-static ./binary 就能跑。静态编译的固件程序在IoT设备里很常见,省去了很多库依赖的麻烦。
4.1.2 系统级模拟:完整板级环境
用户态模拟有个局限:它模拟不了硬件中断、DMA、网络控制器。如果你的固件需要完整的Linux内核启动流程,或者要跟硬件寄存器交互,就得用系统级模拟。
系统级模拟需要三个东西:
- 内核镜像(vmlinux或zImage)
- 根文件系统(squashfs、jffs2等)
- 设备树文件(.dtb)
我一般这么启动:
qemu-system-mips -M malta \
-kernel vmlinux \
-drive file=rootfs.squashfs,format=raw \
-append "root=/dev/sda console=ttyS0" \
-nographic \
-netdev user,id=net0,hostfwd=tcp::8080-:80 \
-device e1000,netdev=net0
注意最后两行:hostfwd=tcp::8080-:80 把宿主机的8080端口映射到QEMU虚拟机的80端口。这样你在浏览器里访问 http://localhost:8080,就能直接打到固件里的Web服务了。
console=ttyS0 写成了 ttyS1——QEMU默认的串口是ttyS0。先确认串口号,再怀疑内核。
4.2 GDB调试固件:在二进制世界里下断点
固件跑起来了,但怎么知道它执行到哪一步了?GDB配合QEMU的GDB stub,能让你像调试普通程序一样调试固件。
QEMU内置了GDB远程调试功能,启动时加个 -s 参数就行:
qemu-system-mips -M malta -kernel vmlinux -nographic -s
-s 是 -gdb tcp::1234 的简写,意思是QEMU在1234端口等待GDB连接。
然后在另一个终端启动GDB:
mips-linux-gnu-gdb vmlinux
(gdb) target remote :1234
(gdb) break *0x80010000
(gdb) continue
这里 break *0x80010000 是在绝对地址0x80010000下断点——这个地址通常是MIPS内核的入口点。如果你想调试某个具体的函数,比如 httpd 里的 process_request,得先找到它的地址。
4.2.1 找函数地址的两种方法
| 方法 | 命令/工具 | 适用场景 |
|---|---|---|
| 符号表 | nm binary | grep process_request |
固件未strip,保留符号 |
| IDA/Ghidra | 反编译后查看函数起始地址 | 固件被strip,无符号表 |
| 动态获取 | GDB中 info functions |
已加载符号,运行时查询 |
我遇到过最坑的情况是:固件里有个函数叫 auth_check,静态分析时觉得它逻辑没问题,结果GDB一跑,发现它根本没被调用——原来代码里有个条件编译的宏把调用路径给切掉了。动态调试就是用来揪这种「你以为有,实际没有」的问题。
4.2.2 调试网络服务的常见断点
调试网络服务时,我通常会在以下几个位置下断点:
recv()/read():看接收到的网络数据strcpy()/sprintf():检查是否存在缓冲区溢出system()/popen():看是否有命令注入- 自定义的
parse_request():看解析逻辑是否安全
举个例子,你想看Web服务收到HTTP请求后怎么处理:
(gdb) break recv
(gdb) continue
# 在宿主机上发送HTTP请求
curl http://localhost:8080/cgi-bin/test
# GDB命中断点,查看接收缓冲区
(gdb) x/32bx $a0
这里 $a0 是MIPS架构的第一个参数寄存器,存放 recv() 的缓冲区地址。用 x/32bx 打印32个字节,就能看到原始HTTP请求内容。
4.3 网络服务模拟与漏洞触发
固件里的网络服务通常依赖硬件外设——比如交换机芯片、GPIO、LED控制器。QEMU模拟不了所有外设,所以网络服务启动时可能会卡住或崩溃。这时候就需要「部分模拟」:把硬件相关的调用hook掉,让服务以为硬件正常工作。
4.3.1 用LD_PRELOAD hook硬件调用
Linux的动态链接器允许你预加载一个共享库,覆盖目标程序的函数。比如固件里的Web服务调用了 ioctl() 操作GPIO,你可以写一个假的 ioctl() 让它返回成功:
// fake_gpio.c
#include <stdio.h>
#include <stdarg.h>
int ioctl(int fd, unsigned long request, ...) {
// 假装所有ioctl都成功
return 0;
}
编译成共享库,然后预加载:
gcc -shared -fPIC -o fake_gpio.so fake_gpio.c
qemu-mips-static -E LD_PRELOAD=/path/to/fake_gpio.so ./httpd
这样Web服务调用 ioctl() 时,实际执行的是你的假函数。它不会报错,会继续往下走,直到进入网络监听循环。
rand() 也给hook了——因为固件里的TLS库初始化时调用了 rand(),而QEMU用户态模拟下 rand() 返回的值导致TLS握手失败。hook掉之后,服务正常启动。
4.3.2 构造触发Payload
服务跑起来了,接下来就是触发漏洞。假设你在静态分析时发现 parse_request() 函数里有个栈缓冲区溢出:
void parse_request(char *data) {
char buf[64];
strcpy(buf, data); // 没有长度检查!
}
用Python构造一个超长请求:
import socket
payload = b"A" * 200 + b"\x00"
s = socket.socket()
s.connect(("localhost", 8080))
s.send(b"GET /" + payload + b" HTTP/1.1\r\n\r\n")
s.close()
然后在GDB里观察:
(gdb) break parse_request
(gdb) continue
# 发送payload后,GDB命中断点
(gdb) stepi # 单步执行,观察strcpy后的栈变化
(gdb) info registers $sp # 查看栈指针
(gdb) x/20wx $sp # 查看栈内容
如果 $sp 附近出现了大量的 0x41414141('A'的十六进制),说明溢出成功覆盖了栈空间。接下来就是计算偏移量、覆盖返回地址、构造ROP链——这些我们在后面的章节会详细讲。
4.3.3 网络服务模拟的完整流程
我把整个流程总结成一张图,方便你对照操作:
这张图从左到右展示了完整的分析链路。你不需要每一步都走完——如果用户态模拟就能让服务跑起来,就别折腾系统级。我个人的原则是:用最少的模拟成本,达到最大的调试收益。
4.4 常见问题与避坑指南
动态分析看着简单,实际坑不少。我把自己踩过的坑列出来,你遇到了可以直接翻:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| QEMU启动后无输出 | 黑屏或卡住 | 检查内核启动参数 console= 是否正确;确认串口号 |
| 网络服务启动失败 | 提示 bind() failed |
检查端口映射是否冲突;确认QEMU的 -netdev 参数 |
| GDB连不上QEMU | Connection refused |
确认QEMU是否加了 -s 参数;检查防火墙 |
| LD_PRELOAD不生效 | 程序仍然调用原始函数 | 确认hook函数签名完全一致;检查是否使用了 -E 传递环境变量 |
| 固件崩溃在未知地址 | QEMU报 Illegal instruction |
可能是CPU架构不匹配(如MIPS32 vs MIPS64);检查QEMU的 -cpu 参数 |
/proc/cpuinfo 或尝试执行特殊指令。遇到这种情况,要么patch掉检测代码,要么用更高级的模拟器(如Unicorn)。不过90%的IoT固件没有这种反模拟机制,放心跑。
动态分析的核心就一句话:让固件动起来,然后看它怎么死。QEMU给了你运行环境,GDB给了你手术刀,剩下的就是耐心和细心。我见过有人一晚上就挖出三个漏洞,也见过有人卡在环境搭建上整整一周——别急,环境搭好了,后面的事就顺了。