VirtualBox安全研究入门:构建覆盖引导的模糊测试工具

本文详细介绍了如何对VirtualBox的模拟网络设备驱动进行安全研究,包括构建调试版本、应用AFL++插桩、编写测试工具以及提升测试稳定性,涵盖技术实现和代码示例。

VirtualBox安全研究入门

引言

本文介绍VirtualBox研究,并解释如何构建基于覆盖的模糊测试工具,重点针对模拟网络设备驱动。在以下示例中,我们将说明如何为非默认网络设备驱动PCNet创建测试工具。该示例可以轻松调整用于不同的网络驱动甚至不同的设备驱动组件。

我们知道有关该主题的优秀资源 - 参见[1], [2]。然而,这些资源从高层次视角覆盖模糊测试过程或省略了一些重要的技术细节。我们的目标是展示所有必要的步骤和代码,以对最新稳定版本的VirtualBox(撰写时为6.1.30)进行插桩和调试。由于SVN版本不同步,我们下载了tarball。

在我们的设置中,我们使用Ubuntu 20.04.3 LTS。由于VirtualBox不完全支持VT-x/AMD-V功能,我们使用原生主机。在使用MacBook时,以下指南支持将Linux安装到外部SSD。

VirtualBox使用kBuild框架进行构建。正如其页面所述,地球上只有极少数(0.5)人理解它,但编辑makefile应该很简单。正如我们稍后将看到的,在注释掉硬件特定组件后,这确实是事实。

kmk是make子系统的kBuild替代品。它允许根据提供的参数创建调试或发布构建。调试构建提供了强大的日志记录机制,我们将在接下来描述。

请注意,在本文中,我们将使用三种不同的构建。其余两个发布构建用于模糊测试和覆盖报告。由于它们涉及修改源代码,我们为每个实例使用单独的目录。

调试构建

Linux的构建说明在此描述。安装所有必需的依赖项后,运行以下命令足够:

1
2
$ ./configure --disable-hardening --disable-docs
$ source ./env.sh && kmk KBUILD_TYPE=debug

如果成功,将在out/linux.amd64/debug/bin/VirtualBox目录中创建二进制文件VirtualBox。在创建我们的第一个客户主机之前,我们必须编译并加载内核模块:

1
2
3
4
5
$ VERSION=6.1.30
$ vbox_dir=~/VirtualBox-$VERSION-debug/
$ (cd $vbox_dir/out/linux.amd64/debug/bin/src/vboxdrv && sudo make && sudo insmod vboxdrv.ko)
$ (cd $vbox_dir/out/linux.amd64/debug/bin/src/vboxnetflt && sudo make && sudo insmod vboxnetflt.ko)
$ (cd $vbox_dir/out/linux.amd64/debug/bin/src/vboxnetadp && sudo make && sudo insmod vboxnetadp.ko)

VirtualBox在include/VBox/log.h中定义了VBOXLOGGROUP枚举,允许选择性地启用特定文件或功能的日志记录。不幸的是,由于日志记录旨在用于调试构建,我们无法在发布构建中启用此功能而无需进行许多繁琐的更改。

与VirtualBox二进制文件不同,位于同一目录中的VBoxHeadless启动实用程序允许直接从命令行界面运行机器。为了说明,我们希望为此组件和PCNet网络驱动启用调试。首先,我们必须识别VBOXLOGGROUP的条目。它们使用我们想要跟踪的文件开头的LOG_GROUP_字符串定义:

1
2
3
4
$ grep LOG_GROUP_ src/VBox/Frontends/VBoxHeadless/VBoxHeadless.cpp src/VBox/Devices/Network/DevPCNet.cpp

src/VBox/Frontends/VBoxHeadless/VBoxHeadless.cpp:#define LOG_GROUP LOG_GROUP_GUI
src/VBox/Devices/Network/DevPCNet.cpp:#define LOG_GROUP LOG_GROUP_DEV_PCNET

我们将输出重定向到终端而不是创建日志文件,并使用来自grep输出的小写字符串指定日志组名称,不带前缀:

1
2
$ export VBOX_LOG_DEST="nofile stdout"
$ VBOX_LOG="+gui.e.l.f+dev_pcnet.e.l.f.l2" out/linux.amd64/debug/bin/VBoxHeadless -startvm vm-test

