Índice

Título
Índice
Índice
Título

Ataques

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

cover-offensive-hyperv-directx-1
Andres Roldan

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:

Host1 Windows 11

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

kali-linux

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.

WSL2

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

Partitionable GPU

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:

kdnet.exe

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:

USBView
  • Habilitar opções de depuração.

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

USB for debugging

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

Lab environment

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:

Hyper-V DirectX component architecture

A seguir está um fluxo detalhado e atualizado dessas interações:

  1. Existe um driver Linux chamado dxgkrnl.ko que expõe um conjunto de comandos IOCTL para interagir com os adaptadores DirectX do host.

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

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

  4. A máquina host implementa a contraparte de recepção e processamento no driver dxgkrnl.sys.

  5. O procedimento dxgkrnl!VmBusProcessPacket é o método de recepção do VMBUS responsável por lidar com o payload de DXGADAPTER_VMBUS_PACKET.

  6. Se o DXGK_VMBCOMMAND for um comando global (listado no enum dxgkvmb_commandtype_global), um ponteiro de função (chamada indireta) é configurado para um método no formato dxgkrnl!DXG_HOST_GLOBAL_VMBUS::<command>, por exemplo dxgkrnl!DXG_HOST_GLOBAL_VMBUS::VmBusDestroyProcess. Caso contrário, o fluxo pula para o ponto 7.

  7. Se o DXGK_VMBCOMMAND não for um pacote de comando global, ele é processado por dxgkrnl!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 formato dxgkrnl!DXG_HOST_VIRTUALGPU_VMBUS::<command>, por exemplo dxgkrnl!DXG_HOST_VIRTUALGPU_VMBUS::VmBusCreateDevice.

  8. O método de tratamento faz a conversão de tipo (cast) do pacote DXGADAPTER_VMBUS_PACKET usando dxgkrnl!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.

  9. 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) ou dxgkrnl!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 drivers dxgmms1.sys e dxgmms2.sys.

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

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

/*
Hyper-V -> DirectX Interaction Sample Code

Compile as: cc -ggdb -Og -o sample1 sample1.c

Author: Andres Roldan
LinkedIn: https://www.linkedin.com/in/andres-roldan/
Twitter: @andresroldan
*/

#define _GNU_SOURCE 1
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "/home/aroldan/WSL2-Linux-Kernel-linux-msft-wsl-5.15.y/include/uapi/misc/d3dkmthk.h"

int open_device() {
    int fd;
    fd = open("/dev/dxg", O_RDWR);
    if (fd < 0) {
        printf("Cannot open device file...\n");
        exit(1);
    }
    printf("Opened /dev/dxg: 0x%x\n", fd);
    return fd;
}

void create_device(int fd) {
    int ret;
    struct d3dkmt_createdevice ddd = { 0 };
    struct d3dkmt_adapterinfo adapterinfo = { 0 };
    struct d3dkmt_enumadapters3 enumada = { 0 };

    enumada.adapter_count = 0xff;
    enumada.adapters = &adapterinfo;
    ret = ioctl(fd, LX_DXENUMADAPTERS3, &enumada);
    if (ret) {
        printf("Error calling LX_DXENUMADAPTERS3: %d: %s\n", ret, strerror(errno));
        exit(1);
    }
    printf("Adapters found: %d\n", enumada.adapter_count);

    ddd.adapter = adapterinfo.adapter_handle;
    printf("Adapter handle: 0x%x\n", ddd.adapter.v);
    printf("Creating device\n");
    ret = ioctl(fd, LX_DXCREATEDEVICE, &ddd);
    if (ret) {
        printf("Error calling LX_DXCREATEDEVICE: %d: %s\n", ret, strerror(errno));
        exit(1);
    }
    printf("Device created: 0x%x\n", ddd.device);
}

int main() {
    int fd;
    struct d3dkmthandle device;

    fd = open_device();
    create_device(fd);
    close(fd);
}
/*
Hyper-V -> DirectX Interaction Sample Code

Compile as: cc -ggdb -Og -o sample1 sample1.c

Author: Andres Roldan
LinkedIn: https://www.linkedin.com/in/andres-roldan/
Twitter: @andresroldan
*/

