4、固件模拟与调试:QEMU系统模式模拟、用户模式模拟、固件调试环境搭建(GDB + QEMU)
固件分析里,最头疼的事是什么?
我个人觉得,是手里只有二进制,没有硬件。
你拿到了一个路由器固件,或者一个IoT设备的镜像,想逆向分析,但板子没到,或者干脆已经停产了。怎么办?
这时候,QEMU就是我们的救星。它能模拟出整个CPU和外围设备,让固件“以为”自己还跑在真硬件上。
这一章,我就带你搭起这套模拟环境。说白了,就是让固件在PC上“诈尸”。
4.1 QEMU的两种模式:系统模式 vs 用户模式
QEMU有两种工作方式,你得先搞清楚区别,不然容易走弯路。
| 模式 | 模拟对象 | 典型场景 | 调试难度 |
|---|---|---|---|
| 系统模式(System Mode) | 完整的硬件平台(CPU + 内存 + 外设) | 启动整个固件系统,运行完整OS | 较高,需要配置设备树 |
| 用户模式(User Mode) | 仅模拟CPU指令集,不模拟硬件 | 运行单个二进制程序,比如某个服务 | 较低,直接运行即可 |
怎么选?我一般这样判断:
- 如果你想分析固件里的某个独立程序(比如一个web服务、一个后台daemon),用用户模式就够了。快,省事。
- 如果你想分析内核驱动,或者程序依赖硬件寄存器、中断,那就必须上系统模式。
核心原则:用户模式是“轻量级沙箱”,系统模式是“全量虚拟机”。
4.2 用户模式模拟:快速上手
用户模式最简单。假设你有一个MIPS架构的固件,里面有个叫 httpd 的二进制文件。你想看看它怎么处理请求。
命令就一行:
# 假设固件是MIPS大端(mips)
qemu-mipsel-static ./httpd
# 如果报错缺少库,需要chroot到固件根目录
sudo chroot . ./qemu-mipsel-static ./httpd
嗯,这里要注意:很多固件里的程序是动态链接的。你直接跑会报“No such file or directory”。
为什么?因为它找不到 /lib 和 /usr/lib 里的库。
解决办法:把固件文件系统解压出来,然后用 chroot 切换根目录。
我的习惯:我会先解压固件,然后写一个脚本,自动挂载 /proc、/sys、/dev,再chroot进去。这样能避免很多“缺这缺那”的报错。
我曾经在分析一个D-Link路由器固件时,用户模式模拟死活跑不起来。后来发现是程序里用了 ioctl 操作硬件寄存器。用户模式不支持这个。最后只能换系统模式。
4.3 系统模式模拟:搭建完整环境
系统模式复杂一些,但能模拟得更真实。你需要三样东西:
- QEMU系统模拟器(比如
qemu-system-arm) - 固件内核(vmlinuz 或 zImage)
- 文件系统镜像(rootfs)
我以ARM架构为例,演示一下启动流程:
# 1. 启动QEMU,指定内核和文件系统
qemu-system-arm \
-M virt \ # 机器类型
-kernel vmlinuz \ # 内核镜像
-initrd rootfs.cpio.gz \ # 初始文件系统
-append "root=/dev/ram console=ttyAMA0" \
-nographic # 不使用图形界面
启动后,你会看到内核打印信息,最后进入一个shell。这时候,固件就“活”了。
避坑指南:我曾经在模拟一个MIPS固件时,内核启动到一半就卡死了。查了半天,发现是 -M 参数指定的机器类型不对。固件原本跑在“Atheros AR9331”上,我用了通用的“malta”机器,导致GPIO和中断控制器不匹配。
所以,机器类型一定要选对。你可以从固件的 /proc/device-tree 或者内核日志里找到线索。
4.4 调试环境搭建:GDB + QEMU
光能跑还不够,我们还得能调试。GDB + QEMU 的组合,是固件漏洞挖掘的标配。
原理很简单:QEMU内置了一个GDB stub,它监听一个端口(默认1234)。GDB连接上去,就能像调试普通程序一样调试固件。
4.4.1 启动QEMU并开启GDB服务
qemu-system-arm \
-M virt \
-kernel vmlinuz \
-initrd rootfs.cpio.gz \
-append "root=/dev/ram console=ttyAMA0" \
-s -S
参数说明:
-s:等价于-gdb tcp::1234,开启GDB服务。-S:启动时暂停CPU,等待GDB连接。这样你可以在第一条指令处开始调试。
4.4.2 连接GDB
另开一个终端,启动对应架构的GDB:
# ARM架构用 arm-none-eabi-gdb 或 gdb-multiarch
gdb-multiarch
# 在GDB里连接QEMU
(gdb) target remote localhost:1234
# 设置架构(如果GDB自动识别失败)
(gdb) set architecture arm
# 现在你可以打断点了
(gdb) break *0x80001000
(gdb) continue
连接成功后,QEMU会继续执行,直到遇到断点。
关键技巧:如果你调试的是内核,建议先加载符号表。很多固件内核是压缩的,你需要先解压,然后用 add-symbol-file 命令加载 vmlinux 的符号。
4.4.3 调试用户态程序
有时候,你只想调试固件里的某个用户态程序,而不是整个内核。怎么做?
我一般用 gdbserver 的方式:
- 在QEMU模拟的固件系统里,运行
gdbserver :1234 ./target_program - 在宿主机上,用
target remote [QEMU的IP]:1234连接
注意:QEMU模拟的系统和宿主机之间,网络是通的(默认是NAT或用户模式网络)。你需要知道QEMU里那个系统的IP地址。
4.5 知识体系结构图
下面这张图,概括了本章的核心逻辑。你可以把它当作一个决策树:
4.6 常见问题与避坑
最后,分享几个我踩过的坑:
- 网络不通:QEMU默认的用户模式网络,宿主机和模拟系统之间是NAT。如果你想从宿主机访问模拟系统里的服务(比如HTTP),需要做端口转发。用
-nic user,hostfwd=tcp::8080-:80把宿主机的8080端口映射到模拟系统的80端口。 - 文件系统空间不足:很多固件的rootfs是只读的,或者空间很小。我习惯用
-drive挂载一个额外的qcow2镜像,用来存放调试工具和日志。 - GDB断点失效:如果你在调试内核,断点打在某个函数上,但GDB说“Cannot insert breakpoint”。这通常是因为内核代码段被映射成了只读。你需要先关闭内核的写保护,或者使用硬件断点(
hbreak)。
我的建议:刚开始接触固件模拟时,别追求一步到位。先拿一个简单的、已知能跑的固件练手(比如OpenWrt的通用镜像)。把流程跑通,再挑战那些“硬骨头”。
好了,这一章的内容就到这里。模拟环境搭好了,下一步就是真正的漏洞挖掘了。你准备好了吗?
公众号:蓝海资料掘金营,微信deep3321