
[CVE-2017-10235] Description et PoC du débordement de tampon du périphérique E1000 de VirtualBox
Le document suivant détaille un bogue trouvé dans VirtualBox v5.1.22 (corrigé dans v5.1.24), dans le composant d'émulation de périphérique invité DevE1000 (émulation du contrôleur Ethernet Intel 82540EM), dans la fonction e1kFallbackAddToFrame, qui conduit à un débordement de tampon dans l'hôte lorsque le système d'exploitation invité est contrôlé par un attaquant.
Le bogue a été reconnu par Oracle dans le CPU de juillet 2017 avec le CVE-2017-10235 émis.
La vulnérabilité a été corroborée à la fois avec un hôte Linux (Ubuntu 16.04) et un hôte Windows (v8.1) exécutant un invité Linux (également Ubuntu 16.04), mais la vulnérabilité pourrait être déclenchée dans de nombreuses combinaisons hôte/invité différentes.
Dans tous les scénarios, la configuration réseau par défaut est supposée : une seule carte réseau attachée au NAT de type Intel PRO/1000 MT Desktop (82540EM).
Étant donné que les structures de contrôle (y compris les pointeurs de fonction) peuvent être écrasées avec des données contrôlées par l'attaquant, il est raisonnable de supposer qu'une exécution de code à distance pourrait être atteinte dans de nombreux scénarios.
Oracle a attribué un score CVSS faible à ce bogue car il considérait qu'il présentait un risque de confidentialité None et une intégrité Low, ce qui, selon nous, ne reflète pas le potentiel de compromission complet de ce bogue (une explication de la possibilité d'une RCE est donnée ci-dessous).
Le code de VirtualBox qui implémente l'émulation du contrôleur Ethernet Intel 82540EM (dans src/VBox/Devices/Network/DevE1000.cpp), dans la fonction e1kFallbackAddToFrame, implémente la segmentation TCP matérielle :
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);
Cette fonction vérifie correctement que la longueur maximale du paquet TX (u16MaxPktLen) est inférieure au maximum standard de 16288 octets (E1K_MAX_TX_PKT_SIZE), mais elle le fait sous la forme d'une macro Assert qui sera désactivée dans une compilation release, rendant ainsi la vérification inutile pour l'utilisateur final.
Cela contraste avec la fonction analogue e1kAddToFrame, qui applique la vérification avec un if explicite au lieu du Assert :
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;
}
La différence entre l'utilisation de la fonction normale (e1kAddToFrame) et de la fonction de repli (e1kFallbackAddToFrame) est décidée dans e1kXmitDesc() et dépend de deux facteurs : que le drapeau TSE soit activé dans les descripteurs de données/contexte (contrôlés par le système d'exploitation de la machine invitée) et que le drapeau GSO soit désactivé.
Ce dernier dépend de nombreux facteurs, et il existe donc de nombreuses façons de le désactiver, mais la plus pratique est d'activer le mode bouclage, configuré via le registre de contrôle de réception (dans les bits RCTL.LBM), également contrôlé par le système d'exploitation invité.
L'activation du mode bouclage fera utiliser par la fonction e1kXmitAllocBuf le tampon aTxPacketFallback (tampon de paquets de transmission utilisé pour le repli TSE et le bouclage) pour l'allocation du tampon PDM scatter/gather, avec la longueur mentionnée de 16288 octets (E1K_MAX_TX_PKT_SIZE), et signalera que le GSO sera désactivé (en définissant un NULL dans pvUser).
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);
}
Cela fera retourner False à la fonction e1kXmitIsGsoBuf (appelée dans e1kXmitDesc) et, avec le TSE activé dans le descripteur de données, le flux d'exécution ira vers e1kFallbackAddToFrame (au lieu de la fonction plus sûre e1kAddToFrame avec la vérification correcte).
/*
* 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);
}
Dans e1kFallbackAddToFrame, avec la vérification susmentionnée désactivée dans une compilation release, le MSS peut être défini arbitrairement grand (jusqu'à 64K moins le HDRLEN), permettant ainsi de passer un DTALEN arbitrairement grand à e1kFallbackAddSegment :
/*
* 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);
La fonction e1kFallbackAddSegment utilisera cette valeur (maintenant comme argument u16Len) pour copier depuis la mémoire invitée vers le tampon aTxPacketFallback dans la mémoire hôte (via PDMDevHlpPhysRead) sans autre vérification de cette longueur, provoquant ainsi le débordement de tampon (d'un tampon d'une capacité de 16288 octets avec une taille mémoire allant jusqu'à 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);
Pour rendre cette vulnérabilité plus propice à une RCE, il faut noter que la variable juste après le tampon est son index (u16TxPktLen), utilisée pour y écrire (comme décalage dans l'argument de PDMDevHlpPhysRead).
Contrôler cette valeur avec un débordement de tampon initial (provoqué par un premier descripteur de données de longueur E1K_MAX_TX_PKT_SIZE + 2 octets) permettrait ensuite d'écrire (dans un second appel à PDMDevHlpPhysRead avec un second descripteur de données) n'importe quelle adresse mémoire jusqu'à 64K de distance du tampon, sans qu'il soit nécessaire d'écraser toute la mémoire intermédiaire (ce qui rendrait l'attaque plus compliquée, en essayant d'éviter un crash potentiel).
Près du tampon cible aTxPacketFallback, quelques lignes en dessous et dans la plage de 64K, la structure g_aE1kRegMap est définie, laquelle comprend un vecteur de pointeurs de fonction implémentant les gestionnaires de lecture et d'écriture (pfnRead et pfnWrite), ce qui serait une cible idéale pour le second débordement de tampon afin de faciliter une RCE.
e1kXmitAllocBufUne complication (mineure) de ce vecteur d'attaque mérite d'être mentionnée par souci d'exhaustivité : il semble y avoir un bogue dans la fonction e1kXmitAllocBuf, où en mode bouclage, cbTxAlloc (nombre d'octets dans le prochain paquet) n'est pas remis à zéro, comme c'est le cas dans le mode normal (dans l'autre branche de son if).
Cela provoque le blocage du thread dans la boucle while de e1kLocateTxPacket (dans e1kXmitPending) :
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;
}
Cela semble se produire parce que e1kLocateTxPacket retourne prématurément True lorsque cbTxAlloc n'est pas nul et n'atteint pas le code qui vérifie si iTxDCurrent est égal à nTxDFetched (le cas habituel où tous les descripteurs ont été traités), ce qui ferait normalement retourner False à la fonction, terminant ainsi la boucle susmentionnée.
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;
}
Cela se traduit par l'exigence que le premier paquet envoyé au périphérique (après avoir activé le mode bouclage) doit être celui qui déclenche le débordement, sinon la VM se bloquera (aboutissant à un DoS plutôt qu'à une RCE).
Étant donné que la configuration du périphérique réseau est loin d'être triviale, et pour éviter de construire un pilote personnalisé pour celui-ci, le pilote E1000 d'un noyau Linux générique a été modifié pour générer les descripteurs (à la fois de contexte et de données) qui déclenchent le débordement. Ce noyau modifié est disponible en téléchargement depuis ce dépôt. Il a été testé dans un invité Ubuntu 16.04, provoquant un crash à la fois sur des hôtes Linux et Windows. Une description détaillée est disponible ici.
La vulnérabilité a été corrigée dans le Changeset 67974 (bugref:8881). Les vérifications effectuées sous forme de Assert dans e1kFallbackAddToFrame ont été converties en vérifications explicites sous forme d'instructions if, qui restent désormais actives dans une compilation release (comme c'était déjà le cas dans e1kAddToFrame). De plus, cbTxAlloc est désormais remis à zéro dans les deux branches (mode bouclage et mode normal) dans e1kXmitAllocBuf.
Une vérification supplémentaire (défensive) suggérée ici, non implémentée dans le changeset, pourrait consister à placer, dans e1kFallbackAddSegment (et de manière similaire dans e1kAddToFrame), avant l'appel à PDMDevHlpPhysRead, une vérification explicite du débordement potentiel du tampon par la mémoire invitée (principalement que u16TxPktLen plus u16Len soit inférieur à la longueur du tampon aTxPacketFallback).