#define _GNU_SOURCE 1
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "/home/aroldan/WSL2-Linux-Kernel-linux-msft-wsl-5.15.y/include/uapi/misc/d3dkmthk.h"

int open_device() {
    int fd;
    fd = open("/dev/dxg", O_RDWR);
    if (fd < 0) {
        printf("Cannot open device file...\n");
        exit(1);
    }
    printf("Opened /dev/dxg: 0x%x\n", fd);
    return fd;
}

void create_device(int fd) {
    int ret;
    struct d3dkmt_createdevice ddd = { 0 };
    struct d3dkmt_adapterinfo adapterinfo = { 0 };
    struct d3dkmt_enumadapters3 enumada = { 0 };

    enumada.adapter_count = 0xff;
    enumada.adapters = &adapterinfo;
    ret = ioctl(fd, LX_DXENUMADAPTERS3, &enumada);
    if (ret) {
        printf("Error calling LX_DXENUMADAPTERS3: %d: %s\n", ret, strerror(errno));
        exit(1);
    }
    printf("Adapters found: %d\n", enumada.adapter_count);

    ddd.adapter = adapterinfo.adapter_handle;
    printf("Adapter handle: 0x%x\n", ddd.adapter.v);
    printf("Creating device\n");
    ret = ioctl(fd, LX_DXCREATEDEVICE, &ddd);
    if (ret) {
        printf("Error calling LX_DXCREATEDEVICE: %d: %s\n", ret, strerror(errno));
        exit(1);
    }
    printf("Device created: 0x%x\n", ddd.device);
}

int main() {
    int fd;
    struct d3dkmthandle device;

    fd = open_device();
    create_device(fd);
    close(fd);
}
/*
Hyper-V -> DirectX Interaction Sample Code

Compile as: cc -ggdb -Og -o sample1 sample1.c

Author: Andres Roldan
LinkedIn: https://www.linkedin.com/in/andres-roldan/
Twitter: @andresroldan
*/

#define _GNU_SOURCE 1
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "/home/aroldan/WSL2-Linux-Kernel-linux-msft-wsl-5.15.y/include/uapi/misc/d3dkmthk.h"

int open_device() {
    int fd;
    fd = open("/dev/dxg", O_RDWR);
    if (fd < 0) {
        printf("Cannot open device file...\n");
        exit(1);
    }
    printf("Opened /dev/dxg: 0x%x\n", fd);
    return fd;
}

void create_device(int fd) {
    int ret;
    struct d3dkmt_createdevice ddd = { 0 };
    struct d3dkmt_adapterinfo adapterinfo = { 0 };
    struct d3dkmt_enumadapters3 enumada = { 0 };

    enumada.adapter_count = 0xff;
    enumada.adapters = &adapterinfo;
    ret = ioctl(fd, LX_DXENUMADAPTERS3, &enumada);
    if (ret) {
        printf("Error calling LX_DXENUMADAPTERS3: %d: %s\n", ret, strerror(errno));
        exit(1);
    }
    printf("Adapters found: %d\n", enumada.adapter_count);

    ddd.adapter = adapterinfo.adapter_handle;
    printf("Adapter handle: 0x%x\n", ddd.adapter.v);
    printf("Creating device\n");
    ret = ioctl(fd, LX_DXCREATEDEVICE, &ddd);
    if (ret) {
        printf("Error calling LX_DXCREATEDEVICE: %d: %s\n", ret, strerror(errno));
        exit(1);
    }
    printf("Device created: 0x%x\n", ddd.device);
}

int main() {
    int fd;
    struct d3dkmthandle device;

    fd = open_device();
    create_device(fd);
    close(fd);
}
/*
Hyper-V -> DirectX Interaction Sample Code

Compile as: cc -ggdb -Og -o sample1 sample1.c

Author: Andres Roldan
LinkedIn: https://www.linkedin.com/in/andres-roldan/
Twitter: @andresroldan
*/

