
VirtualBox E1000: побег из гостевой системы в хост-систему
Я люблю VirtualBox, и это не связано с тем, почему я публикую 0day-уязвимость. Причина — моё несогласие с современным состоянием инфосека, особенно в области исследования безопасности и bug bounty:
Я устал от первых двух пунктов, поэтому мой шаг — полное раскрытие. Инфосек, пожалуйста, двигайся вперёд.
Уязвимое ПО: VirtualBox 5.2.20 и более ранние версии.
Хост-ОС: любая, ошибка находится в общей кодовой базе.
Гостевая ОС: любая.
Конфигурация ВМ: по умолчанию (единственное требование — сетевая карта Intel PRO/1000 MT Desktop (82540EM) и режим NAT).
Пока не вышла пропатченная сборка VirtualBox, вы можете сменить сетевую карту ваших виртуальных машин на PCnet (любую из двух) или на Paravirtualized Network. Если не можете, смените режим с NAT на другой. Первый способ безопаснее.
Сетевое устройство VirtualBox по умолчанию — Intel PRO/1000 MT Desktop (82540EM), а сетевой режим по умолчанию — NAT. Далее будем называть его E1000.
В E1000 есть уязвимость, позволяющая атакующему с правами root/administrator в гостевой системе выйти за её пределы (escape) в ring3 хоста. Затем атакующий может использовать существующие техники для повышения привилегий до ring 0 через /dev/vboxdrv.
Чтобы отправлять сетевые пакеты, гостевая система делает то же, что и обычный ПК: настраивает сетевую карту и передаёт ей сетевые пакеты. Пакеты представляют собой кадры канального уровня и другие заголовки более высокого уровня. Пакеты, передаваемые адаптеру, оборачиваются в Tx-дескрипторы (Tx означает transmit — передача). Tx-дескриптор — это структура данных, описанная в документации 82540EM (317453006EN.PDF, Revision 4.0). Она хранит такую метаинформацию, как размер пакета, VLAN-тег, флаги включения сегментации TCP/IP и так далее.
В документации 82540EM предусмотрено три типа Tx-дескрипторов: legacy, context, data. Legacy, насколько я понимаю, устарел. Два других используются вместе. Нас интересует только то, что контекстные дескрипторы задают максимальный размер пакета и включают/выключают сегментацию TCP/IP, а data-дескрипторы хранят физические адреса сетевых пакетов и их размеры. Размер пакета в data-дескрипторе должен быть меньше максимального размера пакета, заданного контекстным дескриптором. Обычно контекстные дескрипторы передаются сетевой карте перед data-дескрипторами.
Чтобы передать Tx-дескрипторы сетевой карте, гостевая система записывает их в Tx Ring. Это кольцевой буфер, расположенный в физической памяти по заранее заданному адресу. Когда все дескрипторы записаны в Tx Ring, гостевая система обновляет MMIO-регистр TDT (Transmit Descriptor Tail) сетевой карты E1000, чтобы сообщить хосту о наличии новых дескрипторов для обработки.
Рассмотрим следующий массив 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
Мы узнаем, почему они должны быть именно такими, в нашем пошаговом анализе.
Предположим, что описанные выше дескрипторы записываются в Tx Ring в указанном порядке, и регистр TDT обновляется гостем. Теперь хост выполнит функцию e1kXmitPending в файле src/VBox/Devices/Network/DevE1000.cpp (большинство комментариев удалено и будет удалено для удобочитаемости):```c 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 Ring. Затем e1kLocateTxPacket вызывается в первый раз. Эта функция проходит по всем дескрипторам, чтобы установить начальное состояние, но фактически не обрабатывает их. В нашем случае первый вызов e1kLocateTxPacket обработает дескрипторы context_1, data_2 и data_3. Оставшиеся два дескриптора, context_4 и data_5, будут обработаны на второй итерации цикла while (вторую итерацию мы рассмотрим в следующем разделе). Это разделение массива на две части является ключевым для срабатывания уязвимости, поэтому давайте разберёмся, почему.
e1kLocateTxPacket выглядит так:```c
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, если для дескриптора включена сегментация TCP. Для context_1 это верно, поэтому контекст сегментации TCP будет обновлён. (Что именно представляет собой обновление контекста сегментации TCP, не важно — мы используем это лишь для ссылки на код ниже).
Второй дескриптор (data_2) относится к типу E1K_DTYP_DATA, поэтому будет выполнено несколько действий, несущественных для обсуждения.
Третий дескриптор (data_3) также относится к типу E1K_DTYP_DATA, но поскольку data_3.data_length == 0, никаких действий не выполняется.