Tabla de contenidos

Title
Tabla de contenidos
Tabla de contenidos
Title

Ataques

DirectX/Hyper-V; una perspectiva ofensiva: continuación de una charla en Black Hat

cover-offensive-hyperv-directx-1
Andres Roldan

VP of Hacking

8 min

Este año asistí a Black Hat USA. Las charlas ofrecidas fueron variadas y todas resultaron interesantes; algunas, en particular, fueron muy relevantes para mi campo de trabajo actual, centrado principalmente en temas avanzados de Red Teaming y desarrollo de exploits.

Una de las ponencias que me pareció más interesante fue «DirectX: The new Hyper-V Attack surface», presentada por Zhenhao Hong (@rthhh17). En ella se expusieron cuatro vulnerabilidades (CVE-2021-43219, CVE-2022-21898, CVE-2022-21912 y CVE-2022-21918) relacionadas con fallos como la desreferencia de puntero nulo (Null Pointer Dereference), la lectura en direcciones de memoria arbitrarias (Arbitrary Address Read) y la escritura en direcciones de memoria arbitrarias (Arbitrary Address Write). Además, se incluyeron fragmentos de código de prueba de concepto (PoC) para activar cada una de estas vulnerabilidades.

Asimismo, se presentó una visión general de la arquitectura de los componentes de DirectX en Hyper-V, junto con una metodología de fuzzing propuesta para descubrir nuevas vulnerabilidades.

En esta serie de publicaciones, trataré de dar continuidad a dicha investigación y abordar algunos aspectos que, debido a las limitaciones de tiempo de la charla, no pudieron tratarse en profundidad:

No existe acceso público a los códigos de prueba de concepto (PoC).

No existe acceso público a los artefactos utilizados para el fuzzing.

Tampoco se detalló la infraestructura necesaria para llevar a cabo investigaciones en ese entorno específico.

La integración de DirectX en Hyper-V es un proyecto en constante desarrollo por parte de Microsoft; por ello, muchos de los elementos mencionados en la charla ya no funcionan en la versión actual de Windows 11.

Configuración del entorno

Ya hemos publicado un artículo sobre cómo configurar un entorno básico para realizar la depuración remota del kernel. Dicho proceso consistía en crear una máquina virtual, habilitar el modo de depuración mediante una conexión de red y conectar el depurador; todo ello podía realizarse utilizando un único equipo.

Este caso es distinto. Necesitamos depurar un adaptador de GPU DirectX en un equipo Windows que actúa como hipervisor mediante Hyper-V, el cual ejecuta una máquina virtual con Linux. Si bien es posible habilitar la virtualización anidada (nested Hyper-V) activando las extensiones de virtualización (VT-x) en una máquina virtual Windows, el adaptador DirectX no resulta visible. Tras probar diversos escenarios, llegué a la conclusión de que era necesario utilizar dos ordenadores portátiles.

El primer portátil será el sistema a depurar (host1). En él se ha instalado la versión más reciente de Windows 11:

Host1 Windows 11

En esa máquina, se instaló WSL junto con Kali como máquina virtual invitada:

kali-linux

El kernel estable más reciente utilizado en WSL para las máquinas invitadas es 5.10.102.1-microsoft-standard-WSL2. Sin embargo, como quería emplear la versión más reciente de WSL disponible, procedí a compilarla para poder utilizarla. A fecha de este ejercicio, la versión más reciente era la 5.15.57.1.

WSL2

Asegúrate de que el host cuente con GPU particionables mediante el comando Get-VMHost Partitionable Gpu:

Partitionable GPU

En un artículo anterior, vimos cómo realizar una depuración remota mediante una conexión de red. Intenté hacerlo, pero no fue posible porque el adaptador de red físico no admitía la depuración:

kdnet.exe

Tuve que recurrir a otro enfoque. Por suerte, Windows ofrece varias formas de realizar la depuración. En este caso, opté por utilizar la depuración a través de USB 3.0. Para ello, tuve que:

  • Localizar un puerto USB 3.0 en el equipo portátil que iba a depurar (el «equipo destino») que fuera compatible con la depuración. Esto se podía lograr utilizando USBView, una herramienta incluida en el Windows SDK:

USBView
  • Habilitar opciones de depuración.

Debug options
  • Conecta el depurador y el sistema bajo depuración mediante un cable USB 3 de calidad.

USB for debugging

Al final, el entorno de laboratorio quedó así:

Lab environment

¡Ya estamos listos!

Un flujo de datos de DirectX en Hyper-V actualizado

El siguiente gráfico, presentado por Zhenhao Hong (@rthhh17), describe de forma clara los componentes de DirectX y la manera en que una máquina virtual (VM) en Hyper-V accede a ellos:

Hyper-V DirectX component architecture

