
[CVE-2017-10235] Descrição e PoC do Buffer Overflow no dispositivo E1000 do VirtualBox
O documento a seguir detalha um bug encontrado no VirtualBox v5.1.22 (agora corrigido na v5.1.24), no componente de emulação de dispositivo convidado DevE1000 (Emulação do Controlador Ethernet Intel 82540EM), na função e1kFallbackAddToFrame, que leva a um estouro de buffer no hospedeiro quando o SO convidado é controlado por um atacante.
O bug foi reconhecido pela Oracle no CPU de Julho de 2017 com o CVE-2017-10235 emitido.
A vulnerabilidade foi corroborada tanto com um hospedeiro Linux (Ubuntu 16.04) quanto Windows (v8.1) executando um convidado Linux (também Ubuntu 16.04), mas a vulnerabilidade poderia ser desencadeada em muitas combinações diferentes de hospedeiro/convidado. Em todos os cenários, assume-se a configuração de rede padrão: apenas um adaptador de rede conectado ao NAT do tipo Intel PRO/1000 MT Desktop (82540EM).
Como estruturas de controle (incluindo ponteiros de função) podem ser sobrescritas com dados controlados pelo atacante, é seguro assumir que a execução remota de código poderia ser alcançada em muitos cenários. A Oracle atribuiu uma pontuação CVSS baixa a este bug por considerar que ele tinha um risco de confidencialidade Nenhum e integridade Baixa, o que acreditamos não refletir o potencial total de comprometimento deste bug (uma explicação para a possibilidade de RCE é fornecida abaixo).
O código do VirtualBox que implementa a emulação do Controlador Ethernet Intel 82540EM (em src/VBox/Devices/Network/DevE1000.cpp), na função e1kFallbackAddToFrame, implementa a Segmentação TCP por hardware:
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);
Esta função verifica corretamente que o comprimento máximo do pacote TX (u16MaxPktLen) está abaixo do máximo padrão de 16288 bytes (E1K_MAX_TX_PKT_SIZE), mas o faz na forma de uma macro Assert que será desabilitada em uma versão de lançamento, efetivamente tornando a verificação inútil para o usuário final. Isso pode ser contrastado com a função análoga e1kAddToFrame, que impõe a verificação com um if explícito em vez do 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;
}
A diferença entre o uso da função normal (e1kAddToFrame) e a fallback (e1kFallbackAddToFrame) é decidida em e1kXmitDesc() e depende de dois fatores: que o flag TSE esteja habilitado nos descritores de dados/contexto (controlado pelo SO usando a máquina convidada) e que o flag GSO esteja desabilitado. Este último depende de muitos fatores e, portanto, existem muitas maneiras de desabilitá-lo, mas a mais conveniente é habilitar o modo loopback, que é configurado através do Registro de Controle de Recepção (nos bits RCTL.LBM), também controlado pelo SO convidado.
Habilitar o modo loopback fará com que a função e1kXmitAllocBuf use o buffer aTxPacketFallback (Buffer de pacotes de transmissão usado para fallback TSE e loopback) para a alocação do buffer PDM scatter/gather, com o comprimento mencionado de 16288 bytes (E1K_MAX_TX_PKT_SIZE), e para sinalizar que o GSO será desabilitado (configurando um NULL em 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);
}
Isso fará com que a chamada à função e1kXmitIsGsoBuf (dentro de e1kXmitDesc) retorne False e, com o TSE habilitado no descritor de dados, o fluxo de execução irá para e1kFallbackAddToFrame (em vez da função mais segura e1kAddToFrame com a verificação correta).
/*
* 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);
}
Dentro de e1kFallbackAddToFrame, com a verificação mencionada desabilitada em uma versão de lançamento, o MSS pode ser definido arbitrariamente grande (até 64K menos o HDRLEN), permitindo assim que um DTALEN arbitrariamente grande seja passado para 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);
A função e1kFallbackAddSegment usará este valor (agora como argumento u16Len) para copiar da memória convidada para o buffer aTxPacketFallback na memória do hospedeiro (através de PDMDevHlpPhysRead) sem verificações adicionais neste comprimento, causando assim o estouro de buffer (de uma capacidade de buffer de 16288 bytes com um tamanho de memória de até 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);
Para tornar esta vulnerabilidade mais propensa a um RCE, deve-se notar que a variável logo após o buffer é o seu índice (u16TxPktLen), usado para escrever nele (como um deslocamento no argumento de PDMDevHlpPhysRead). Controlar este valor com um estouro de buffer inicial (causado por um primeiro descritor de dados de comprimento E1K_MAX_TX_PKT_SIZE + 2 bytes) permitiria então escrever (em uma segunda chamada a PDMDevHlpPhysRead com um segundo descritor de dados) qualquer endereço de memória até 64K de distância do buffer, sem ser necessário sobrescrever toda a memória intermediária (o que tornaria o ataque mais complicado, tentando evitar uma potencial falha).
Próximo ao buffer alvo aTxPacketFallback, algumas linhas abaixo e dentro do intervalo de 64K, a estrutura g_aE1kRegMap é definida, que inclui um vetor de ponteiros de função que implementam manipuladores de leitura e escrita (pfnRead e pfnWrite) que seriam um alvo ideal para o segundo estouro de buffer para facilitar um RCE.
e1kXmitAllocBufUma complicação (menor) neste vetor de ataque vale a pena ser mencionada para completude: há o que parece ser um bug na função e1kXmitAllocBuf, onde no caso de estar em modo loopback, cbTxAlloc (Número de bytes no próximo pacote) não é zerado, como é feito no caso normal (no outro ramo do seu if). Isso faz com que a thread fique presa no loop while de e1kLocateTxPacket (dentro de 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;
}
Isso parece acontecer porque e1kLocateTxPacket retorna prematuramente com True no caso onde cbTxAlloc não é zero, e não atinge o código que verifica se iTxDCurrent é igual a nTxDFetched (o caso usual onde todos os descritores foram processados), o que normalmente faria a função retornar False, efetivamente terminando o mencionado loop.
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;
}
Isso se traduz na exigência de que o primeiro pacote enviado ao dispositivo (após configurar o modo loopback) deve ser aquele que desencadeia o estouro; caso contrário, a VM irá travar (terminando em um DoS em vez de um RCE).
Como a configuração do dispositivo de rede está longe de ser trivial, e para evitar construir um driver personalizado para ele, o driver E1000 de um kernel Linux genérico foi modificado para gerar os descritores (tanto de contexto quanto de dados) que desencadeiam o estouro. Este kernel modificado está disponível para download neste repositório. Ele foi testado em um convidado Ubuntu 16.04, causando uma falha tanto em hospedeiros Linux quanto Windows. Uma descrição detalhada está disponível aqui.
A vulnerabilidade foi corrigida no Changeset 67974 (bugref:8881). As verificações feitas como Assert em e1kFallbackAddToFrame foram convertidas para verificações explícitas como instruções if, que agora permanecem ativas em uma versão de lançamento (semelhante ao que já era feito em e1kAddToFrame). Além disso, cbTxAlloc agora é definido como zero em ambos os ramos (modo loopback e modo normal) em e1kXmitAllocBuf.
Uma verificação adicional (defensiva) sugerida aqui, não implementada no changeset, poderia ser colocar, em e1kFallbackAddSegment (e similarmente em e1kAddToFrame), antes da chamada a PDMDevHlpPhysRead, para verificar explicitamente um potencial estouro do buffer com a memória convidada (principalmente que u16TxPktLen mais u16Len seja menor que o comprimento do buffer aTxPacketFallback).