Oracle VirtualBox VHWA Use-After-Free权限提升漏洞分析与利用

本文详细分析了Oracle VirtualBox VHWA功能中的Use-After-Free漏洞,包括漏洞成因、堆喷利用、信息泄露技术和ROP链构建,最终实现虚拟机逃逸并执行任意代码。

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

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计