
Escape de Convidado para Anfitrião no VirtualBox E1000
Gosto do VirtualBox e isso não tem nada a ver com o motivo pelo qual publico uma vulnerabilidade 0day. A razão é o meu desacordo com o estado contemporâneo da segurança da informação, especialmente da pesquisa de segurança e bug bounty:
Estou exausto dos dois primeiros, portanto a minha jogada é divulgação total. SegInfo, por favor, avance.
Software vulnerável: VirtualBox 5.2.20 e versões anteriores.
SO anfitrião: qualquer, o bug está numa base de código partilhada.
SO convidado: qualquer.
Configuração da VM: padrão (o único requisito é que a placa de rede seja Intel PRO/1000 MT Desktop (82540EM) e o modo seja NAT).
Até que a versão corrigida do VirtualBox seja lançada, pode alterar a placa de rede das suas máquinas virtuais para PCnet (uma das duas) ou para Rede Paravirtualizada. Se não puder, altere o modo de NAT para outro. A primeira forma é mais segura.
O dispositivo de rede virtual padrão do VirtualBox é Intel PRO/1000 MT Desktop (82540EM) e o modo de rede padrão é NAT. Referir-nos-emos a ele como E1000.
O E1000 tem uma vulnerabilidade que permite a um atacante com privilégios de root/administrador num convidado escapar para o ring3 do anfitrião. Depois, o atacante pode usar técnicas existentes para escalar privilégios para ring 0 via /dev/vboxdrv.
Para enviar pacotes de rede, um convidado faz o que um PC comum faz: configura uma placa de rede e fornece pacotes de rede para ela. Os pacotes são frames da camada de enlace de dados e outros cabeçalhos de nível superior. Os pacotes fornecidos ao adaptador são envolvidos em descritores Tx (Tx significa transmissão). O descritor Tx é uma estrutura de dados descrita na folha de dados do 82540EM (317453006EN.PDF, Revisão 4.0). Ele armazena meta-informações como tamanho do pacote, tag VLAN, flags de segmentação TCP/IP ativadas, etc.
A folha de dados do 82540EM prevê três tipos de descritores Tx: legacy, contexto, dados. Legacy está obsoleto, acredito. Os outros dois são usados juntos. A única coisa com que nos importamos é que os descritores de contexto definem o tamanho máximo do pacote e ativam a segmentação TCP/IP, e que os descritores de dados contêm endereços físicos dos pacotes de rede e seus tamanhos. O tamanho do pacote do descritor de dados deve ser menor que o tamanho máximo do pacote do descritor de contexto. Normalmente, os descritores de contexto são fornecidos à placa de rede antes dos descritores de dados.
Para fornecer descritores Tx à placa de rede, um convidado escreve-os no Tx Ring. Este é um buffer circular residente na memória física num endereço predefinido. Quando todos os descritores são escritos no Tx Ring, o convidado atualiza o registo MMIO TDT (Transmit Descriptor Tail) do E1000 para informar o anfitrião de que existem novos descritores para processar.
Considere o seguinte array de descritores Tx:``` [context_1, data_2, data_3, context_4, data_5]
Vamos atribuir os campos da sua estrutura da seguinte forma (os nomes dos campos são hipotéticos para serem legíveis por humanos, mas mapeiam diretamente para a especificação 82540EM):```
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
Aprenderemos por que eles devem ser assim em nossa análise passo a passo.
Vamos assumir que os descritores acima são escritos no Tx Ring na ordem especificada e o registrador TDT é atualizado pelo guest. Agora o host executará a função e1kXmitPending no arquivo src/VBox/Devices/Network/DevE1000.cpp (a maioria dos comentários são e serão removidos por questão de legibilidade):```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 lerá todos os 5 descritores Tx do Tx Ring. Então e1kLocateTxPacket é chamado pela primeira vez. Esta função itera por todos os descritores para configurar um estado inicial, mas não os processa de fato. No nosso caso, a primeira chamada a e1kLocateTxPacket manipulará os descritores context_1, data_2 e data_3. Os dois descritores restantes, context_4 e data_5, serão manipulados na segunda iteração do loop while (abordaremos a segunda iteração na próxima seção). Essa divisão do array em duas partes é crucial para desencadear a vulnerabilidade, então vamos descobrir por quê.
e1kLocateTxPacket tem a seguinte aparência:```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!"));
}
O primeiro descritor (context_1) é do tipo E1K_DTYP_CONTEXT, então a função e1kUpdateTxContext é chamada. Esta função atualiza um Contexto de Segmentação TCP se a Segmentação TCP estiver habilitada para o descritor. Isso é verdade para context_1, então o Contexto de Segmentação TCP será atualizado. (O que realmente é a Atualização do Contexto de Segmentação TCP não é importante, e usaremos isso apenas para nos referir ao código abaixo).
O segundo descritor (data_2) é do tipo E1K_DTYP_DATA, então várias ações desnecessárias para a discussão serão realizadas.
O terceiro descritor (data_3) também é do tipo E1K_DTYP_DATA, mas como data_3.data_length == 0, nenhuma ação é realizada.