笔者之前也写过 qemu
类似的博客,但是发现以前对 qemu
理解并不深刻,并且有些方法已经过时,在此重新更新一版,并且覆盖了使用 qemu
的各种场景。我们的目的只有一个,模拟固件中的单个二进制程序。本篇博客会讲述各种场景下,使用 qemu
来运行和调试二进制程序,涵盖了 qemu
所有主流的用法。
0x10 qemu 的安装
qemu
发展这么多年,已经相当稳定了,你可以使用源码编译的方式,也可以使用 apt 安装,甚至还可以使用 Windows
平台下的 .exe
一键安装。
0x11 源码编译
安装 qemu 需要的依赖
sudo apt-get install zlib1g-dev libglib2.0-0 libglib2.0-dev libtool libsdl1.2-dev autoconf
sudo apt-get install libpixman-1-dev
qemu 官方站点 提供了源码,版本在不断更新。最新的 5.2.0 版本已经使用 ninja 进行编译。
Ninja 是Google的一名程序员推出的注重速度的构建工具,一般在Unix/Linux上的程序通过make/makefile来构建编译,而Ninja通过将编译任务并行组织,大大提高了构建速度。
本次还是使用 5.0.1 版本,因为其编译方式与大众所熟悉的 Linux C 编译方式一样,具有通用性。
下载源码,解压
wget https://download.qemu.org/qemu-5.0.1.tar.xz
tar -xf qemu-5.0.1.tar.xz
在源码中新建 build
目录,用于存放编译生成的过程文件
mkdir build
cd build
自定义配置需要安装 qemu
中的哪些功能
- xxx-softmmu 将编译 qemu-system-xxx,这是一个用于 xxx 架构(系统仿真,system mode)的仿真机器。重置时,起点将是该架构的重置向量。
- xxx-linux-user 则编译 qemu-xxx,可以在 xxx 架构中运行用户应用程序(用户模式仿真,user mode),这将寻找用户应用程序的主要功能,并从那里开始执行。
简言之:如果你想在 x86 中模拟一个完整的其他架构的系统,使用 qemu-system
,如果只是想单纯的运行一个其他架构的二进制程序,那么就使用 qemu-xxx
../configure --target-list=arm-softmmu,arm-linux-user --prefix=./out
编译和安装
make -j$(nproc)
sudo make install
★ | 备注:新版本的 qemu 貌似不会在运行 make install 之后将编译好的 qemu 二进制复制到目标文件夹,而是放在编译生成的中间文件。 |
┌──(lys㉿kali)-[~/Tools/qemu-5.0.1/build]
└─$ find ./ -name "qemu-system*"
./arm-softmmu/qemu-system-arm
./arm-softmmu/out/bin/qemu-system-arm
┌──(lys㉿kali)-[~/Tools/qemu-5.0.1/build]
└─$ find ./ -name "qemu-arm*"
./arm-linux-user/out/bin/qemu-arm
./arm-linux-user/qemu-arm
0x12 apt 安装
如果不想使用源码编译,最为简单的方法就是通过 debian
自带的包管理器进行安装。
sudo apt search qemu
按照需求安装即可
这里,我们选择 qemu-user-binfmt
以及 qemu-system
# sudo apt-get install qemu-user-static # 静态链接的 qemu-user
sudo apt-get install qemu-user-binfmt # 此命令有用,会在 /usr/bin 目录下生成 qemu-*
sudo apt-get install qemu-system # 此命令有用,会在 /usr/bin 目录下生成 qemu-system-*
如果嫌弃 qemu
安装太占空间(所有 system
模式的架构大概 800MB
)当然也可以指定只安装我们需要模拟的架构,例如 qemu-system-arm
sudo apt-get install qemu-system-arm
安装好的二进制如下
0x13 Windows 平台安装 qemu
QEMU Binaries for Windows 为我们提供了 32 位和 64 位的 qemu
二进制安装程序以及相关的文档。选择一个合适的版本,下载后就可以直接安装了。
安装好之后,会在安装目录下生成相应的 exe
,跟 Linux
是类似的,我们将安装目录放到环境变量,就可以直接在 cmd
中使用相关命令了。
0x20 模拟和调试
我们以一个实际的案例,来分析如何在 x86 平台中使用 qemu 运行 arm 架构的程序。以 Tenda AC 15 路由器为例,从官网下载含漏洞版本固件 V15.03.1.16。下载后,用 binwalk
解压。
binwalk -Me AC15.bin
解压出来的文件系统如下
架构为 arm32,且是动态链接的。现在有两个思路来运行其中的程序
- 方法一:使用
qemu-arm
,在宿主机中直接运行 - 方法二:使用
qemu-system-arm
,建立一个 arm 的虚拟机,在虚拟机中运行相应的 arm 程序
0x21 运行目标程序
用户模式
方法一:设置环境变量,指定相应根目录
如果是静态链接的程序,可以直接用 qemu-arm
运行。但是如今固件中的程序大多是动态链接的,直接运行,会提示找不到库
┌──(lys㉿kali)-[~/Documents/IoT/_AC15.bin.extracted/squashfs-root]
└─$ qemu-arm ./bin/ip 127 ⨯
qemu-arm: Could not open '/lib/ld-uClibc.so.0': No such file or directory
因此需要设置共享库的目录,qemu
提供了设置共享库的环境变量,设置完之后,即可成功运行
┌──(lys㉿kali)-[~/Documents/IoT/_AC15.bin.extracted/squashfs-root]
└─$ export QEMU_LD_PREFIX=./ 255 ⨯
┌──(lys㉿kali)-[~/Documents/IoT/_AC15.bin.extracted/squashfs-root]
└─$ qemu-arm ./bin/ip
Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }
ip [ -force ] [-batch filename
where OBJECT := { link | addr | addrlabel | route | rule | neigh | ntable |
tunnel | maddr | mroute | monitor | xfrm }
OPTIONS := { -V[ersion] | -s[tatistics] | -d[etails] | -r[esolve] |
-f[amily] { inet | inet6 | ipx | dnet | link } |
-o[neline] | -t[imestamp] }
方法二:使用 qemu-user 提供的参数指定目录
qemu-user
模式提供了 -L
参数
Argument Env-variable Description
-L path QEMU_LD_PREFIX set the elf interpreter prefix to 'path'
运行结果如下
┌──(lys㉿kali)-[~/Documents/IoT/_AC15.bin.extracted/squashfs-root]
└─$ qemu-arm -L ./ ./bin/ip 130 ⨯
Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }
ip [ -force ] [-batch filename
where OBJECT := { link | addr | addrlabel | route | rule | neigh | ntable |
tunnel | maddr | mroute | monitor | xfrm }
OPTIONS := { -V[ersion] | -s[tatistics] | -d[etails] | -r[esolve] |
-f[amily] { inet | inet6 | ipx | dnet | link } |
-o[neline] | -t[imestamp] }
方法三:chroot 命令更改当前系统根目录
chroot
命令用来在指定的根目录下运行指令。chroot
,即 change root directory
(更改 root
目录)。在 linux
系统中,系统默认的目录结构都是以 /
,即是以根 (root
) 开始的。而在使用 chroot
之后,系统的目录结构将以指定的位置作为 /
位置
┌──(lys㉿kali)-[~/Documents/IoT/_AC15.bin.extracted/squashfs-root]
└─$ sudo chroot ./ ./qemu-arm-static ./bin/ip 125 ⨯
[sudo] password for lys:
Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }
ip [ -force ] [-batch filename
where OBJECT := { link | addr | addrlabel | route | rule | neigh | ntable |
tunnel | maddr | mroute | monitor | xfrm }
OPTIONS := { -V[ersion] | -s[tatistics] | -d[etails] | -r[esolve] |
-f[amily] { inet | inet6 | ipx | dnet | link } |
-o[neline] | -t[imestamp] }
注意一下,需要把静态链接的 qemu-arm-static
复制到目标文件系统中,此功能才能生效。
系统模式
想使用 qemu
的系统模式运行 arm
架构的程序,必然需要一个 arm
架构的虚拟机。可以自己编译内核和文件系统,制作一个 arm
架构的 Linux
系统,但是这样毕竟花费很多精力,如笔者之前写过的博文 为 QEMU ARM 仿真器编译 Linux 内核:QEMU 模拟 ARM 环境。
1 寻找合适的 qemu 虚拟机映像
好在已经有现成的专门适用于 qemu
的 Linux
虚拟机。Debian
为我们提供了适配 qemu 的镜像。
对于 arm32
,分为 armel
和 armhf
,一般来说,都可以使用,都是可以用于浮点计算,主要区别在于,armel 传参数用普通寄存器,而 armhf 传参数用的是 fpu 的寄存器,因此 armhf
的浮点运算性能更高。
★ | 如何选择合适的 qemu 虚拟机,是根据该虚拟机支持的 arm 指令的版本,配合待测二进制的指令架构的版本来决定的。 |
就拿本例来说,我们待运行的是 tenda AC15
中的程序,其 arm 指令的具体架构是 armv7
而提供的 Debian qemu
两个 arm32
的虚拟机,具体的指令架构如下
Linux debian-armel 3.2.0-4-versatile #1 Debian 3.2.51-1 armv5tejl
Linux debian-armhf 3.2.0-4-vexpress #1 SMP Debian 3.2.51-1 armv7l
通过这个信息,相信你已经知道如何选择了吧。
文件说明
- debian_wheezy,file 镜像文件,相当于实际的文件系统
- initdrd.img,初始 RAM 磁盘,系统引导过程中挂载的一个临时根文件系统,用来支持两阶段的引导过程。initrd 文件中包含了各种可执行程序和驱动程序
- vmlinuz,裁剪过的 Linux 内核
2 配置网络
为了方便 qemu
虚拟机和宿主机通信,我们在宿主机上新建一个新的虚拟网卡。在此之前先安装 tunctl
sudo apt-get install uml-utilities
配置虚拟网卡
sudo tunctl -t virtual0 -u lys #virtual0是网卡名字 lsy是你已有账户名
sudo ifconfig virtual0 192.168.2.2/24 #给virtual0配置IP地址,这里配什么地址都可以
下载文件之后,使用 qemu-system
命令加载,即可成功启动虚拟机。
qemu-system-arm \
-M vexpress-a9 \
-kernel vmlinuz-3.2.0-4-vexpress \
-initrd initrd.img-3.2.0-4-vexpress \
-drive if=sd,file=debian_wheezy_armhf_standard.qcow2 \
-append "root=/dev/mmcblk0p2 console=ttyAMA0" \
-net nic -net tap,ifname=virtual0,script=no,downscript=no \
-nographic
参数说明
-M // 选择开发板
-m // 指定内存大小
-drive // 定义存储驱动器
file= // 定义镜像文件
-net nic // 创建客户机网卡
-net tap // 创建 tap 设备,以桥接方式跟宿主机通信
ifname=virtual0 // tap 设备与名为 virtual0 的虚拟网卡进行桥接通信
-nographic // 以非图形化模式启动
-append // 内核启动附加参数
-console=ttyAMA0 // console指向串口,有此启动参数,内核启动日志才能输出到宿主机终端
-nographic // 不再启用额外的终端界面
关于 qemu-system
参数的详细描述,可见 qemu-system-x86_64命令总结
用户名:user/root,密码:user/root
登录系统后,给 qemu 虚拟机中的网卡设置静态地址,方便与宿主机进行通信。
ifconfig eth0 192.168.2.3/24
3 待模拟的文件系统复制到 qemu 虚拟机
将宿主机上的待测文件系统打包
# tar -czvf squashfs-root.tar.gz squashfs-root
tar -cjpf squashfs-root.tar.bz2 squashfs-root
在宿主机上新建一个简单的服务器,可以使用 Kali 自带的 Apache2,也可以使用 python 自带的模块。
python -m SimpleHTTPServer 8000
qemu
虚拟机中,下载文件,并解压
wget http://192.168.2.2:8000/ squashfs-root
# tar -xzvf squashfs-root.tar.gz
tar -xvf squashfs-root.tar.bz2
4 运行目标程序
使用 chroot
命令更改系统根目录
chroot squashfs-root sh
运行目标文件系统中的进程
0x22 调试
用户模式
qemu-user
相关命令,自带了 gdbserver
,通过 -g
参数指定即可。
$ qemu-arm -L squashfs-root -g 8888 squashfs-root/bin/ip
新建另一个终端,使用 gdb-multiarch
调试不同架构的远程目标程序
# 安装 gdb-multiarch
sudo apt-get install gdb-multiarch
# 远程调试
$ gdb-multiarch
(gdb) file ./squashfs-root/bin/ip # 也要在本地加载远程的目标程序
Reading symbols from ./squashfs-root/bin/ip...
(No debugging symbols found in ./squashfs-root/bin/ip)
(gdb) set architecture arm # 设置架构
The target architecture is set to "arm".
(gdb) target remote localhost:8888
这里推荐使用 gef
插件,而不是 peda
,peda
不能很好的识别相关寄存器。
git clone https://github.com/hugsy/gef.git
echo "source /home/lys/Tools/peda/peda.py" >> ~/.gdbinit
系统模式
需要下载支持 arm
架构的 gdbserver
,好在已经有开源项目为我们编译好了静态链接的支持各种不同架构的 gdbserver
。选择 gdbserver-7.7.1-armhf-eabi5-v1-sysv
,下载,并放入 qemu 虚拟机。
gdbserver 192.168.2.3:8888
宿主机中的 gdb
客户端使用相同的 gdb-multiarch
连接即可。
0x30 Q&A
在我的一台电脑上的测试环境中(Kali 2020.4
),启动 qemu
虚拟机的时候,出现了如下错误,大概就是 sd
卡大小不是 2 的幂(奇怪的是在我的另外一台电脑上,同样也是 Kali 2020.4 的虚拟机中,就没有出现该问题)
qemu-system-arm: Invalid SD card size: 25 GiB
SD card size has to be a power of 2, e.g. 32 GiB.
You can resize disk images with 'qemu-img resize <imagefile> <new-size>'
(note that this will lose data if you make the image smaller than it currently is).
解决方案一:那就按照它的意思,使用 qemu-img
重新修改 SD
卡文件的大小
qemu-img resize debian_wheezy_armhf_standard.qcow2 32G
解决方案二:我们可以使用 armel
,debian
提供的 armel
的开发板没有使用 sd 卡(该版本的虚拟机支持架构较老,即armv5
,不建议使用此方案)。
qemu-system-arm \
-M versatilepb \
-kernel vmlinuz-3.2.0-4-versatile \
-initrd initrd.img-3.2.0-4-versatile \
-hda debian_wheezy_armel_standard.qcow2 \
-append "root=/dev/sda1" \
-net nic -net tap,ifname=virtual0,script=no,downscript=no
为了避免与刚刚所用的方法重复,这里,我们没有使用 -nographic
参数,并且删去了启动参数 console=ttyAMA0
。这种方式会另外启动一个 qemu
窗口。如下所示
这种打开 qemu
模拟器窗口的方式不利于将宿主机中的命令复制到虚拟机中,所以还是建议使用之前的方法,即加上相应参数,不另外启动 qemu
窗口。