VirtualBox日志记录设施和所有参数的含义在此澄清。输出易于grep,对于理解内部结构至关重要。

AFL插桩用于afl-clang-fast / afl-clang-fast++

安装Clang

对于Ubuntu,我们可以按照官方说明安装Clang编译器。我们使用了clang-12,因为使用先前版本无法构建。或者,也支持clang-13。完成后,验证安装并创建符号链接以确保AFLplusplus不会抱怨缺少位置是有用的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ rehash
$ clang --version
$ clang++ --version
$ llvm-config --version
$ llvm-ar --version

$ sudo ln -sf /usr/bin/llvm-config-12 /usr/bin/llvm-config
$ sudo ln -sf /usr/bin/clang++-12 /usr/bin/clang++
$ sudo ln -sf /usr/bin/clang-12 /usr/bin/clang
$ sudo ln -sf /usr/bin/llvm-ar-12 /usr/bin/llvm-ar

构建AFLplusplus (AFL++)

我们选择的模糊测试工具是AFL++,尽管一切都可以轻松地用libFuzzer复制。由于我们不需要黑盒插桩,只包含源部分就足够了:

1
2
3
4
5
6
7
8
$ git clone https://github.com/AFLplusplus/AFLplusplus
$ cd AFLplusplus

# 如果VirtualBox编译失败,使用此修订版
$ git checkout 66ca8618ea3ae1506c96a38ef41b5f04387ab560

$ make source-only
$ sudo make install

应用补丁

要使用clang进行模糊测试,必须通过使用vbox-fuzz/AFL.kmk文件创建新的模板kBuild/tools/AFL.kmk,可在https://github.com/doyensec/vbox-fuzz上找到。

此外,我们必须修复与未定义符号或不同注释样式相关的多个问题。最重要的更改是禁用Ring-0组件的插桩(TEMPLATE_VBoxR0_TOOL)。否则无法启动客户机。所有这些更改都包含在补丁文件中。

有趣的是,当我调查编译失败期间获得的错误消息时,我发现了HITB会议的一些近期幻灯片,描述了完全相同的问题。这证实了我走在正确的轨道上,并且更多的人在尝试相同的方法。幻灯片还提到了VBoxHeadless,这是我们使用的自然选择的工具。

如果未修改的VirtualBox位于~/VirtualBox-6.1.30-release-afl目录中,我们运行这些命令来应用所有必要的补丁:

1
2
3
4
5
6
7
8
9
$ TO_PATCH=6.1.30
$ SRC_PATCH=6.1.30
$ cd ~/VirtualBox-$TO_PATCH-release-afl

$ patch -p1 < ~/vbox-fuzz/$SRC_PATCH/Config.patch
$ patch -p1 < ~/vbox-fuzz/$SRC_PATCH/undefined_xfree86.patch
$ patch -p1 < ~/vbox-fuzz/$SRC_PATCH/DevVGA-SVGA3d-glLdr.patch
$ patch -p1 < ~/vbox-fuzz/$SRC_PATCH/VBoxDTraceLibCWrappers.patch
$ patch -p1 < ~/vbox-fuzz/$SRC_PATCH/os_Linux_x86_64.patch

运行没有KBUILD_TYPEkmk会产生插桩的二进制文件,其中设备驱动捆绑在VBoxDD.so共享对象中。nm的输出确认了插桩符号的存在:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ nm out/linux.amd64/release/bin/VBoxDD.so | egrep "afl|sancov"
                 U __afl_area_ptr
                 U __afl_coverage_discard
                 U __afl_coverage_off
                 U __afl_coverage_on
                 U __afl_coverage_skip
000000000033e124 d __afl_selective_coverage
0000000000028030 t sancov.module_ctor_trace_pc_guard
000000000033f5a0 d __start___sancov_guards
000000000036f158 d __stop___sancov_guards

创建覆盖报告

首先,我们必须应用前一节中描述的AFL补丁。之后,我们复制插桩版本并删除先前编译的二进制文件(如果存在):

1
2
3
4
$ VERSION=6.1.30
$ cp -r ~/VirtualBox-$VERSION-release-afl ~/VirtualBox-$VERSION-release-afl-gcov
$ cd ~/VirtualBox-$VERSION-release-afl-gcov
$ rm -rf out

