
Prueba de concepto para CVE-2024-54756, una vulnerabilidad que encontré en el motor de scripting ZScript de GZDoom.
Una prueba de concepto para una vulnerabilidad de ejecución de código arbitrario que encontré en la funcionalidad ZScript de GZDoom (https://github.com/zdoom/gzdoom). Un atacante puede compartir un archivo PK3 que contenga un archivo fuente ZScript malicioso y obtener acceso al PC de la víctima.
Muchas gracias a Rachael y Agent Ash del equipo de desarrollo de GZDoom por sus respuestas rápidas, y a ellos y a los demás desarrolladores de GZDoom por solucionarlo rápidamente.
Confirmado que funciona en 4.13.0 y 4.13.1, y probablemente funcione en versiones anteriores. Tenga cuidado con cualquiera que le diga que degrade a la versión 4.13.1 o inferior para poder jugar su WAD.
Esta PoC funciona solo en Linux, pero es probable que la vulnerabilidad exista también en Windows. No probado en ZDoom o LZDoom, pero la vulnerabilidad podría existir allí también.
La vulnerabilidad fue revelada a los desarrolladores antes de la publicación de esta PoC y ya no debería estar presente en la versión 4.13.2. Hasta donde sé, esta versión no incluye cambios disruptivos prácticos.
Esta PoC se crea y publica con fines educativos, para que los desarrolladores de motores de juegos/scripting puedan entender cómo pueden surgir las vulnerabilidades y para que los jugadores puedan entender cómo puede verse un mod de juego malicioso. No soy responsable ni me hago responsable por cualquier mal uso de esta PoC. Por favor, no la use para comprometer las PC de sus compañeros jugadores; es ilegal (no debería necesitar que se lo diga), y es una acción particularmente despreciable tomar el control de la computadora de alguien a través de un videojuego.
Para usar esta PoC, descargue este repositorio y cree un archivo PK3 (que en realidad es un archivo zip con extensión .pk3) que contenga zscript.zs y MAPINFO:
git clone https://github.com/Chainmanner/GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC
cd GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC
zip PoC.pk3 zscript.zs MAPINFO
La carga útil predeterminada es ejecutar una shell inversa a localhost en el puerto 1337. Inicie el listener:
nc -nvlp 1337
Ejecute la PoC así:
gzdoom -iwad <tu-wad-de-doom-o-freedoom> -file PoC.pk3
Si funcionó, ahora debería tener una shell inversa hacia sí mismo.
Esta PoC es solo para Linux. Puede que no funcione en el primer intento; simplemente inténtelo de nuevo hasta que funcione.
NOTA: Esta es mi primera redacción de exploit y todavía estoy mejorando mi habilidad para hacer redacciones de bajo nivel. Además, hice gran parte de mi depuración con GDB, y desafortunadamente no tuve la buena idea de guardar algunos volcados de memoria para ilustrar mejor mi explicación. ¡Lo siento! Mi próxima redacción será mejor, lo prometo.
GZDoom es un puerto fuente de Doom diseñado para rendimiento y extensibilidad. Gracias a sus potentes características, se han creado muchos WADs, mods e incluso conversiones totales comerciales impresionantes. Desafortunadamente, donde hay complejidad, hay oportunidad para vulnerabilidades, y en este caso había dos presentes en el motor de scripting ZScript que permitieron que surgiera una cadena de exploit completa.
Este exploit derrota a ASLR y evita la necesidad de derrotar los canarios de pila. No creo que CFI de Clang o las shadow stacks hubieran ayudado aquí.
La primera y más importante vulnerabilidad estaba en cómo se manejaban los arrays enormes. Si asigna un array lo suficientemente pequeño, la región de memoria asignada tiende a llenarse con ceros y está adecuadamente separada de otros objetos; no se puede obtener información leyendo memoria no inicializada, y ningún objeto se superpone con el array. Sin embargo, si asigna un array enorme – digamos, 1073741823 palabras de 32 bits o más – podrá leer y escribir hasta 4 GiB de memoria potencialmente no inicializada desde el punto de inicio del array, permitiendo al atacante modificar directamente otros objetos y derrotar ASLR al encontrar direcciones con desplazamientos conocidos. Además, cualquier otro array creado después de este punto se superpondrá con el enorme.
La segunda vulnerabilidad estaba en los permisos del mapa de memoria. Para un rendimiento más rápido, el código ZScript se compila JIT a bytecode x86 o x86-64 siempre que sea posible. Para lograr esto, el código debe escribirse en una región de memoria, y esa región de memoria debe ejecutarse. Sin embargo, la regla W^X establece que una región debe ser escribible o ejecutable, pero no ambas. Si se aplican ambas al mismo tiempo (en lugar de hacer la región escribible, escribir el código, y luego hacerla ejecutable y no escribible), entonces un atacante con una primitiva de escritura arbitraria podrá escalarla a ejecución de código arbitrario; puede escribir shellcode y saltar a él modificando, por ejemplo, la dirección de retorno en la pila (asumiendo que el atacante no tiene una primitiva de ejecución arbitraria). Si observa los mapas de memoria de GZDoom cuando se está ejecutando, puede ver varias regiones RWX:
7fcd19700000-7fcd19800000 rwxp 00000000 00:00 0
7fcd1a100000-7fcd1a200000 rwxp 00000000 00:00 0
7fcd1eb00000-7fcd1ec00000 rwxp 00000000 00:00 0
Por lo tanto, si las primitivas de escritura arbitraria y ejecución arbitraria están disponibles, y el atacante sabe dónde está presente alguna región RWX, puede escribir shellcode arbitrario y ejecutarlo. Hacer estas regiones RW- al escribir el código compilado JIT y luego R-X cuando esté listo para ejecutarse detendría esta PoC, pero no detendría a un atacante de obtener ejecución de código mediante, por ejemplo, modificar datos en la pila (ROP) o el heap.
Además, hay un gadget útil. Recuerde cómo al asignar un array enorme, cualquier otro array creado después de él se superpondrá. Eso incluye arrays de punteros a objetos. Al igual que los objetos C++, los objetos ZScript pueden contener variables y punteros a funciones. Supongamos que tenemos este objeto:
class WeirdObject
{
uint one;
uint two;
uint three;
uint four;
Function<clearscope void()> funcptr;
}
Si creamos un array que contenga un puntero a una instancia de WeirdObject, entonces el atacante puede cambiar el puntero a donde quiera usando el array enorme y cambiar los datos apuntados accediendo a los campos del objeto, dándonos una primitiva de lectura/escritura arbitraria que va más allá del heap. Los punteros en ZScript se verifican para asegurarse de que no sean nulos, pero no para asegurarse de que sean razonables.
La presencia de un puntero a función también nos da una primitiva de ejecución arbitraria; eso, sin embargo, es un poco menos directo, requiriendo la creación de un VMFunction falso para satisfacer la máquina virtual. Tan pronto como se introduce una llamada a una función ZScript en el código del exploit, ese código ya no se compila JIT. Todavía funciona, pero se vuelve un poco más complicado de depurar y explotar. Podría haber una mejor manera de hacer esta parte, pero no estudié lo suficiente los internals de GZDoom para conocerla.
Una cosa a tener en cuenta: WeirdObject tiene variables miembro heredadas, por lo que el primer miembro comienza en el offset 0x28.
Entonces, ahora tenemos las siguientes herramientas:
¿Cómo las encadenamos para hacer un exploit?