Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

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

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

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

工具目录

分类

查看所有分类
Loading categories
virtualbox_e1000_0day — VirtualBox E1000 客户机到主机逃逸 | Kitploit
工具/GitHubGitHub/mortenoir1/virtualbox_e1000_0day
漏洞分析漏洞利用渗透测试硬件安全二进制利用
GitHubmortenoir1/virtualbox_e1000_0day

virtualbox_e1000_0day

VirtualBox E1000 客户机到主机逃逸

查看仓库
1.4k196227年前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]

让我们如下分配其结构字段(字段名是假设的,便于人类阅读,但直接映射到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; }

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; }

如果 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); } } ...

传递给 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 函数前后,这两个数据描述符的这些变量值。

Tx 描述符执行前/后u16MaxPktLenpThis->u16TxPktLenpDesc->data.cmd.u20DTALEN
data_2执行前0x301000x10
-执行后0x30100x100
data_3执行前0x30100x100
-执行后0x30100x100

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

下载工具