
Sfruttare una vulnerabilità corretta in JavaScriptCore
Questo è un exploit per una vulnerabilità WebKit originariamente scoperta da Fluoroacetate durante la competizione pwn2own a Vancouver. Sebbene non abbia scoperto questo bug, ho scritto questo exploit per esercitare le mie capacità di sviluppo degli exploit. La descrizione originale dell'exploit è qui di Zero Day Initiative. Sebbene questa descrizione sia molto buona e mi abbia aiutato a comprendere la vulnerabilità, è dal punto di vista di chi verifica la vulnerabilità. Ho scoperto che alcuni dettagli chiave mancano quando si cerca di progettare questo exploit da zero e spero di colmare alcune delle lacune che la descrizione ZDI ha tralasciato e acquisire competenze pratiche su come progettare un exploit complesso da zero.
Questi passi servono come schema per ottenere l'esecuzione di codice arbitrario all'interno di JavaScriptCore (JSC), il motore JavaScript di WebKit
La vulnerabilità che verrà sfruttata è un overflow di interi che si verifica nel codice prodotto dal compilatore just-in-time (JIT) DFG per WebKit. Questo si verifica specificamente nella funzione compileNewArrayWithSpread. Questa funzione verrà chiamata quando il codice che utilizza la sintassi spread di JavaScript per creare un nuovo array viene compilato JIT da DFG.

All'interno del codice JIT, prima verrà calcolata la dimensione dell'array. Lo fa aggiungendo la lunghezza di ogni argomento passato al costruttore dell'array. Mentre calcola la dimensione per ogni aggiunta, controlla un eventuale overflow della dimensione. Successivamente chiamerà la funzione compileAllocateNewArray passando la lunghezza calcolata in questa funzione.

La funzione compileAllocateNewArray passerà quindi la lunghezza calcolata in precedenza a emitAllocateButterfly.

La funzione emitAllocateButterfly sposterà quindi la dimensione di 3 bit a sinistra, il che equivale a moltiplicarla per 8. Tuttavia, non c'è alcun controllo per un overflow e quindi un numero come 0x20000001 può overfloware a 0x8
Questo programma C illustra questa vulnerabilità:


Possiamo usare questa vulnerabilità per ingannare il motore JavaScript facendogli credere di aver allocato un array con dimensione 0x20000001 mentre in realtà ha allocato spazio sufficiente solo per 1 JSValue (8 byte). Ciò risulterà in una primitiva di lettura e scrittura fuori dai limiti (OOB R/W) che potrà poi essere sfruttata per ottenere lettura/scrittura arbitraria e infine esecuzione di codice remota (RCE).
Per confermare che abbiamo una lettura OOB proveremo ad attivare questa vulnerabilità su una build di JSC con address sanitizer (ASAN).
Per farlo dalla directory WebKit possiamo eseguire i comandi:```bash Tools/Scripts/set-webkit-configuration --asan Tools/Scripts/build-jsc --jsc--only --debug
Ciò compilerà una build di debug di JSC con ASAN abilitato, permettendoci di verificare se abbiamo attivato con successo la vulnerabilità.
Ecco la prima iterazione di 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)
Quando eseguo questo, ottengo il seguente errore:
Program terminated with signal SIGKILL, Killed. The program no longer exists.
La mia ipotesi era che si stesse consumando troppa memoria nel tentativo di allocare un array così grande. Per confermarlo, ho aggiunto un breakpoint nel codice JITed aggiungendo una chiamata a m_jit.breakpoint() all'interno di compileNewArrayWithSpread, che inserisce un'istruzione int3 nel codice JITed.
Dopo aver aggiunto il breakpoint, ho scoperto che non veniva colpito, e quindi ho deciso di testare una lunghezza di 0x20001. Ho quindi realizzato che il codice non veniva nemmeno compilato, così ho aggiunto più iterazioni per attivare il compilatore 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)
Testare il programma così com'è porta comunque al SIGKILL, tuttavia, quando si testa con una lunghezza minore, il breakpoint viene raggiunto. A questo punto mi sembra ancora che JSC stia esaurendo la memoria nel tentativo di elaborare quell'enorme array.
Per gestire questo problema, ho deciso di allocare un array `a` più piccolo e quindi utilizzare la sintassi spread per usarlo più volte durante la creazione dell'array corrotto, ottenendo il seguente 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 questo codice siamo riusciti a raggiungere il breakpoint senza un SIGKILL! Come spesso accade, risolvere un problema ne fa emergere un altro e abbiamo ottenuto invece un SIGABORT... Usando il comando bt di gdb possiamo vedere che operationNewArrayWithSize è stato chiamato, il quale ha chiamato create.

Sembra strano che il nostro codice JITato stia chiamando operationNewArrayWithSize e deve essere che il codice JITato abbia dovuto prendere un percorso lento verso il motore JavaScript per qualche motivo.

Possiamo vedere in compileAllocateNewArrayWithSize che c'è effettivamente un bailout verso operationNewArrayWithSize. Dobbiamo quindi scoprire esattamente perché stiamo facendo bailout al caso lento.