VirtualBox安全研究:构建基于覆盖率的设备驱动模糊测试框架

本文详细介绍如何对VirtualBox模拟网络设备驱动进行安全研究,包括调试构建、AFL++插桩、覆盖率报告生成及定制化测试 harness 开发,提供完整技术实现方案。

VirtualBox安全研究介绍

2022年4月26日 - 作者:Norbert Szetei

引言

本文介绍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是kBuild的make子系统替代工具,可根据参数创建调试或发布构建。调试构建提供强大的日志机制,下文将描述。

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

调试构建

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进行模糊测试,需通过https://github.com/doyensec/vbox-fuzz上的vbox-fuzz/AFL.kmk文件创建新模板kBuild/tools/AFL.kmk

此外,必须修复与未定义符号或不同注释风格相关的多个问题。最重要更改是禁用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_TYPE的kmk生成插桩二进制文件,其中设备驱动捆绑在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句柄:

[IMachineDebugger类图示]

由此,以下代码片段演示如何访问pVM:

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
6
__AFL_FUZZ_INIT();

/**
 * 入口点。
 */
extern "C" DECLEXPORT(int) TrustedMain(int argc, char **argv, char **envp)
  1. 编辑src/VBox/Frontends/VBoxHeadless/Makefile.kmk,更改VBoxHeadless_DEFSVBoxHeadless_INCS从:
1
2
3
4
5
VBoxHeadless_TEMPLATE := $(if $(VBOX_WITH_HARDENING),VBOXMAINCLIENTDLL,VBOXMAINCLIENTEXE)
VBoxHeadless_DEFS     += $(if $(VBOX_WITH_RECORDING),VBOX_WITH_RECORDING,)
VBoxHeadless_INCS      = \
  $(VBOX_GRAPHICS_INCS) \
  ../Common

改为:

1
2
3
4
5
6
VBoxHeadless_TEMPLATE := $(if $(VBOX_WITH_HARDENING),VBOXMAINCLIENTDLL,VBOXMAINCLIENTEXE)
VBoxHeadless_DEFS     += $(if $(VBOX_WITH_RECORDING),VBOX_WITH_RECORDING,) $(VMM_COMMON_DEFS)
VBoxHeadless_INCS      = \
        $(VBOX_GRAPHICS_INCS) \
        ../Common \
        ../../VMM/include

使用多个输入进行模糊测试

对于网络驱动,有多种方式提供用户控制数据:使用I/O端口指令或通过MMIO(PDMDevHlpPhysRead)从模拟设备读取数据。如果此部分不清楚,请参考[1],这可能是解释攻击面的最佳可用资源。此外,许多端口或值仅限于特定集合,为节省时间,我们只想使用这些值。因此,在考虑实现模糊测试框架后,我们发现了Fuzzed Data Provider(后称FDP)。

FDP是LLVM的一部分,在我们传递AFL生成的缓冲区后,它可以利用该缓冲区生成受限的数字、字节或枚举。我们可以将指向FDP的指针存储在设备驱动实例中,并在需要馈送缓冲区时随时检索。

回想一下,我们可以使用pfnReserved字段实现模糊测试辅助函数。为此,编辑include/VBox/vmm/pdmdev.h并更改PDMDEVREGR3结构以符合我们的原型:

1
2
3
DECLR3CALLBACKMEMBER(int, pfnAFL, (PPDMDEVINS pDevIns, unsigned char *buf, int len));
DECLR3CALLBACKMEMBER(void *, pfnGetFDP, (PPDMDEVINS pDevIns));
DECLR3CALLBACKMEMBER(int, pfnReserved2, (PPDMDEVINS pDevIns));

所有设备驱动都有状态,我们可以使用便捷宏PDMDEVINS_2_DATA访问。同样,我们可以扩展状态结构(本例中为PCNETSTATE)以通过指向FDP的指针包含FDP头文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// src/VBox/Devices/Network/DevPCNet.cpp

#ifdef IN_RING3
# include <iprt/mem.h>
# include <iprt/semaphore.h>
# include <iprt/uuid.h>
# include <fuzzer/FuzzedDataProvider.h> /* 添加此行 */
#endif

// [ 截断 ]

typedef struct PCNETSTATE
{
  // [ 截断 ]
#endif /* VBOX_WITH_STATISTICS */
    void * fdp; /* 添加此行 */
} PCNETSTATE;
/** 指向共享PCnet状态结构的指针。 */
typedef PCNETSTATE *PPCNETSTATE;

为反映这些更改,必须更新g_DevicePCNet结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/**
 * 设备注册结构。
 */
