Oracle VirtualBox VHWA Use-After-Free权限提升漏洞 | STAR Labs
June 26, 2020 · 12 min · Calvin Fong (@__lord_idiot)
目录
在STAR Labs为期一个月的实习期间,我接触了VirtualBox,并深入学习了漏洞挖掘与分类、根因分析和利用技术。本文将详细描述我在实习期间发现的一个Use-After-Free漏洞,以及利用该漏洞编写的虚拟机逃逸利用程序。报告时的最新版本为VirtualBox 6.1.2 r135662。
Setup
本博客基于Windows 10主机和Windows 7客户机VM编写。此设置适合跟随操作:客户机VM应配置为使用VBoxVGA图形控制器,并勾选“启用2D视频加速”。
在进行任何漏洞挖掘或利用之前,应设置调试环境以便于分析崩溃和调试利用程序。VirtualBox受进程硬化保护,因此无法或难以从用户态调试器附加到发布版本的VirtualBox进程。幸运的是,VirtualBox是开源软件,可以构建非硬化的调试版本,从而允许附加调试器。在我的情况下,我的导师anhdaden提供了带有符号的VirtualBox 6.1.0 r135406调试版本,这极大地帮助了我直接进入调试。
遵循@_niklasb的明智建议:
最后,需要一些编写Windows内核驱动程序的知识。这对于利用是必要的,因为我们将从客户机内核态与主机模拟设备交互。这是我编写WDM驱动程序时的参考之一。深入的知识对于跟随此利用并非必需。
Background
漏洞位于由主机-客户机共享内存接口(HGSMI)提供的VirtualBox视频加速(VBVA)功能中。要使用此功能,客户机内核驱动程序应映射物理地址0xE0000000以执行内存映射I/O(MMIO)。客户机应在物理地址0xE0000000的VRAM缓冲区中写入格式化的HGSMI命令,指示使用的通道和其他细节。之后,客户机应向IO端口VGA_PORT_HGSMI_GUEST(0x3d0)发送out指令,以允许模拟设备开始处理。细节可以从HGSMIBufferProcess函数反向工程得知。为了避免重复工作,我使用了voidsecurity在此攻击面上的另一个利用代码。
对于VBVA服务,它由vbvaChannelHandler函数处理。可以发送各种VBVA命令,漏洞位于VBVA_VHWA_CMD命令中,该命令用于视频硬件加速(VHWA)。在调试器中跟踪函数调用,可以确定VHWA命令的实际处理程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
vbvaChannelHandler
|_ vbvaVHWAHandleCommand
|_ vbvaVHWACommandSubmit(Inner)
|_ pThisCC->pDrv->pfnVHWACommandProcess = Display::i_handleVHWACommandProcess
|_ pFramebuffer->ProcessVHWACommand = VBoxOverlayFrameBuffer.ProcessVHWACommand
|_ mOverlay.onVHWACommand = VBoxQGLOverlay::onVHWACommand
|_ mCmdPipe.postCmd = VBoxVHWACommandElementProcessor::postCmd
|_ pCmd->setData
|_ RTListAppend(&mCommandList, &pCmd->ListNode);
*命令添加到列表后,会被处理
VBoxQGLOverlay::onVHWACommandEvent
|_ mCmdPipe.getCmd
|_ processCmd = VBoxQGLOverlay::processCmd
|_ vboxDoVHWACmd = VBoxQGLOverlay::vboxDoVHWACmd
|_ vboxDoVHWACmdExec = VBoxQGLOverlay::vboxDoVHWACmdExec
|
VBoxQGLOverlay::vboxDoVHWACmdExec将是开始分析的最重要函数,因为它包含VHWA命令处理的核心。
Vulnerability
现在我们已经大致熟悉了代码,可以深入分析易受攻击的VHWA命令。在VBoxQGLOverlay::vboxDoVHWA cmdExec中,有各种命令可以分配、删除和操作对象,这对于CTF堆挑战爱好者来说可能很熟悉。
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
|
// src/VBox/Frontends/VirtualBox/src/VBoxFBOverlay.cpp:4669
void VBoxQGLOverlay::vboxDoVHWACmdExec(void RT_UNTRUSTED_VOLATILE_GUEST *pvCmd, int /*VBOXVHWACMD_TYPE*/ enmCmdInt, bool fGuestCmd)
{
struct VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *pCmd = (struct VBOXVHWACMD RT_UNTRUSTED_VOLATILE_GUEST *)pvCmd;
VBOXVHWACMD_TYPE enmCmd = (VBOXVHWACMD_TYPE)enmCmdInt;
switch (enmCmd)
{
...
case VBOXVHWACMD_TYPE_SURF_CREATE:
{
VBOXVHWACMD_SURF_CREATE RT_UNTRUSTED_VOLATILE_GUEST *pBody = VBOXVHWACMD_BODY(pCmd, VBOXVHWACMD_SURF_CREATE);
Assert(!mGlOn == !mOverlayImage.hasSurfaces());
initGl();
makeCurrent();
vboxSetGlOn(true);
pCmd->rc = mOverlayImage.vhwaSurfaceCreate(pBody);
...
case VBOXVHWACMD_TYPE_SURF_OVERLAY_UPDATE:
{
VBOXVHWACMD_SURF_OVERLAY_UPDATE RT_UNTRUSTED_VOLATILE_GUEST *pBody = VBOXVHWACMD_BODY(pCmd, VBOXVHWACMD_SURF_OVERLAY_UPDATE);
Assert(!mGlOn == !mOverlayImage.hasSurfaces());
initGl();
makeCurrent();
pCmd->rc = mOverlayImage.vhwaSurfaceOverlayUpdate(pBody);
...
|
此漏洞位于VBOXVHWACMD_TYPE_SURF_CREATE中。当提供VBOXVHWACMD_TYPE_SURF_CREATE命令时,将调用VBoxVHWAImage::vhwaSurfaceCreate,它可以创建新的VBoxVHWASurfaceBase对象。指向该VBoxVHWASurfaceBase对象的指针将存储在调用对象的mSurfHandleTable成员中,该成员只是一个按句柄索引的指针数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// src/VBox/Frontends/VirtualBox/src/VBoxFBOverlay.cpp:2287
int VBoxVHWAImage::vhwaSurfaceCreate(struct VBOXVHWACMD_SURF_CREATE RT_UNTRUSTED_VOLATILE_GUEST *pCmd)
{
...
VBoxVHWASurfaceBase *surf = NULL;
...
if (format.isValid())
{
surf = new VBoxVHWASurfaceBase(this,
surfSize,
primaryRect,
QRect(0, 0, surfSize.width(), surfSize.height()),
mViewport,
format,
pSrcBltCKey, pDstBltCKey, pSrcOverlayCKey, pDstOverlayCKey,
#ifdef VBOXVHWA_USE_TEXGROUP
0,
#endif
fFlags);
}
...
handle = mSurfHandleTable.put(surf);
pCmd->SurfInfo.hSurf = (VBOXVHWA_SURFHANDLE)handle;
|
但是,当启用某些命令标志时,surf将被设置为现有的VBoxVHWASurfaceBase对象,而不是创建新对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// src/VBox/Frontends/VirtualBox/src/VBoxFBOverlay.cpp:2287
int VBoxVHWAImage::vhwaSurfaceCreate(struct VBOXVHWACMD_SURF_CREATE RT_UNTRUSTED_VOLATILE_GUEST *pCmd)
{
...
VBoxVHWASurfaceBase *surf = NULL;
...
if (pCmd->SurfInfo.surfCaps & VBOXVHWA_SCAPS_PRIMARYSURFACE)
{
bNoPBO = true;
bPrimary = true;
VBoxVHWASurfaceBase *pVga = vgaSurface(); /* == mDisplay.getVGA() == mDisplay.mSurfVGA */
...
surf = pVga;
...
handle = mSurfHandleTable.put(surf);
pCmd->SurfInfo.hSurf = (VBOXVHWA_SURFHANDLE)handle;
|
当遵循此代码路径时,我们的mSurfHandleTable将持有对mDisplay对象的mSurfVGA的引用。但是,此mSurfVGA成员可能在其他功能期间被替换,例如调整大小功能。在屏幕调整大小(可由客户机触发)之后,将执行以下代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// src/VBox/Frontends/VirtualBox/src/VBoxFBOverlay.cpp:3752
void VBoxVHWAImage::resize(const VBoxFBSizeInfo &size)
{
...
VBoxVHWASurfaceBase *pDisplay = mDisplay.setVGA(NULL);
if (pDisplay)
delete pDisplay;
...
pDisplay = new VBoxVHWASurfaceBase(this,
dispSize,
dispRect,
dispRect,
dispRect, /* we do not know viewport at the stage of precise, set as a
disp rect, it will be updated on repaint */
format,
NULL, NULL, NULL, NULL,
#ifdef VBOXVHWA_USE_TEXGROUP
0,
#endif
0 /* VBOXVHWAIMG_TYPE fFlags */);
|
虽然mDisplay成员的mSurfVGA已被释放并更新为新分配,但mSurfHandleTable仍将持有指向旧释放的VBoxVHWASurfaceBase对象的指针。这创建了Use-After-Free场景,因为其他VHWA命令(如VBOXVHWACMD_TYPE_SURF_OVERLAY_UPDATE)仍然可以通过其句柄访问此释放的指针,进行各种操作。
1
2
3
4
5
6
7
8
|
// src/VBox/Frontends/VirtualBox/src/VBoxFBOverlay.cpp:2823
int VBoxVHWAImage::vhwaSurfaceOverlayUpdate(struct VBOXVHWACMD_SURF_OVERLAY_UPDATE RT_UNTRUSTED_VOLATILE_GUEST *pCmd)
{
VBoxVHWASurfaceBase *pSrcSurf = handle2Surface(pCmd->u.in.hSrcSurf); /*pSrcSurf = freed chunk*/
AssertReturn(pSrcSurf, VERR_INVALID_PARAMETER);
VBoxVHWASurfList *pList = pSrcSurf->getComplexList();
...
|
要从客户机内核驱动程序执行调整大小操作,可以使用另一个VBVA命令而不是VHWA命令(VBVA_VHWA_CMD)。VBVA_INFO_SCREEN命令最终调用resize,允许我们触发Use-After-Free。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// src/VBox/Devices/Graphics/DevVGA_VBVA.cpp:2444
static DECLCALLBACK(int) vbvaChannelHandler(void *pvHandler, uint16_t u16ChannelInfo,
void RT_UNTRUSTED_VOLATILE_GUEST *pvBuffer, HGSMISIZE cbBuffer)
{
...
switch (u16ChannelInfo)
{
...
case VBVA_INFO_SCREEN:
rc = VERR_INVALID_PARAMETER;
if (cbBuffer >= sizeof(VBVAINFOSCREEN))
rc = vbvaInfoScreen(pThisCC, (VBVAINFOSCREEN RT_UNTRUSTED_VOLATILE_GUEST *)pvBuffer);
break;
|
1
2
3
|
vbvaInfoScreen
|_ vbvaResize
|_ pThisCC->pDrv->pfnVBVAResize = Display::i_displayVBVAResize
|
Exploitation
Heap Spray
鉴于我们的Use-After-Free漏洞,利用的第一步是通过另一个受控分配回收此释放的分配。由于我们的主机是Windows 10机器,这意味着主机堆将由低碎片堆(LFH)和各种令人困惑的事情处理。因此,回收分配的半可靠方法是找到VirtualBox代码中的一个原语,允许客户机进行许多与VBoxVHWASurfaceBase大小相同的分配,同时允许我们控制分配的内容。最终使用的原语是drvNATNetworkUp_AllocBuf,它在发送以太网帧时调用。幸运的是,我的导师已经有了实现此堆喷的代码,节省了我理解以太网协议的大量精力。关于此原语,只需要知道它可以分配16字节对齐的大小,并提供由客户机提供的数据。下图说明了此堆喷的使用。
rip control
通过此堆喷,我们控制了损坏的VBoxVHWASurfaceBase对象的vtable成员。那么如何使用此控制rip?
在检查VBoxVHWASurfaceBase的有效vtable时,可以注意到唯一的条目是指向向量删除析构函数的函数指针。因此,一旦我们知道可以写入的内存区域的地址,就可以写入一个假的vtable,将vtable成员替换为指向我们控制的vtable,并通过VBOXVHWACMD_TYPE_SURF_DESTROY VHWA命令删除VBoxVHWASurfaceBase对象,从而控制指令指针rip。
目前,我们必须通过漏洞实现信息泄露,才能进一步进行利用。
Getting an infoleak
在花费数小时查看各种VHWA命令以找到可能将某些指针泄漏回客户机VRAM的命令后,我在寻找信息泄露方面陷入了死胡同。在一些提示下,anhdaden引导我意识到一种半可靠的信息泄露方法。此技术基于客户机可以在内存中控制的VRAM MMIO缓冲区(如前所述)。在我的VM配置为256 MB视频内存的情况下,VRAM缓冲区将具有0x10000000字节的巨大大小。有了这样的大小,猜测此缓冲区在主机的虚拟地址变得更加可能。重新启动VM几次,我注意到缓冲区分配在以下地址。
1
2
3
4
5
6
7
8
9
|
start end
0x00000000ACB0000-0x00000001ACB0000
0x00000000AEE0000-0x00000001AEE0000
0x00000000B4F0000-0x00000001B4F0000
0x00000000B1A0000-0x00000001B1A0000
0x00000000ADE0000-0x00000001ADE0000
0x00000000A670000-0x00000001A670000
0x00000000B0B0000-0x00000001B0B0000
0x00000000AC10000-0x00000001AC10000
|
有了如此大的范围,即使我们猜测一个地址如0x00000000C000000,它仍然落在VRAM缓冲区内!有了这些信息,我们可以开始构建更好的原语来泄漏DLL地址,并可能形成任意读/写原语。
然而,仍然存在一个小问题,尽管我们知道像0x00000000C000000这样的任意地址可能在我们可控的VRAM缓冲区中,但我们不知道此地址相对于缓冲区起始的偏移量。幸运的是,有一种方法可以解决这个问题。
让我们看看实现VBOXVHWACMD_TYPE_SURF_OVERLAY_UPDATE命令的VBoxVHWAImage::vhwaSurfaceOverlayUpdate函数。由于源代码中有许多函数调用实际上被内联,以及其他宏在编译后被优化,我发现使用反编译器检查函数更容易。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// src/VBox/Frontends/VirtualBox/src/VBoxFBOverlay.cpp:2823
signed __int64 __fastcall VBoxVHWAImage::vhwaSurfaceOverlayUpdate(__int64 _this, uint8_t *_pCmd)
{
...
pSrcSurf = *(VBoxVHWASurfaceBase **)(*(_QWORD *)(_this + 72) + 8i64 * (unsigned int)*((_QWORD *)_pCmd + 4));
...
pList = (__int64 ***)pSrcSurf->mComplexList; /* [1] */
...
if ( _bittest(&v24, 9u) )
{
...
}
else
{
...
if ( _bittest(&v25, 0xEu) )
*(_QWORD *)(pList + 0x18) = pSrcSurf; /* [2] */
...
}
|
查看第[1]行,pList是从VBoxVHWASurfaceBase对象pSrcSurf获取的指针。通过堆喷,我们可以控制pList的值。稍后,在第[2]行,pList被解引用,偏移量0x18的成员被设置为指向pSrcSurf的指针。如果我们可以将pList指向我们的VRAM缓冲区,我们将能够泄漏堆中的指针!此外,如果pList = 0x00000000C000000,指针将被放置在0x00000000C000018,我们可以扫描内存中的此变化,并根据指针在VRAM中的索引计算VRAM缓冲区的基地址。
1
2
3
4
5
6
7
8
|
// src/VBox/Frontends/VirtualBox/src/VBoxFBOverlay.cpp:2823
signed __int64 __fastcall VBoxVHWAImage::vhwaSurfaceOverlayUpdate(__int64 _this, uint8_t *_pCmd)
{
...
for ( i = **(__int64 ***)pList; i != *(__int64 **)pList; i = (__int64 *)*i )
VBoxVHWAImage::vhwaDoSurfaceOverlayUpdate(this, pDstSurf, (VBoxVHWASurfaceBase *)i[2], pCmd);
...
|
应该注意,pList的第一个成员应是指向单链表的有效指针,否则VM在遍历时会崩溃,如上代码片段所示。因此,地址0x00000000C000000应包含有效的链表指针。在我们不知道VRAM缓冲区基地址的情况下,一个简单的解决方案是用0x00000000C000000喷洒VR