Oracle VM VirtualBox 漏洞:通过 VGA 设备实现 VM Escape

漏洞描述

概要

VirtualBoxvmsvga3dSurfaceMipBufferSize函数中存在整数溢出漏洞[来源]。该漏洞允许攻击者操纵 malloc 调用,使得分配 0 字节,同时 VirtualBox 将缓冲区大小跟踪为大于 0 的值。

攻击者可以利用此条件并获得线性读/写原语,然后将其升级为主机内存中的任意读/写访问权限。我们提供了一个概念验证,演示了如何利用此漏洞完全逃离虚拟机。

严重性

概念验证

我们能够利用定义在 VMSVGAMOB 对象中的 VMSVGAGBO [链接],其定义如下:

typedef struct VMSVGAGBO
{
  uint32_t        fGboFlags;
  uint32_t        cTotalPages;
  uint32_t        cbTotal;
  uint32_t        cDescriptors;
  PVMSVGAGBODESCRIPTOR  paDescriptors;
  void          *pvHost; /* Pointer to cbTotal bytes on the host if VMSVGAGBO_F_HOST_BACKED is set. */
} VMSVGAGBO, *PVMSVGAGBO;
typedef struct VMSVGAMOB
{
  AVLU32NODECORE     Core; /* Key is the mobid. */
  RTLISTNODE       nodeLRU;
  VMSVGAGBO        Gbo;
} VMSVGAMOB, *PVMSVGAMOB;

该算法如下:

  1. 触发分配 buggy_surface(分配大小为 0 的 surface)。
  2. 分配一个 GBO 对象,其 cbTotal 中的值可用于识别我们的对象(即“egg”)。 经过反复试验,0x1421337 值被证明足够可靠(大约 100% 的成功率)。
  3. 利用越界读取,并在 buggy_surface 之后检查字节,看看是否可以在短范围内(前 0x5a 字节)找到 egg,并假设我们的目标 GBO 对象分配在 surface 之后。
  4. 如果没有,则返回第 1 步。

该算法被证明在 10 次尝试内 100% 可靠地进行堆 grooming。

通过使用带有攻击者选择值的线性写入越界来破坏 cbTotalpvHost,可以实现任意读取,然后 guest 可以发出一个 vmsvga3dDXReadbackCOTable [链接] 命令,该命令最终会调用 vmsvgaR3MobBackingStoreWriteToGuest [链接],这将使用两个损坏的变量将 pvHost 中的 cbTotal 字节写入 guest 内存。 类似地,可以通过 GrowCOTable 命令实现任意写入,该命令在调用 vmsvgaR3MobBackingStoreCreate [链接]时,最终将导致设备从 guest 内存中 读取 cbTotal 字节到 pvHost 中。

任意堆分配

通过 GrowCOTable 可以实现的另一个有用的原语是分配任意堆内存块的能力,这是通过破坏 fGboFlags 字段来完成的,这将导致设备 分配 cbTotal 大小的内存块。 事实证明,此原语在稍后阶段很有用,可以找到一个存储 shellcode 的位置,以用于最终的利用阶段。

破坏 ASLR 并获得 RIP 控制权

VMSVGAMOB 对象的另一个巨大好处是,字段 nodeLRU 包含指向设备 VMSVGAR3STATE 结构的 指针。 后者结构很有用,因为它包含各种 函数指针,这些函数指针可以通过任意写入原语进行破坏,然后再用于获得 RIP 控制和任意代码执行。

逃离 VM

  1. 泄露函数指针 pfnCommandClear [链接] 的值。
  2. 用步骤 1 中的值推导出 VBoxDD.so 的基地址。
  3. 读取 VBoxDD.so 的 GOT 表,找到一个函数指针,该指针将指向 VBoxRT.so 的基地址。
  4. 使用任意堆分配原语来植入 shellcode。
  5. 构建一个 ROP 链,其中包含在两个文件中找到的 gadget。 5a. 将堆栈透视到堆的可控部分。 5b. 调用 memprotect 使 shellcode 位置可执行。 5c. 跳转到 shellcode 中。
  6. 用第一个 ROP 链 gadget 破坏 pfnCommandClear 的值,从而透视堆栈。
  7. 发出 vmsvga3dCommandClear [链接] 命令。
  8. 启动 RCE。

进一步分析

线性越界读取

假设 guest 分配了两个 surface:

  1. Buggy_surface,它有一个 0 的 allocation 支持,并且将扮演 src 的角色。
  2. Transfer_surface,它有一个有效的大小和 allocation,并且将扮演 dest 的角色。

