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


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:

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

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.

Asegúrate de que el host cuente con GPU particionables mediante el comando Get-VMHost 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:

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:

Habilitar opciones de depuración.

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

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

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

A continuación, se presenta un flujo detallado y actualizado de estas interacciones:
Existe un controlador de Linux llamado
dxgkrnl.koque expone un conjunto de comandos IOCTL para interactuar con los adaptadores DirectX del sistema anfitrión (host).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.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.La máquina anfitriona implementa la contraparte encargada de la recepción y el procesamiento en el controlador
dxgkrnl.sys.El procedimiento
dxgkrnl!VmBusProcessPacketconstituye el método de recepción del VMBUS que gestiona la carga útilDXGADAPTER_VMBUS_PACKET.Si el
DXGK_VMBCOMMANDes un comando global (listado en la enumeracióndxgkvmb_commandtype_global), se asigna un puntero a función (llamada indirecta) a un método con el formatodxgkrnl!DXG_HOST_GLOBAL_VMBUS::<comando>; por ejemplo,dxgkrnl!DXG_HOST_GLOBAL_VMBUS::VmBusDestroyProcess. De lo contrario, el flujo continúa en el punto 7.Si el
DXGK_VMBCOMMANDno corresponde a un paquete de comando global, es procesado pordxgkrnl!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 formatodxgkrnl!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 paqueteDXGADAPTER_VMBUS_PACKETmediantedxgkrnl!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.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) odxgkrnl!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 controladoresdxgmms1.sysydxgmms2.sys.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).
Finalmente, la respuesta se envía de vuelta a la máquina virtual (VM) a través de
dxgkrnl!VmBusCompletePacket.
Es un código sencillo:
Abre un manejador para /dev/dxg.
Utiliza dicho manejador para enumerar los adaptadores disponibles.
Crea el manejador del dispositivo.
La salida en la consola debería ser algo como:
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:

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

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

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

Ahora, coloquemos un punto de interrupción en dxgkrnl!VmBus ExecuteCommand Process Context.
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:

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

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

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:

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:

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):
Aquí está el código descompilado:

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

¡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.
Otros posts























