Post sul blog 29 settembre 2021 di Jim Aspers, Specialista in Sicurezza di Bureau Veritas Cybersecurity
Applicazioni iOS su Mac ARM: Opportunità di pentesting | Parte II
La possibilità di eseguire applicazioni iOS su Mac ARM è una caratteristica interessante per i ricercatori di sicurezza delle applicazioni. Nella prima parte, abbiamo dimostrato che, con alcuni accorgimenti, è possibile eseguire qualsiasi applicazione iOS arbitraria su macOS. Le applicazioni che richiedono l'accesso al microfono, alla fotocamera, al Bluetooth o ai Servizi di localizzazione del dispositivo hanno funzionato come previsto.
Anche se abbiamo dimostrato la possibilità di eseguire applicazioni iOS arbitrarie, lo abbiamo fatto utilizzando un'applicazione legittima e sostituendo il suo contenuto con l'applicazione che stavamo cercando di eseguire; il processo di lancio non è stato compreso a fondo. Per ottenere il pieno controllo dell'applicazione lanciata e dell'ambiente in cui viene distribuita, è necessaria una comprensione più solida del processo di lancio.
Inoltre, è emerso che molte applicazioni che integravano il rilevamento del jailbreak segnalavano i nostri Mac come jailbroken, richiedendo un bypass manuale. Abbiamo cercato un modo solido per aggirare questo problema, senza applicare patch o strumentare l'applicazione di destinazione.
Lanciatore di applicazioni personalizzabile: launchr
È auspicabile avere il maggior controllo possibile sull'applicazione in prova. Concretamente, questo include (ma non si limita a):
- Avere la possibilità di specificare i file per i descrittori di input, output ed errori dell'applicazione;
- la possibilità di specificare le variabili d'ambiente;
- Avviare il processo dell'applicazione in uno stato di sospensione per una strumentazione precoce.
L'avvio di binari iOS non grafici su macOS può essere fatto in modo abbastanza banale utilizzando la syscall privata posix_spawnattr_set_platform_np() per impostare l'attributo della piattaforma del processo su '2'(PLATFORM_IOS, vedere <mach-o/loader.h nei sorgenti XNU prima dello spawn (come descritto da Samuel Groß nel suo post). Lo spawn di un'applicazione grafica iOS è però un altro tipo di gioco. Abbiamo in parte fatto un reverse engineering su come macOS gestisce l'avvio di applicazioni grafiche in generale e abbiamo applicato questa conoscenza al caso specifico di iOS.
Diversi componenti del sistema operativo si sono rivelati coinvolti nell'avvio di un'applicazione iOS, tra cui:
- runningboardd
- secinitd
- lsd
- appinstalld/com.apple.MobileInstallationService
- containermanagerd
Molti di questi componenti sono coinvolti nel sandboxing e in altri compiti rilevanti per la sicurezza. Poiché preferiremmo lanciare direttamente un'applicazione aggirando uno qualsiasi di questi componenti legati alla sicurezza, abbiamo analizzato il lancio di una semplice applicazione di sistema (Calculator.app) e abbiamo confrontato il flusso osservato. Questa volta, è emerso che solo runningboardd era coinvolto attivamente. Questo ha spostato l'attenzione su runningboardd.
A questo punto, si è stabilito il sospetto che fosse necessario parlare con runningboardd in qualche modo per lanciare direttamente le applicazioni. A parte una serie limitata di componenti principali, Apple tiene per sé il codice sorgente e la documentazione API privata, quindi non è possibile cercare semplicemente nel codice sorgente di runningboardd o leggere le interfacce che espone. Anche il reverse engineering completo del codice che sta dietro (RunningBoard.framework, RunningBoardServices.framework) non è un compito realistico.
Come fanno gli altri
Il modo più semplice per imparare come avviare un'applicazione è quello di imbrogliare un po' e di spiare i programmi di Apple. Esistono (almeno) due modi comuni in cui un utente di macOS può avviare un'applicazione iOS in uno scenario di utilizzo regolare: utilizzando Finder.app facendo doppio clic sul wrapper bundle dell'applicazione (vedere il post precedente su questo argomento), oppure utilizzando il comando 'apri' con il wrapper bundle come argomento. Dalla nostra prima analisi, sospettiamo che entrambi invieranno a un certo punto un messaggio XPC a runningboardd, chiedendogli di avviare l'applicazione di destinazione. Sebbene 'open' sembri essere la scelta più ovvia per il reverse engineering, a causa delle sue dimensioni limitate e quindi del rumore limitato nei risultati, l'aspetto positivo di Finder.app è che è un processo costantemente in esecuzione. Questo lo rende un obiettivo facile da raggiungere con strumenti di tracciamento come xpcspy.
Osservando l'output, troviamo le chiamate XPC previste:
Il contenuto completo è troppo grande per essere visualizzato qui. Il punto che stiamo facendo qui è che questa chiamata sembra contenere informazioni che vengono utilizzate per lo spawn a livello di sistema operativo dell'applicazione di destinazione (identificatore del bundle di destinazione, variabili d'ambiente, attributi di LaunchServices come i flag di spawn e le opzioni di esecuzione, ...). Un'altra chiave di lettura di questo output è il selettore inviato al destinatario: executeLaunchRequest:identifier:error:. In Objective-C su macOS/iOS, un 'selettore' inviato a un oggetto può essere interpretato come un metodo di classe o di istanza che viene eseguito. Poiché sappiamo che il selettore viene inviato a runningboardd e vediamo la chiave "rbs_selector", la nostra prima ipotesi è che la chiamata API da cui deriva questo messaggio XPC sia esposta da RunningBoardServices.
Utilizziamo frida-trace lanciando nuovamente Calculator.app dal Finder per scoprire a quale classe appartiene esattamente questo selettore:
Il selettore esatto che ci aspettavamo non è stato trovato, ma abbiamo trovato qualcosa di simile. L'argomento più interessante del selettore, un oggetto "LaunchRequest", è presente anche qui. Un'ulteriore ispezione di questo oggetto ha mostrato che si trattava di un oggetto RBSLaunchRequest costruito a partire da un oggetto RBSLaunchContext, che a sua volta conteneva il contenuto del dizionario mostrato nell'output di xpcspy. Questa e altre ricerche simili ci hanno mostrato che dovevamo esaminare l'uso dell'oggetto RBSLaunchRequest e degli oggetti correlati per raggiungere i nostri obiettivi.
Gioco di imitazione
Sulla base di alcune ricerche nelle varie implementazioni di metodi correlati esposti da RunningBoardServices.framework e di alcune sperimentazioni per tentativi, abbiamo scoperto che potevamo lanciare un'applicazione grafica macOS dal nostro codice, utilizzando solo alcune chiamate ad API non documentate. Come sapevamo dagli esperimenti con lo spawn di semplici binari iOS su macOS, la chiave per lanciare un eseguibile iOS invece di uno macOS si trova in un parametro di specifica della piattaforma. Cercando i simboli contenenti la stringa "platform", abbiamo notato che un oggetto RBSProcessIdentity poteva essere inizializzato con un argomento di piattaforma. Tracciando la chiamata in particolare, si è visto che è stata colpita e che il valore di questo argomento era 1 ("PLATFORM_MACOS") quando si lanciava Calculator.app:
Questo si è rivelato essere la chiave per lanciare le applicazioni grafiche iOS dal nostro codice. Se forniamo PLATFORM_IOS invece di PLATFORM_MACOS con questo selettore quando creiamo l'oggetto RBSProcessIdentity e richiediamo l'avvio di un bundle di applicazioni iOS (non solo un wrapper, ma un vero e proprio bundle di applicazioni iOS), macOS avvia felicemente l'applicazione. Poiché non viene applicato alcun ulteriore controllo se lanciamo l'applicazione in questo modo, possiamo firmare ad hoc l'intero bundle e la firma verrà accettata. Questo è ideale quando si sperimentano le patch sull'applicazione di destinazione.
Usare la forza
Ora che possiamo lanciare un'applicazione iOS con gli oggetti RBSProcessIdentity e RBSLaunchContext completamente sotto il nostro controllo, possiamo metterci al lavoro. In questo momento stiamo cercando soluzioni almeno per i seguenti punti:
- Vogliamo poter interrompere l'esecuzione del processo figlio subito dopo averlo generato;
- Vogliamo essere in grado di agganciare comodamente le chiamate all'interno del processo spawnato, senza dover fare affidamento sulla strumentazione;
- Vogliamo poter controllare la politica sandbox del processo figlio.
Avvio sospeso
Quando valutiamo la resilienza delle applicazioni al cracking o alla modifica del runtime in generale, a volte incontriamo applicazioni che implementano contromisure anti-debug in una fase iniziale dell'avvio dell'applicazione. Potrebbe trattarsi delle routine di inizializzazione delle librerie, che possono essere dolorose da aggirare se si utilizza l'offuscamento o se semplicemente ci sono centinaia di routine di inizializzazione da enumerare. In questi casi, è importante poter collegare un debugger in una fase molto precoce, anche prima che venga attivato il meccanismo di protezione anti-debug. Questo è ciò che chiamiamo strumentazione precoce.
Se fossimo a corto di opzioni, un semplice while() che attende che venga generato un processo e poi aggancia immediatamente un debugger sarebbe probabilmente sufficiente. Tuttavia, poiché ora abbiamo il pieno controllo sul processo generato, possiamo istruire il kernel a mettere il processo figlio in modalità sospesa subito dopo la sua creazione (cioè si ferma proprio a _dyld_start, il punto di ingresso di qualsiasi eseguibile *OS):
Una volta terminato ciò che vogliamo fare con l'applicazione (ad esempio, inserire punti di interruzione, saltare istruzioni, applicare patch alla memoria), possiamo continuare la sua esecuzione inviando un SIGCONT.
Un effetto collaterale molto scomodo dell'avvio di un'applicazione in modalità sospesa è che il Puppet Master di macOS, sotto forma di runningboardd, si aspetta un 'battito cardiaco' dal processo generato. Se non riceve questo segno di vita in tempo, l'applicazione viene lasciata in uno stato inutilizzabile e non sarà più utilizzabile (sarà necessario kill -9). Questo accade circa 30 secondi dopo aver generato l'applicazione in modalità sospesa e non continuando l'esecuzione, con il seguente messaggio di errore:
Non è stata ancora trovata una soluzione per affrontare questo problema. L'uso di lldb tramite script fa sì che 30 secondi siano sufficienti per fare la nostra magia in generale, ma una soluzione adeguata sarebbe gradita.
Riportare DYLD_INTERPOSE
Il secondo requisito era un metodo conveniente per modificare il comportamento dell'applicazione. Quando si tratta di modificare il flusso di esecuzione delle applicazioni, ci sono generalmente tre possibilità:
- Patching del codice macchina su disco;
- Correggere il codice in memoria utilizzando uno strumento simile al debugger;
- Utilizzando un framework di strumentazione dinamica (ad esempio Frida);
- Utilizzando le funzioni di aggancio native di macOS(dyld interposing).
Poiché il patch del codice macchina su disco o in memoria richiede la scrittura di codice assembly, queste non sono le scelte più convenienti e flessibili. Uno strumento come Frida è ottimo e ha una vasta gamma di casi d'uso. Tuttavia, per applicare modifiche persistenti e potenzialmente complesse, è preferibile un metodo meno macchinoso, come l'interposizione di dyld (per le chiamate di funzione C) o lo swizzling (per le classi Obj-C). Possiamo swizzare perfettamente già con la soluzione a portata di mano, scrivendo la nostra dylib e iniettandola con una variabile d 'ambiente DYLD_INSERT_LIBRARIES. Considerando le chiamate di funzione C, come indicato da Samuel Groß nel suo articolo (citato in precedenza in questo post), macOS non consente l'interposizione per gli eseguibili iOS. Tuttavia, poiché ora possiamo lanciare l'applicazione in uno stato di sospensione, possiamo applicare il metodo di iniezione di codice di Groß per applicare una patch a dyld in memoria, al fine di aggirare questa restrizione. Questo è implementato anche nel nostro launcher.
Un'applicazione pratica banale di questa funzione è la correzione dell'incompatibilità di un'applicazione con la piattaforma hardware iOS emulata da macOS (iPad Pro3rd gen con iOS 14.7 al momento della scrittura). Per questo motivo, l'applicazione del nostro cliente non poteva essere eseguita su macOS; abbiamo ricevuto un errore che indicava che il nostro 'iPad' era troppo nuovo per eseguire l'applicazione. Tuttavia, con poche righe di codice, è stata compilata una libreria dinamica per impersonare un modello di iPad più vecchio supportato dall'applicazione (iPad7). Di conseguenza, abbiamo potuto eseguire e valutare correttamente l'applicazione.
Correzione della Sandbox delle applicazioni
Durante il test di un'applicazione basata su Xamarin che implementava Cryoprison per il rilevamento del jailbreak, è stato riscontrato che le applicazioni iOS lanciate in questo modo potevano accedere a file come /bin/bash. Poiché tali file non dovrebbero essere presenti nelle installazioni iOS bloccate, o almeno non dovrebbero essere accessibili dall'interno della sandbox dell'applicazione, le comuni implementazioni di rilevamento del jailbreak verificano l'accesso a tali file di sistema per determinare se vengono eseguiti in un ambiente jailbroken o non jailbroken. Il fatto che l'app Xamarin sottoposta a test abbia rilevato una piattaforma jailbroken in questo caso ha fatto sospettare che il profilo sandbox applicato da macOS alle applicazioni iOS non fosse del tutto in linea con la sandbox nativa di iOS.
Per comprendere i meccanismi di sandboxing di macOS, abbiamo in parte eseguito il reverse engineering del programma sandbox-exec. Con una sezione di testo di circa 1kB, questo binario sembrava essere il candidato perfetto per iniziare il processo di reverse engineering:
Il flusso del programma consisteva fondamentalmente in due fasi:
- Impostare il profilo sandbox da utilizzare e applicare il profilo al proprio processo;
- eseguire() l'eseguibile di destinazione.
Poiché execve() esegue un'immagine binaria nel processo del chiamante, il profilo sandbox applicato nel primo passaggio viene applicato all'eseguibile di destinazione. In questo modo, l'eseguibile di destinazione non deve essere consapevole della sandbox.
Un'ulteriore analisi ha mostrato che due chiamate API non documentate possono essere sufficienti per preparare e applicare un profilo sandbox:
- sandbox_compile_file(), che prende in input un file di definizione di sandbox;
- sandbox_apply(), che prende il risultato della prima chiamata e lo applica al processo del chiamante.
Utilizzando il metodo di iniezione della libreria descritto in precedenza (impiegando DYLD_INSERT_LIBRARIES) per ottenere l'esecuzione anticipata del codice nel contesto del processo dell'applicazione iOS lanciata, abbiamo imitato questo flusso. Tuttavia, la chiamata a sandbox_apply() è fallita, poiché in quel momento era già presente un profilo sandbox per il processo. La possibilità di modificare una politica di sandbox dall'interno del processo stesso, una volta attivata, potrebbe portare a vulnerabilità di sicurezza, quindi è logico che questo non sia consentito.
Abbiamo quindi cercato un metodo per lanciare la nostra applicazione target senza sandbox, per poter poi applicare la nostra sandbox personalizzata. L'analisi del processo di avvio dell'applicazione ci aveva già insegnato che il componente macOS secinitd era coinvolto nell'applicazione dei profili sandbox. L'analisi della libreria sottostante libsystem_secinit.dylib ha mostrato che esiste effettivamente un modo per lanciare anche le applicazioni iOS senza che venga applicata automaticamente una sandbox: il diritto "com.apple.private.security.no-sandbox". Infatti, dopo aver aggiunto questo diritto ai diritti della nostra applicazione di destinazione (dato che controlliamo completamente anche quelli, ovviamente!), possiamo ora applicare il nostro profilo sandbox personalizzato iniettando una libreria contenente il seguente semplice codice:
La specificazione di un profilo sandbox accuratamente realizzato per l'utilizzo delle nostre applicazioni target dovrebbe consentirci di bypassare completamente qualsiasi meccanismo di rilevamento jailbreak comune per impostazione predefinita, senza dover fare affidamento su bypass di rilevamento jailbreak convenzionali. L'efficacia del rilevamento del jailbreak può essere valutata semplicemente rimuovendo le restrizioni della sandbox e ispezionando il comportamento dell'applicazione. In conclusione, questo renderà le valutazioni delle applicazioni più efficienti.
In pratica, utilizzando questo approccio con un profilo sandbox che vieta l'accesso in lettura/scrittura a qualsiasi posizione al di fuori del container dell'applicazione, un'applicazione di prova non ha più sollevato segnalazioni di jailbreak:
Conclusione
In questo post, abbiamo discusso di come eseguire applicazioni grafiche iOS sotto macOS con il pieno controllo. Si è discusso di come questo possa essere utilizzato per il pentesting delle applicazioni e la ricerca sulla sicurezza. Ci sono miglioramenti da apportare, ma finora sembra che la soluzione a disposizione sia perfettamente adatta per la ricerca e il security testing. Nel prossimo futuro testeremo il maggior numero possibile di applicazioni per poter concludere sull'applicabilità di questo approccio per i test quotidiani delle applicazioni.
Per un'implementazione WIP dello strumento di lancio discusso, veda srepsa/launchr (github.com).