macOS Thread Injection via Task port
Codice
1. Hijacking del thread
Inizialmente, la funzione task_threads()
viene invocata sulla porta del task per ottenere un elenco di thread dal task remoto. Viene selezionato un thread da dirottare. Questo approccio si discosta dai metodi di iniezione di codice convenzionali in quanto la creazione di un nuovo thread remoto è vietata a causa della nuova mitigazione che blocca thread_create_running()
.
Per controllare il thread, viene chiamata la funzione thread_suspend()
, interrompendo la sua esecuzione.
Le uniche operazioni consentite sul thread remoto riguardano l'arresto e l'avvio dello stesso, il recupero e la modifica dei suoi valori di registro. Le chiamate di funzione remote vengono avviate impostando i registri x0
a x7
agli argomenti, configurando pc
per puntare alla funzione desiderata e attivando il thread. Per garantire che il thread non si blocchi dopo il ritorno, è necessario rilevare il ritorno.
Una strategia prevede la registrazione di un gestore di eccezioni per il thread remoto utilizzando thread_set_exception_ports()
, impostando il registro lr
su un indirizzo non valido prima della chiamata alla funzione. Ciò provoca un'eccezione dopo l'esecuzione della funzione, inviando un messaggio alla porta delle eccezioni, consentendo l'ispezione dello stato del thread per recuperare il valore di ritorno. In alternativa, come adottato dall'exploit triple_fetch di Ian Beer, lr
viene impostato per eseguire un loop all'infinito. I registri del thread vengono quindi monitorati continuamente fino a quando pc
punta a quell'istruzione.
2. Porte Mach per la comunicazione
La fase successiva prevede l'instaurazione di porte Mach per facilitare la comunicazione con il thread remoto. Queste porte sono fondamentali per il trasferimento di diritti di invio e ricezione arbitrari tra i task.
Per la comunicazione bidirezionale, vengono create due porte Mach di ricezione: una nel task locale e l'altra nel task remoto. Successivamente, viene trasferito un diritto di invio per ogni porta al task corrispondente, consentendo lo scambio di messaggi.
Concentrandosi sulla porta locale, il diritto di ricezione è detenuto dal task locale. La porta viene creata con mach_port_allocate()
. La sfida consiste nel trasferire un diritto di invio a questa porta nel task remoto.
Una strategia prevede di sfruttare thread_set_special_port()
per inserire un diritto di invio alla porta locale nella THREAD_KERNEL_PORT
del thread remoto. Quindi, viene istruito il thread remoto a chiamare mach_thread_self()
per recuperare il diritto di invio.
Per la porta remota, il processo è essenzialmente invertito. Al thread remoto viene indicato di generare una porta Mach tramite mach_reply_port()
(poiché mach_port_allocate()
non è adatto a causa del suo meccanismo di restituzione). Dopo la creazione della porta, viene invocato mach_port_insert_right()
nel thread remoto per stabilire un diritto di invio. Questo diritto viene quindi nascosto nel kernel utilizzando thread_set_special_port()
. Nel task locale, viene utilizzato thread_get_special_port()
sul thread remoto per acquisire un diritto di invio alla nuova porta Mach allocata nel task remoto.
Il completamento di questi passaggi porta all'instaurazione di porte Mach, gettando le basi per la comunicazione bidirezionale.
3. Primitive di base per la lettura/scrittura di memoria
In questa sezione, l'attenzione è rivolta all'utilizzo della primitiva di esecuzione per stabilire primitive di base per la lettura e la scrittura di memoria. Questi passaggi iniziali sono cruciali per ottenere un maggiore controllo sul processo remoto, anche se le primitive in questa fase non serviranno a molti scopi. Presto, saranno aggiornate a versioni più avanzate.
Lettura e scrittura di memoria utilizzando la primitiva di esecuzione
L'obiettivo è eseguire la lettura e la scrittura di memoria utilizzando funzioni specifiche. Per la lettura della memoria, vengono utilizzate funzioni che assomigliano alla seguente struttura:
E per scrivere in memoria, vengono utilizzate funzioni simili a questa struttura:
Queste funzioni corrispondono alle istruzioni assembly fornite:
Identificazione delle funzioni adatte
Una scansione delle librerie comuni ha rivelato candidati appropriati per queste operazioni:
Lettura della memoria: La funzione
property_getName()
della libreria Objective-C runtime è identificata come una funzione adatta per la lettura della memoria. La funzione è descritta di seguito:
Questa funzione agisce efficacemente come la read_func
restituendo il primo campo di objc_property_t
.
Scrittura di memoria: Trovare una funzione predefinita per la scrittura di memoria è più difficile. Tuttavia, la funzione
_xpc_int64_set_value()
di libxpc è un candidato adatto con la seguente disassemblazione:
Per eseguire una scrittura a 64 bit in un indirizzo specifico, la chiamata remota è strutturata come segue:
Con queste primitive stabilite, il palcoscenico è pronto per creare una memoria condivisa, segnando un significativo progresso nel controllo del processo remoto.
4. Configurazione della memoria condivisa
L'obiettivo è stabilire una memoria condivisa tra i task locali e remoti, semplificando il trasferimento dei dati e agevolando la chiamata di funzioni con argomenti multipli. L'approccio prevede di sfruttare libxpc
e il suo tipo di oggetto OS_xpc_shmem
, che si basa su voci di memoria Mach.
Panoramica del processo:
Assegnazione della memoria:
Assegnare la memoria per la condivisione utilizzando
mach_vm_allocate()
.Utilizzare
xpc_shmem_create()
per creare un oggettoOS_xpc_shmem
per la regione di memoria allocata. Questa funzione gestirà la creazione dell'entry di memoria Mach e memorizzerà il diritto di invio Mach all'offset0x18
dell'oggettoOS_xpc_shmem
.
Creazione della memoria condivisa nel processo remoto:
Allocare memoria per l'oggetto
OS_xpc_shmem
nel processo remoto con una chiamata remota amalloc()
.Copiare il contenuto dell'oggetto
OS_xpc_shmem
locale nel processo remoto. Tuttavia, questa copia iniziale avrà nomi di entry di memoria Mach errati all'offset0x18
.
Correzione dell'entry di memoria Mach:
Utilizzare il metodo
thread_set_special_port()
per inserire un diritto di invio per l'entry di memoria Mach nel task remoto.Correggere il campo dell'entry di memoria Mach all'offset
0x18
sovrascrivendolo con il nome dell'entry di memoria remota.
Finalizzazione della configurazione della memoria condivisa:
Validare l'oggetto
OS_xpc_shmem
remoto.Stabilire la mappatura della memoria condivisa con una chiamata remota a
xpc_shmem_remote()
.
Seguendo questi passaggi, la memoria condivisa tra i task locali e remoti verrà configurata in modo efficiente, consentendo trasferimenti di dati semplici e l'esecuzione di funzioni che richiedono argomenti multipli.
Esempi di codice aggiuntivi
Per l'allocazione della memoria e la creazione dell'oggetto di memoria condivisa:
Per creare e correggere l'oggetto di memoria condivisa nel processo remoto:
Ricorda di gestire correttamente i dettagli delle porte Mach e dei nomi delle voci di memoria per garantire il corretto funzionamento della configurazione della memoria condivisa.
5. Ottenere il pieno controllo
Una volta stabilita con successo la memoria condivisa e acquisita la capacità di esecuzione arbitraria, abbiamo essenzialmente ottenuto il pieno controllo sul processo target. Le funzionalità chiave che consentono questo controllo sono:
Operazioni di memoria arbitrarie:
Eseguire letture di memoria arbitrarie invocando
memcpy()
per copiare dati dalla regione condivisa.Eseguire scritture di memoria arbitrarie utilizzando
memcpy()
per trasferire dati alla regione condivisa.
Gestione delle chiamate di funzione con argomenti multipli:
Per le funzioni che richiedono più di 8 argomenti, disporre gli argomenti aggiuntivi nello stack in conformità con la convenzione di chiamata.
Trasferimento di porte Mach:
Trasferire porte Mach tra task tramite messaggi Mach tramite porte precedentemente stabilite.
Trasferimento di descrittori di file:
Trasferire descrittori di file tra processi utilizzando fileport, una tecnica evidenziata da Ian Beer in
triple_fetch
.
Questo controllo completo è racchiuso nella libreria threadexec, che fornisce un'implementazione dettagliata e un'API user-friendly per l'interazione con il processo vittima.
Considerazioni importanti:
Assicurarsi di utilizzare correttamente
memcpy()
per le operazioni di lettura/scrittura di memoria al fine di mantenere la stabilità del sistema e l'integrità dei dati.Quando si trasferiscono porte Mach o descrittori di file, seguire i protocolli appropriati e gestire le risorse in modo responsabile per evitare perdite o accessi non intenzionali.
Seguendo queste linee guida e utilizzando la libreria threadexec
, è possibile gestire ed interagire con i processi a un livello granulare, ottenendo il pieno controllo sul processo target.
Riferimenti
Last updated