Ataques
DirectX/Hyper-V; uma perspectiva ofensiva: desdobramento de uma palestra na Black Hat


VP of Hacking
8 min
Este ano, participei da Black Hat USA. As palestras oferecidas eram diversificadas e interessantes; algumas, em particular, mostraram-se bastante relevantes para a minha área de atuação atual, que se concentra principalmente em tópicos avançados de Red Teaming e desenvolvimento de exploits.
Uma das palestras que achei mais interessantes foi "DirectX: The new Hyper-V Attack surface" ("DirectX: A nova superfície de ataque do Hyper-V"), apresentada por Zhenhao Hong (@rthhh17). Nela, foram abordadas quatro vulnerabilidades (CVE-2021-43219, CVE-2022-21898, CVE-2022-21912 e CVE-2022-21918) envolvendo falhas como Null Pointer Dereference (desreferenciação de ponteiro nulo), leitura de endereço arbitrário e escrita em endereço arbitrário. A apresentação incluiu, inclusive, trechos de código de PoC (Proof-of-Concept — prova de conceito) para acionar cada uma dessas vulnerabilities.
Além disso, foi apresentada uma visão geral da arquitetura dos componentes DirectX do Hyper-V, bem como uma metodologia de fuzzing para a descoberta de novas vulnerabilidades.
Nesta série de publicações, pretendo dar continuidade a essa pesquisa e suprir algumas lacunas que, por questões de tempo, não puderam ser abordadas na palestra:
Não há acesso público aos códigos de PoC.
Não há acesso público aos artefatos de fuzzing.
A infraestrutura necessária para realizar pesquisas nesse ambiente específico também não foi detalhada.
A integração entre Hyper-V e DirectX ainda está em desenvolvimento pela Microsoft; por isso, muitos dos pontos mencionados na palestra já não funcionam na versão atual do Windows 11.
Configuração do ambiente
Já publicamos um artigo sobre como configurar um ambiente básico para realizar a depuração remota do kernel. O processo envolvia a criação de uma máquina virtual, a ativação do modo de depuração via conexão de rede e a conexão do depurador — tudo isso podendo ser feito utilizando um único computador.
Este caso é diferente. Precisamos depurar um adaptador de GPU DirectX em uma máquina Windows que atua como hipervisor (usando o Hyper-V), com uma máquina virtual executando Linux. Embora a ativação das extensões de virtualização (VT-x) em uma máquina virtual Windows permita o uso de um Hyper-V aninhado (nested Hyper-V), o adaptador DirectX não fica visível. Após testar diferentes cenários, cheguei à conclusão de que seria necessário utilizar dois notebooks.
O primeiro notebook será o sistema sob depuração (debuggee ou host1). Nele, foi instalada a versão mais recente do Windows 11:

Nessa máquina, o WSL foi instalado juntamente com o Kali como uma VM convidada:

O kernel estável mais recente utilizado no WSL para as máquinas convidadas é o 5.10.102.1-microsoft-standard-WSL2. No entanto, como eu queria utilizar a versão mais recente disponível do WSL, realizei a compilação para poder usá-la. Na data em que este exercício foi realizado, a versão mais recente era a 5.15.57.1.

Certifique-se de que o host possua GPUs particionáveis, utilizando o comando Get-VMHostPartitionableGpu:

Em um artigo anterior, vimos como realizar a depuração remota por meio de uma conexão de rede. Tentei fazer isso, mas não obtive sucesso, pois o adaptador de rede físico não oferecia suporte à depuração:

Precisei recorrer a outra abordagem. Felizmente, o Windows oferece diversas formas de depuração. Neste caso, optei pela depuração via USB3 . Para isso, precisei:
Localizar, no notebook que seria depurado, uma porta USB 3.0 com suporte a depuração. Isso pôde ser feito utilizando o USBView, presente no Windows SDK:

Habilitar opções de depuração.

Conecte o depurador e o alvo da depuração usando um cabo USB 3 de boa qualidade.

No final, o ambiente de laboratório ficou assim:

Já estamos prontos!
Um fluxo de dados do DirectX no Hyper-V atualizado
O gráfico a seguir foi apresentado por Zhenhao Hong (@rthhh17) e descreve, de forma clara, os componentes do DirectX e como eles são acessados por uma máquina virtual (VM) no Hyper-V:

