
VirtualBox E1000 Gast-zu-Host-Escape
Ich mag VirtualBox, und das hat nichts damit zu tun, warum ich eine 0day-Schwachstelle veröffentliche. Der Grund ist meine Ablehnung des gegenwärtigen Zustands der Infosec, insbesondere der Sicherheitsforschung und des Bug Bounty:
Ich bin der ersten beiden überdrüssig, daher ist mein Schritt Full Disclosure. Infosec, bitte kommt voran.
Verwundbare Software: VirtualBox 5.2.20 und frühere Versionen.
Host-Betriebssystem: beliebig, der Fehler befindet sich in einer gemeinsamen Codebasis.
Gast-Betriebssystem: beliebig.
VM-Konfiguration: Standard (die einzige Anforderung ist, dass eine Netzwerkkarte Intel PRO/1000 MT Desktop (82540EM) ist und der Modus NAT ist).
Bis der gepatchte VirtualBox-Build verfügbar ist, können Sie die Netzwerkkarte Ihrer virtuellen Maschinen auf PCnet (eine der beiden) oder auf Paravirtualized Network umstellen. Wenn das nicht möglich ist, ändern Sie den Modus von NAT auf einen anderen. Der erstere Weg ist sicherer.
Ein standardmäßiges virtuelles Netzwerkgerät von VirtualBox ist Intel PRO/1000 MT Desktop (82540EM) und der Standard-Netzwerkmodus ist NAT. Wir bezeichnen es als E1000.
Das E1000 weist eine Schwachstelle auf, die es einem Angreifer mit Root-/Administratorrechten in einem Gast ermöglicht, in den ring3 des Hosts auszubrechen. Anschließend kann der Angreifer vorhandene Techniken nutzen, um die Privilegien über /dev/vboxdrv auf ring 0 auszuweiten.
Um Netzwerkpakete zu senden, macht ein Gast das, was ein gewöhnlicher PC tut: Er konfiguriert eine Netzwerkkarte und übergibt ihr Netzwerkpakete. Die Pakete bestehen aus Frames der Sicherungsschicht und anderen, höheren Headern. Pakete, die dem Adapter übergeben werden, sind in Tx-Deskriptoren verpackt (Tx bedeutet Transmit). Der Tx-Deskriptor ist eine Datenstruktur, die im 82540EM-Datenblatt (317453006EN.PDF, Revision 4.0) beschrieben ist. Er speichert Metainformationen wie Paketgröße, VLAN-Tag, Flags für aktivierte TCP/IP-Segmentierung usw.
Das 82540EM-Datenblatt sieht drei Tx-Deskriptortypen vor: Legacy, Context und Data. Legacy ist meines Erachtens veraltet. Die anderen beiden werden zusammen verwendet. Uns interessiert nur, dass Context-Deskriptoren die maximale Paketgröße festlegen und die TCP/IP-Segmentierung umschalten, und dass Data-Deskriptoren physische Adressen von Netzwerkpaketen und deren Größen enthalten. Die Paketgröße des Data-Deskriptors muss kleiner sein als die maximale Paketgröße des Context-Deskriptors. Normalerweise werden Context-Deskriptoren vor den Data-Deskriptoren an die Netzwerkkarte übergeben.
Um der Netzwerkkarte Tx-Deskriptoren zuzuführen, schreibt ein Gast sie in den Tx-Ring. Dies ist ein Ringpuffer, der sich an einer vordefinierten Adresse im physischen Speicher befindet. Wenn alle Deskriptoren in den Tx-Ring geschrieben wurden, aktualisiert der Gast das E1000-MMIO-TDT-Register (Transmit Descriptor Tail), um dem Host mitzuteilen, dass neue Deskriptoren zu verarbeiten sind.
Betrachten Sie das folgende Array von Tx-Deskriptoren:``` [context_1, data_2, data_3, context_4, data_5]
Ordnen wir ihre Strukturfelder wie folgt zu (die Feldnamen sind hypothetisch, damit sie für Menschen lesbar sind, bilden aber direkt die 82540EM-Spezifikation ab):```
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
Wir werden in unserer Schritt-für-Schritt-Analyse erfahren, warum sie so sein sollten.
Nehmen wir an, die obigen Descriptoren werden in der angegebenen Reihenfolge in den Tx Ring geschrieben und das TDT-Register wird vom Gast aktualisiert. Nun führt der Host die Funktion e1kXmitPending in der Datei src/VBox/Devices/Network/DevE1000.cpp aus (die meisten Kommentare wurden und werden zugunsten der Lesbarkeit entfernt):```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 liest alle 5 Tx-Deskriptoren aus dem Tx-Ring. Dann wird e1kLocateTxPacket zum ersten Mal aufgerufen. Diese Funktion iteriert über alle Deskriptoren, um einen Anfangszustand einzurichten, verarbeitet sie aber nicht tatsächlich. In unserem Fall verarbeitet der erste Aufruf von e1kLocateTxPacket die Deskriptoren context_1, data_2 und data_3. Die beiden verbleibenden Deskriptoren, context_4 und data_5, werden bei der zweiten Iteration der while-Schleife verarbeitet (wir werden die zweite Iteration im nächsten Abschnitt behandeln). Diese zweiteilige Array-Aufteilung ist entscheidend, um die Schwachstelle auszulösen, also lass uns herausfinden, warum.
e1kLocateTxPacket sieht wie folgt aus:```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!"));
}
Der erste Deskriptor (context_1) ist vom Typ E1K_DTYP_CONTEXT, daher wird die Funktion e1kUpdateTxContext aufgerufen. Diese Funktion aktualisiert einen TCP Segmentation Context, wenn TCP Segmentation für den Deskriptor aktiviert ist. Für context_1 trifft das zu, daher wird der TCP Segmentation Context aktualisiert. (Was das Aktualisieren des TCP Segmentation Context tatsächlich bewirkt, ist nicht wichtig, und wir verwenden dies nur, um uns auf den folgenden Code zu beziehen.)