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 系统模式模拟:搭建完整环境

系统模式复杂一些,但能模拟得更真实。你需要三样东西:

  1. QEMU系统模拟器(比如 qemu-system-arm
  2. 固件内核(vmlinuz 或 zImage)
  3. 文件系统镜像(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 的方式:

  1. 在QEMU模拟的固件系统里,运行 gdbserver :1234 ./target_program
  2. 在宿主机上,用 target remote [QEMU的IP]:1234 连接

注意:QEMU模拟的系统和宿主机之间,网络是通的(默认是NAT或用户模式网络)。你需要知道QEMU里那个系统的IP地址。

4.5 知识体系结构图

下面这张图,概括了本章的核心逻辑。你可以把它当作一个决策树:

固件模拟与调试决策流程 固件二进制 是否需要完整硬件模拟? 用户模式模拟 系统模式模拟 qemu-xxx-static + chroot qemu-system-xxx + 内核 + 文件系统 GDB + gdbserver 调试用户态 GDB + QEMU -s -S 调试内核

4.6 常见问题与避坑

最后,分享几个我踩过的坑:

  • 网络不通:QEMU默认的用户模式网络,宿主机和模拟系统之间是NAT。如果你想从宿主机访问模拟系统里的服务(比如HTTP),需要做端口转发。用 -nic user,hostfwd=tcp::8080-:80 把宿主机的8080端口映射到模拟系统的80端口。
  • 文件系统空间不足:很多固件的rootfs是只读的,或者空间很小。我习惯用 -drive 挂载一个额外的qcow2镜像,用来存放调试工具和日志。
  • GDB断点失效:如果你在调试内核,断点打在某个函数上,但GDB说“Cannot insert breakpoint”。这通常是因为内核代码段被映射成了只读。你需要先关闭内核的写保护,或者使用硬件断点(hbreak)。

我的建议:刚开始接触固件模拟时,别追求一步到位。先拿一个简单的、已知能跑的固件练手(比如OpenWrt的通用镜像)。把流程跑通,再挑战那些“硬骨头”。

好了,这一章的内容就到这里。模拟环境搭好了,下一步就是真正的漏洞挖掘了。你准备好了吗?


公众号:蓝海资料掘金营,微信deep3321