
CVE-2018-4280: vulnerabilità di sostituzione delle porte Mach in launchd su iOS 11.2.6 che porta a fuga dalla sandbox, escalation dei privilegi e bypass della firma del codice.
Blanket è una sandbox escape che prende di mira iOS 11.2.6, anche se la vulnerabilità principale è stata corretta solo in iOS 11.4.1. Sfrutta una vulnerabilità di sostituzione di Mach port in launchd (CVE-2018-4280), oltre a diverse vulnerabilità minori in altri servizi, per eseguire codice all'interno del processo ReportCrash, che non è in sandbox, viene eseguito come root e possiede l' entitlement task_for_pid-allow. Questo garantisce a blanket il controllo su ogni processo in esecuzione sul telefono, inclusi quelli critici per la sicurezza come amfid.
L'exploit è composto da diverse fasi. Questo README spiegherà la vulnerabilità principale e le fasi della sandbox escape passo dopo passo.
Durante la ricerca sul crash reporting su iOS, ho scoperto una vulnerabilità di sostituzione di Mach port in launchd. Crashing in un modo particolare, un processo può far sì che il kernel invii un messaggio Mach a launchd che causa a launchd di deallocare eccessivamente un send right per una Mach port nel suo namespace IPC. Questo consente a un utente malintenzionato di impersonare qualsiasi servizio launchd che possa cercare verso il resto del sistema, aprendo numerose strade per l'escalation dei privilegi.
Questa vulnerabilità è presente anche su macOS, ma innescarla su iOS è più difficile a causa dei controlli in launchd che garantiscono che il messaggio di eccezione Mach provenga dal kernel.
Launchd multiplexa diversi gestori di messaggi Mach sulla sua porta 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 alla propria 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 è buggata. Se il tipo di eccezione è EXC_CRASH, allora launchd dealloca le thread e task port inviate nel messaggio e poi restituisce KERN_FAILURE dalla service routine, causando al sistema MIG di deallocare nuovamente le thread e task port. (L'assunto è che se una service routine restituisce successo, allora ha preso possesso di tutte le risorse nel messaggio Mach, mentre se la service routine restituisce un errore, allora non ha preso possesso di nessuna risorsa.)
Ecco il codice della service routine di launchd per i messaggi mach_exception_raise, decompilato usando IDA/Hex-Rays e leggermente modificato per leggibilità:```C
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
exception_type_t exception, // thread and task ports could
mach_exception_data_t code, // be arbitrary send rights.
mach_msg_type_number_t codeCnt)
{
__int64 __stack_guard; // ST28_8@1
kern_return_t kr; // w0@1 MAPDST
kern_return_t result; // w0@4
__int64 codes_left; // x25@6
mach_exception_data_type_t code_value; // t1@7
int pid; // [xsp+34h] [xbp-44Ch]@1
char codes_str[1024]; // [xsp+38h] [xbp-448h]@7
__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 ( current_audit_token.val[5] ) // (b) If the message was sent by
{ // a process with a nonzero PID
result = KERN_FAILURE; // (any non-kernel process),
} // the message is rejected.
else
{
if ( codeCnt )
{
codes_left = codeCnt;
do
{
code_value = *code;
++code;
__snprintf_chk(codes_str, 0x400uLL, 0, 0x400uLL, "0x%llx", code_value);
--codes_left;
}
while ( codes_left );
}
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_port(thread); // (c) The "thread" port sent in
if ( kr ) // the message is deallocated.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
kr = deallocate_port(task); // (d) The "task" port sent in the
if ( kr ) // message is deallocated.
{
_os_assumes_log(kr);
_os_avoid_tail_call();
}
if ( exception == EXC_CRASH ) // (e) If the exception type is
result = KERN_FAILURE; // EXC_CRASH, then KERN_FAILURE
else // is returned. MIG will
result = 0; // deallocate the ports again.
}
*__stack_chk_guard_ptr;
return result;
}
Ecco cosa fa il codice:
1. Questa funzione è la routine di servizio Mach per i messaggi di eccezione `mach_exception_raise`: viene invocata direttamente dal sistema Mach quando launchd elabora un messaggio di eccezione Mach `mach_exception_raise`. Gli argomenti della routine di servizio vengono parsificati dal messaggio Mach e quindi sono controllati dal mittente del messaggio.
2. In (b), launchd verifica che il messaggio di eccezione Mach sia stato inviato dal kernel. Il token di audit del mittente contiene il PID del processo mittente nel campo 5, che sarà zero solo per il kernel. Se il messaggio non è stato inviato dal kernel, viene respinto.
3. I port di thread e task del messaggio vengono esplicitamente deallocati in (c) e (d).
4. In (e), launchd controlla se il tipo di eccezione è `EXC_CRASH` e restituisce `KERN_FAILURE` in caso affermativo. L'intento è di non gestire i messaggi `EXC_CRASH`, presumibilmente in modo che ReportCrash venga invocato come gestore del corpse. Tuttavia, restituire `KERN_FAILURE` a questo punto causerà la deallocazione dei port di task e thread quando il messaggio di eccezione verrà pulito successivamente. Ciò significa che quei due port verranno sovra-deallocati.