#define _GNU_SOURCE 1
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include "/home/aroldan/WSL2-Linux-Kernel-linux-msft-wsl-5.15.y/include/uapi/misc/d3dkmthk.h"

int open_device() {
    int fd;
    fd = open("/dev/dxg", O_RDWR);
    if (fd < 0) {
        printf("Cannot open device file...\n");
        exit(1);
    }
    printf("Opened /dev/dxg: 0x%x\n", fd);
    return fd;
}

void create_device(int fd) {
    int ret;
    struct d3dkmt_createdevice ddd = { 0 };
    struct d3dkmt_adapterinfo adapterinfo = { 0 };
    struct d3dkmt_enumadapters3 enumada = { 0 };

    enumada.adapter_count = 0xff;
    enumada.adapters = &adapterinfo;
    ret = ioctl(fd, LX_DXENUMADAPTERS3, &enumada);
    if (ret) {
        printf("Error calling LX_DXENUMADAPTERS3: %d: %s\n", ret, strerror(errno));
        exit(1);
    }
    printf("Adapters found: %d\n", enumada.adapter_count);

    ddd.adapter = adapterinfo.adapter_handle;
    printf("Adapter handle: 0x%x\n", ddd.adapter.v);
    printf("Creating device\n");
    ret = ioctl(fd, LX_DXCREATEDEVICE, &ddd);
    if (ret) {
        printf("Error calling LX_DXCREATEDEVICE: %d: %s\n", ret, strerror(errno));
        exit(1);
    }
    printf("Device created: 0x%x\n", ddd.device);
}

int main() {
    int fd;
    struct d3dkmthandle device;

    fd = open_device();
    create_device(fd);
    close(fd);
}

É 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:

aroldan@host1:~$ cc -ggdb -Og -o sample1 sample1.c
aroldan@host1:~$ ./sample1
Opened /dev/dxg: 0x3
Adapters found: 2
Adapter handle: 0x40000000
Creating device
Device created: 0x40000000
aroldan@host1:~$ cc -ggdb -Og -o sample1 sample1.c
aroldan@host1:~$ ./sample1
Opened /dev/dxg: 0x3
Adapters found: 2
Adapter handle: 0x40000000
Creating device
Device created: 0x40000000
aroldan@host1:~$ cc -ggdb -Og -o sample1 sample1.c
aroldan@host1:~$ ./sample1
Opened /dev/dxg: 0x3
Adapters found: 2
Adapter handle: 0x40000000
Creating device
Device created: 0x40000000
aroldan@host1:~$ cc -ggdb -Og -o sample1 sample1.c
aroldan@host1:~$ ./sample1
Opened /dev/dxg: 0x3
Adapters found: 2
Adapter handle: 0x40000000
Creating device
Device created: 0x40000000

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:

Calling LX_DXCREATEDEVICE

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

ddd.device

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

Populated device 1Populated device 2

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.


Putting a breakpoint

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:

Another breakpoint

Agora, vamos definir um ponto de interrupção (breakpoint) em dxgkrnl!VmBus ExecuteCommand Process Context:

Final breakpoint

Agora devemos ter cinco pontos de interrupção, conforme a seguir:

All breakpoints

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

Hitting the first breakpoint

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

The handling function was resolved

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:

Sending the result

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

Command to create a device

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

Extracting the needed parts

Aqui está o código descompilado:

Decompiled code

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

Calling dxgkrnl!DxgkCreateDeviceImpl

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:

Device handler

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

Reduza o risco sem atrasar suas entregas

Reduza o risco sem atrasar suas entregas

Resultados rápidos e precisos a partir de um único programa de segurança contínuo impulsionado por IA, scanners e pentesters.

Resultados rápidos e precisos a partir de um único programa de segurança contínuo impulsionado por IA, scanners e pentesters.

Previna

Previna

Previna

Detecte

Detecte

Detecte

Gerencie

Gerencie

Gerencie

Corrija

Corrija

Corrija