A continuación, se presenta un flujo detallado y actualizado de estas interacciones:

  1. Existe un controlador de Linux llamado dxgkrnl.ko que expone un conjunto de comandos IOCTL para interactuar con los adaptadores DirectX del sistema anfitrión (host).

  2. Cuando se invoca un IOCTL, interviene otro controlador llamado hv_vmbus.ko, el cual utiliza el VMBUS para crear un paquete y un canal de bus entre la máquina virtual (VM), el hipervisor y el núcleo (kernel) de la máquina anfitriona.

  3. La carga útil (payload) del IOCTL se encapsula en una estructura denominada DXGADAPTER_VMBUS_PACKET, la cual contiene el comando (DXGK_VMBCOMMAND) y las opciones de comando que deben enviarse.

  4. La máquina anfitriona implementa la contraparte encargada de la recepción y el procesamiento en el controlador dxgkrnl.sys.

  5. El procedimiento dxgkrnl!VmBusProcessPacket constituye el método de recepción del VMBUS que gestiona la carga útil DXGADAPTER_VMBUS_PACKET.

  6. Si el DXGK_VMBCOMMAND es un comando global (listado en la enumeración dxgkvmb_commandtype_global), se asigna un puntero a función (llamada indirecta) a un método con el formato dxgkrnl!DXG_HOST_GLOBAL_VMBUS::<comando>; por ejemplo, dxgkrnl!DXG_HOST_GLOBAL_VMBUS::VmBusDestroyProcess. De lo contrario, el flujo continúa en el punto 7.

  7. Si el DXGK_VMBCOMMAND no corresponde a un paquete de comando global, es procesado por dxgkrnl!VmBusExecuteCommandInProcessContext, que también emplea llamadas indirectas (punteros a función) para determinar el método encargado de gestionar ese comando específico de la solicitud IOCTL. En este caso, el manejador adopta el formato dxgkrnl!DXG_HOST_VIRTUALGPU_VMBUS::<comando>; por ejemplo, dxgkrnl!DXG_HOST_VIRTUALGPU_VMBUS::VmBusCreateDevice. El método de gestión realiza una conversión (casting) del paquete DXGADAPTER_VMBUS_PACKET mediante dxgkrnl!CastToVmBusCommand<DXGKVMB_COMMAND_<comando>> (por ejemplo, dxgkrnl!CastToVmBusCommand<DXGKVMB_COMMAND_DESTROYPROCESS>), con el fin de filtrar los datos según sea necesario para el controlador de comandos específico.

  8. El controlador realiza comprobaciones estándar y ejecuta la acción deseada. En algunos casos, transfiere el paquete a una función que sigue el patrón dxgkrnl!*Internal (por ejemplo, dxgkrnl!SignalSynchronizationObjectInternal) o dxgkrnl!Dxgk<comando>Impl (por ejemplo, dxgkrnl!DxgkCreateDeviceImpl); estas funciones disponen de las interfaces necesarias para entregar el paquete a los componentes MMS (Microsoft Media System) de DirectX, los cuales residen en los controladores dxgmms1.sys y dxgmms2.sys.

  9. El sistema MMS es, en última instancia, el encargado de comunicarse con el controlador de GPU correspondiente, el cual expone el adaptador (que puede ser virtual o físico).

  10. Finalmente, la respuesta se envía de vuelta a la máquina virtual (VM) a través de dxgkrnl!VmBusCompletePacket.

