
Explotando una vulnerabilidad parcheada en JavaScriptCore
Este es un exploit para una vulnerabilidad de WebKit que fue descubierta originalmente por Fluoroacetate durante la competencia pwn2own en Vancouver. Aunque no descubrí este error, escribí este exploit para practicar mis habilidades de desarrollo de exploits. El artículo original sobre este exploit está aquí de Zero Day Initiative. Si bien este artículo es muy bueno y fue fundamental para ayudarme a entender la vulnerabilidad, está desde el punto de vista de alguien que verifica la vulnerabilidad. Encontré que algunos detalles clave faltan al intentar diseñar este exploit desde cero y espero llenar algunos de los vacíos que el artículo de ZDI omitió y adquirir habilidades prácticas sobre cómo diseñar un exploit complicado desde cero.
Estos pasos sirven como un esquema para lograr ejecución de código arbitrario dentro de JavaScriptCore (JSC), el motor JavaScript de WebKit
La vulnerabilidad que será explotada es un desbordamiento de enteros que ocurre en el código producido por el compilador DFG justo a tiempo (JIT) para WebKit. Esto ocurre específicamente en la función compileNewArrayWithSpread. Esta función será llamada cuando el código que usa sintaxis de propagación de JavaScript para crear un nuevo array sea compilado JIT por DFG.

Dentro del código JIT, primero se calculará el tamaño del array. Lo hace sumando la longitud de cada argumento pasado al constructor del array. Mientras calcula el tamaño para cada suma, verifica si hay un desbordamiento del tamaño. Después de esto, llamará a la función compileAllocateNewArray pasando la longitud que se computó en esta función.

La función compileAllocateNewArray pasará entonces la longitud que se calculó antes a emitAllocateButterfly.

emitAllocateButterfly desplazará entonces a la izquierda el tamaño 3 bits, lo que equivale a multiplicarlo por 8. Sin embargo, no hay verificación de desbordamiento, por lo que un número como 0x20000001 puede desbordarse a 0x8
Este programa en C ilustra esta vulnerabilidad:


Podemos usar esta vulnerabilidad para engañar al motor JavaScript haciéndole creer que hemos asignado un array con tamaño 0x20000001 pero en realidad solo hemos asignado suficiente espacio para 1 JSValue (8 bytes). Esto resultará en una primitiva de lectura y escritura fuera de los límites (OOB) que luego puede ser aprovechada para lograr lectura/escritura arbitraria y eventualmente ejecución remota de código (RCE).
Identificar la vulnerabilidad
Para confirmar que tenemos una lectura OOB, intentaremos disparar esta vulnerabilidad en una compilación de JSC con address sanitizer (ASAN).
Para hacer esto desde el directorio de WebKit podemos ejecutar los comandos:```bash Tools/Scripts/set-webkit-configuration --asan Tools/Scripts/build-jsc --jsc--only --debug
Esto construirá una compilación de depuración de JSC con ASAN habilitado, lo que nos permitirá verificar si hemos desencadenado exitosamente la vulnerabilidad o no.
Aquí está la primera iteración de exploit.js```javascript
function jitMe(array){
return [...array]
}
let dummy = [1.1]
for(let i = 0; i < 200; i++){
jitMe(dummy);
}
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
Cuando ejecuto esto, obtengo el siguiente error:
Program terminated with signal SIGKILL, Killed. The program no longer exists.
Mi suposición era que se estaba consumiendo demasiada memoria al intentar asignar un array tan grande. Para confirmarlo, agregué un punto de interrupción al código JITed añadiendo una llamada a m_jit.breakpoint() dentro de compileNewArrayWithSpread, lo que agrega una instrucción int3 al código JITed.
Después de agregar el punto de interrupción, descubrí que no se disparaba y entonces decidí probar con una longitud de 0x20001. Me di cuenta de que el código ni siquiera se estaba compilando, así que agregué más iteraciones para activar el compilador DFG.```javascript function jitMe(array){ for(let i = 0; i < 0x4000; i++){ let x = 1 + 1 } return [...array] }
let dummy = [1.1] for(let i = 0; i < 60; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000001
for(let i = 0; i < len; i++){ a[i] = 1.1 }
jitMe(a)
Probar el programa tal como está aún lleva a SIGKILL, sin embargo, al probar con una longitud más pequeña, el punto de interrupción se activa. En este punto, todavía me parece que JSC se está quedando sin memoria al intentar procesar ese enorme array.
Para lidiar con esto, decidí asignar un array `a` más pequeño y luego usar la sintaxis de propagación para usarlo varias veces al crear el array corrupto resultando en el siguiente exploit.js```
function jitMe(array){
for(let i = 0; i < 0x4000; i++){
let x = 1 + 1
}
return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array]
}
let dummy = [1.1]
for(let i = 0; i < 100; i++){
print(i)
jitMe(dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
jitMe(a)
Usando este código pudimos alcanzar el punto de interrupción sin un SIGKILL. Como suele ocurrir, solucionar un problema revela otro y obtuvimos un SIGABORT en su lugar... Usando el comando bt de gdb podemos ver que se llamó a operationNewArrayWithSize, que a su vez llamó a create.
Parece extraño que nuestro código JIT esté llamando a operationNewArrayWithSize y debe ser que el código JIT tuvo que tomar una ruta lenta hacia el motor de JavaScript por alguna razón.

Podemos ver en compileAllocateNewArrayWithSize que efectivamente hay una salida hacia operationNewArrayWithSize. Entonces necesitamos descubrir por qué exactamente estamos saliendo al caso lento.
Podemos ver que en compileNewArrayWithSpread, shouldConvertLargeSizeToArrayStorage está establecido en falso y esa ruta lenta no estará en el código compilado.