
Ausnutzen einer gepatchten Schwachstelle in JavaScriptCore
Dies ist ein Exploit für eine WebKit-Schwachstelle, die ursprünglich von Fluoroacetate während des pwn2own-Wettbewerbs in Vancouver entdeckt wurde. Obwohl ich diesen Fehler nicht entdeckt habe, habe ich diesen Exploit geschrieben, um meine Exploit-Entwicklungsfähigkeiten zu üben. Der ursprüngliche Writeup für diesen Exploit befindet sich hier von der Zero Day Initiative. Dieser Writeup ist sehr gut und hat mir wesentlich geholfen, die Schwachstelle zu verstehen, aber er ist aus der Sicht einer Person geschrieben, die die Schwachstelle verifiziert. Ich stellte fest, dass einige wichtige Details fehlen, wenn man versucht, diesen Exploit von Grund auf zu entwickeln, und ich hoffe, einige der Lücken zu füllen, die der ZDI-Writeup übersehen hat, und praktische Fähigkeiten zu erlangen, wie man einen komplizierten Exploit von Grund auf entwickelt.
Diese Schritte dienen als Überblick, um eine beliebige Codeausführung innerhalb von JavaScriptCore (JSC), der JavaScript-Engine von WebKit, zu erreichen.
leakAddr und fakeObj erhaltenDie ausgenutzte Schwachstelle ist ein Integer-Überlauf, der im Code auftritt, der vom DFG-Just-in-Time (JIT)-Compiler für WebKit erzeugt wird. Dies geschieht speziell in der Funktion compileNewArrayWithSpread. Diese Funktion wird aufgerufen, wenn Code, der die JavaScript Spread-Syntax verwendet, um ein neues Array zu erstellen, von DFG JIT-kompiliert wird.

Im JIT-kompilierten Code wird zuerst die Größe des Arrays berechnet. Dies geschieht durch Addition der Länge jedes an den Array-Konstruktor übergebenen Arguments. Bei jeder Addition wird auf einen Überlauf der Größe geprüft. Danach wird die Funktion compileAllocateNewArray aufgerufen, wobei die in dieser Funktion berechnete Länge übergeben wird.

compileAllocateNewArray übergibt dann die zuvor berechnete Länge an emitAllocateButterfly.

emitAllocateButterfly verschiebt dann die Größe um 3 Bits nach links, was einer Multiplikation mit 8 entspricht. Es gibt jedoch keine Prüfung auf einen Überlauf, sodass eine Zahl wie 0x20000001 auf 0x8 überlaufen kann.
Dieses C-Programm veranschaulicht diese Schwachstelle:


Wir können diese Schwachstelle nutzen, um die JavaScript-Engine dazu zu bringen, zu glauben, wir hätten ein Array mit der Größe 0x20000001 allokiert, obwohl wir tatsächlich nur genug Platz für einen JSValue (8 Bytes) allokiert haben. Dies führt zu einem Lese- und Schreibprimitive außerhalb der Grenzen (OOB), das dann genutzt werden kann, um beliebiges Lesen/Schreiben und schließlich entfernte Codeausführung (RCE) zu erreichen.
Um zu bestätigen, dass wir einen OOB-Zugriff haben, werden wir versuchen, diese Schwachstelle auf einem AddressSanitizer (ASAN)-Build von JSC auszulösen.
Dazu können wir im WebKit-Verzeichnis die folgenden Befehle ausführen:```bash Tools/Scripts/set-webkit-configuration --asan Tools/Scripts/build-jsc --jsc--only --debug
Dies wird einen Debug-Build von JSC mit aktiviertem ASAN erstellen, der es uns ermöglicht zu überprüfen, ob wir die Schwachstelle erfolgreich ausgelöst haben oder nicht.
Hier ist die erste Iteration von 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)
When running this I get the following error:
Program terminated with signal SIGKILL, Killed. The program no longer exists.
My guess was that too much memory was being consumed when trying to allocate such a large array. In order to confirm this, I added a breakpoint to the JITed code by adding a call to m_jit.breakpoint() inside of the compileNewArrayWithSpread which adds an int3 instruction to the JITed code.
After adding the breakpoint I found that it wasn't hit and then I decided to test a length of 0x20001. I then realized that the code wasn't even being compiled so I added more iterations to activate the DFG compiler```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)
Das Testen des Programms wie es ist führt immer noch zum SIGKILL, jedoch wird beim Testen mit einer kleineren Länge der Haltepunkt erreicht. An diesem Punkt scheint es mir immer noch, dass JSC nicht genügend Speicher hat, um dieses riesige Array zu verarbeiten.
Um damit umzugehen, habe ich beschlossen, ein kleineres `a`-Array zu allozieren und dann die Spread-Syntax zu verwenden, um es mehrmals beim Erstellen des korrupten Arrays zu nutzen, was zu folgendem exploit.js führt.```
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)
Mit diesem Code konnten wir den Breakpoint erreichen, ohne einen SIGKILL zu erhalten! Wie so oft bringt die Behebung eines Problems ein weiteres hervor, und stattdessen erhielten wir einen SIGABORT... Mit dem gdb-Befehl bt können wir sehen, dass operationNewArrayWithSize aufgerufen wurde, was create aufrief.
Es scheint seltsam, dass unser JIT-kompilierter Code operationNewArrayWithSize aufruft, und es muss daran liegen, dass der JIT-Code aus irgendeinem Grund einen langsamen Pfad zur JavaScript-Engine nehmen musste.

Wir können in compileAllocateNewArrayWithSize sehen, dass es tatsächlich einen Bailout zu operationNewArrayWithSize gibt. Wir müssen dann herausfinden, warum wir genau zu dem langsamen Fall ausweichen.