/*
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);
}

Es un código sencillo:

  1. Abre un manejador para /dev/dxg.

  2. Utiliza dicho manejador para enumerar los adaptadores disponibles.

  3. Crea el manejador del dispositivo.

La salida en la consola debería 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

Analicemos primero el entorno de Linux. Para empezar, voy a pausar la ejecución en gdb en *create_device+176 (sample1.c:43), que es el punto en el que se realiza la llamada IOCTL que invoca el comando LX_DXCREATEDEVICE:

Calling LX_DXCREATEDEVICE

Si observamos el valor de la variable ddd.device antes de la llamada, deberíamos ver algo como esto:

ddd.device

Después de la IOCTL, podemos ver que el controlador del dispositivo ahora está lleno:

Populated device 1Populated device 2

Ahora, examinemos el host que ejecuta Windows. Deberíamos poder observar la creación del manejador de dispositivos (0x40000000) durante una respuesta dxgkrnl!VmBusCompletePacket. Para verificar el flujo, necesitaremos establecer algunos puntos de interrupción (breakpoints). En primer lugar, coloquemos un punto de interrupción en dxgkrnl!VmBusProcessPacket.

Putting a breakpoint

Al inspeccionar dxgkrnl!VmBusProcessPacket, podemos observar que, en dxgkrnl!VmBusProcessPacket+0x568, se realiza una llamada indirecta. Es aquí donde dxgkrnl!VmBusProcessPacket gestiona los comandos globales DXGK_VMBCOMMAND. Es posible encontrar llamadas indirectas (punteros a funciones) en el espacio del kernel, ya que estas aparecen envueltas por llamadas a _guard_dispatch_icall_fptr, un mecanismo que se añade cuando el kernel se compila con CFG.

Coloquemos otro punto de interrupción (breakpoint) en ese lugar:

Another breakpoint

Ahora, coloquemos un punto de interrupción en dxgkrnl!VmBus ExecuteCommand Process Context.

Third breakpoint

En esa función, en dxgkrnl!VmBus ExecuteCommand InProcess Context+0x1f0, también podemos observar que se realiza una llamada indirecta. Podemos establecer un nuevo punto de interrupción en ese lugar:

All breakpoints

Voy a hacer referencia a los pasos descritos anteriormente en el siguiente flujo de ejecución.

Al ejecutar de nuevo el código de ejemplo, este llega a nuestro primer punto de interrupción (paso 5):

Hitting the first breakpoint

Al reanudar la ejecución, se alcanza el siguiente punto de interrupción en la llamada dxgkrnl!_guard_dispatch_icall_fptr, la cual constituye una llamada indirecta al controlador del primer comando. En este caso, la función encargada de gestionar la operación se identificó como dxgkrnl!DXG_HOST_GLOBAL_VMBUS::VmBusCreateProcess (paso 6):

The handling function was resolved

Si reanudamos la ejecución, se realiza una llamada a dxgkrnl!VmBusCompletePacket para enviar al llamador el resultado del comando dxgkrnl!DXG_HOST_GLOBAL_VMBUS::VmBus CreateProcess:

Next breakpoint hit

Ahora, al reanudar la ejecución, se alcanza el siguiente punto de interrupción en dxgkrnl!VmBusExecuteCommandInProcessContext+0x1f0, el cual contiene una llamada indirecta que se resuelve en un comando no global. En este caso, observamos el comando que enviamos (LX_DXCREATEDEVICE) para la creación de un dispositivo:

Command to create a device

En dicho método, en dxgkrnl!DXG_HOST_VIRTUALGPU_VMBUS::VmBusCreateDevice+0x8d, podemos observar una llamada a dxgkrnl!CastToVmBusCommand<DXGKVMB_COMMAND_CREATEDEVICE>, la cual extraerá los elementos necesarios del DXGADAPTER_VMBUS_PACKET (paso 8):

Extracting the needed parts

Aquí está el código descompilado:

Decompiled code

Más adelante en esa función, en dxgkrnl!DXG_HOST_VIRTUALGPU_VMBUS::VmBusCreateDevice+0x3b0, podemos observar una llamada a dxgkrnl!Dx vkCreateDevice Impl, que se encarga del trabajo pesado (paso 9):

Calling dxgkrnl!DxgkCreateDeviceImpl

Y, finalmente, al reanudar la ejecución, se alcanza el punto de interrupción en dxgkrnl!VmBusCompletePacket. Según este artículo, el segundo parámetro de la función dxgkrnl!VmBusCompletePacket corresponde a los datos que deben devolverse a quien realizó la llamada (paso 11). Esto significa que, si examinamos el dato de tipo «doble palabra» (double word) al que apunta el registro rdx, deberíamos ver el manejador del dispositivo (0x40000000) devuelto, tal como observamos anteriormente en la salida de la máquina virtual (VM) Linux:

Device handler

¡Genial!

Aquí puedes descargar el archivo sample1.c.

Conclusión

La interacción entre Hyper-V y DirectX no está documentada oficialmente. Es posible comprender gran parte de su funcionamiento interno analizando el código de WSL, realizando ingeniería inversa en los controladores de Windows y llevando a cabo la depuración del Kernel. En el próximo artículo, veremos que la mayoría de los comandos de dxgkrnl no son «sin estado» (stateless) y que algunos de ellos requieren la creación previa de determinados objetos en el kernel. También veremos cómo aprovechar esta arquitectura desde una perspectiva ofensiva.

Si deseas seguir esta serie y recibir nuestras próximas publicaciones, no dudes en suscribirte a nuestro boletín semanal.

Empieza ya con el PTaaS de Fluid Attacks

Etiquetas:

vulnerabilidad

hacking

exploit

software

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Reduce el riesgo sin retrasar tus entregas

Reduce el riesgo sin retrasar tus entregas

Resultados rápidos y exactos desde un único programa de seguridad continuo impulsado por IA, escáneres y pentesters.

Resultados rápidos y exactos desde un único programa de seguridad continuo impulsado por IA, escáneres y pentesters.

Prevén

Prevén

Prevén

Detecta

Detecta

Detecta

Gestiona

Gestiona

Gestiona

Remedia

Remedia

Remedia