以下步骤将执行几乎任意大小的线性越界读取,将内容从 buggy_surface 传输到 transfer_surface。为了实现线性越界读取,guest 可以发出命令 SVGA_3D_CMD_DX_BUFFER_COPY [链接],该命令在两个 surface 之间传输数据。

  /*
   * Map the source buffer.
   */
  VMSVGA3D_MAPPED_SURFACE mapBufferSrc;
  rc = vmsvga3dSurfaceMap(pThisCC, &imageBufferSrc, NULL, VMSVGA3D_SURFACE_MAP_READ, &mapBufferSrc);
  if (RT_SUCCESS(rc))
  {
    /*
     * Map the destination buffer.
     */
    VMSVGA3D_MAPPED_SURFACE mapBufferDest;
    rc = vmsvga3dSurfaceMap(pThisCC, &imageBufferDest, NULL, VMSVGA3D_SURFACE_MAP_WRITE, &mapBufferDest);
    if (RT_SUCCESS(rc))
    {
      /*
       * Copy the source buffer to the destination.
       */
      uint8_t const *pu8BufferSrc = (uint8_t *)mapBufferSrc.pvData;
      uint32_t const cbBufferSrc = mapBufferSrc.cbRow;
      uint8_t *pu8BufferDest = (uint8_t *)mapBufferDest.pvData;
      uint32_t const cbBufferDest = mapBufferDest.cbRow;
      if (  pCmd->srcX < cbBufferSrc
        && pCmd->width <= cbBufferSrc- pCmd->srcX
        && pCmd->destX < cbBufferDest
        && pCmd->width <= cbBufferDest - pCmd->destX)
      {
        RT_UNTRUSTED_VALIDATED_FENCE();
        memcpy(&pu8BufferDest[pCmd->destX], &pu8BufferSrc[pCmd->srcX], pCmd->width);
      }

memcpy 操作的源参数 mapBufferSrc.pvData 对应于先前分配的大小为 0 的缓冲区。由于 cbBufferSrc(以及扩展名 mapBufferSrc.cbRow)的计算方式,因此可以绕过保护 memcpy 调用的条件:vmsvga3dSurfaceMap 最终会调用 vmsvga3dSurfaceMapInit,其中包含在上一步骤中计算的 surface 的 尺寸

 else
  {
    clipBox.x = 0;
    clipBox.y = 0;
    clipBox.z = 0;
    clipBox.w = pMipLevel->mipmapSize.width;
    clipBox.h = pMipLevel->mipmapSize.height;
    clipBox.d = pMipLevel->mipmapSize.depth;
  }
  /// @todo Zero the box?
  //if (enmMapType == VMSVGA3D_SURFACE_MAP_WRITE_DISCARD)
  //  RT_BZERO(.);
  vmsvga3dSurfaceMapInit(pMap, enmMapType, &clipBox, pSurface,
              pMipLevel->pSurfaceData, pMipLevel->cbSurfacePitch, pMipLevel->cbSurfacePlane);

vmsvga3dSurfaceMapInit 内部,这些尺寸将用于确定 cbRow

void vmsvga3dSurfaceMapInit(VMSVGA3D_MAPPED_SURFACE *pMap, VMSVGA3D_SURFACE_MAP enmMapType, SVGA3dBox const *pBox,
              PVMSVGA3DSURFACE pSurface, void *pvData, uint32_t cbRowPitch, uint32_t cbDepthPitch)
{
  uint32_t const cxBlocks = (pBox->w + pSurface->cxBlock - 1) / pSurface->cxBlock;
  uint32_t const cyBlocks = (pBox->h + pSurface->cyBlock - 1) / pSurface->cyBlock;
  pMap->enmMapType  = enmMapType;
  pMap->format    = pSurface->format;
  pMap->box     = *pBox;
  pMap->cbBlock   = pSurface->cbBlock;
  pMap->cxBlocks   = cxBlocks;
  pMap->cyBlocks   = cyBlocks;
  pMap->cbRow    = cxBlocks * pSurface->cbPitchBlock;
  pMap->cbRowPitch  = cbRowPitch;
  pMap->cRows    = (cyBlocks * pSurface->cbBlock) / pSurface->cbPitchBlock;
  pMap->cbDepthPitch = cbDepthPitch;
  pMap->pvData    = (uint8_t *)pvData
            + (pBox->x / pSurface->cxBlock) * pSurface->cbPitchBlock
            + (pBox->y / pSurface->cyBlock) * cbRowPitch
            + pBox->z * cbDepthPitch;
}

cxBlocks 源自 guest 在其定义中提供的 surface 宽度。由于控制上述 memcpy 操作的检查仅取决于 cbRow 的值,而不取决于内存区域的大小,因此 vm 可以从分配的大小为 0 的缓冲区中最多读取 cbRow 字节:

...
      uint32_t const cbBufferSrc = mapBufferSrc.cbRow;
...
if (  pCmd->srcX < cbBufferSrc
        && pCmd->width <= cbBufferSrc- pCmd->srcX
        && pCmd->destX < cbBufferDest
        && pCmd->width <= cbBufferDest - pCmd->destX)
      {
        RT_UNTRUSTED_VALIDATED_FENCE();
        memcpy(&pu8BufferDest[pCmd->destX], &pu8BufferSrc[pCmd->srcX], pCmd->width);
      }

memcpy 调用会将越界读取的内容从 buggy_surface 复制到 transfer_surface 中,然后攻击者可以发出 READBACK_SUBRESOURCE 命令 [链接] 以将 transfer_surface 的内容返回到 guest 内存。

线性越界写入

对于这种情况,恶意 guest 只需要定义一个 surface:buggy_surface。 通过发出 UPDATE_SUBRESOURCE [链接] 命令,可以实现线性越界写入主机内存,该命令反过来将使用攻击者控制的参数调用函数 vmsvgaR3TransferSurfaceLevel

与线性越界读取的情况类似,设备将首先映射要传输数据的 surface 尺寸,这次它使用函数 vmsvga3dGetBoxDimensions 完成此操作,该函数几乎与 vmsvga3dSurfaceMapInit 执行的操作相同。

与线性读取相比,对于这种情况,攻击者有机会定义要传输的 surface 的“框”,设备将确保所述框在图像大小的范围内:

  [...]
  SVGA3dBox clipBox;
  if (pBox)
  {
    clipBox = *pBox;
    vmsvgaR3ClipBox(&pMipLevel->mipmapSize, &clipBox);
    ASSERT_GUEST_RETURN(clipBox.w && clipBox.h && clipBox.d, VERR_INVALID_PARAMETER);
  }
  
  [...]

[来源] vmsvga3dGetBoxDimensionsvmsvga3dSurfaceMapInit 都有相同的错误:它们仅使用 guest 指定的尺寸来计算 cbRow 的大小,而没有考虑支持它们的 surface 的缓冲区的大小[1,2]。

[...]
  pMap->cbRow    = cxBlocks * pSurface->cbPitchBlock;
[...]

然后,该值用于将几乎任意数量的字节从 guest 内存传输到大小为 0 的缓冲区中:

 if (enmTransfer == SVGA3D_READ_HOST_VRAM)
  rc = vmsvgaR3GboWrite(pSvgaR3State, &pMob->Gbo, offMob, pu8Map, dims.cbRow);
 else
  rc = vmsvgaR3GboRead(pSvgaR3State, &pMob->Gbo, offMob, pu8Map, dims.cbRow);

[来源]

时间线

报告日期: 2025年4月1日 修复日期: 2025年4月15日 披露日期: 2025年5月15日

严重性

CVSS 总体评分

此分数计算从 0 到 10 的总体漏洞严重性,并且基于通用漏洞评分系统 (CVSS)。 8.1 / 10

CVSS v3 基本指标

攻击向量:本地 攻击复杂度:低 所需权限:高 用户交互:无 范围:已更改 机密性:高 完整性:高 可用性:低 了解有关基本指标的更多信息

CVSS v3 基本指标

攻击向量:攻击者可以远程(在逻辑上和物理上)利用漏洞的程度越高,漏洞就越严重。 攻击复杂度:攻击越简单,漏洞就越严重。 所需权限:如果不需要权限,则更严重。 用户交互:如果不需要用户交互,则更严重。 范围:当发生范围更改时,例如,一个易受攻击的组件会影响超出其安全范围的组件中的资源时,则更严重。 机密性:当数据机密性的丧失最高时,更严重,衡量未经授权的用户可访问的数据级别。 完整性:当数据完整性的丧失最高时,更严重,衡量未经授权的用户可能进行的数据修改的后果。 可用性:当受影响组件可用性的损失最高时,更严重。

CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:L

CVE ID

CVE-2025-30712

弱点

无 CWE

鸣谢