现在我们必须编辑kBuild/tools/AFL.kmk模板,以附加-fprofile-instr-generate -fcoverage-mapping开关,如下所示:

1
2
3
4
TOOL_AFL_CC  ?= afl-clang-fast$(HOSTSUFF_EXE)   -m64 -fprofile-instr-generate -fcoverage-mapping
TOOL_AFL_CXX ?= afl-clang-fast++$(HOSTSUFF_EXE) -m64 -fprofile-instr-generate -fcoverage-mapping
TOOL_AFL_AS  ?= afl-clang-fast$(HOSTSUFF_EXE)   -m64 -fprofile-instr-generate -fcoverage-mapping
TOOL_AFL_LD  ?= afl-clang-fast++$(HOSTSUFF_EXE) -m64 -fprofile-instr-generate -fcoverage-mapping

为了避免重复,我们与模糊测试构建共享srcinclude文件夹:

1
2
3
4
5
$ rm -rf ./src
$ rm -rf ./include

$ ln -s ../VirtualBox-$VERSION-release-afl/src $PWD/src
$ ln -s ../VirtualBox-$VERSION-release-afl/include $PWD/include

最后,我们通过添加以下内容来扩展src/VBox/Additions/x11/undefined_xfree86中未定义符号的列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ftell
uname
strerror
mkdir
__cxa_atexit
fclose
fileno
fdopen
strrchr
fseek
fopen
ftello
prctl
strtol
getpid
mmap
getpagesize
strdup

此外,由于此构建仅用于报告,我们禁用所有不必要的功能:

1
2
$ ./configure --disable-hardening --disable-docs --disable-java --disable-qt
$ source ./env.sh && kmk

原始配置文件通过设置LLVM_PROFILE_FILE生成。有关更多信息,Clang文档提供了必要的细节。

编写测试工具

获取pVM

此时,VirtualBox驱动已完全插桩,在开始模糊测试之前唯一剩下的事情是测试工具。PCNet设备驱动在src/VBox/Devices/Network/DevPCNet.cpp中定义,并导出多个函数。我们的输出被截断以仅包括R3组件,因为这些是我们目标的部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
 * 设备注册结构。
 */