A seguir está um fluxo detalhado e atualizado dessas interações:
Existe um driver Linux chamado
dxgkrnl.koque expõe um conjunto de comandos IOCTL para interagir com os adaptadores DirectX do host.Quando um IOCTL é chamado, entra em ação outro driver chamado
hv_vmbus.ko, que usa o VMBUS para criar um pacote e um canal de barramento entre a máquina virtual, o hipervisor e o kernel da máquina host.A carga útil (payload) do IOCTL está contida em uma estrutura chamada
DXGADAPTER_VMBUS_PACKET, que contém o comando (DXGK_VMBCOMMAND) e as opções de comando a serem enviadas.A máquina host implementa a contraparte de recepção e processamento no driver
dxgkrnl.sys.O procedimento
dxgkrnl!VmBusProcessPacketé o método de recepção do VMBUS responsável por lidar com o payload deDXGADAPTER_VMBUS_PACKET.Se o
DXGK_VMBCOMMANDfor um comando global (listado no enumdxgkvmb_commandtype_global), um ponteiro de função (chamada indireta) é configurado para um método no formatodxgkrnl!DXG_HOST_GLOBAL_VMBUS::<command>, por exemplodxgkrnl!DXG_HOST_GLOBAL_VMBUS::VmBusDestroyProcess. Caso contrário, o fluxo pula para o ponto 7.Se o
DXGK_VMBCOMMANDnão for um pacote de comando global, ele é processado pordxgkrnl!VmBusExecuteCommandInProcessContext, que também usa chamadas indiretas (ponteiros de função) para calcular o método de tratamento de destino dessa requisição IOCTL específica. Nesse caso, o manipulador tem o formatodxgkrnl!DXG_HOST_VIRTUALGPU_VMBUS::<command>, por exemplodxgkrnl!DXG_HOST_VIRTUALGPU_VMBUS::VmBusCreateDevice.O método de tratamento faz a conversão de tipo (cast) do pacote
DXGADAPTER_VMBUS_PACKETusandodxgkrnl!CastToVmBusCommand<DXGKVMB_COMMAND_<command>>(por exemplo,dxgkrnl!CastToVmBusCommand<DXGKVMB_COMMAND_DESTROYPROCESS>) para filtrar os dados conforme necessário para esse manipulador de comando específico.O manipulador realiza verificações padrão (boilerplate) e executa a ação desejada. Em alguns casos, encaminha o pacote para uma função com o padrão
dxgkrnl!*Internal(por exemplo,dxgkrnl!SignalSynchronizationObjectInternal) oudxgkrnl!Dxgk<command>Impl(por exemplo,dxgkrnl!DxgkCreateDeviceImpl), que possui as interfaces necessárias para entregar o pacote aos componentes MMS (Microsoft Media System) do DirectX presentes nos driversdxgmms1.sysedxgmms2.sys.O sistema MMS é finalmente o encarregado de se comunicar com o driver de GPU correspondente, o qual expõe o adaptador (seja ele virtual ou físico).
Ao final, a resposta é enviada de volta para a máquina virtual via
dxgkrnl!VmBusCompletePacket.
É um processo complexo na leitura, mas vamos vê-lo em ação. Vejamos um exemplo executando apenas um comando: Create Device. Aqui está o código de exemplo.
É um código simples:
Abre um identificador (handle) para /dev/dxg.
Usa esse identificador para enumerar os adaptadores disponíveis.
Cria o identificador do dispositivo.
A saída no console deve ser algo como:
Vamos primeiro dar uma olhada no lado do Linux. Primeiro, vou pausar a execução no gdb em *create_device+176 (sample1.c:43), que é quando o IOCTL que chama o comando LX_DXCREATEDEVICE é executado:

Se verificarmos o valor da variável ddd.device antes da chamada, você deverá ver algo parecido com isto:

Após o IOCTL, podemos ver que o identificador (handle) do dispositivo agora está preenchido:

