9.2 使用QEMU模拟虚拟环境
通常来说,当对Arm二进制文件进行逆向工程时,购买和配置一个完整的物理Arm环境可能会让人感觉像是不必要的开销,尤其是在我们没有基于Arm架构的计算机,且不需要原始性能来执行CPU密集型任务的时候。实际上有许多需要模拟Arm环境的场景,模拟环境最大的优点是可以灵活地引导不同类型的CPU核心和处理器架构。可用于创建此类虚拟环境的最流行的处理器模拟器是QEMU,这是一个免费的开源机器模拟器和虚拟机,可以在Linux、macOS和Windows上运行。QEMU支持两种主要的模拟模式:系统模式模拟(full-system emulation)和用户模式模拟。
在系统模式模拟下,QEMU创建一个完整的独立"虚拟机"。该虚拟机模拟Arm CPU以及数十个虚拟化外设,如硬盘驱动器、网络适配器、输入设备等。我们可以在这个虚拟机上安装操作系统,将要测试的二进制文件复制进去,并在这个虚拟Arm环境中执行它。
系统模式模拟有其好处,尤其是当你想运行的软件需要专用环境(例如,固件模拟)或需要执行恶意软件的动态分析时。如果你只想利用Arm汇编语言,测试非恶意的Arm二进制文件,或者执行简单的调试任务,并且不需要完整的模拟系统,则可以使用QEMU提供的另一种模拟模式:用户模式模拟。
9.2.1 QEMU用户模式模拟
当执行用户模式模拟时,QEMU运行一个单独的二进制文件,该二进制文件是针对与你的主机系统支持的架构不同的架构编译的,例如在x86_64上运行的AArch64。在底层,QEMU可以通过解码和运行软件中的每个Arm指令来模拟Arm处理器。由程序发出的系统调用会被拦截并发送到主机系统,这使程序可以与系统的其余部分无缝交互。
在本例中,主机操作系统是在x86_64处理器架构上运行的Ubuntu 20.04.1 LTS。我们将设置用户模式模拟,用来运行针对Arm 32位和Arm 64位架构编译的二进制文件。我们先安装以下软件包:
sudo apt install qemu-user qemu-user-static
对于Arm 32位架构,我们需要Arm的编程语言工具程序和Arm兼容的GCC版本:
sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf binutils-arm-linux-gnueabihf-dbg
对于AArch64,请安装以下程序:
sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu binutils-aarch64-linux-gnu-dbg
现在我们已经安装完QEMU,让我们编译一个简单的AArch64程序,并使用QEMU的用户模式模拟在基于Intel的x64 Linux主机上模拟运行它。我们的测试程序(保存为 hello64.c)的代码如下:
#include<stdio.h>
int main(void) {
return printf("Hello, I am an ARM64 binary!\n");
}
现在,我们可以用GCC的AArch64版本交叉编译这个程序,创建一个静态可执行文件:
aarch64-linux-gnu-gcc -static -o hello64 hello64.c
下面的测试表明,我们的主机系统是基于x64的Ubuntu机器,并且我们的二进制文件已正确编译为AArch64可执行文件:
uname -a
输出:
Linux ubuntu 5.4.0-58-generic #64-Ubuntu SMP Wed Dec 9 08:16:25 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
file hello64
输出:
hello64: ELF 64-bit LSB executable, ARM aarch64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=66307a9ec0ecfdcb05002f8ceecd310cc6f6792e, for GNU/Linux 3.7.0, not stripped
我们现在可以直接使用QEMU的用户模式模拟来运行这个二进制文件:
qemu-aarch64 ./hello64
输出:
Hello, I am an ARM64 binary!
QEMU的用户模式模拟直接对Arm二进制文件进行模拟,在软件中处理和运行每个Arm指令。虚拟化的Arm程序试图调用write系统调用将消息写入控制台,这个操作是通过基于Arm的系统调用接口来实现的。QEMU无缝地拦截了这个请求,并将其转换为x64 Ubuntu的等效系统调用,使程序将消息输出到控制台。
在前面的命令行中,我们通过 qemu-aarch64 直接使用了QEMU的用户模式模拟,但是QEMU还有另一个诀窍,即我们也可以直接从命令行运行此二进制文件,如下所示:
./hello64
输出:
Hello, I am an ARM64 binary!
你可能会好奇发生了什么,或者认为这是一个错误。x64 Linux怎么可能突然可以直接运行Arm二进制文件了?这里的奥秘来自 qemu-user-binfmt 包。查看 /proc/sys/fs/binfmt_misc 文件内部,便可以看到奥秘来自何处:
user@ubuntu:/proc/sys/fs/binfmt_misc$ cat qemu-aarch64
输出:
enabled
interpreter /usr/bin/qemu-aarch64-static
flags: OCF
offset 0
magic 7f454c460201010000000000000000000200b700
mask ffffffffffffff00fffffffffffffffffeffffff
这个文件告诉Linux内核如何解释与给定签名相匹配的文件。在本例中,签名对应一个ELF文件,该文件头将 e_machine 字段设置为 EM_AARCH64(0xb7)。如果执行了一个匹配的文件,Linux将启动相应的解释器——在本例中是AArch64用户模式模拟程序,然后由它运行该程序。同样的逻辑也适用于32位二进制文件:
user@ubuntu:~$ arm-linux-gnueabihf-gcc -static -o hello32 hello32.c
user@ubuntu:~$ ./hello32
输出:
Hello, I am an ARM32 binary!
对于动态链接的可执行文件,我们可以通过命令行选项 -L 提供ELF解释器和库的路径:
user@ubuntu:~$ aarch64-linux-gnu-gcc -o hello64dyn hello64.c
user@ubuntu:~$ qemu-aarch64 -L /usr/aarch64-linux-gnu ./hello64dyn
输出:
Hello, I'm executing ARM64 instructions!
对于Arm 32位二进制文件,它看起来像这样:
user@ubuntu:~$ arm-linux-gnueabihf-gcc -o hello32 hello32.c
user@ubuntu:~$ qemu-arm -L /usr/arm-linux-gnueabihf ./hello32
输出:
Hello, I am an ARM32 binary!