
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.
Affinché questa vulnerabilità sia utile, vorremo liberare il diritto di invio di launchd verso un servizio Mach che esso fornisce, in modo da poter poi impersonare quel servizio per il resto del sistema. Ciò significa che avremo bisogno che i port di task e thread nel messaggio di eccezione siano effettivamente diritti di invio verso il port del servizio Mach che vogliamo liberare in launchd. Poi, una volta inviato a launchd il messaggio di eccezione malevolo e liberato il port del servizio, cercheremo di far riutilizzare lo stesso nome di port, ma questa volta per un port Mach di cui possediamo il diritto di ricezione. In questo modo, quando un client chiede a launchd di dargli un diritto di invio verso il port Mach per il servizio, launchd gli darà invece un diritto di invio verso il nostro port, permettendoci di impersonare quel servizio verso il client. Dopo di ciò, ci sono molte strade diverse per ottenere privilegi di sistema.
### Attivazione della vulnerabilità
Per attivare effettivamente la vulnerabilità, dovremo bypassare il controllo che il messaggio sia stato inviato dal kernel. Questo perché se inviamo il messaggio di eccezione direttamente a launchd, verrà semplicemente scartato. In qualche modo, dobbiamo far sì che il kernel invii un messaggio di eccezione "malevolo" contenente un diritto di invio Mach per un servizio di sistema al posto dei veri port di thread e task.
Si scopre che esiste un trap Mach, `task_set_special_port`, che può essere utilizzato per impostare un diritto di invio personalizzato da usare al posto del vero port del task in determinate situazioni. Una di queste situazioni è quando il kernel genera un messaggio di eccezione per conto di un task: invece di inserire il vero diritto di invio del task nel messaggio di eccezione, il kernel userà il diritto di invio fornito da `task_set_special_port`. Più specificamente, se un task chiama `task_set_special_port` per impostare un valore personalizzato per il suo port speciale `TASK_KERNEL_PORT` e poi il task crasha, il messaggio di eccezione generato dal kernel avrà un diritto di invio verso il port personalizzato, non il vero port del task, nel campo "task". Un'API equivalente, `thread_set_special_port`, può essere utilizzata per impostare un port personalizzato nel campo "thread" del messaggio di eccezione generato.
A causa di questo comportamento, in realtà non è affatto difficile far generare al kernel un messaggio di eccezione "malevolo" contenente un port di servizio Mach al posto del port di task e thread. Tuttavia, dobbiamo ancora assicurarci che il messaggio di eccezione che generiamo venga consegnato a launchd.
Ancora una volta, assicurarsi che il kernel consegni il messaggio di eccezione "malevolo" a launchd non è difficile se si conosce l'API giusta. La funzione `thread_set_exception_ports` imposterà qualsiasi diritto di invio Mach come port a cui vengono consegnati i messaggi di eccezione su questo thread. Quindi, tutto ciò che dobbiamo fare è invocare `thread_set_exception_ports` con il bootstrap port, e qualsiasi eccezione che genereremo farà sì che il kernel invii un messaggio di eccezione a launchd.
L'ultimo pezzo del puzzle è ottenere il giusto tipo di eccezione. La vulnerabilità verrà attivata solo per eccezioni `EXC_CRASH`. Un po' di tentativi ed errori rivela che possiamo facilmente generare eccezioni `EXC_CRASH` chiamando la funzione standard `abort`.
Quindi, in sintesi, possiamo utilizzare API esistenti e ben documentate per far generare al kernel un messaggio di eccezione `EXC_CRASH` malevolo per nostro conto e consegnarlo a launchd, attivando la vulnerabilità e liberando il port del servizio Mach:
1. Usa `thread_set_exception_ports` per impostare launchd come gestore delle eccezioni per questo thread.
2. Chiama `bootstrap_look_up` per ottenere da launchd il port del servizio che vogliamo impersonare.
3. Chiama `task_set_special_port`/`thread_set_special_port` per utilizzare quel port di servizio al posto dei veri port di task e thread nei messaggi di eccezione.
4. Chiama `abort`. Il kernel invierà un messaggio di eccezione `EXC_CRASH` a launchd, ma i port di task e thread nel messaggio saranno il port del servizio target.
5. Launchd elaborerà il messaggio di eccezione e libererà il port del servizio.
### Esecuzione del codice dopo il crash
C'è un problema con la strategia sopra: chiamare `abort` ucciderà il nostro processo. Se vogliamo essere in grado di eseguire codice dopo aver attivato la vulnerabilità, abbiamo bisogno di un modo per eseguire il crash in un altro processo.
(Con altri tipi di eccezione un processo potrebbe effettivamente riprendersi dall'eccezione. Il modo in cui un processo si riprende è impostare il suo gestore di eccezioni di thread su launchd e il suo gestore di eccezioni di task su se stesso. Dopo che launchd elabora e non riesce a gestire l'eccezione, il kernel invierebbe l'eccezione al gestore del task, che ripristinerebbe lo stato del thread e informerebbe il kernel che l'eccezione è stata gestita. Tuttavia, un processo non può catturare le proprie eccezioni `EXC_CRASH`, quindi abbiamo bisogno di due processi.)
Una strategia è sfruttare prima una vulnerabilità in un altro processo su iOS e forzare quel processo a impostare i suoi port del kernel e crashare. Tuttavia, per un proof-of-concept, è più semplice creare un'app extension.
Le app extension, introdotte in iOS 8, forniscono un modo per impacchettare alcune funzionalità di un'applicazione in modo che siano disponibili al di fuori dell'applicazione. Il codice di un'app extension viene eseguito in un processo separato e in sandbox. Questo rende molto facile lanciare un processo che imposterà i suoi port speciali, registrerà launchd come suo gestore di eccezioni per `EXC_CRASH`, e poi chiamerà `abort`.
Non esiste un modo supportato per un'app di lanciare programmaticamente la propria app extension e comunicare con essa. Tuttavia, Ian McDowell ha scritto un [ottimo articolo][Multi-Process iOS App Using NSExtension] che descrive come utilizzare l'API privata `NSExtension` per lanciare e comunicare con un processo di app extension. Ho utilizzato una strategia quasi identica qui. L'unica differenza è che abbiamo bisogno di comunicare un port Mach al processo dell'app extension, il che comporta la registrazione di un servizio fittizio con launchd a cui l'app extension si connette.
[Multi-Process iOS App Using NSExtension]: https://ianmcdowell.net/blog/nsextension/
### Prevenire il riutilizzo del port in launchd
Una sfida che noteresti se eseguisse l'exploit come descritto è che occasionalmente non saresti in grado di riacquisire il port liberato. Il motivo è che il kernel tiene traccia delle voci IPC libere di un processo in una freelist, quindi un nome di port appena liberato verrà riutilizzato (con un numero di generazione diverso) quando un nuovo port viene allocato nella tabella IPC. Quindi, riallocherai il nome di port che desideri solo se launchd non riutilizza prima quella voce di slot IPC per un altro port.
Il modo per aggirare questo problema è seppellire lo slot IPC libero in fondo alla freelist, in modo che se launchd alloca nuovi port, quegli altri slot vengano usati per primi. Come facciamo? Possiamo registrare un mucchio di servizi Mach fittizi in launchd con port di cui possediamo il diritto di ricezione. Quando chiamiamo `abort`, il gestore di eccezioni scatterà per primo, e poi lo stato del processo, inclusi i port Mach, verrà ripulito. Quando launchd riceve l'eccezione `EXC_CRASH`, libererà involontariamente il port del servizio target, posizionando lo slot IPC corrispondente a quel nome di port in cima alla freelist. Poi, quando il resto dei port Mach della nostra app extension vengono distrutti, launchd riceverà notifiche e libererà i port dei servizi fittizi, seppellendo lo slot IPC target dietro gli slot dei port appena liberati. Quindi, fintanto che launchd alloca meno port del numero di servizi fittizi che abbiamo registrato, lo slot target sarà ancora nella freelist, il che significa che possiamo ancora far sì che launchd riallochi lo slot con lo stesso nome di port del servizio originale.
Il limite di questa strategia è che abbiamo bisogno dell'entitlement `com.apple.security.application-groups` per registrare servizi con launchd. Ci sono altri modi per nascondere port Mach in launchd, ma usare i gruppi di applicazioni è sicuramente il più semplice, e sufficiente per questo proof-of-concept.
### Impersonare il servizio liberato
Una volta che abbiamo generato l'app extension crashante e liberato un diritto di invio Mach in launchd, dobbiamo riallocare quel nome di port Mach con un diritto di invio di cui possediamo il diritto di ricezione. In questo modo, tutti i messaggi che launchd invia a quel nome di port verranno ricevuti da noi, e ogni volta che launchd condivide quel nome di port con un client, il client riceverà un diritto di invio verso il nostro port. In particolare, se riusciamo a liberare il diritto di invio di launchd verso un servizio Mach, allora qualsiasi processo che richiede quel servizio da launchd riceverà un diritto di invio verso il nostro port invece del vero port del servizio. Questo ci permette di impersonare il servizio o di effettuare un attacco man-in-the-middle, ispezionando tutti i messaggi che il client invia al servizio.
Ottenere il riutilizzo del nome di port liberato in modo che si riferisca a un port di nostra proprietà è anche abbastanza semplice, dato che abbiamo già deciso di utilizzare l'entitlement dei gruppi di applicazioni: basta registrare servizi Mach fittizi con launchd finché uno di essi non riutilizza il nome di port originale. Dovremo farlo in lotti, registrando un gran numero di servizi fittizi insieme, controllando se qualcuno ha riutilizzato con successo il nome di port liberato, e poi deregistrandoli. Il motivo è che dobbiamo essere sicuri che le nostre registrazioni vadano fino in fondo nella freelist dei port IPC per recuperare il nome di port sepolto che vogliamo.
Possiamo verificare se siamo riusciti a riutilizzare con successo il nome di port liberato cercando il servizio originale con `bootstrap_look_up`: se restituisce uno dei nostri port di servizio registrati, abbiamo finito.
Una volta che siamo riusciti a registrare un nuovo servizio che ottiene lo stesso nome di port dell'originale, qualsiasi client che cerca il servizio originale in launchd riceverà un diritto di invio verso il nostro port, non il vero port del servizio. Quindi, stiamo effettivamente impersonando il servizio originale per il resto del sistema (o almeno per quei processi che cercano il servizio dopo il nostro attacco).
Fase 1: Ottenere il port host-priv
---------------------------------------------------------------------------------------------------
Una volta che abbiamo la capacità di impersonare servizi di sistema arbitrari, il passo successivo è ottenere il port host-priv. Questo passaggio è semplice e non è influenzato dalle modifiche in iOS 11.3. L'idea generale di questo attacco è impersonare SafetyNet, crashare ReportCrash, e poi recuperare il port host-priv dal port del task di ReportCrash morente inviato nel messaggio di eccezione.
### Informazioni su ReportCrash e SafetyNet
ReportCrash è responsabile della generazione dei rapporti di crash su iOS. Questo singolo binario fornisce in realtà 4 servizi diversi (ciascuno in un processo diverso, anche se potrebbero non essere tutti in esecuzione in un dato momento):
1. `com.apple.ReportCrash` è responsabile della generazione di rapporti di crash per i processi che crashano. È il gestore di eccezioni a livello di host per le eccezioni `EXC_CRASH`, `EXC_GUARD` e `EXC_RESOURCE`.
2. `com.apple.ReportCrash.Jetsam` gestisce i rapporti Jetsam.
3. `com.apple.ReportCrash.SimulateCrash` crea rapporti per crash simulati.
4. `com.apple.ReportCrash.SafetyNet` è il gestore di eccezioni registrato per il servizio `com.apple.ReportCrash`.
Quelli di nostro interesse sono `com.apple.ReportCrash` e `com.apple.ReportCrash.SafetyNet`, d'ora in poi denominati semplicemente ReportCrash e SafetyNet. Entrambi sono servizi basati su MIG e eseguono essenzialmente lo stesso codice.
Quando ReportCrash si avvia, cerca il servizio SafetyNet in launchd e imposta il port restituito come gestore di eccezioni a livello di task. L'intento sembra essere che se ReportCrash stesso dovesse crashare, un processo separato genererebbe il rapporto di crash per esso. Tuttavia, questo percorso di codice sembra inutilizzato: ReportCrash registra SafetyNet per i messaggi `mach_exception_raise`, nonostante sia ReportCrash che SafetyNet gestiscano solo messaggi `mach_exception_raise_state_identity`. Ciononostante, entrambi i servizi sono ancora presenti e raggiungibili dall'interno del container sandbox di iOS.
### Primitiva di manipolazione di ReportCrash
Per eseguire l'attacco seguente, dobbiamo essere in grado di manipolare ReportCrash (o SafetyNet) per comportarsi come vogliamo. Nello specifico, abbiamo bisogno delle seguenti capacità: avviare ReportCrash su richiesta, forzare ReportCrash a uscire, crashare ReportCrash e assicurarci che ReportCrash non esca mentre lo stiamo usando. Qui descriverò come raggiungiamo ciascun obiettivo.
Per avviare ReportCrash, dobbiamo semplicemente inviargli un messaggio Mach: launchd lo avvierà su richiesta. Tuttavia, a causa del suo design peculiare, qualsiasi tipo di messaggio tranne `mach_exception_raise_state_identity` farà sì che ReportCrash smetta di rispondere a nuovi messaggi e alla fine esca. Quindi, dobbiamo inviare un messaggio `mach_exception_raise_state_identity` se vogliamo che rimanga vivo in seguito.
Per far uscire ReportCrash, possiamo semplicemente inviargli qualsiasi altro tipo di messaggio Mach.
Ci sono molti modi per crashare ReportCrash. Il più semplice è probabilmente inviare un messaggio `mach_exception_raise_state_identity` con il port del thread impostato a `MACH_PORT_NULL`.
Infine, dobbiamo assicurarci che ReportCrash non esca mentre lo stiamo usando. Ogni messaggio `mach_exception_raise_state_identity` che elabora fa sì che generi un altro thread per ascoltare il prossimo messaggio mentre il thread originale genera il rapporto di crash. ReportCrash uscirà una volta che tutti i thread in sospeso che generano un rapporto di crash saranno terminati. Quindi, se riusciamo a bloccare uno di quei thread mentre è nel processo di generazione di un rapporto di crash, possiamo impedirgli di uscire.
Il modo più semplice che ho trovato per farlo è stato inviare un messaggio `mach_exception_raise_state_identity` con un port personalizzato nei campi del task e del thread. Una volta che ReportCrash tenta di generare un rapporto di crash, chiamerà `task_policy_get` sul port "task", il che gli farà inviare un messaggio Mach al port che abbiamo inviato e attendere una risposta. Ma poiché il port "task" è solo un normale vecchio port Mach, possiamo semplicemente non rispondere al messaggio Mach, e ReportCrash attenderà indefinitamente che `task_policy_get` ritorni.
### Estrazione di host-priv da ReportCrash
Per la prima fase dell'exploit, il piano di attacco è relativamente semplice:
1. Avvia il servizio SafetyNet e forzalo a rimanere attivo per la durata del nostro attacco.
2. Usa la primitiva di impersonificazione del servizio launchd per impersonare SafetyNet. Questo ci dà un nuovo port su cui possiamo ricevere messaggi destinati al vero servizio SafetyNet.
3. Fai uscire qualsiasi istanza esistente di ReportCrash. In questo modo, possiamo assicurarci che ReportCrash cerchi il nostro port SafetyNet nel passaggio successivo.
4. Avvia ReportCrash. ReportCrash cercherà SafetyNet in launchd e imposterà il port risultante, che è il falso port SafetyNet di cui possediamo il diritto di ricezione, come destinazione per i messaggi `EXC_CRASH`.
5. Innesca un crash in ReportCrash. Dopo aver constatato che non ci sono gestori registrati per il tipo di eccezione originale, ReportCrash entrerà nella fase di morte del processo. A questo punto XNU vedrà che ReportCrash ha registrato il falso port SafetyNet per ricevere eccezioni `EXC_CRASH`, quindi genererà un messaggio di eccezione e lo invierà a quel port.
6. Quindi ascoltiamo sul falso port SafetyNet per il messaggio `EXC_CRASH`. Sarà di tipo `mach_exception_raise`, il che significa che conterrà il port del task di ReportCrash.
7. Infine, usiamo `task_get_special_port` sul port del task di ReportCrash per ottenere il port host di ReportCrash. Poiché ReportCrash è senza sandbox e viene eseguito come root, questo è il port host-priv.
Alla fine di questa fase dell'escape dalla sandbox, otteniamo un port host-priv utilizzabile. Questo da solo dimostra che si tratta di un grave problema di sicurezza.
Fase 2: Uscire dalla sandbox
---------------------------------------------------------------------------------------------------
Anche se abbiamo il port host-priv, il nostro obiettivo è uscire completamente dalla sandbox ed eseguire codice come root con l'entitlement `task_for_pid-allow`. Il primo passo per raggiungerlo è semplicemente uscire dalla sandbox.
Tecnicamente parlando, non c'è motivo di dover ottenere il port host-priv prima di uscire dalla sandbox: questi due passaggi sono indipendenti e possono avvenire in qualsiasi ordine. Tuttavia, questa fase lascerà il sistema instabile se essa o le fasi successive falliscono, quindi vale la pena metterla dopo.
L'attacco generale è utilizzare di nuovo la stessa vulnerabilità di launchd per impersonare un servizio di sistema. Tuttavia, questa volta il nostro obiettivo è impersonare un servizio a cui un client invierà il suo port del task in un messaggio Mach. È facile scoprire per esperimento su iOS 11.2.6 che se impersoniamo `com.apple.CARenderServer` (d'ora in poi CARenderServer) ospitato da backboardd e poi comunichiamo con `com.apple.DragUI.druid.source`, il demone druid senza sandbox invierà il suo port del task in un messaggio Mach al falso port del servizio.
Questo passaggio dell'exploit è rotto su iOS 11.3 perché druid non invia più il suo port del task nel messaggio Mach a CARenderServer. Nonostante ciò, sono fiducioso che questa vulnerabilità possa ancora essere utilizzata per uscire dalla sandbox. Un modo per affrontare questo problema è cercare servizi senza sandbox che si fidano dell'input di altri servizi. Questi tipi di "vulnerabilità" non sarebbero mai sfruttabili senza la capacità di sostituire i servizi di sistema, il che significa che sono probabilmente una superficie di attacco a bassa priorità, sia internamente che esternamente ad Apple.
### Crash di druid
Proprio come con ReportCrash, abbiamo bisogno di essere in grado di forzare il riavvio di druid nel caso sia già in esecuzione in modo che cerchi il nostro falso port CARenderServer in launchd. Ho deciso di utilizzare un bug in libxpc che era già programmato per essere risolto per questo scopo.
Esaminando libxpc, ho trovato una lettura out-of-bounds che potrebbe essere utilizzata per forzare il crash di qualsiasi servizio XPC:```C
void _xpc_dictionary_apply_wire_f
(
OS_xpc_dictionary *xdict,
OS_xpc_serializer *xserializer,
const void *context,
bool (*applier_fn)(const char *, OS_xpc_serializer *, const void *)
)
{
...
uint64_t count = (unsigned int)*serialized_dict_count;
if ( count )
{
uint64_t depth = xserializer->depth;
uint64_t index = 0;
do
{
const char *key = _xpc_serializer_read(xserializer, 0, 0, 0);
size_t keylen = strlen(key);
_xpc_serializer_advance(xserializer, keylen + 1);
if ( !applier_fn(key, xserializer, context) )
break;
xserializer->depth = depth;
++index;
}
while ( index < count );
}
...
}
Il problema è che l'uso di un strlen non controllato su dati controllati dall'attaccante permette alla chiave per la voce del dizionario serializzato di estendersi oltre la fine del buffer di dati. Ciò significa che il servizio XPC che deserializza il dizionario andrà in crash, sia quando strlen dereferenzia memoria fuori dai limiti, sia quando _xpc_serializer_advance cerca di far avanzare il serializzatore oltre la fine dei dati forniti.
Questo bug era già stato corretto in iOS 11.3 Beta quando l'ho scoperto, quindi non l'ho segnalato ad Apple. L'exploit è disponibile come progetto indipendente nel mio repository xpc-crash.
Per utilizzare questo bug per mandare in crash druid, abbiamo semplicemente bisogno di inviare al servizio druid un messaggio XPC malformato tale per cui la chiave del dizionario non sia terminata e si estenda fino all'ultimo byte del messaggio.
Ottenere il task port di druid su iOS 11.2.6 utilizzando la nostra primitiva di impersonificazione del servizio è facile:
Una volta ottenuto il task port di druid, dobbiamo ancora capire come eseguire codice all'interno del processo druid.
Il problema è che XNU protegge i task port per i binari di piattaforma dall'essere modificati da binari non di piattaforma. La difesa è implementata nella funzione task_conversion_eval, che viene chiamata da convert_port_to_locked_task e convert_port_to_task_with_exec_token:```C
kern_return_t
task_conversion_eval(task_t caller, task_t victim)
{
/*
* Tasks are allowed to resolve their own task ports, and the kernel is
* allowed to resolve anyone's task port.
*/
if (caller == kernel_task) {
return KERN_SUCCESS;
}
if (caller == victim) {
return KERN_SUCCESS;
}
/*
* Only the kernel can can resolve the kernel's task port. We've established
* by this point that the caller is not kernel_task.
*/
if (victim == kernel_task) {
return KERN_INVALID_SECURITY;
}
#if CONFIG_EMBEDDED /* * On embedded platforms, only a platform binary can resolve the task port * of another platform binary. / if ((victim->t_flags & TF_PLATFORM) && !(caller->t_flags & TF_PLATFORM)) { #if SECURE_KERNEL return KERN_INVALID_SECURITY; #else if (cs_relax_platform_task_ports) { return KERN_SUCCESS; } else { return KERN_INVALID_SECURITY; } #endif / SECURE_KERNEL / } #endif / CONFIG_EMBEDDED */
return KERN_SUCCESS;
}
Le routine di conversione MIG che si basano su queste funzioni, incluse `convert_port_to_task` e
`convert_port_to_map`, falliranno quindi quando le chiameremo sul task di druid. Ad esempio,
`mach_vm_write` non ci permetterà di manipolare la memoria di druid.
Tuttavia, guardando il file MIG `osfmk/mach/task.defs` in XNU, ho notato qualcosa
di interessante:```C
/*
* Returns the set of threads belonging to the target task.
*/
routine task_threads(
target_task : task_inspect_t;
out act_list : thread_act_array_t);
La funzione task_threads, che enumera i thread in un task, accetta in realtà un
task_inspect_t invece di un task_t, il che significa che MIG lo converte usando
convert_port_to_task_inspect piuttosto che convert_port_to_task. Un rapido sguardo a
convert_port_to_task_inspect rivela che questa funzione non esegue il controllo
task_conversion_eval, quindi possiamo chiamarla con successo su binarî di piattaforma. Questo è
interessante perché i thread restituiti non sono diritti thread_inspect_t, bensì diritti completi
thread_act_t. In altre parole, task_threads promuove un diritto di task non modificabile in
diritti di thread modificabili. E poiché non esiste un equivalente thread_conversion_eval, ciò significa che
possiamo usare le API Mach dei thread per modificare i thread in un task anche se quel task è un binario
di piattaforma.
Per sfruttare questa possibilità, ho scritto una libreria chiamata threadexec che costruisce una capacità di chiamata di funzioni completa sopra l'API Mach dei thread. Il progetto threadexec in sé è stato un'impresa significativa, ma poiché è solo indirettamente rilevante per questo exploit, ometterò una spiegazione dettagliata del suo funzionamento interno.
Una volta che abbiamo il port host-priv e l'esecuzione di codice non sandboxata all'interno di druid, la fase successiva della fuga dalla sandbox completa è installare un nuovo gestore di eccezioni a livello di host. Questo processo è semplice date le nostre attuali capacità:
EXC_BAD_ACCESS chiamando
host_get_exception_ports.EXC_BAD_ACCESS.host_set_exception_ports per registrare il nostro
port Mach come gestore di eccezioni a livello di host per EXC_BAD_ACCESS.Dopo questa fase, ogni volta che un processo accede a un indirizzo di memoria non valido (e inoltre non ha un
gestore di eccezioni registrato), un messaggio di eccezione EXC_BAD_ACCESS verrà inviato al nostro nuovo
port del gestore di eccezioni. Questo ci darà il port del task di qualsiasi processo che si blocca, e poiché
EXC_BAD_ACCESS è un'eccezione recuperabile, questa volta possiamo usare il port del task per eseguire codice.
La fase successiva è innescare un'eccezione EXC_BAD_ACCESS in ReportCrash in modo che il suo port del task
venga inviato in un messaggio di eccezione al nostro nuovo port del gestore di eccezioni:
EXC_BAD_ACCESS. Poiché ReportCrash non ha un gestore di eccezioni registrato
per EXC_BAD_ACCESS (ricorda che SafetyNet è registrato per EXC_CRASH), l'eccezione verrà
consegnata al gestore di eccezioni a livello di host.KERN_SUCCESS per indicare al kernel che l'eccezione è stata
gestita e ReportCrash può essere ripreso.A questo punto, abbiamo l'esecuzione di codice all'interno di un processo non sandboxato, root, con task_for_pid-allow.
Le due fasi successive non sono strettamente necessarie ma dovrebbero comunque essere eseguite.
Una volta che abbiamo l'esecuzione di codice all'interno di ReportCrash, dovremmo reimpostare il gestore di eccezioni a livello di host
per EXC_BAD_ACCESS usando druid:
host_set_exception_ports in druid per ri-registrare il vecchio gestore di eccezioni a livello di host per
EXC_BAD_ACCESS.Questo fermerà il nostro port del gestore di eccezioni dal ricevere messaggi di eccezione per altri processi che crashano.
L'ultimo passo è riparare il danno che abbiamo fatto a launchd quando abbiamo liberato i port dei servizi nel suo spazio IPC per impersonarli:
task_for_pid in ReportCrash per ottenere il port del task di launchd.mach_port_insert_right in ReportCrash per inserire il port del servizio reale nello spazio IPC di
launchd con il nome originale.Dopo questo passaggio, il sistema dovrebbe essere di nuovo completamente funzionale. Dopo un'esploitazione riuscita, non dovrebbe essere necessario forzare il reset del dispositivo, poiché l'exploit ripara tutti i danni da sé.
Blanket include anche un payload post-sfruttamento che bypassa amfid e genera una shell di ascolto. Questa sezione descriverà come viene ottenuto.
Anche dopo aver ottenuto l'esecuzione di codice in ReportCrash, usare quella capacità non è facile: siamo limitati a eseguire singole chiamate di funzioni all'interno del processo, il che rende doloroso eseguire compiti complessi. Idealmente, vorremmo un modo per eseguire codice nativamente con i privilegi di ReportCrash, sia iniettando codice in ReportCrash sia generando un nuovo processo con gli stessi (o superiori) privilegi.
Blanket sceglie la via della generazione del processo. Usiamo task_for_pid e il nostro status di binario di piattaforma in
ReportCrash per ottenere il port del task di launchd e creare un nuovo thread all'interno di launchd che possiamo
controllare. Usiamo quindi quel thread per chiamare posix_spawn per lanciare il nostro binario payload. Il binario
payload può essere firmato con entitlements ristretti, incluso task_for_pid-allow, per garantire capacità
aggiuntive.
Affinché iOS accetti il nostro binario appena generato, dobbiamo bypassare la firma del codice. Varie
strategie sono state discusse nel corso degli anni, ma la strategia attuale più comune è registrare
un gestore di eccezioni per amfid e quindi eseguire una patch dei dati in modo che amfid vada in crash quando tenta
di chiamare MISValidateSignatureAndCopyInfo. Questo ci permette di falsificare l'implementazione di quella funzione
per far finta che la firma del codice sia valida.
Tuttavia, c'è un altro approccio che ritengo più robusto e flessibile: invece di patchare amfid del tutto, possiamo semplicemente registrare un nuovo port amfid nel kernel.
Il kernel tiene traccia di quale port inviare i messaggi ad amfid usando un port speciale host chiamato
HOST_AMFID_PORT. Se abbiamo l'esecuzione di codice root non sandboxata, possiamo impostare questo port su un nuovo valore.
Apple ha protetto contro questo attacco controllando se la risposta a una richiesta di validazione
proviene realmente da amfid: il cdhash del mittente viene confrontato con il cdhash di amfid. Tuttavia,
questo in realtà non impedisce che il messaggio venga inviato a un processo diverso da amfid; impedisce solo
che la risposta provenga da un processo non amfid. Se impostiamo un triangolo in cui il kernel
invia messaggi a noi, generiamo la risposta e la passiamo ad amfid, e poi amfid invia la risposta al
kernel, allora saremo in grado di bypassare il controllo del mittente.
Ci sono numerosi vantaggi in questo approccio, di cui il più grande è probabilmente l'accesso a
flag aggiuntivi nella routine di servizio verify_code_directory. Anche se amfid non li usa tutti,
ci sono molti altri flag di output che amfid potrebbe impostare per controllare il comportamento della
firma del codice. Ecco un prototipo parziale di verify_code_directory:```C
kern_return_t
verify_code_directory(
mach_port_t amfid_port,
amfid_path_t path,
uint64_t file_offset,
int32_t a4,
int32_t a5,
int32_t a6,
int32_t * entitlements_valid,
int32_t * signature_valid,
int32_t * unrestrict,
int32_t * signer_type,
int32_t * is_apple,
int32_t * is_developer_code,
amfid_a13_t a13,
amfid_cdhash_t cdhash,
audit_token_t audit);
Di particolare interesse per gli sviluppatori di jailbreak è il parametro `is_apple`. Questo parametro non sembra essere utilizzato da amfid, ma se impostato, farà sì che il kernel imposti il flag di codesigning `CS_PLATFORM_BINARY`, che concede all'applicazione privilegi di binary di piattaforma. In particolare, ciò significa che l'applicazione può ora utilizzare i task port per modificare direttamente i binary di piattaforma.
Scappatoie utilizzate in questo attacco
---------------------------------------------------------------------------------------------------
Questo attacco sfrutta diverse scappatoie che non sono esse stesse vulnerabilità di sicurezza, ma riducono l'efficacia di varie mitigazioni degli exploit. Non tutte devono essere chiuse insieme, poiché alcune sono parzialmente ridondanti, ma vale comunque la pena elencarle tutte.
Nel kernel:
1. `task_threads` può promuovere un `task_inspect_t` di sola ispezione a un `thread_act_t` in grado di modificare.
2. Non esiste un `thread_conversion_eval` che svolga il ruolo di `task_conversion_eval` per i thread.
3. Un binary non di piattaforma può utilizzare un diritto `task_inspect_t` per un binary di piattaforma.
4. I messaggi di eccezione per processi non sandboxati possono essere inviati a processi sandboxati, anche se ciò fornisce un modo per uscire dalla sandbox. Non è chiaro se esista una soluzione pulita per questa scappatoia.
5. L'esecuzione di codice non sandboxato, il port host-priv e la capacità di far crashare un processo `task_for_pid-allow` possono essere combinati per creare un workaround per `task_for_pid`. (Il workaround è: chiamare `host_set_exception_ports` per impostare un nuovo gestore di eccezioni a livello di host, quindi far crashare il processo `task_for_pid-allow` per ricevere il suo task port ed eseguire codice con l'entitlement.)
Nelle estensioni delle app:
1. Le estensioni delle app che condividono un gruppo di applicazioni possono comunicare utilizzando messaggi Mach, nonostante la documentazione suggerisca che la comunicazione tra l'app host e l'estensione dell'app dovrebbe essere impossibile.
Correzioni e mitigazioni consigliate
---------------------------------------------------------------------------------------------------
Raccomando le seguenti correzioni, approssimativamente in ordine di importanza:
1. Deallocare i port Mach nelle routine di servizio di launchd solo quando si restituisce `KERN_SUCCESS`. Questo risolverà la vulnerabilità di sostituzione del port Mach.
2. Chiudere la scappatoia di `task_threads` che permette a un binary non di piattaforma di utilizzare il task port di un binary di piattaforma per ottenere l'esecuzione di codice.
3. Risolvere i problemi di crash in ReportCrash.
4. L'insieme dei servizi Mach raggiungibili dall'interno della sandbox del container dovrebbe essere minimizzato. Non vedo un motivo legittimo per cui la maggior parte delle app iOS debba comunicare con ReportCrash o SafetyNet.
5. Il maggior numero possibile di processi dovrebbe essere sandboxato. Non sono sicuro se druid debba essere non sandboxato per funzionare correttamente, ma in caso contrario, dovrebbe essere posto in una sandbox appropriata.
6. Il codice morto dovrebbe essere eliminato. SafetyNet non sembra svolgere la sua funzionalità prevista. Se non è più necessario, dovrebbe probabilmente essere rimosso.
7. Chiudere il workaround per `task_for_pid` basato su `host_set_exception_ports`. Ad esempio, valutare se vale la pena limitare `host_set_exception_ports` a root o limitare l'usabilità del port host-priv in alcune configurazioni. Questo viola l'elegante design basato sulle capabilities di Mach, ma `host_set_exception_ports` potrebbe essere un bersaglio promettente per abusi.
8. Valutare se vale la pena aggiungere `task_conversion_eval` a `task_inspect_t`.
Esecuzione di blanket
---------------------------------------------------------------------------------------------------
Blanket dovrebbe funzionare su qualsiasi dispositivo con iOS 11.2.6.
1. Scarica il progetto: ```
git clone https://github.com/bazad/blanket
cd blanket
headers/config.h e cambia APP_GROUP con l'identificatore del gruppo di applicazioni che hai specificato in precedenza.Dopodiché, dovresti essere in grado di compilare ed eseguire il progetto sul dispositivo.
Se blanket ha successo, eseguirà il binario del payload (sorgente in
blanket_payload/blanket_payload.c), che di default apre una bind shell sulla porta 4242. Puoi
connetterti a quella porta con netcat ed eseguire comandi shell arbitrari.
Un ringraziamento speciale a Ian Beer e Jonathan Levin per le loro eccellenti ricerche sulla sicurezza e gli interni di iOS.
Ho scoperto questa vulnerabilità nel gennaio 2018 e ho iniziato a sviluppare l'exploit a fine febbraio. Ho segnalato il problema ad Apple il 13 aprile. Apple ha assegnato la vulnerabilità di sostituzione del Mach port in launchd come CVE-2018-4280, ed è stata corretta in iOS 11.4.1 e macOS 10.13.6 il 9 luglio.
Blanket è rilasciato sotto licenza MIT.
Brandon Azad