Agora, vamos examinar o host que executa o Windows. Devemos conseguir observar a criação do manipulador de dispositivo (0x40000000) em uma resposta dxgkrnl!VmBusCompletePacket. Precisaremos definir alguns pontos de interrupção (breakpoints) para verificar o fluxo. Primeiro, vamos inserir um ponto de interrupção em dxgkrnl!VmBusProcessPacket.
Ao inspecionar dxgkrnl!VmBusProcessPacket, podemos observar, em dxgkrnl!VmBusProcessPacket+0x568, a execução de uma chamada indireta. É nesse ponto que a função dxgkrnl!VmBusProcessPacket processa comandos globais do tipo DXGK_VMBCOMMAND. É possível encontrar chamadas indiretas (ponteiros de função) no espaço do kernel, uma vez que elas são encapsuladas por chamadas a _guard_dispatch_icall_fptr — um mecanismo adicionado quando o kernel é compilado com CFG.
Vamos definir outro ponto de interrupção (breakpoint) nesse local:

Agora, vamos definir um ponto de interrupção (breakpoint) em dxgkrnl!VmBus ExecuteCommand Process Context:
Agora devemos ter cinco pontos de interrupção, conforme a seguir:

Vou fazer referência aos passos descritos acima no seguinte fluxo de execução.
Ao executarmos o código de exemplo novamente, ele atinge nosso primeiro ponto de interrupção (passo 5):

Ao retomarmos a execução, o próximo ponto de interrupção (breakpoint) é atingido na chamada dxgkrnl!_guard_dispatch_icall_fptr, que consiste em uma chamada indireta para a rotina de tratamento (handler) do primeiro comando. Nesse caso, a função de tratamento foi resolvida como dxgkrnl!DXG_HOST_GLOBAL_VMBUS::VmBusCreateProcess (passo 6):

Se retomarmos a execução, uma chamada para dxgkrnl!VmBusCompletePacket é realizada para enviar ao chamador o resultado do comando dxgkrnl!DXG_HOST_GLOBAL_VMBUS::VmBus CreateProcess:

Ao retomarmos a execução duas vezes, o ponto de interrupção (breakpoint) em dxgkrnl!VmBusProcessPacket é acionado primeiro (passo 5), conforme esperado; no entanto, o próximo ponto de interrupção a ser acionado ocorre em dxgkrnl!VmBusExecuteCommandInProcessContext, o que significa que o comando recebido não é um comando global (passo 7):

Nesse método, em dxgkrnl!DXG_HOST_VIRTUALGPU_VMBUS::VmBusCreateDevice+0x8d, podemos observar uma chamada para dxgkrnl!CastToVmBusCommand<DXGKVMB_COMMAND_CREATEDEVICE>, que extrairá as partes necessárias do DXGADAPTER_VMBUS_PACKET (etapa 8):
Aqui está o código descompilado:

Mais adiante nessa função, em dxgkrnl!DXG_HOST_VIRTUALGPU_VMBUS::VmBusCreateDevice+0x3b0, podemos observar uma chamada para dxgkrnl!DxgkCreateDeviceImpl, que realiza o trabalho pesado (passo 9):
E, finalmente, ao prosseguirmos com a execução, o ponto de interrupção (breakpoint) em dxgkrnl!VmBusCompletePacket é atingido. De acordo com este artigo, o segundo parâmetro da função dxgkrnl!VmBusCompletePacket consiste nos dados a serem enviados de volta ao chamador (passo 11). Isso significa que, se verificarmos o dado do tipo double word apontado pelo registrador rdx, deveremos encontrar o manipulador de dispositivo (0x40000000) sendo retornado — tal como observamos anteriormente na saída da VM Linux:

Ótimo!
Aqui você pode baixar o arquivo sample1.c.
Conclusão
A interação entre o Hyper-V e o DirectX não é documentada oficialmente. É possível compreender a maior parte do funcionamento interno analisando o código do WSL, realizando engenharia reversa nos drivers do Windows e efetuando a depuração do kernel. No próximo artigo, veremos que a maioria dos comandos do dxgkrnl não é "sem estado" (stateless) e que alguns deles dependem da criação prévia de determinados objetos no kernel. Também veremos como explorar essa arquitetura sob uma perspectiva ofensiva.
Se você deseja acompanhar esta série e receber nossas próximas publicações, não deixe de assinar nossa newsletter semanal.
Comece agora com o PTaaS da Fluid Attacks
Tags:
vulnerabilidade
hacking
exploit
software
Assine nossa newsletter
Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.
Outros posts























