
un framework de hooking de funciones del kernel de iOS para dispositivos checkra1n'able

Salida del registro del kernel después de compilar y ejecutar example/open1_hook.c
xnuspy es un módulo de pongoOS que instala una nueva llamada al sistema, xnuspy_ctl, que permite enganchar funciones del kernel desde el espacio de usuario. Es compatible con iOS 13.x, iOS 14.x y iOS 15.x en checkra1n 0.12.2 y superiores. Los dispositivos 4K no son compatibles.
Este módulo neutraliza completamente KTRR/KPP y hace posible crear memoria RWX dentro de EL1. No lo uses en tu dispositivo principal.
Requiere libusb: brew install libusb
Ejecuta make en el directorio raíz. Esto compilará el cargador y el módulo.
Añade estos antes de make.
XNUSPY_DEBUG=1
kprintf).XNUSPY_SERIAL=1
IOLog.XNUSPY_LEAKED_PAGE_LIMIT=n
64. Más información se puede encontrar en Depuración de pánicos del kernel.XNUSPY_TRAMP_PAGES=n
XNUSPY_DEBUG y XNUSPY_SERIAL no dependen el uno del otro.
Después de haber construido todo, haz que checkra1n arranque tu dispositivo en un shell pongo: /Applications/checkra1n.app/Contents/MacOS/checkra1n -p
En el mismo directorio donde construiste el cargador y el módulo, ejecuta loader/loader module/xnuspy. Después de hacer eso, xnuspy hará lo suyo y en unos segundos tu dispositivo arrancará. loader esperará un par de segundos más después de emitir xnuspy-getkernelv en caso de que SEPROM necesite ser explotado.
A veces un par de mis teléfonos se quedaban atascados en "Booting" después de que se ejecuta el KPF de checkra1n. Aún no he descubierto qué causa esto, pero si sucede, inténtalo de nuevo. Además, si el dispositivo se cuelga después de bootx, inténtalo de nuevo. Finalmente, marcar el código compilado de xnuspy_ctl como ejecutable en mi iPhone X con iOS 13.3.1 es un poco irregular, pero tiene éxito el 100% de las veces en mis otros teléfonos. Si obtienes un pánico con una falla de recuperación de instrucción del kernel cuando ejecutas tu programa de enganche, inténtalo de nuevo.
xnuspy parcheará una llamada al sistema enosys para que apunte a xnuspy_ctl_tramp. Este es un pequeño trampolín que marca el código compilado de xnuspy_ctl como ejecutable y salta a él. Puedes encontrar la implementación de xnuspy_ctl en module/el1/xnuspy_ctl/xnuspy_ctl.c y ejemplos en el directorio example.
Dentro de include/xnuspy/ está xnuspy_ctl.h, un archivo de cabecera que define constantes para xnuspy_ctl. Está destinado a ser incluido en todos los programas que enganchan funciones del kernel.
Puedes usar sysctlbyname para averiguar qué llamada al sistema fue parcheada:```
size_t oldlen = sizeof(long);
long SYS_xnuspy_ctl = 0;
sysctlbyname("kern.xnuspy_ctl_callnum", &SYS_xnuspy_ctl, &oldlen, NULL, 0);
Esta llamada al sistema toma cuatro argumentos: `flavor`, `arg1`, `arg2` y `arg3`.
El `flavor` puede ser `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`.
El significado de los siguientes tres argumentos depende del `flavor`.
## `XNUSPY_CHECK_IF_PATCHED`
Esto existe para que puedas comprobar si `xnuspy_ctl` está presente. Invocarlo con este
`flavor` hará que devuelva `999`. Los valores de los otros argumentos se ignoran.
## `XNUSPY_INSTALL_HOOK`
Diseñé este `flavor` para que coincida con la API de [`MSHookFunction`](http://www.cydiasubstrate.com/api/c/MSHookFunction/).
`arg1` es la dirección *sin slide* de la función del kernel que deseas enganchar. Si
proporcionas una dirección con slide, lo más probable es que causes un pánico. `arg2` es un puntero a tu
función de reemplazo compatible con ABI. `arg3` es un puntero para que `xnuspy_ctl`
`copyout` la dirección de un trampolín que representa la función original del kernel.
Puede ser `NULL` si no tienes intención de llamar a la original.
## `XNUSPY_REGISTER_DEATH_CALLBACK`
Este `flavor` te permite registrar un "callback de muerte" opcional, una función que xnuspy
llamará cuando tu programa de hooks termine. Te da una oportunidad para limpiar cualquier cosa
que hayas creado desde tus hooks del kernel. Si creaste algún hilo del kernel, deberías
indicarles que terminen en esta función.
Tu callback no se invoca de forma asíncrona, así que si te bloqueas, estás impidiendo
que el hilo de recolección de basura de xnuspy se ejecute.
`arg1` es un puntero a tu función callback. Los valores de los otros argumentos
se ignoran.
## `XNUSPY_CALL_HOOKME`
`hookme` es un pequeño stub en ensamblador que xnuspy exporta a través de la caché de xnuspy
para que lo enganches. Invocar `xnuspy_ctl` con este `flavor` hará que se llame a `hookme`,
proporcionándote una forma de obtener fácilmente ejecución de código en el kernel sin
tener que enganchar una función real del kernel.
`arg1` es un argumento que se pasará a `hookme` cuando se invoque.
Puede ser `NULL`.
## `XNUSPY_CACHE_READ`
Este `flavor` te da una forma de leer desde la caché de xnuspy. Contiene muchas cosas
útiles como `kprintf`, `current_proc`, `kernel_thread_start`, algunas funciones de libc,
y el slide del kernel para que no tengas que encontrarlos tú mismo. Para una lista completa
de los IDs de caché, consulta `example/xnuspy_ctl.h`.
`arg1` es uno de los IDs de caché definidos en `xnuspy_ctl.h` y `arg2` es un
puntero para que `xnuspy_ctl` `copyout` la dirección o el valor de lo que solicitaste.
Los valores de los otros argumentos se ignoran.
## `XNUSPY_KREAD`
Este `flavor` te da una forma sencilla de leer memoria del kernel desde espacio de usuario sin
tfp0.
`arg1` es una dirección virtual del kernel, `arg2` es la dirección de un buffer de espacio de usuario,
y `arg3` es el tamaño de ese buffer de espacio de usuario. Se escribirán `arg3` bytes
desde `arg1` hasta `arg2`.
## `XNUSPY_KWRITE`
Este `flavor` te da una forma sencilla de escribir en la memoria del kernel desde espacio de usuario sin
tfp0.
`arg1` es una dirección virtual del kernel, `arg2` es la dirección de un buffer de espacio de usuario,
y `arg3` es el tamaño de ese buffer de espacio de usuario. Se escribirán `arg3` bytes
desde `arg2` hasta `arg1`.
## `XNUSPY_GET_CURRENT_THREAD`
Este `flavor` proporciona al espacio de usuario la dirección del kernel del hilo que realiza la llamada.
`arg1` es un puntero para que `xnuspy_ctl` `copyout` el valor de retorno de
`current_thread`. Los valores de los otros argumentos se ignoran.
### Errores
Para todos los `flavor` excepto `XNUSPY_CHECK_IF_PATCHED`, se devuelve `0` en caso de éxito.
Ante un error, se devuelve `-1` y se establece `errno`. `XNUSPY_CHECK_IF_PATCHED`
no devuelve ningún error. Se utiliza `mach_to_bsd_errno` de XNU para convertir un
`kern_return_t` al `errno` correspondiente.
#### Errores relacionados con `XNUSPY_INSTALL_HOOK`
`errno` se establece en...
- `EEXIST` si:
- Ya existe un hook para la función del kernel sin slide indicada por `arg1`.
- `ENOMEM` si:
- `unified_kalloc` devolvió `NULL`.
- `ENOSPC` si:
- No hay structs `xnuspy_tramp` libres, una estructura de datos interna de
xnuspy. Esto no debería ocurrir a menos que estés enganchando cientos de funciones del kernel
*al mismo tiempo*. Si necesitas más hooks de funciones, consulta [Límites](#limits).
- `ENOTSUP` si:
- Quien llama no proviene de un ejecutable Mach-O o una biblioteca dinámica.
- `ENOENT` si:
- `mh_for_addr` no pudo determinar el encabezado Mach-O correspondiente a
`arg2` dentro del espacio de direcciones de quien llama.
- `EFAULT` si:
- El encabezado Mach-O determinado no es realmente un encabezado Mach-O. Esto probablemente
nunca sucederá.
- `EIO` si:
- `mach_make_memory_entry_64` no devolvió una entrada de memoria para la totalidad
de los segmentos `__TEXT` y `__DATA` del encabezado Mach-O determinado.
`errno` también depende del valor de retorno de `vm_map_wire_external`,
`mach_vm_map_external`, `mach_make_memory_entry_64`, `copyin`, `copyout`, y
si corresponde, de la función de inicialización única.
Si este `flavor` devuelve un error, la función del kernel objetivo no fue enganchada.
Si pasaste un puntero no `NULL` para `arg3`, puede que haya sido inicializado o no.
No es seguro usarlo si lo fue.
#### Errores relacionados con `XNUSPY_REGISTER_DEATH_CALLBACK`
`errno` se establece en...
- `ENOENT` si:
- El proceso que llama no ha enganchado ninguna función del kernel.
Si este `flavor` devuelve un error, tu callback de muerte no fue registrado.
#### Errores relacionados con `XNUSPY_CALL_HOOKME`
`errno` se establece en...
- `ENOTSUP` si:
- `hookme` está demasiado lejos de la memoria que contiene las estructuras `xnuspy_tramp`.
Esto se determina dentro de pongoOS, y solo puede suceder si
xnuspy tuvo que recurrir a código no utilizado ya dentro del kernelcache.
En ese caso, llamar a `hookme` casi con seguridad causaría un pánico del kernel,
y tendrás que encontrar otra función del kernel para enganchar.
Si este `flavor` devuelve un error, `hookme` no fue llamado.
#### Errores relacionados con `XNUSPY_CACHE_READ`
`errno` se establece en...
- `EINVAL` si:
- La constante indicada por `arg1` no representa nada en la caché.
- `arg1` era `IO_LOCK`, pero el kernel es iOS 14.4.2 o inferior o iOS 15.x.
- `arg1` era `IPC_OBJECT_LOCK`, pero el kernel es iOS 15.x.
- `arg1` era `IPC_PORT_RELEASE_SEND`, pero el kernel es iOS 14.5 o superior.
- `arg1` era `IPC_PORT_RELEASE_SEND_AND_UNLOCK`, pero el kernel es iOS 14.4.2 o inferior.
- `arg1` era `KALLOC_CANBLOCK`, pero el kernel es iOS 14.x o superior.
- `arg1` era `KALLOC_EXTERNAL`, pero el kernel es iOS 13.x.
- `arg1` era `KFREE_ADDR`, pero el kernel es iOS 14.x o superior.
- `arg1` era `KFREE_EXT`, pero el kernel es iOS 13.x.
- `arg1` era `PROC_REF`, pero el kernel es iOS 14.8 o inferior.
- `arg1` era `PROC_REF_LOCKED`, pero el kernel es iOS 15.x.
- `arg1` era `PROC_RELE`, pero el kernel es iOS 14.8 o inferior.
- `arg1` era `PROC_RELE_LOCKED`, pero el kernel es iOS 15.x.
- `arg1` era `VM_MAP_UNWIRE`, pero el kernel es iOS 15.x.
- `arg1` era `VM_MAP_UNWIRE_NESTED`, pero el kernel es iOS 14.8 o inferior.
`errno` también depende del valor de retorno de `copyout` y, si corresponde, del
valor de retorno de la función de inicialización única.
Si este `flavor` devuelve un error, el puntero que pasaste para `arg2` no fue
inicializado.
#### Errores relacionados con `XNUSPY_KREAD` y `XNUSPY_KWRITE`
`errno` se establece en...
- `EFAULT` si:
- La traducción de direcciones falló para `arg1` o `arg2`. Si compilaste con
`XNUSPY_DEBUG=1`, se imprime un mensaje al respecto en el registro del kernel.
Si este `flavor` devuelve un error, la memoria del kernel no fue leída/escrita.
#### Errores relacionados con `XNUSPY_GET_CURRENT_THREAD`
Si `copyout` falla, `errno` se establece en su valor de retorno.
# Información importante
### Errores comunes
Al escribir funciones de reemplazo, era fácil olvidar que estaba escribiendo
código del kernel. Aquí hay un par de cosas a tener en cuenta cuando escribas hooks:
- *No puedes ejecutar ningún código de espacio de usuario que viva fuera del segmento
`__TEXT` de tu programa*. Entrarás en pánico si, por ejemplo, accidentalmente llamas a `printf`
en lugar de `kprintf`. Necesitas reimplementar cualquier función de libc que quieras llamar
si esa función no está ya disponible mediante `XNUSPY_CACHE_READ`.
Sin embargo, puedes crear punteros a función a otras funciones del kernel y llamar a esas.
- *Muchas macros comunes en código de espacio de usuario no son seguras para el kernel.* Por
ejemplo, `PAGE_SIZE` se expande a `vm_page_size`, no una constante. Necesitas deshabilitar
PAN (en A10+, lo que tampoco recomiendo hacer) antes de leer esta variable o entrarás en pánico.
- *Asegúrate de compilar tu código con `-fno-stack-protector` y `-D_FORTIFY_SOURCE=0`*. En algunos casos,
el dispositivo tendrá que leer `___stack_chk_guard` desreferenciando otro puntero
de espacio de usuario, lo que provocará pánico en A10+.
- *Por si acaso, no compiles tus programas de hook con optimizaciones del compilador.*
También se recomienda echar un vistazo a https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/style/style.html.
### Depuración de pánicos del kernel
Los errores son inevitables al escribir código, así que eventualmente causarás un
pánico del kernel. Un pánico no significa necesariamente que haya un error en xnuspy, así que
antes de abrir un issue, asegúrate de que todavía entras en pánico cuando no haces
nada más que llamar a la función original y devolver su valor (si es necesario). Si
aún entras en pánico, entonces probablemente sea un error de xnuspy (y por favor abre un issue),
pero si no, hay algo mal en tu reemplazo.
Dado que xnuspy no redirige realmente la ejecución a páginas EL0, depurar
un pánico no es tan directo. Abre `module/el1/xnuspy_ctl/xnuspy_ctl.c`,
y justo antes de la única llamada a `kwrite_instr` en `xnuspy_install_hook`,
agrega una llamada a `IOSleep` de un par de segundos. Esto se hace para asegurarse de que haya
suficiente tiempo antes de que el dispositivo entre en pánico para que los registros se propaguen. Recompila xnuspy con
`XNUSPY_DEBUG=1 make -B` y carga el módulo nuevamente. Después de cargar el módulo,
si aún no lo has hecho, compila `klog` desde `klog/`. Súbelo a tu dispositivo
y ejecuta `stdbuf -o0 ./klog | grep shared_mapping_kva`. Ejecuta tu programa de hook nuevamente
y busca una línea de `klog` que se vea así:
`shared_mapping_kva: dist 0x7af4 uaddr 0x104797af4 umh 0x104790000 kmh 0xfffffff00c90c000`
Si estás instalando más de un hook, habrá más de una ocurrencia.
En ese caso, `dist` y `uaddr` variarán, pero `umh` y `kmh` no. `kmh`
apunta al comienzo del mapeo del kernel del segmento `__TEXT` de tu programa.
Abre tu programa de hook en tu desensamblador favorito y reubicarlo para que su encabezado Mach-O
esté en la dirección de `kmh`. Para IDA Pro, eso es `Edit -> Segments -> Rebase
program...` con `Image base` activado. Después de que tu dispositivo entre en pánico y se reinicie nuevamente,
si hay direcciones que corresponden al mapeo del kernel de tu reemplazo
en el registro de pánico, coincidirán con el desensamblado. Si no hay ninguna, entonces
probablemente tienes algún tipo de corrupción de memoria sutil dentro de tu reemplazo.
xnuspy tampoco tiene forma de saber si un hilo del kernel todavía está ejecutando (o ejecutará)
en el mapeo del kernel del segmento `__TEXT` de tu programa después de que tus hooks
se desinstalen. Una de las cosas que xnuspy hace para lidiar con esto es no
desasignar este mapeo inmediatamente después de que tu programa de hook muera. En su lugar, se
agrega al final de una cola. Una vez que el hilo de recolección de basura de xnuspy nota
que se ha excedido un límite establecido con respecto a cuántas páginas de mapeos se mantienen
en esa cola, comenzará a desasignar desde el frente de la cola y
continuará hasta que ese límite ya no se exceda. Por defecto, este límite es 1 MB,
o 64 páginas.
Si bien esto ayuda enormemente, cuanto más grandes sean los segmentos `__TEXT` y `__DATA`
de tu programa de hook, menos probable es que xnuspy gane esta carrera. Si estás
entrando en pánico regularmente y tienes un programa de hook algo grande, intenta
aumentar este límite agregando `XNUSPY_LEAKED_PAGE_LIMIT=n` antes de `make`. Esto establecerá
este límite en `n` páginas en lugar de 64.
### Límites
xnuspy reserva una página de memoria estática del kernel antes de que XNU arranque para sus structs
`xnuspy_tramp`, permitiéndote enganchar simultáneamente alrededor de 225 funciones del kernel. Si quieres
más, puedes agregar `XNUSPY_TRAMP_PAGES=n` antes de `make`. Esto le indicará a xnuspy que
reserve `n` páginas de memoria estática para las estructuras `xnuspy_tramp`. Sin embargo, si
xnuspy tiene que recurrir a código no utilizado ya dentro del kernelcache, entonces esto
se ignora. Cuándo sucede esto se detalla en [Cómo funciona](#how-it-works).
### Registro
Por alguna razón, los registros de `os_log_with_args` no aparecen en el flujo
de salida de la herramienta de línea de comandos `oslog`. Los registros de `kprintf` tampoco
llegan allí, pero *se pueden* ver con `dmesg`. Sin embargo, `dmesg`
no es un feed en vivo, así que escribí `klog`, una herramienta que muestra los registros de `kprintf`
en tiempo real. Encuéntrala en `klog/`. Recomiendo encarecidamente usar esa en lugar
de spamear `dmesg` para tus mensajes `kprintf`.
Si obtienes `open: Resource busy` después de ejecutar `klog`, ejecuta este comando
`launchctl unload /System/Library/LaunchDaemons/com.apple.syslogd.plist`
e inténtalo de nuevo.
Desafortunadamente, no podrás ver ningún `NSLog` si
`atm_diagnostic_config=0x20000000` está establecido en los bootargs de XNU. `klog` depende
de que este argumento de arranque esté presente. Si quieres recuperar `NSLog`, elimina ese
argumento de arranque de `pongo_send_command` dentro de `loader.c`.
### Desinstalación de hooks
xnuspy gestionará esto por ti. Una vez que un proceso termina, todos los hooks del kernel
que fueron instalados por ese proceso se desinstalan en aproximadamente un segundo.
### Funciones del kernel enganchables
La mayoría de los frameworks de hooking de funciones tienen alguna longitud mínima que hace que una función
dada sea enganchable. xnuspy tiene este límite *solo* si planeas llamar a la función original
*y* la primera instrucción de la función enganchada no es `B`. En ese caso,
la longitud mínima es de ocho bytes. De lo contrario, no hay longitud mínima.
xnuspy usa `X16` y `X17` para sus trampolines, por lo que las funciones del kernel que
esperan que estos persistan entre llamadas a funciones no se pueden enganchar (no hay
muchas que esperen esto). Si la función que quieres enganchar comienza con `BL`,
y tienes la intención de llamar a la original, solo puedes hacerlo si ejecutar la
función original no modifica `X17`.
### Seguridad de hilos
`xnuspy_ctl` realizará una inicialización única la primera vez que se llame
después de un arranque reciente. Esta es la única parte de xnuspy que es susceptible a condiciones de carrera ya que
no puedo inicializar estáticamente el bloqueo de lectura/escritura que uso. Después de que la primera
llamada regrese, cualquier llamada futura estará garantizada para ser segura en cuanto a hilos.
# Cómo funciona
Esto está simplificado, pero captura bien la idea principal. Un hook de función en xnuspy
es una estructura que reside en memoria de kernel escribible y ejecutable. En la mayoría de los casos,
esta es memoria devuelta por `alloc_static` dentro de pongoOS. Se puede resumir en
esto:```
struct {
uint64_t replacement;
uint32_t tramp[2];
uint32_t orig[10];
};
Donde replacement es la dirección virtual del kernel (que se detalla más adelante) de la función de reemplazo, tramp es un pequeño trampolín que redirige la ejecución a replacement, y orig es un trampolín más grande y complejo que representa la función original.
Una de las primeras cosas que hace xnuspy es determinar dónde reside el reemplazo de EL0 dentro del espacio de direcciones del proceso llamante. Esto se hace para que las funciones del kernel puedan ser enganchadas desde bibliotecas dinámicas. Se guarda el encabezado Mach-O que corresponde a la dirección de ese reemplazo.
Después, se crea un mapeo compartido usuario-kernel de los segmentos __TEXT y __DATA de ese encabezado (así como cualquier segmento intermedio, si lo hay). __TEXT se comparte para que puedas llamar a otras funciones desde tus hooks. __DATA se comparte para que los cambios en variables globales sean vistos tanto por EL1 como por EL0.
Dado que este mapeo es una copia uno a uno de __TEXT y __DATA, es fácil determinar la dirección de la función de reemplazo del usuario en él. Dada la dirección del encabezado Mach-O del proceso llamante u, la dirección del inicio del mapeo compartido k, y la dirección de la función de reemplazo del usuario r, aplicamos la siguiente fórmula: replacement = k + (r - u)
Después de eso, replacement es la dirección virtual del kernel de la función de reemplazo del usuario en el mapeo compartido y se escribe en la estructura del hook de función. xnuspy no redirige la ejecución a la dirección EL0 de la función de reemplazo porque eso es extremadamente inseguro: no solo nos pone a merced del planificador, sino que no nos da control sobre el escenario donde un proceso con un hook del kernel muere mientras un hilo del kernel todavía está ejecutando el reemplazo.
Finalmente, el mapeo compartido se marca como ejecutable y se ensambla un salto incondicional e inmediato (B). Dirige la ejecución al inicio de tramp, y es lo que reemplaza la primera instrucción de la función del kernel ahora enganchada. Desafortunadamente, esto nos limita a saltar a estructuras de hook a más de 128 MB de distancia de una función del kernel dada. xnuspy verifica este escenario antes de arrancar y recurre a código no utilizado que ya está en el kernelcache para que las estructuras de hook residan allí si detecta que esto podría ocurrir.
Hago todo lo posible para asegurar que los localizadores de parches funcionen, así que si algo no funciona, por favor abre un issue.