
[CVE-2017-10235] Beschreibung und PoC des Pufferüberlaufs im VirtualBox E1000 Gerät
Das folgende Dokument beschreibt einen Fehler in VirtualBox v5.1.22 (behoben in v5.1.24) in der Gastgeräte-Emulationskomponente DevE1000 (Intel 82540EM Ethernet Controller Emulation), in der Funktion e1kFallbackAddToFrame, der zu einem Pufferüberlauf im Host führt, wenn das Gast-Betriebssystem von einem Angreifer kontrolliert wird.
Der Fehler wurde von Oracle im CPU vom Juli 2017 mit der veröffentlichten CVE-2017-10235 bestätigt.
Die Schwachstelle wurde sowohl mit einem Linux- (Ubuntu 16.04) als auch einem Windows- (v8.1) Host, auf dem ein Linux-Gast (ebenfalls Ubuntu 16.04) ausgeführt wurde, bestätigt, aber die Schwachstelle könnte in vielen verschiedenen Host/Gast-Kombinationen ausgelöst werden. In allen Szenarien wird die Standard-Netzwerkkonfiguration angenommen: nur ein Netzwerkadapter verbunden mit NAT vom Typ Intel PRO/1000 MT Desktop (82540EM).
Da Kontrollstrukturen (einschließlich Funktionszeigern) mit vom Angreifer kontrollierten Daten überschrieben werden können, ist davon auszugehen, dass in vielen Szenarien eine entfernte Codeausführung (RCE) erreicht werden kann. Oracle hat diesem Fehler einen niedrigen CVSS-Score zugewiesen, da es davon ausging, dass er ein Kein Vertraulichkeitsrisiko und eine Niedrige Integrität aufweist, was nach unserer Einschätzung das vollständige Kompromittierungspotential dieses Fehlers nicht widerspiegelt (eine Erklärung für die Möglichkeit von RCE wird unten gegeben).
Der VirtualBox-Code, der die Emulation des Intel 82540EM Ethernet Controllers implementiert (in src/VBox/Devices/Network/DevE1000.cpp), in der Funktion e1kFallbackAddToFrame, implementiert das Hardware-TCP-Segmenting:
static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc,
bool fOnWorkerThread)
{
#ifdef VBOX_STRICT
PPDMSCATTERGATHER pTxSg = pThis->CTX_SUFF(pTxSg);
Assert(e1kGetDescType(pDesc) == E1K_DTYP_DATA);
Assert(pDesc->data.cmd.fTSE);
Assert(!e1kXmitIsGsoBuf(pTxSg));
#endif
uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN +
pThis->contextTSE.dw3.u16MSS;
Assert(u16MaxPktLen != 0);
Assert(u16MaxPktLen < E1K_MAX_TX_PKT_SIZE);
Diese Funktion prüft korrekt, dass die maximale TX-Paketlänge (u16MaxPktLen) unter dem Standardmaximum von 16288 Bytes (E1K_MAX_TX_PKT_SIZE) liegt, tut dies jedoch in Form eines Assert-Makros, das in einem Release-Build deaktiviert wird, wodurch die Prüfung für den Endbenutzer effektiv nutzlos wird. Dies kann mit der analogen Funktion e1kAddToFrame verglichen werden, die die Prüfung mit einem expliziten if anstelle des Assert durchsetzt:
static bool e1kAddToFrame(PE1KSTATE pThis, RTGCPHYS PhysAddr,
uint32_t cbFragment)
{
PPDMSCATTERGATHER pTxSg = pThis->CTX_SUFF(pTxSg);
bool const fGso = e1kXmitIsGsoBuf(pTxSg);
uint32_t const cbNewPkt = cbFragment + pThis->u16TxPktLen;
if (RT_UNLIKELY( !fGso && cbNewPkt > E1K_MAX_TX_PKT_SIZE ))
{
E1kLog(("%s Transmit packet is too large: %u > %u(max)\n",
pThis->szPrf, cbNewPkt, E1K_MAX_TX_PKT_SIZE));
return false;
}
Der Unterschied zwischen der Verwendung der normalen Funktion (e1kAddToFrame) und der Fallback-Funktion (e1kFallbackAddToFrame) wird in e1kXmitDesc() entschieden und hängt von zwei Faktoren ab: dass das TSE-Flag in den Daten/Kontext-Deskriptoren aktiviert ist (vom OS des Gast-Systems kontrolliert) und dass das GSO-Flag deaktiviert ist. Letzteres hängt von vielen Faktoren ab, daher gibt es viele Möglichkeiten, es zu deaktivieren, aber die bequemste ist, den Loopback-Modus zu aktivieren, der über das Receive Control Register (in den RCTL.LBM-Bits) konfiguriert wird, das ebenfalls vom Gast-OS kontrolliert wird.
Die Aktivierung des Loopback-Modus führt dazu, dass die Funktion e1kXmitAllocBuf den aTxPacketFallback-Puffer (Transmit packet buffer use for TSE fallback and loopback) für die Allokation des PDM-Streugather-Puffers mit der genannten Länge von 16288 Bytes (E1K_MAX_TX_PKT_SIZE) verwendet und signalisiert, dass GSO deaktiviert wird (indem ein NULL in pvUser gesetzt wird).
if (RT_LIKELY(GET_BITS(RCTL, LBM) != RCTL_LBM_TCVR))
{
...
}
else
{
/* Create a loopback using the fallback buffer and preallocated SG. */
AssertCompileMemberSize(E1KSTATE, uTxFallback.Sg, 8 * sizeof(size_t));
pSg = &pThis->uTxFallback.Sg;
pSg->fFlags = PDMSCATTERGATHER_FLAGS_MAGIC |
PDMSCATTERGATHER_FLAGS_OWNER_3;
pSg->cbUsed = 0;
pSg->cbAvailable = 0;
pSg->pvAllocator = pThis;
pSg->pvUser = NULL; /* No GSO here. */
pSg->cSegs = 1;
pSg->aSegs[0].pvSeg = pThis->aTxPacketFallback;
pSg->aSegs[0].cbSeg = sizeof(pThis->aTxPacketFallback);
}
Dies führt dazu, dass der Aufruf der Funktion e1kXmitIsGsoBuf (innerhalb von e1kXmitDesc) False zurückgibt und mit aktiviertem TSE im Daten-Deskriptor der Ausführungsfluss zu e1kFallbackAddToFrame geht (anstelle der sichereren Funktion e1kAddToFrame mit der korrekten Prüfung).
/*
* Add the descriptor data to the frame. If the frame is complete,
* transmit it and reset the u16TxPktLen field.
*/
if (e1kXmitIsGsoBuf(pThis->CTX_SUFF(pTxSg)))
{
...
}
else if (!pDesc->data.cmd.fTSE)
{
...
}
else
{
STAM_COUNTER_INC(&pThis->StatTxPathFallback);
rc = e1kFallbackAddToFrame(pThis, pDesc, fOnWorkerThread);
}
Innerhalb von e1kFallbackAddToFrame kann bei deaktivierter Prüfung in einem Release-Build die MSS beliebig groß gesetzt werden (bis zu 64K minus HDRLEN), wodurch eine beliebig große DTALEN an e1kFallbackAddSegment übergeben werden kann:
/*
* Carve out segments.
*/
int rc;
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);
Die Funktion e1kFallbackAddSegment verwendet diesen Wert (nun als Argument u16Len), um aus dem Gastspeicher in den Puffer aTxPacketFallback im Hostspeicher zu kopieren (durch PDMDevHlpPhysRead) ohne weitere Prüfungen dieser Länge, wodurch der Pufferüberlauf verursacht wird (eines Puffers mit einer Kapazität von 16288 Bytes mit einer Speichergröße von bis zu 64K).
static int e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr,
uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
int rc = VINF_SUCCESS;
/* TCP header being transmitted */
struct E1kTcpHeader *pTcpHdr = (struct E1kTcpHeader *)
(pThis->aTxPacketFallback + pThis->contextTSE.tu.u8CSS);
/* IP header being transmitted */
struct E1kIpHeader *pIpHdr = (struct E1kIpHeader *)
(pThis->aTxPacketFallback + pThis->contextTSE.ip.u8CSS);
E1kLog3(("%s e1kFallbackAddSegment: Length=%x, remaining payload=%x,
header=%x, send=%RTbool\n", pThis->szPrf, u16Len,
pThis->u32PayRemain, pThis->u16HdrRemain, fSend));
Assert(pThis->u32PayRemain + pThis->u16HdrRemain > 0);
PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);
Um diese Schwachstelle für ein RCE anfälliger zu machen, ist zu beachten, dass die Variable direkt nach dem Puffer sein Index ist (u16TxPktLen), der zum Schreiben darauf verwendet wird (als Offset im Argument von PDMDevHlpPhysRead). Die Kontrolle dieses Werts mit einem anfänglichen Pufferüberlauf (verursacht durch einen ersten Daten-Deskriptor der Länge E1K_MAX_TX_PKT_SIZE + 2 Bytes) würde dann erlauben (in einem zweiten Aufruf von PDMDevHlpPhysRead mit einem zweiten Daten-Deskriptor) jede Speicheradresse bis zu 64K Entfernung vom Puffer zu überschreiben, ohne dass der gesamte dazwischenliegende Speicher überschrieben werden müsste (was den Angriff komplizierter machen würde, um einen potenziellen Absturz zu vermeiden).
In der Nähe des Zielpuffers aTxPacketFallback, einige Zeilen darunter und innerhalb des 64K-Bereichs, ist die Struktur g_aE1kRegMap definiert, die einen Vektor von Funktionszeigern enthält, die Lese- und Schreib-Handler implementieren (pfnRead und pfnWrite), was ein ideales Ziel für den zweiten Pufferüberlauf wäre, um ein RCE zu ermöglichen.
e1kXmitAllocBufEine (geringfügige) Komplikation bei diesem Angriffsvektor ist der Vollständigkeit halber erwähnenswert: Es gibt einen scheinbaren Fehler in der Funktion e1kXmitAllocBuf, bei dem im Fall des Loopback-Modus cbTxAlloc (Anzahl der Bytes im nächsten Paket) nicht auf Null zurückgesetzt wird, wie es im normalen Fall (im anderen Zweig seines if) geschieht. Dies führt dazu, dass der Thread in der while-Schleife von e1kLocateTxPacket (innerhalb von e1kXmitPending) stecken bleibt:
while (e1kLocateTxPacket(pThis))
{
fIncomplete = false;
/* Found a complete packet, allocate it. */
rc = e1kXmitAllocBuf(pThis, pThis->fGSO);
/* If we're out of bandwidth we'll come back later. */
if (RT_FAILURE(rc))
goto out;
/* Copy the packet to allocated buffer and send it. */
rc = e1kXmitPacket(pThis, fOnWorkerThread);
/* If we're out of bandwidth we'll come back later. */
if (RT_FAILURE(rc))
goto out;
}
Dies scheint zu passieren, weil e1kLocateTxPacket vorzeitig mit True zurückgibt, wenn cbTxAlloc nicht Null ist, und nicht den Code erreicht, der prüft, ob iTxDCurrent gleich nTxDFetched ist (der übliche Fall, wenn alle Deskriptoren verarbeitet wurden), was normalerweise dazu führen würde, dass die Funktion False zurückgibt und die genannte Schleife beendet.
static bool e1kLocateTxPacket(PE1KSTATE pThis)
{
LogFlow(("%s e1kLocateTxPacket: ENTER cbTxAlloc=%d\n",
pThis->szPrf, pThis->cbTxAlloc));
/* Check if we have located the packet already. */
if (pThis->cbTxAlloc)
{
LogFlow(("%s e1kLocateTxPacket: RET true cbTxAlloc=%d\n",
pThis->szPrf, pThis->cbTxAlloc));
return true;
}
Dies bedeutet, dass das erste an das Gerät gesendete Paket (nach dem Setzen des Loopback-Modus) dasjenige sein muss, das den Überlauf auslöst, da sonst die VM hängt (was zu einem DoS statt eines RCE führt).
Da die Einrichtung des Netzwerkgeräts alles andere als trivial ist, und um die Erstellung eines benutzerdefinierten Treibers zu vermeiden, wurde der E1000-Treiber eines generischen Linux-Kernels modifiziert, um die Deskriptoren (sowohl Kontext als auch Daten) zu erzeugen, die den Überlauf auslösen. Dieser modifizierte Kernel steht zum Download in diesem Repository zur Verfügung. Er wurde in einem Ubuntu 16.04-Gast getestet und verursachte sowohl unter Linux- als auch Windows-Hosts einen Absturz. Eine detaillierte Beschreibung ist hier verfügbar.
Die Schwachstelle wurde in Changeset 67974 (bugref:8881) behoben. Die als Assert in e1kFallbackAddToFrame durchgeführten Prüfungen wurden in explizite Prüfungen als if-Anweisungen umgewandelt, die nun in einem Release-Build aktiv bleiben (ähnlich wie bereits in e1kAddToFrame geschehen). Außerdem wird cbTxAlloc jetzt in beiden Zweigen (Loopback-Modus und Normalmodus) in e1kXmitAllocBuf auf Null gesetzt.
Eine zusätzliche (defensive) Prüfung, die hier vorgeschlagen wird, aber nicht im Changeset implementiert ist, könnte darin bestehen, in e1kFallbackAddSegment (und ähnlich in e1kAddToFrame) vor dem Aufruf von PDMDevHlpPhysRead explizit auf einen potenziellen Überlauf des Puffers mit Gastspeicher zu prüfen (hauptsächlich, dass u16TxPktLen plus u16Len kleiner als die Länge des aTxPacketFallback-Puffers ist).