
CVE-2018-4280: Vulnerabilità di sostituzione delle porte Mach in launchd su macOS 10.13.5 che porta a escalation dei privilegi locali e bypass di SIP.
launchd-portrep è un exploit per una vulnerabilità di sostituzione dei port in launchd, il processo iniziale nello spazio utente e demone di gestione dei servizi su macOS. Inviando un messaggio Mach appositamente costruito al bootstrap port, launchd può essere indotto a deallocare il suo send right per qualsiasi port Mach a cui l'attaccante ha anch'esso un send right. Ciò consente all'attaccante di impersonare qualsiasi servizio launchd che può cercare davanti al resto del sistema.
Launchd multiplexa diversi gestori di messaggi Mach sul suo port principale, incluso un gestore MIG per i messaggi di eccezione. Se un processo invia un messaggio mach_exception_raise o mach_exception_raise_state_identity al proprio bootstrap port, launchd riceverà e processerà quel messaggio come un'eccezione a livello di host.
Purtroppo, la gestione di questi messaggi da parte di launchd è difettosa. Se il tipo di eccezione è EXC_CRASH, allora launchd dealloca i port di thread e task inviati nel messaggio e poi restituisce KERN_FAILURE dalla routine di servizio, causando la deallocazione da parte del sistema MIG dei port di thread e task una seconda volta. (L'assunzione è che se una routine di servizio restituisce successo, allora ha preso possesso di tutte le risorse nel messaggio Mach, mentre se la routine di servizio restituisce un errore, allora non ha preso possesso di nessuna risorsa.)
Ecco il codice dalla routine di servizio di launchd per i messaggi mach_exception_raise, decompilato con IDA/Hex-Rays e leggermente modificato per migliorare la leggibilità:
kern_return_t __fastcall
catch_mach_exception_raise( // (a) La routine di servizio
mach_port_t exception_port, // viene chiamata con i valori
mach_port_t thread, // direttamente dal messaggio
mach_port_t task, // Mach inviato dal client.
unsigned int exception, // I port di thread e task
mach_exception_data_t code, // potrebbero essere send right
unsigned int codeCnt) // arbitrari.
{
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) Il port "thread" inviato
if ( kr ) // nel messaggio viene deallocato.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
kr = deallocate_mach_port(task); // (c) Il port "task" inviato nel
if ( kr ) // messaggio viene deallocato.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
result = 0;
if ( *__stack_chk_guard_ptr == __stack_guard )
{
LOBYTE(result) = exception == 10; // (d) Se il tipo di eccezione è 10
result *= 5; // (EXC_CRASH), allora viene
} // restituito un errore
return result; // KERN_FAILURE. MIG dealloca
} // i port di nuovo.
Questa doppia deallocazione dei nomi dei port è problematica perché un processo può impostare qualsiasi port desideri come port di task e thread nel messaggio di eccezione. Launchd non esegue alcun controllo che i send right ricevuti corrispondano effettivamente a un thread e un task; i port potrebbero, ad esempio, essere send right verso port già presenti nello spazio IPC di launchd. Quindi la doppia deallocazione farebbe effettivamente sì che launchd perda un riferimento utente su uno dei suoi port.
Questo bug può essere sfruttato per liberare il send right di launchd verso qualsiasi port Mach a cui il processo attaccante ha anch'esso un send right. In particolare, se il processo attaccante può cercare un servizio di sistema usando launchd, allora può liberare il send right di launchd verso quel servizio e quindi impersonare il servizio al resto del sistema. Dopo di che ci sono molte strade diverse per ottenere privilegi di sistema.
Questo bug è una versione meno generale del CVE-2016-7637, un problema di gestione dei riferimenti utente di Mach port in XNU scoperto da Ian Beer che permetteva ai processi di liberare port Mach in altri processi. Ian Beer ha sfruttato quella vulnerabilità su macOS sostituendo il send right di launchd verso l'endpoint com.apple.CoreServices.coreservicesd e impersonando coreservicesd al resto del sistema. Coreservicesd è un bersaglio interessante perché è uno dei pochi servizi a cui i client invieranno il loro task port in un messaggio Mach. Sostituendo il send right di launchd verso coreservicesd con il proprio port e quindi inducendo client privilegiati a cercare e comunicare con coreservicesd, è riuscito a ottenere il task port per un processo privilegiato e quindi eseguire codice all'interno di quel processo.
Poiché il comportamento su macOS non è cambiato, ho sostanzialmente copiato la strategia di exploit di Ian Beer per questa vulnerabilità. Inviamo messaggi di eccezione a launchd contenenti il port di servizio di coreservicesd finché non liberiamo il send right di launchd verso quel port. Possiamo rilevare quando abbiamo liberato il diritto chiamando di nuovo bootstrap_look_up() sul servizio: se launchd restituisce un nome di port non valido, allora abbiamo liberato con successo il send right di launchd verso il port. Quindi, registriamo e annulliamo ripetutamente la registrazione di un gran numero di servizi con launchd finché uno dei servizi che registriamo non viene assegnato allo stesso nome di port Mach nello spazio IPC di launchd del port originale di coreservicesd. A questo punto, qualsiasi processo che cerca com.apple.CoreServices.coreservicesd in launchd riceverà un send right verso il nostro servizio falso invece che verso il vero coreservicesd. Poi eseguiamo un server MITM sul port di servizio falso, ispezionando tutti i port Mach nei messaggi ricevuti dai client prima di inviarli al vero coreservicesd. A questo punto inviamo un messaggio a sysdiagnose che lo fa eseguire un tailspin, il che fa sì che sysdiagnose si connetta al nostro port falso di coreservicesd e ci invii il suo task port. Poiché sysdiagnose ha l'entitlement task_for_pid-allow, possiamo ora ottenere il task port per qualsiasi processo.