Oracle VM VirtualBox – VM Escape via VGA Device
Oracle VM VirtualBox 漏洞:通过 VGA 设备实现 VM Escape
漏洞描述
概要
VirtualBox
的vmsvga3dSurfaceMipBufferSize
函数中存在整数溢出漏洞[来源]。该漏洞允许攻击者操纵 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;
该算法如下:
- 触发分配
buggy_surface
(分配大小为 0 的 surface)。 - 分配一个
GBO
对象,其cbTotal
中的值可用于识别我们的对象(即“egg”)。 经过反复试验,0x1421337
值被证明足够可靠(大约 100% 的成功率)。 - 利用越界读取,并在
buggy_surface
之后检查字节,看看是否可以在短范围内(前0x5a
字节)找到 egg,并假设我们的目标GBO
对象分配在 surface 之后。 - 如果没有,则返回第 1 步。
该算法被证明在 10 次尝试内 100% 可靠地进行堆 grooming。
通过使用带有攻击者选择值的线性写入越界来破坏 cbTotal
和 pvHost
,可以实现任意读取,然后 guest 可以发出一个 vmsvga3dDXReadbackCOTable
[链接] 命令,该命令最终会调用 vmsvgaR3MobBackingStoreWriteToGuest
[链接],这将使用两个损坏的变量将 pvHost
中的 cbTotal
字节写入 guest 内存。 类似地,可以通过 GrowCOTable
命令实现任意写入,该命令在调用 vmsvgaR3MobBackingStoreCreate
[链接]时,最终将导致设备从 guest 内存中 读取 cbTotal
字节到 pvHost
中。
任意堆分配
通过 GrowCOTable
可以实现的另一个有用的原语是分配任意堆内存块的能力,这是通过破坏 fGboFlags
字段来完成的,这将导致设备 分配 cbTotal
大小的内存块。 事实证明,此原语在稍后阶段很有用,可以找到一个存储 shellcode 的位置,以用于最终的利用阶段。
破坏 ASLR 并获得 RIP 控制权
VMSVGAMOB
对象的另一个巨大好处是,字段 nodeLRU
包含指向设备 VMSVGAR3STATE
结构的 指针。 后者结构很有用,因为它包含各种 函数指针,这些函数指针可以通过任意写入原语进行破坏,然后再用于获得 RIP 控制和任意代码执行。
逃离 VM
- 泄露函数指针
pfnCommandClear
[链接] 的值。 - 用步骤 1 中的值推导出
VBoxDD.so
的基地址。 - 读取
VBoxDD.so
的 GOT 表,找到一个函数指针,该指针将指向VBoxRT.so
的基地址。 - 使用任意堆分配原语来植入 shellcode。
- 构建一个 ROP 链,其中包含在两个文件中找到的 gadget。 5a. 将堆栈透视到堆的可控部分。 5b. 调用
memprotect
使 shellcode 位置可执行。 5c. 跳转到 shellcode 中。 - 用第一个 ROP 链 gadget 破坏
pfnCommandClear
的值,从而透视堆栈。 - 发出
vmsvga3dCommandClear
[链接] 命令。 - 启动 RCE。
进一步分析
线性越界读取
假设 guest 分配了两个 surface:
Buggy_surface
,它有一个 0 的 allocation 支持,并且将扮演src
的角色。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);
}
[...]
[来源] vmsvga3dGetBoxDimensions
和 vmsvga3dSurfaceMapInit
都有相同的错误:它们仅使用 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
鸣谢
- thatjiaozi Finder