const PDMDEVREG g_DevicePCNet =
{
    /* .u32Version = */             PDM_DEVREG_VERSION,
    /* .uReserved0 = */             0,
    /* .szName = */                 "pcnet",
#ifdef PCNET_GC_ENABLED
    /* .fFlags = */                 PDM_DEVREG_FLAGS_DEFAULT_BITS | PDM_DEVREG_FLAGS_RZ | PDM_DEVREG_FLAGS_NEW_STYLE,
#else
    /* .fFlags = */                 PDM_DEVREG_FLAGS_DEFAULT_BITS,
#endif
    /* .fClass = */                 PDM_DEVREG_CLASS_NETWORK,
    /* .cMaxInstances = */          ~0U,
    /* .uSharedVersion = */         42,
    /* .cbInstanceShared = */       sizeof(PCNETSTATE),
    /* .cbInstanceCC = */           sizeof(PCNETSTATECC),
    /* .cbInstanceRC = */           sizeof(PCNETSTATERC),
    /* .cMaxPciDevices = */         1,
    /* .cMaxMsixVectors = */        0,
    /* .pszDescription = */         "AMD PCnet Ethernet controller.\n",
#if defined(IN_RING3)
    /* .pszRCMod = */               "VBoxDDRC.rc",
    /* .pszR0Mod = */               "VBoxDDR0.r0",
    /* .pfnConstruct = */           pcnetR3Construct,
    /* .pfnDestruct = */            pcnetR3Destruct,
    /* .pfnRelocate = */            pcnetR3Relocate,
    /* .pfnMemSetup = */            NULL,
    /* .pfnPowerOn = */             NULL,
    /* .pfnReset = */               pcnetR3Reset,
    /* .pfnSuspend = */             pcnetR3Suspend,
    /* .pfnResume = */              NULL,
    /* .pfnAttach = */              pcnetR3Attach,
    /* .pfnDetach = */              pcnetR3Detach,
    /* .pfnQueryInterface = */      NULL,
    /* .pfnInitComplete = */        NULL,
    /* .pfnPowerOff = */            pcnetR3PowerOff,
    /* .pfnSoftReset = */           NULL,
    /* .pfnReserved0 = */           NULL,
    /* .pfnReserved1 = */           NULL,
    /* .pfnReserved2 = */           NULL,
    /* .pfnReserved3 = */           NULL,
    /* .pfnReserved4 = */           NULL,
    /* .pfnReserved5 = */           NULL,
    /* .pfnReserved6 = */           NULL,
    /* .pfnReserved7 = */           NULL,
#elif defined(IN_RING0)
// [ 截断 ]

最有趣的字段是.pfnReset,它重置驱动的状态,以及.pfnReserved函数。后者目前未使用,但我们可以添加自己的函数并通过修改PDM(可插拔设备管理器)头文件来调用它们。PDM是一个抽象接口,用于相对容易地添加新的虚拟设备。

但首先,如果我们想使用修改后的VboxHeadless,它提供对VirtualBox功能的高级接口(VirtualBox Main API),我们需要找到一种访问pdm结构的方法。

通过阅读源代码,我们可以看到多种模式,其中pVM(指向VM句柄的指针)被解引用以遍历包含所有设备实例的链表:

1
2
3
4
5
6
// src/VBox/VMM/VMMR3/PDMDevice.cpp

for (PPDMDEVINS pDevIns = pVM->pdm.s.pDevInstances; pDevIns; pDevIns = pDevIns->Internal.s.pNextR3)
{
    // [ 截断 ]
}

非Windows平台上的VirtualBox Main API使用Mozilla XPCOM。因此,我们想找出是否可以利用它来访问低级结构。经过一些挖掘,我们发现确实可以通过IMachineDebugger类检索VM句柄:

1
2
3
4
LONG64 llVM;
HRESULT hrc = machineDebugger->COMGETTER(VM)(&llVM);
PUVM pUVM = (PUVM)(intptr_t)llVM; /* 用户模式VM句柄 */
PVM pVM = pUVM->pVM;

获得指向VM的指针后,我们必须再次更改构建脚本,允许VboxHeadlessVBoxHeadless.cpp访问内部PDM定义。

我们尝试最小化更改量,经过一些实验,我们提出了以下步骤:

  1. 创建一个名为src/VBox/Frontends/Common/harness.h的新文件,内容如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* 没有这个,include/VBox/vmm/pdmtask.h 不会导入 PDMTASKTYPE 枚举 */
#define VBOX_IN_VMM 1

#include "PDMInternal.h"

/* machineDebugger COM VM getter 所需 */
#include <VBox/vmm/vm.h>
#include <VBox/vmm/uvm.h>

/* AFL 所需 */
#include <unistd.h>
  1. 修改src/VBox/Frontends/VBoxHeadless/VBoxHeadless.cpp文件,在事件循环开始之前,靠近文件末尾添加以下代码:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
            LogRel(("VBoxHeadless: failed to start windows message monitor: %Rrc\n", irc));
#endif /* RT_OS_WINDOWS */

        /* --------------- 开始 --------------- */
        LONG64 llVM;
        HRESULT hrc = machineDebugger->COMGETTER(VM)(&llVM);
        PUVM pUVM = (PUVM)(intptr_t)llVM; /* 用户模式VM句柄 */
        PVM pVM = pUVM->pVM;

        if (SUCCEEDED(hrc)) {
            PUVM pUVM = (PUVM)(intptr_t)llVM; /* 用户模式VM句柄 */
            PVM pVM = pUVM->pVM;

            for (PPDMDEVINS pDevIns = pVM->pdm.s.pDevInstances; pDevIns; pDevIns = pDevIns->Internal.s.pNextR3) {
                if (!strcmp(pDevIns->pReg->szName, "pcnet")) {
                    unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
                    while (__AFL_LOOP(10000)) {
                        int len = __AFL_FUZZ_TESTCASE_LEN;
                        pDevIns->pReg->pfnAFL(pDevIns, buf, len);
                    }
                }
            }
        }
        exit(0);
        /* --------------- 结束 --------------- */

        /*
         * 永久泵送vbox事件
         */
        LogRel(("VBoxHeadless: starting event loop\n"));
        for (;;)

在同一文件中,在#include "PasswordInput.h"指令之后添加:

1
#include "harness.h"

最后,在定义TrustedMain函数之前附加__AFL_FUZZ_INIT();

1
2
3
4
5
__AFL_FUZZ_INIT();

/**
 * 入口点。
 */
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计