
Exploit de prueba de concepto para CVE-2026-29923, una escalada de privilegios BYOVD en pstrip64.sys. Demuestra la lectura/escritura de memoria física a través de IOCTL para robar el token de SYSTEM y generar un shell elevado.
Descargo de responsabilidad: Este código se proporciona únicamente con fines educativos y de investigación defensiva. Fue escrito para profundizar en la comprensión de la explotación del kernel y ayudar a los defensores a protegerse contra vulnerabilidades similares. Cualquier uso no autorizado, ilegal o malintencionado de este proyecto está estrictamente prohibido.
Hash: ab01485bb7c8bc1a9c86096eeea6d31d8fad557bf4d44072b46373d2203faa6e
Nombre del controlador: pstrip64.sys
CVE: CVE-2026-29923
Un ataque "Bring Your Own Vulnerable Driver" (BYOVD) es una técnica antigua pero muy efectiva que los atacantes utilizan para eludir las protecciones de seguridad modernas de Windows mediante un controlador heredado que el sistema operativo todavía confía oficialmente. Una vez que el controlador se carga, el atacante aprovecha sus fallos para cerrar la brecha entre un proceso estándar sin privilegios y el control total a nivel de sistema.
A principios de esta semana, se reveló una nueva vulnerabilidad en el controlador pstrip64.sys, registrada como CVE-2026-29923. Esta publicación de blog desglosa todo el ciclo de vida del exploit: desde mi investigación inicial de la vulnerabilidad y el desarrollo de la Prueba de Concepto (PoC) hasta las estrategias de mitigación prácticas para los defensores que protegen sus entornos.
El controlador pstrip64.sys es un componente de modo kernel heredado vinculado a EnTech Taiwan PowerStrip (hasta la versión 3.90.736). Si bien su propósito legítimo es permitir ajustes avanzados en la visualización de tarjetas gráficas, sus profundos privilegios del sistema lo convierten en un objetivo muy atractivo para los atacantes.
Cuando se reveló la vulnerabilidad por primera vez, comencé analizando su función DriverEntry. Esta sirve como la rutina de inicialización principal del controlador del kernel, creando el objeto de dispositivo \Device\PSTRIP64 y exponiéndolo a las aplicaciones en modo usuario a través del enlace simbólico \DosDevices\PSTRIP64. Más importante aún, configura la tabla de despacho del controlador. La entrada que inmediatamente me llamó la atención fue la del índice 14 (IRP_MJ_DEVICE_CONTROL), que dirige todas las solicitudes IOCTL proporcionadas por el usuario directamente al manejador de la función sub_11340, nuestra área principal de interés.
La función sub_11340 actúa como el despachador principal de IOCTL, interpretando las solicitudes provenientes del modo usuario.
De todos los IOCTL expuestos, 0x80002008 es sin duda el más interesante. Mientras que los casos predeterminados gestionan interacciones menores con puertos de E/S, 0x80002008 actúa como una puerta de entrada a sub_11000, pasando el SystemBuffer directamente a esta función.
Esta rutina sub_11000 es la prueba irrefutable. Primero, usa HalTranslateBusAddress para tomar la dirección proporcionada por el usuario y traducirla a una dirección física de sistema válida. Luego, abre \Device\PhysicalMemory y la mapea usando ZwMapViewOfSection. Al codificar el identificador del proceso objetivo como (HANDLE)0xFFFFFFFFFFFFFFFFLL (que representa ZwCurrentProcess()), el controlador mapea esta memoria física directamente en el espacio de direcciones virtuales de nuestro proceso que realiza la llamada. De manera crucial, luego escribe esta dirección virtual recién mapeada de vuelta en el SystemBuffer para devolverla al usuario, entregando oficialmente a nuestra aplicación un puntero directo para leer y escribir memoria física.
Con la vulnerabilidad completamente comprendida y una primitiva de lectura/escritura física establecida, tengo todas las piezas del rompecabezas necesarias. Ahora, es hora de comenzar a escribir la Prueba de Concepto.
Nota: Esta PoC fue desarrollada y probada específicamente en un entorno Windows 10 22H2. Debido a que el exploit se basa en la manipulación directa de la memoria física, los desplazamientos de las estructuras del kernel y los límites de la memoria física están actualmente codificados para mi configuración. Para probar esto en su propia máquina, debe actualizar los desplazamientos del kernel de Windows y ajustar los rangos de escaneo de direcciones físicas para que coincidan con su compilación específica del sistema operativo y la configuración de RAM.
El primer paso en mi exploit es establecer comunicación con el controlador. Hice esto llamando a CreateFileA en el enlace simbólico del controlador (\\.\PSTRIP64). Una vez que tuve un identificador válido, necesitaba una forma limpia de abusar del IOCTL 0x80002008 que analicé anteriormente. Creé una función envoltorio llamada MapPhysicalMemory(). Esta función llena mi estructura personalizada PSTRIP_MAP_REQUEST con la dirección física objetivo y la longitud del bloque de memoria que quiero leer.
Luego envío esta estructura directamente al controlador mediante DeviceIoControl. Si tiene éxito, el controlador mapea esa memoria física directamente en mi aplicación en modo usuario y devuelve la dirección base virtual en el campo OutputResult. Ahora puedo convertir esta dirección devuelta en un puntero estándar de C++, lo que me da acceso sin privilegios a la RAM física del sistema.
Con mi primitiva de lectura/escritura física completamente operativa, mi objetivo era encontrar las estructuras de datos del kernel que contienen los privilegios de los procesos. En Windows, cada proceso en ejecución está representado por una estructura EPROCESS.
Windows asigna estructuras EPROCESS en el pool del kernel utilizando un identificador de 4 bytes específico llamado Pool Tag. Para los procesos, esta etiqueta es la cadena Proc (que se traduce como 0x636F7250 en hexadecimal). Al escanear la RAM física del sistema, pude buscar esta cadena exacta.
Mi exploit itera a través del espacio de memoria física desde 0x10000000 hasta 0x140000000, mapeando memoria en bloques de 2MB (STEP_SIZE = 0x200000). Convierto cada bloque mapeado en un arreglo de bytes sin procesar y lo escaneo en fragmentos de 16 bytes (sizeof(_POOL_HEADER)).
Sin embargo, simplemente encontrar la etiqueta Proc en la memoria física no es suficiente. La memoria es desordenada; esa etiqueta podría ser un artefacto residual de un proceso terminado, o simplemente datos aleatorios que coinciden con el valor hexadecimal. Si asumía ciegamente que cada etiqueta Proc era una estructura EPROCESS válida y comenzaba a modificar la memoria, causaría inmediatamente un BSOD.
Para garantizar la estabilidad, tuve que validar la estructura mediante heurísticas. Primero, calculo el inicio de la estructura EPROCESS (que se encuentra ligeramente desplazada de la etiqueta del pool). A partir de ahí, verifico algunas constantes conocidas para un proceso en ejecución:
0x2 (Prioridad Normal).0x0.Si todas estas heurísticas pasan, puedo tener una alta confianza de que estoy viendo un proceso válido y activo. Luego leo su ID de proceso único (PID). Si el PID coincide con el de mi propio proceso de exploit, guardo la dirección física de su puntero de token. Si el PID es 4 (el proceso System de Windows), extraigo y guardo el valor real de su token altamente privilegiado.
Finalmente, alineo la dirección física guardada del puntero de token de mi proceso al límite de 4KB más cercano y uso MapPhysicalMemory() una última vez para mapear solo esa página específica.
A continuación, navego hasta el desplazamiento exacto y sobrescribo mi token con el valor del token del Sistema. Instantáneamente, el kernel de Windows trata mi proceso de exploit como NT AUTHORITY\SYSTEM.
Después de desmapear la página para garantizar la estabilidad del sistema, simplemente llamo a CreateProcessA para generar cmd.exe. Debido a que mi proceso actual está elevado, el nuevo símbolo del sistema hereda estos privilegios de primer nivel, ¡completando con éxito el ataque!
Nota: Un detalle crítico que descubrí durante mi fase inicial de depuración es cómo el controlador maneja el puntero mapeado. Al ejecutar SystemBuffer->LowPart = (unsigned int)BaseAddress;, el controlador convierte la dirección base virtual de 64 bits a un valor de 32 bits antes de devolverlo. Esta truncación pierde los bits altos de la dirección, lo que resultó en violaciones de acceso inmediatas cuando intenté desreferenciarlo en mi exploit de 64 bits. Para evitar este problema de manera limpia, simplemente compilé mi PoC en modo usuario como una aplicación de 32 bits, asegurando que el puntero devuelto siguiera siendo perfectamente válido.
Nota: Durante mis pruebas iniciales, me encontré con un caso límite fascinante: mi PoC localizó con éxito mi proceso de exploit en la memoria, pero no pudo encontrar el proceso System (PID 4).
Para entender por qué, necesitaba inspeccionar la memoria física directamente. Adjunté un depurador de kernel (WinDbg) y usé comandos para recuperar la dirección virtual y la base de directorio del proceso System. Luego usé !vtop para traducir esa dirección virtual a su dirección física exacta en la RAM.
Volví a mi depurador en modo usuario adjunto a mi PoC. Establecí un punto de interrupción condicional en mi bucle de escaneo de memoria, indicándole que pausara la ejecución en el momento en que mi función MapPhysicalMemory() obtuviera el bloque de 2MB que contenía la dirección física del proceso System.
Una vez que se alcanzó el punto de interrupción, comencé a inspeccionar manualmente los bytes sin procesar de la memoria mapeada. Aquí es donde descubrí un detalle crucial sobre las asignaciones del pool del kernel de Windows.
Cuando Windows asigna memoria para un proceso, comienza con un _POOL_HEADER (que contiene nuestra etiqueta Proc), seguido de un _OBJECT_HEADER, y finalmente la estructura EPROCESS en sí. Para las aplicaciones estándar en modo usuario, estos encabezados contienen datos de seguimiento adicionales, lo que significa que la estructura EPROCESS real comienza 0x80 bytes después de la etiqueta del pool.
Sin embargo, inspeccionar la memoria del proceso System reveló un diseño diferente. El proceso System carece de algunos de estos encabezados de seguimiento estándar. ¡El desplazamiento desde la etiqueta Proc hasta el inicio de la estructura EPROCESS era solo de 0x40 bytes!
La solución fue sencilla. Actualicé mi PoC para manejar ambos tamaños de encabezado del pool iterando a través de un arreglo de posibles desplazamientos (0x40 y 0x80) cada vez que encuentra una etiqueta Proc.
La ciberseguridad es un juego interminable del gato y el ratón entre atacantes y defensores. Mientras los atacantes buscan constantemente controladores vulnerables, los productos de seguridad modernos y los equipos azules tienen varias formas robustas de detectar y bloquear esta operación exacta.
La forma más efectiva de detener un ataque BYOVD (Bring Your Own Vulnerable Driver) es evitar que el controlador se cargue en primer lugar.
Si el controlador ya está cargado, los productos de seguridad aún pueden detectar el exploit durante la fase de manipulación de tokens.
NT AUTHORITY\SYSTEM sin una cadena de autenticación legítima es una enorme señal de alerta.cmd.exe), especialmente cuando el proceso padre no tiene razón para ejecutarse como SYSTEM.