const PDMDEVREG g_DevicePCNet =
{
  // [[ 截断 ]]
  /* .pfnConstruct = */           pcnetR3Construct,
  // [[ 截断 ]]
  /* .pfnReserved0 = */           pcnetR3_AFL,
  /* .pfnReserved1 = */           pcnetR3_GetFDP,

添加新函数时,必须小心并将其包含在仅R3部分中。最简单的方法是找到R3构造函数并在其后添加新代码,因为它已为条件编译定义了IN_RING3宏。

PCNet测试套件示例:

 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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
static DECLCALLBACK(void *) pcnetR3_GetFDP(PPDMDEVINS pDevIns) {
    PPCNETSTATE     pThis   = PDMDEVINS_2_DATA(pDevIns, PPCNETSTATE);
    return pThis->fdp;
}

__AFL_COVERAGE();
static DECLCALLBACK(int) pcnetR3_AFL(PPDMDEVINS pDevIns, unsigned char *buf, int len)
{
    if (len > 0x2000) {
        __AFL_COVERAGE_SKIP();
        return VINF_SUCCESS;
    }

    static unsigned char buf2[0x2000];
    memcpy(buf2, buf, len);
    FuzzedDataProvider provider(buf2, len);

    PPCNETSTATE     pThis   = PDMDEVINS_2_DATA(pDevIns, PPCNETSTATE);

    pThis->fdp = &provider; // 使其他模块可访问
    FuzzedDataProvider *pfdp = (FuzzedDataProvider *) pDevIns->pReg->pfnGetFDP(pDevIns);

    void *pvUser = NULL;
    uint32_t u32;
    const std::array<int, 3> Array = {1, 2, 4};
    uint16_t offPort;
    uint16_t cb;

    pcnetR3Reset(pDevIns);

    __AFL_COVERAGE_DISCARD();
    __AFL_COVERAGE_ON();

    while (pfdp->remaining_bytes() > 0) {
        auto choice = pfdp->ConsumeIntegralInRange(0, 3);
        offPort = pfdp->ConsumeIntegral<uint16_t>();

        u32 = pfdp->ConsumeIntegral<uint32_t>();
        cb = pfdp->PickValueInArray(Array);

        switch (choice) {
            case 0:
                // pcnetIoPortWrite(PPDMDEVINS pDevIns, void *pvUser, 
                //   RTIOPORT offPort, uint32_t u32, unsigned cb)
                pcnetIoPortWrite(pDevIns, pvUser, offPort, u32, cb);
                break;
            case 1:
                // pcnetIoPortAPromWrite(PPDMDEVINS pDevIns, void *pvUser, 
                //   RTIOPORT offPort, uint32_t u32, unsigned cb)
                pcnetIoPortAPromWrite(pDevIns, pvUser, offPort, u32, cb);
                break;
            case 2:
                // pcnetR3MmioWrite(PPDMDEVINS pDevIns, void *pvUser,
                //   RTGCPHYS off, void const *pv, unsigned cb)
                pcnetR3MmioWrite(pDevIns, pvUser, offPort, &u32, cb);
                break;
            default:
                break;
        }
    }
    __AFL_COVERAGE_OFF();

    pThis->fdp = NULL;
    return VINF_SUCCESS;
}

模糊测试PDMDevHlpPhysRead

由于设备驱动多次调用此函数,我们决定修补包装器而非修改每个实例。可通过编辑src/VBox/VMM/VMMR3/PDMDevHlp.cpp、添加相关FDP头文件并更改pdmR3DevHlp_PhysRead方法以仅模糊特定驱动来实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "dtrace/VBoxVMM.h"
#include "PDMInline.h"

#include <fuzzer/FuzzedDataProvider.h> /* 添加此行 */

// [ 截断 ]

/** @interface_method_impl{PDMDEVHLPR3,pfnPhysRead} */
static DECLCALLBACK(int) pdmR3DevHlp_PhysRead(PPDMDEVINS pDevIns, RTGCPHYS GCPhys, void *pvBuf, size_t cbRead)
{
    PDMDEV_ASSERT_DEVINS(pDevIns);
    PVM pVM = pDevIns->Internal.s.pVMR3;
    LogFlow(("pdmR3DevHlp_PhysRead: caller='%s'/%d: GCPhys=%RGp pvBuf=%p cbRead=%#x\n",
             pDevIns->pReg->szName, pDevIns->iInstance, GCPhys, pvBuf, cbRead));

    /* 为模糊测试驱动更改此部分 */
    if (!strcmp(pDevIns->pReg->szName, "pcnet")) {
        FuzzedDataProvider *pfdp = (FuzzedDataProvider *) pDevIns->pReg->pfnGetFDP(pDevIns);
        if (pfdp && pfdp->remaining_bytes() >= cbRead) {
            pfdp->ConsumeData(pvBuf, cbRead);
            return VINF_SUCCESS;
        }
    }

使用out/linux.amd64/release/bin/VBoxNetAdpCtl,我们可以添加网络适配器并以持久模式开始模糊测试。然而,即使每秒可达到超过10k次执行,我们仍有一些工作需做以提高稳定性。

提高稳定性

不幸的是,此处描述的这些方法均无效,因为我们无法使用LTO插桩。我们猜测这是因为设备驱动模块是动态加载的,因此部分禁用插桩不可行,也无法识别不稳定边缘。不稳定性由未正确重置驱动状态引起,且由于我们运行整个VM,幕后有许多不易影响的事物,如内部锁或VMM。

改进之一已包含在测试套件中,因为我们可以在开始模糊测试前丢弃覆盖率,并仅对短模糊测试块启用它。

此外,我们可以禁用当前未模糊测试的所有设备的实例化。相关代码在src/VBox/VMM/VMMR3/PDMDevice.cpp中,通过pdmR3DevInit实现初始化完成例程。对于PCNet驱动,至少必须启用pci、VMMDev和pcnet模块。因此,可以跳过其余的初始化。

 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
50
51
52
53
    /*
     *
     * 实例化设备。
     *
     */
    for (i = 0; i < cDevs; i++)
    {
        PDMDEVREGR3 const * const pReg = paDevs[i].pDev->pReg;

        // if (!strcmp(pReg->szName, "pci")) {continue;}
        if (!strcmp(pReg->szName, "ich9pci")) {continue;}
        if (!strcmp(pReg->szName, "pcarch")) {continue;}
        if (!strcmp(pReg->szName, "pcbios")) {continue;}
        if (!strcmp(pReg->szName, "ioapic")) {continue;}
        if (!strcmp(pReg->szName, "pckbd")) {continue;}
        if (!strcmp(pReg->szName, "piix3ide")) {continue;}
        if (!strcmp(pReg->szName, "i8254")) {continue;}
        if (!strcmp(pReg->szName, "i8259")) {continue;}
        if (!strcmp(pReg->szName, "hpet")) {continue;}
        if (!strcmp(pReg->szName, "smc")) {continue;}
        if (!strcmp(pReg->szName, "flash")) {continue;}
        if (!strcmp(pReg->szName, "efi")) {continue;}
        if (!strcmp(pReg->szName, "mc146818")) {continue;}
        if (!strcmp(pReg->szName, "vga")) {continue;}
        // if (!strcmp(pReg->szName, "VMMDev")) {continue;}
        // if (!strcmp(pReg->szName, "pcnet")) {continue;}
        if (!strcmp(pReg->szName, "e1000")) {continue;}
        if (!strcmp(pReg->szName, "virtio-net")) {continue;}
        // if (!strcmp(pReg->szName, "IntNetIP")) {continue;}
        if (!strcmp(pReg->szName, "ichac97")) {continue;}
        if (!strcmp(pReg->szName, "sb16")) {continue;}
        if (!strcmp(pReg->szName, "hda")) {continue;}
        if (!strcmp(pReg->szName, "usb-ohci")) {continue;}
        if (!strcmp(pReg->szName, "acpi")) {continue;}
        if (!strcmp(pReg->szName, "8237A")) {continue;}
        if (!strcmp(pReg->szName, "i82078")) {continue;}
        if (!strcmp(pReg->szName, "serial")) {continue;}
        if (!strcmp(pReg->szName, "oxpcie958uart")) {continue;}
        if (!strcmp(pReg->szName, "parallel")) {continue;}
        if (!strcmp(pReg->szName, "ahci")) {continue;}
        if (!strcmp(pReg->szName, "buslogic")) {continue;}
        if (!strcmp(pReg->szName, "pcibridge")) {continue;}
        if (!strcmp(pReg->szName, "ich9pcibridge")) {continue;}
        if (!strcmp(pReg->szName, "lsilogicscsi")) {continue;}
        if (!strcmp(pReg->szName, "lsilogicsas")) {continue;}
        if (!strcmp(pReg->szName, "virtio-scsi")) {continue;}
        if (!strcmp(pReg->szName, "GIMDev")) {continue;}
        if (!strcmp(pReg->szName, "lpc")) {continue;}

       /*
         * 收集一些配置。
         */
        /* 受信任 */

最显著的问题是,当稳定性低时(百分比取决于我们模糊测试的驱动),最小化测试用例不是选项。如果无法重现崩溃,我们至少可以拦截它并在事后在gdb中分析。

作为解决方法,我们在调试模式下运行AFL,每次崩溃后生成核心文件。运行模糊测试器前,可通过以下方式启用此行为:

1
2
$ export AFL_DEBUG=1
$ ulimit -c unlimited

结论

我们提出了模糊测试VirtualBox设备驱动的一种可能方法。希望这有助于更好地理解VirtualBox内部机制。作为启发,我留下doc/VBox-CodingGuidelines.cpp中的引用:

  • (2) “真正高级的黑客理解机器的真正内部工作原理 - 他看透正在使用的语言,并窥见二进制代码的秘密运作 - 成为某种Ba’al Shem。” (Neal Stephenson “Snow Crash”)

参考文献

[1] Pham Hong Phi, “Adventures in Hypervisor: Oracle VirtualBox Research,” 2020年4月 [2] ChenNan, “Box Escape: Discovering 10+ Vulnerabilities in VirtualBox,” HITB安全会议, 2021年5月 [3] Pavel Cheremushkin, “Hunting for bugs in VirtualBox (First Take),” 2020年7月

其他相关文章

  • ksmbd漏洞研究 - 2025年1月7日
  • 使用Fuzzilli模糊测试JavaScript引擎 - 2020年9月9日
  • 从ASN.1语法模糊测试TLS证书 - 2020年5月14日
  • Staring into the Spotlight - 2017年11月15日
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计