
[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][intel_manual] (em [src/VBox/Devices/Network/DevE1000.cpp][DevE1000_cpp]), na função [e1kFallbackAddToFrame][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][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][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][e1kXmitDesc]) retorne False e, com o TSE habilitado no descritor de dados, o fluxo de execução irá para [e1kFallbackAddToFrame][e1kFallbackAddToFrame_call] (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][e1kFallbackAddSegment_call]:
/*
* 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][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);