
Prueba de concepto CVE-2025-24990 (controlador de Agere Systems)
ltmdm64.sys). Este controlador es muy antiguo y no se carga de forma predeterminada en mi máquina de pruebas, por lo que lo explotaré en un escenario BYOVD. Curiosamente, según mi investigación, este controlador ha existido desde Windows 7 y tiene al menos un error. aquí
En ese momento, MSRC no tomó ninguna medida 🤡
Algunos IOCTL dentro de este controlador usan METHOD_NEITHER pero no verifican si el búfer de direcciones proporcionado por el llamador proviene del modo de usuario o del modo kernel. Aquí hay un ejemplo de código IOCTL que decodifiqué con OSR:
Esto significa que puedes proporcionar una dirección de kernel a la API DeviceIoControl y el controlador la manejará normalmente.
Ten en cuenta que primero debes omitir kASLR para filtrar la dirección del kernel. Usaré EnumDeviceDrivers (En Windows 24h2 necesitas SeDebugPriv para hacer esto).
El problema está en el IOCTL 0x802b200f (ud_response). Nuevamente, este envío de IOCTL no valida la dirección que proporciono desde el modo de usuario, pero lo aprovecharé más adelante.
ud_response llama a ll_load_diagnostics, y llegaré al siguiente código:
Al principio, la variable global eeprom no está inicializada, por lo que contendrá NULL. Aquí hay un código simple que activará esto.
Aprovecharé esto más adelante.
Este IOCTL simplemente convierte la cadena de versión del controlador "8.36" al número 0x836 (un DWORD) y lo escribe en la dirección proporcionada por el llamador (gracias a METHOD_NEITHER). Técnicamente, puedo escribir estos cuatro bytes (36 08 00 00) en una dirección de kernel arbitraria. Aprovecharé esto para sobrescribir las variables globales del controlador y cambiar el flujo de ejecución.
Llamaré a este 0x802b2003 IOCTL_GET_VERSION
Byte nulo arbitrario:
Volviendo al caso de desreferencia nula, uso la API VirtualAlloc para asignar una dirección fija (0x083600000000). Luego uso IOCTL_GET_VERSION para escribir en *(eeprom + 4) los cuatro bytes descritos anteriormente. Cuando el controlador desreferencie eeprom más tarde, leerá desde la dirección que asigné.
Después de corregir la desreferencia nula, el IOCTL escribe una cadena en la dirección que proporciono desde el modo de usuario, según el tamaño del búfer.
Este código simplemente demuestra lo que describí anteriormente: asignar un búfer y llenarlo con 0xAA, corregir la desreferencia nula y luego llamar al controlador. Ten en cuenta que asigno 11 bytes pero solo proporciono un tamaño de búfer de 10 al controlador para ver cómo se comporta.
Escribe una secuencia fija de bytes en mi búfer y luego anula el byte final (el 11), incluso si solo proporciono un tamaño de 10. Reemplaza el último 0xAA en mi búfer con 0x00. Esto indica que si proporciono un tamaño de 0, el controlador aún escribe un solo byte 0x00 en la dirección de destino.
Decremento arbitrario
Ahora que tengo el byte nulo y los 4 bytes fijos arbitrarios, vamos a crear otra primitiva.
Este IOCTL establecerá el LtMsgEvent global en mi búfer de usuario, luego verificará si WDM es nulo y lo establecerá en cero nuevamente.
Luego, en 0x802b2207, llamará a la API ObfReferenceObject.
En el estado inicial, WDM es nulo, pero con la ayuda de IOCTL_GET_VERSION puedo establecer WDM en 0x36 (su tamaño es solo 1 byte) y LtMsgEvent sigue siendo mi búfer. Luego anularé WDM y llamaré a 0x802b2207. Finalmente llegaré a ObfReferenceObject. Llamaré a estos dos IOCTL IOCTL_SET_LtMsgEvent y IOCTL_DEREF_LtMsgEvent.
La técnica de exploit que usa ObfReferenceObject cambia el PreviousMode de nuestro KTHREAD de UserMode a KernelMode; puedes leer sobre ello aquí. Sin embargo, Windows ha corregido este exploit, por lo que no podemos usarlo.
Pero la primitiva en ObfReferenceObject aún existe. La API resta 0x30 de la dirección que proporcionamos, convierte el resultado en un entero de 8 bytes y luego resta 1.
*(signed long long)(LtMsgEvent-0x30) -= 1
Pero el problema es que verifica si el siguiente valor es 0 o si el valor actual es < 1 (interpretado como un entero con signo de 8 bytes). Si cualquiera de las condiciones es verdadera, salta a KeBugCheckEx y bloquea el sistema.
Escritura arbitraria
Con el decremento arbitrario en mano, necesito encontrar otro lugar para escribir el byte 0xFF y luego decrementarlo al byte que quiero. Encontré este IOCTL 0x802b2243:
Nos centraremos en la rama flip. pbVar5 es la dirección que proporciono desde el modo de usuario y puede ser cualquier dirección de destino que elija. Escribo el byte 0x0C en DAT_TARGET_EX (con la ayuda de IOCTL_GET_VERSION y la primitiva de decremento arbitrario), y también anulo 1 byte en la dirección de destino. La primera llamada a este IOCTL establece 0xC0 en la dirección de destino, que luego se decrementa a 0xBF. Una segunda llamada establece 0xFF en la dirección de destino (0xBF | 0xC0 = 0xFF). Una vez que el destino contiene 0xFF, solo lo decremento al valor deseado.
Escribiré un byte a la vez y tendré cuidado con el KeBugCheckEx en ObfReferenceObject.
Lectura arbitraria
Para la primitiva de lectura, uso la técnica descrita aquí (@carrot_c4k3). Simplemente sobrescribo el objeto UNICODE_STRING en el kernel (ExpManufacturingInformation) y luego llamo a NtQuerySystemInformation. Debido a que ObfReferenceObject llama a KeBugCheckEx, anularé 8 bytes adyacentes a ExpManufacturingInformation.
Eso es todo, ahora tenemos R/W arbitrario, podemos usar esas primitivas para hacer muchas cosas. El controlador no se carga de forma predeterminada, por lo que lo explotaré en un escenario BYOVD y estableceré el PPL de un proceso.
El exploit que he descrito anteriormente funciona en todas las versiones de Windows, pero es inestable debido al KeBugCheckEx. Pero en Windows 11 22h2+ existe una técnica llamada ioring. Esta técnica simplemente sobrescribe ioring->Buffer con una dirección controlable. Concretamente, podemos sobrescribir ioring->Buffer y su tamaño con 0x083600000000 y 0x836 respectivamente (usando IOCTL_GET_VERSION). Con esta técnica, solo realizo 2 escrituras y luego uso la primitiva R/W de manera muy estable. Ten en cuenta que este enfoque requiere filtrar la dirección del kernel.
Windows no carga el controlador en el estado predeterminado. Por lo tanto, debes cargarlo manualmente. El archivo ltmdm64.sys se encuentra en C:\Windows\System32\DriverStore\...\ltmdm64.sys. Ejecuta este comando como administrador y ejecuta el exploit:
sc create ltmdm64_srv binPath="C:\Windows\System32\DriverStore...\ltmdm64.sys" type=kernel && sc start ltmdm64_srv
El exploit usará la técnica ioring para desactivar el PPL de lsass.exe y usará mi técnica de solo datos para establecer el PPL en notepad.exe (en win 11 24h2 se necesita SeDebugPriv habilitado)
https://github.com/user-attachments/assets/05a35b38-d26c-484f-9fb7-137f8fe8c079
Informé de este error a ZDI. Pero parece que es un duplicado de la presentación de Fabian Mosch y Jordan Jay a MSRC, por lo que este PoC solo muestra el error y agradece su trabajo. Casi mi primer CVE 😍