
un framework per l'hooking di funzioni del kernel iOS per dispositivi compatibili con checkra1n

Output del log del kernel dopo la compilazione e l'esecuzione di example/open1_hook.c
xnuspy è un modulo di pongoOS che installa una nuova syscall, xnuspy_ctl,
che consente di hookare funzioni del kernel dallo spazio utente. Supporta iOS 13.x,
iOS 14.x e iOS 15.x su checkra1n 0.12.2 e superiori. I dispositivi 4K non sono supportati.
Questo modulo neutralizza completamente KTRR/KPP e rende possibile creare memoria RWX all'interno di EL1. Non usarlo sul tuo dispositivo principale.
Richiede libusb: brew install libusb
Esegui make nella directory principale. Costruirà il loader e il modulo.
Aggiungi queste opzioni prima di make.
XNUSPY_DEBUG=1
kprintf).XNUSPY_SERIAL=1
IOLog.XNUSPY_LEAKED_PAGE_LIMIT=n
64. Maggiori informazioni
possono essere trovate in Debugging Kernel Panics.XNUSPY_TRAMP_PAGES=n
XNUSPY_DEBUG e XNUSPY_SERIAL non dipendono l'uno dall'altro.
Dopo aver costruito tutto, fai avviare il dispositivo con checkra1n in una shell
pongo: /Applications/checkra1n.app/Contents/MacOS/checkra1n -p
Nella stessa directory in cui hai costruito il loader e il modulo, esegui
loader/loader module/xnuspy. Dopo aver fatto ciò, xnuspy farà il suo lavoro e
in pochi secondi il dispositivo si avvierà. loader attenderà ancora un paio di
secondi dopo aver emesso xnuspy-getkernelv nel caso in cui SEPROM debba essere sfruttato.
A volte un paio dei miei telefoni si bloccano su "Booting" dopo che checkra1n's KPF
viene eseguito. Non ho ancora capito cosa lo causa, ma se succede, riprova.
Inoltre, se il dispositivo si blocca dopo bootx, riprova. Infine, marcare il
codice compilato di xnuspy_ctl come eseguibile sul mio iPhone X con iOS 13.3.1 è
un po' incostante, ma riesce al 100% delle volte sugli altri miei telefoni. Se ottieni
un panic con un kernel instruction fetch abort quando esegui il tuo programma di hook,
riprova.
xnuspy modificherà una syscall enosys per puntare a xnuspy_ctl_tramp.
Questo è un piccolo trampolino che marca il codice compilato di xnuspy_ctl come
eseguibile e salta ad esso. Puoi trovare l'implementazione di xnuspy_ctl in
module/el1/xnuspy_ctl/xnuspy_ctl.c e degli esempi nella directory example.
All'interno di include/xnuspy/ si trova xnuspy_ctl.h, un header che definisce le costanti
per xnuspy_ctl. È pensato per essere incluso in tutti i programmi che hookano
funzioni del kernel.
Puoi usare sysctlbyname per scoprire quale syscall è stata modificata:```
size_t oldlen = sizeof(long);
long SYS_xnuspy_ctl = 0;
sysctlbyname("kern.xnuspy_ctl_callnum", &SYS_xnuspy_ctl, &oldlen, NULL, 0);
Questa chiamata di sistema accetta quattro argomenti: `flavor`, `arg1`, `arg2` e `arg3`.
Il flavor può essere `XNUSPY_CHECK_IF_PATCHED`, `XNUSPY_INSTALL_HOOK`,
`XNUSPY_REGISTER_DEATH_CALLBACK`, `XNUSPY_CALL_HOOKME`, `XNUSPY_CACHE_READ`,
`XNUSPY_KREAD`, `XNUSPY_KWRITE` o `XNUSPY_GET_CURRENT_THREAD`.
Il significato dei successivi tre argomenti dipende dal flavor.
## `XNUSPY_CHECK_IF_PATCHED`
Questo esiste per permetterti di verificare se `xnuspy_ctl` è presente. Invocarlo con questo
flavor farà sì che restituisca `999`. I valori degli altri argomenti vengono
ignorati.
## `XNUSPY_INSTALL_HOOK`
Ho progettato questo flavor per corrispondere all'API di [`MSHookFunction`](http://www.cydiasubstrate.com/api/c/MSHookFunction/).
`arg1` è l'indirizzo *NON SLIDATO* della funzione del kernel che desideri agganciare. Se
fornisci un indirizzo slidato, molto probabilmente causerai un panic. `arg2` è un puntatore alla tua
funzione sostitutiva compatibile con l'ABI. `arg3` è un puntatore affinché `xnuspy_ctl`
`copyout` l'indirizzo di un trampolino che rappresenta la funzione originale del
kernel. Può essere `NULL` se non intendi chiamare l'originale.
## `XNUSPY_REGISTER_DEATH_CALLBACK`
Questo flavor ti permette di registrare un "callback di terminazione" opzionale, una funzione che xnuspy
chiamerà quando il tuo programma di hook termina. Ti dà la possibilità di pulire tutto ciò
che hai creato dai tuoi hook del kernel. Se hai creato thread del kernel, dovresti
dire loro di terminare in questa funzione.
Il tuo callback non viene invocato in modo asincrono, quindi se ti blocchi, impedisci
al thread di garbage collection di xnuspy di eseguire.
`arg1` è un puntatore alla tua funzione di callback. I valori degli altri argomenti
vengono ignorati.
## `XNUSPY_CALL_HOOKME`
`hookme` è un piccolo stub in assembly che xnuspy esporta attraverso la cache di xnuspy
per permetterti di agganciarlo. Invocare `xnuspy_ctl` con questo flavor farà sì che
`hookme` venga chiamato, fornendoti un modo per ottenere facilmente l'esecuzione di codice nel kernel
senza dover agganciare una funzione reale del kernel.
`arg1` è un argomento che verrà passato a `hookme` quando viene invocato.
Può essere `NULL`.
## `XNUSPY_CACHE_READ`
Questo flavor ti offre un modo per leggere dalla cache di xnuspy. Contiene molte cose
utili come `kprintf`, `current_proc`, `kernel_thread_start`, alcune funzioni libc,
e lo slide del kernel, in modo che tu non debba trovarli da solo. Per un elenco completo
degli ID della cache, consulta `example/xnuspy_ctl.h`.
`arg1` è uno degli ID della cache definiti in `xnuspy_ctl.h` e `arg2` è un
puntatore affinché `xnuspy_ctl` `copyout` l'indirizzo o il valore di ciò che hai richiesto.
I valori degli altri argomenti vengono ignorati.
## `XNUSPY_KREAD`
Questo flavor ti offre un modo semplice per leggere la memoria del kernel dallo spazio utente senza
tfp0.
`arg1` è un indirizzo virtuale del kernel, `arg2` è l'indirizzo di un buffer nello spazio utente,
e `arg3` è la dimensione di quel buffer nello spazio utente. `arg3` byte verranno scritti
da `arg1` a `arg2`.
## `XNUSPY_KWRITE`
Questo flavor ti offre un modo semplice per scrivere nella memoria del kernel dallo spazio utente senza
tfp0.
`arg1` è un indirizzo virtuale del kernel, `arg2` è l'indirizzo di un buffer nello spazio utente,
e `arg3` è la dimensione di quel buffer nello spazio utente. `arg3` byte verranno scritti
da `arg2` a `arg1`.
## `XNUSPY_GET_CURRENT_THREAD`
Questo flavor fornisce allo spazio utente l'indirizzo nel kernel del thread chiamante.
`arg1` è un puntatore affinché `xnuspy_ctl` `copyout` il valore di ritorno di
`current_thread`. I valori degli altri argomenti vengono ignorati.
### Errori
Per tutti i flavor tranne `XNUSPY_CHECK_IF_PATCHED`, viene restituito `0` in caso di successo.
In caso di errore, viene restituito `-1` e `errno` viene impostato. `XNUSPY_CHECK_IF_PATCHED`
non restituisce alcun errore. Viene utilizzato `mach_to_bsd_errno` di XNU per convertire un
`kern_return_t` nell'`errno` appropriato.
#### Errori relativi a `XNUSPY_INSTALL_HOOK`
`errno` viene impostato su...
- `EEXIST` se:
- Esiste già un hook per la funzione del kernel non slidata indicata da `arg1`.
- `ENOMEM` se:
- `unified_kalloc` ha restituito `NULL`.
- `ENOSPC` se:
- Non ci sono strutture `xnuspy_tramp` libere, una struttura dati interna a
xnuspy. Questo non dovrebbe accadere a meno che tu non stia agganciando centinaia di funzioni del kernel
*contemporaneamente*. Se hai bisogno di più hook di funzioni, consulta [Limiti](#limits).
- `ENOTSUP` se:
- Il chiamante non proviene da un eseguibile Mach-O o da una libreria dinamica.
- `ENOENT` se:
- `mh_for_addr` non è stato in grado di determinare l'intestazione Mach-O corrispondente a
`arg2` all'interno dello spazio degli indirizzi del chiamante.
- `EFAULT` se:
- L'intestazione Mach-O determinata non è in realtà un'intestazione Mach-O. Questo probabilmente
non accadrà mai.
- `EIO` se:
- `mach_make_memory_entry_64` non ha restituito una voce di memoria per l'interezza
dei segmenti `__TEXT` e `__DATA` dell'intestazione Mach-O determinata.
`errno` dipende anche dal valore di ritorno di `vm_map_wire_external`,
`mach_vm_map_external`, `mach_make_memory_entry_64`, `copyin`, `copyout`, e
se applicabile, dalla funzione di inizializzazione una tantum.
Se questo flavor restituisce un errore, la funzione target del kernel non è stata agganciata.
Se hai passato un puntatore non `NULL` per `arg3`, potrebbe essere stato inizializzato
oppure no. È pericoloso usarlo se lo è stato.
#### Errori relativi a `XNUSPY_REGISTER_DEATH_CALLBACK`
`errno` viene impostato su...
- `ENOENT` se:
- Il processo chiamante non ha agganciato alcuna funzione del kernel.
Se questo flavor restituisce un errore, il tuo callback di terminazione non è stato registrato.
#### Errori relativi a `XNUSPY_CALL_HOOKME`
`errno` viene impostato su...
- `ENOTSUP` se:
- `hookme` è troppo lontano dalla memoria contenente le strutture `xnuspy_tramp`.
Questo viene determinato all'interno di pongoOS e può accadere solo se
xnuspy ha dovuto ripiegare su codice già presente nel kernelcache non utilizzato.
In questo caso, chiamare `hookme` causerebbe quasi certamente un kernel panic,
e dovrai trovare un'altra funzione del kernel da agganciare.
Se questo flavor restituisce un errore, `hookme` non è stato chiamato.
#### Errori relativi a `XNUSPY_CACHE_READ`
`errno` viene impostato su...
- `EINVAL` se:
- La costante indicata da `arg1` non rappresenta nulla nella cache.
- `arg1` era `IO_LOCK`, ma il kernel è iOS 14.4.2 o inferiore o iOS 15.x.
- `arg1` era `IPC_OBJECT_LOCK`, ma il kernel è iOS 15.x.
- `arg1` era `IPC_PORT_RELEASE_SEND`, ma il kernel è iOS 14.5 o superiore.
- `arg1` era `IPC_PORT_RELEASE_SEND_AND_UNLOCK`, ma il kernel è iOS 14.4.2 o inferiore.
- `arg1` era `KALLOC_CANBLOCK`, ma il kernel è iOS 14.x o superiore.
- `arg1` era `KALLOC_EXTERNAL`, ma il kernel è iOS 13.x.
- `arg1` era `KFREE_ADDR`, ma il kernel è iOS 14.x o superiore.
- `arg1` era `KFREE_EXT`, ma il kernel è iOS 13.x.
- `arg1` era `PROC_REF`, ma il kernel è iOS 14.8 o inferiore.
- `arg1` era `PROC_REF_LOCKED`, ma il kernel è iOS 15.x.
- `arg1` era `PROC_RELE`, ma il kernel è iOS 14.8 o inferiore.
- `arg1` era `PROC_RELE_LOCKED`, ma il kernel è iOS 15.x.
- `arg1` era `VM_MAP_UNWIRE`, ma il kernel è iOS 15.x.
- `arg1` era `VM_MAP_UNWIRE_NESTED`, ma il kernel è iOS 14.8 o inferiore.
`errno` dipende anche dal valore di ritorno di `copyout` e, se applicabile, dal
valore di ritorno della funzione di inizializzazione una tantum.
Se questo flavor restituisce un errore, il puntatore che hai passato per `arg2` non è stato
inizializzato.
#### Errori relativi a `XNUSPY_KREAD` e `XNUSPY_KWRITE`
`errno` viene impostato su...
- `EFAULT` se:
- La traduzione dell'indirizzo è fallita per `arg1` o `arg2`. Se hai compilato con
`XNUSPY_DEBUG=1`, viene stampato un messaggio a riguardo nel log del kernel.
Se questo flavor restituisce un errore, la memoria del kernel non è stata letta/scritta.
#### Errori relativi a `XNUSPY_GET_CURRENT_THREAD`
Se `copyout` fallisce, `errno` viene impostato sul suo valore di ritorno.
# Informazioni Importanti
### Insidie Comuni
Mentre scrivevo funzioni sostitutive, era facile dimenticare che stavo scrivendo
codice per il kernel. Ecco un paio di cose da tenere a mente quando scrivi hook:
- *Non puoi eseguire alcun codice dallo spazio utente che risiede al di fuori del segmento
`__TEXT` del tuo programma*. Causerai un panic se, ad esempio, chiami accidentalmente `printf`
invece di `kprintf`. Devi reimplementare qualsiasi funzione libc che desideri chiamare
se tale funzione non è già disponibile tramite `XNUSPY_CACHE_READ`.
Puoi comunque creare puntatori a funzione ad altre funzioni del kernel e chiamarle.
- *Molte macro comunemente usate nel codice dello spazio utente non sono sicure per il kernel.* Per
esempio, `PAGE_SIZE` si espande in `vm_page_size`, non una costante. Devi disabilitare
PAN (su A10+, cosa che inoltre non raccomando di fare) prima di leggere questa
variabile altrimenti causerai un panic.
- *Assicurati di compilare il tuo codice con `-fno-stack-protector` e `-D_FORTIFY_SOURCE=0`* In alcuni casi,
il dispositivo dovrà leggere `___stack_chk_guard` dereferenziando un altro puntatore
dello spazio utente, il che causerà un panic su A10+.
- *Per sicurezza, non compilare i tuoi programmi di hook con ottimizzazioni del compilatore.*
Si consiglia anche di dare un'occhiata a https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/style/style.html .
### Debugging dei Kernel Panic
I bug sono inevitabili quando si scrive codice, quindi alla fine causerai un
kernel panic. Un panic non significa necessariamente che ci sia un bug in xnuspy, quindi
prima di aprire un issue, assicurati di avere ancora il panic quando non fai
nulla se non chiamare la funzione originale e restituire il suo valore (se necessario). Se
hai ancora il panic, allora probabilmente è un bug di xnuspy (e per favore apri un issue),
ma se non è così, c'è qualcosa di sbagliato nella tua sostituzione.
Poiché xnuspy non reindirizza effettivamente l'esecuzione verso pagine EL0, il debugging
di un panic non è così semplice. Apri `module/el1/xnuspy_ctl/xnuspy_ctl.c`,
e subito prima dell'unica chiamata a `kwrite_instr` in `xnuspy_install_hook`,
aggiungi una chiamata a `IOSleep` per un paio di secondi. Questo viene fatto per assicurarsi che ci sia
abbastanza tempo prima che il dispositivo vada in panic perché i log possano propagarsi. Ricompila xnuspy con
`XNUSPY_DEBUG=1 make -B` e carica di nuovo il modulo. Dopo aver caricato il modulo,
se non lo hai già fatto, compila `klog` da `klog/`. Caricalo sul tuo dispositivo
ed esegui `stdbuf -o0 ./klog | grep shared_mapping_kva`. Esegui di nuovo il tuo programma di hook
e cerca una riga da `klog` che assomigli a questa:
`shared_mapping_kva: dist 0x7af4 uaddr 0x104797af4 umh 0x104790000 kmh 0xfffffff00c90c000`
Se stai installando più di un hook, ci sarà più di un'occorrenza.
In quel caso, `dist` e `uaddr` varieranno, ma `umh` e `kmh` no. `kmh`
punta all'inizio del mapping nel kernel del segmento `__TEXT` del tuo programma.
Carica il tuo programma di hook nel tuo disassemblatore preferito e riallinealo in modo che la sua intestazione Mach-O
sia all'indirizzo di `kmh`. Per IDA Pro, questo è `Edit -> Segments -> Rebase
program...` con `Image base` selezionato. Dopo che il tuo dispositivo è andato in panic e si è riavviato di nuovo,
se nel log del panic ci sono indirizzi che corrispondono al mapping nel kernel della tua sostituzione,
corrisponderanno al disassemblato. Se non ce ne sono, allora probabilmente hai
una sorta di corruzione di memoria sottile all'interno della tua sostituzione.
xnuspy inoltre non ha modo di sapere se un thread del kernel sta ancora eseguendo (o eseguirà)
sul mapping nel kernel del segmento `__TEXT` del tuo programma dopo che i tuoi
hook sono stati disinstallati. Una delle cose che xnuspy fa per gestire questo è non
deallocare immediatamente questo mapping dopo la morte del tuo programma di hook. Invece, viene
aggiunto alla fine di una coda. Una volta che il thread di garbage collection di xnuspy nota
un limite impostato è stato superato per quanto riguarda quante pagine di mapping sono tenute
in quella coda, inizierà a deallocare dalla parte anteriore della coda e
continuerà fino a quando quel limite non viene più superato. Per impostazione predefinita, questo limite è 1 MB,
ovvero 64 pagine.
Anche se questo aiuta enormemente, più grandi diventano i segmenti `__TEXT` e `__DATA`
del tuo programma di hook, meno è probabile che xnuspy vinca questa gara. Se hai
panic regolarmente e hai un programma di hook piuttosto grande, prova ad aumentare
questo limite aggiungendo `XNUSPY_LEAKED_PAGE_LIMIT=n` prima di `make`. Questo imposterà
questo limite a `n` pagine anziché 64.
### Limiti
xnuspy riserva una pagina di memoria statica del kernel prima che XNU si avvii per le sue strutture `xnuspy_tramp`,
permettendoti di agganciare simultaneamente circa 225 funzioni del kernel. Se ne desideri
di più, puoi aggiungere `XNUSPY_TRAMP_PAGES=n` prima di `make`. Questo dirà a xnuspy di
riservare `n` pagine di memoria statica per le strutture `xnuspy_tramp`. Tuttavia, se
xnuspy deve ripiegare su codice già presente nel kernelcache non utilizzato, questo viene
ignorato. Quando ciò accade è descritto in [Come Funziona](#come-funziona).
### Log
Per qualche motivo, i log di `os_log_with_args` non appaiono nello stream
emesso dallo strumento a riga di comando `oslog`. I log di `kprintf` non
arrivano nemmeno lì, ma *possono* essere visti con `dmesg`. Tuttavia, `dmesg`
non è un feed live, quindi ho scritto `klog`, uno strumento che mostra i log
di `kprintf` in tempo reale. Lo trovi in `klog/`. Raccomando vivamente di usare quello
invece di spammare `dmesg` per i tuoi messaggi `kprintf`.
Se ottieni `open: Resource busy` dopo aver eseguito `klog`, esegui questo comando
`launchctl unload /System/Library/LaunchDaemons/com.apple.syslogd.plist`
e riprova.
Sfortunatamente, non potrai vedere nessun `NSLog` se
`atm_diagnostic_config=0x20000000` è impostato nei bootargs di XNU. `klog` dipende
da questo argomento di avvio. Se vuoi `NSLog` di nuovo, rimuovi quel
argomento di avvio da `pongo_send_command` all'interno di `loader.c`.
### Disinstallazione degli Hook
xnuspy gestirà questo per te. Una volta che un processo termina, tutti gli hook del kernel
che sono stati installati da quel processo vengono disinstallati entro circa un secondo.
### Funzioni del Kernel Agganciabili
La maggior parte dei framework per l'hooking di funzioni hanno una lunghezza minima che rende una data
funzione agganciabile. xnuspy ha questo limite *solo* se prevedi di chiamare la funzione originale
*e* la prima istruzione della funzione agganciata non è `B`. In questo
caso, la lunghezza minima è di otto byte. Altrimenti, non c'è una lunghezza minima.
xnuspy usa `X16` e `X17` per i suoi trampolini, quindi le funzioni del kernel che
si aspettano che questi persistano attraverso le chiamate di funzione non possono essere agganciate (non ce ne sono
molte che si aspettano questo). Se la funzione che desideri agganciare inizia con `BL`,
e intendi chiamare l'originale, puoi farlo solo se eseguire
la funzione originale non modifica `X17`.
### Thread-safety
`xnuspy_ctl` eseguirà un'inizializzazione una tantum la prima volta che viene chiamato
dopo un avvio a freddo. Questa è l'unica parte di xnuspy che è soggetta a race condition poiché
non posso inizializzare staticamente il blocco di lettura/scrittura che uso. Dopo che la prima chiamata
restituisce, qualsiasi chiamata futura è garantita essere thread-safe.
# Come Funziona
Questo è semplificato, ma cattura bene l'idea principale. Un hook di funzione in xnuspy
è una struttura che risiede su una memoria del kernel scrivibile ed eseguibile. Nella maggior parte dei casi,
questa è memoria restituita da `alloc_static` all'interno di pongoOS. Può essere riassunto così:```
struct {
uint64_t replacement;
uint32_t tramp[2];
uint32_t orig[10];
};
Dove replacement è l'indirizzo virtuale del kernel (descritto più avanti) della funzione sostitutiva, tramp è un piccolo trampolino che reindirizza l'esecuzione a replacement, e orig è un trampolino più grande e complesso che rappresenta la funzione originale.
Una delle prime cose che xnuspy fa è determinare dove risiede la sostituzione EL0 all'interno dello spazio degli indirizzi del processo chiamante. Questo viene fatto in modo che le funzioni del kernel possano essere hookate da librerie dinamiche. L'intestazione Mach-O che corrisponde all'indirizzo di quella sostituzione viene salvata.
Successivamente, viene creata una mappatura condivisa utente-kernel dei segmenti __TEXT e __DATA di quella intestazione (così come qualsiasi segmento intermedio, se presente). __TEXT è condiviso in modo da poter chiamare altre funzioni dai tuoi hook. __DATA è condiviso in modo che le modifiche alle variabili globali siano viste sia da EL1 che da EL0.
Poiché questa mappatura è una copia uno-a-uno di __TEXT e __DATA, è facile determinare l'indirizzo della funzione sostitutiva dell'utente su di essa. Dato l'indirizzo dell'intestazione Mach-O del processo chiamante u, l'indirizzo dell'inizio della mappatura condivisa k e l'indirizzo della funzione sostitutiva dell'utente r, applichiamo la seguente formula: replacement = k + (r - u)
Successivamente, replacement è l'indirizzo virtuale del kernel della funzione sostitutiva dell'utente sulla mappatura condivisa e viene scritto nella struttura dell'hook di funzione. xnuspy non reindirizza l'esecuzione all'indirizzo EL0 della funzione sostitutiva perché è estremamente pericoloso: non solo ci mette alla mercé dello scheduler, ma non ci dà alcun controllo sullo scenario in cui un processo con un hook del kernel muore mentre un thread del kernel sta ancora eseguendo sulla sostituzione.
Infine, la mappatura condivisa viene marcata come eseguibile e viene assemblato un branch incondizionato immediato (B). Esso indirizza l'esecuzione all'inizio di tramp e sostituisce la prima istruzione della funzione del kernel ora hookata. Sfortunatamente, questo ci limita a non poter fare branch alle strutture hook a più di 128 MB di distanza da una determinata funzione del kernel. xnuspy controlla questo scenario prima dell'avvio e, se scopre che potrebbe verificarsi, ripiega su codice non utilizzato già presente nella kernelcache per far risiedere lì le strutture hook.
Faccio del mio meglio per assicurarmi che i patchfinder funzionino, quindi se qualcosa non funziona, per favore apri una issue.