
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.