
um framework de hooking de funções do kernel iOS para dispositivos compatíveis com checkra1n

Saída do log do kernel após compilar e executar example/open1_hook.c
xnuspy é um módulo do pongoOS que instala uma nova chamada de sistema, xnuspy_ctl,
que permite interceptar funções do kernel a partir do espaço do usuário. Suporta iOS 13.x,
iOS 14.x e iOS 15.x no checkra1n 0.12.2 e superiores. Dispositivos 4K não são suportados.
Este módulo neutraliza completamente o KTRR/KPP e torna possível criar memória RWX dentro do EL1. Não use isso no seu dispositivo principal.
Requer libusb: brew install libusb
Execute make no diretório raiz. Ele compilará o carregador e o módulo.
Adicione estas antes de make.
XNUSPY_DEBUG=1
kprintf).XNUSPY_SERIAL=1
IOLog.XNUSPY_LEAKED_PAGE_LIMIT=n
64. Mais informações podem ser encontradas em Depuração de panics do kernel.XNUSPY_TRAMP_PAGES=n
XNUSPY_DEBUG e XNUSPY_SERIAL não dependem um do outro.
Depois de compilar tudo, faça o checkra1n iniciar seu dispositivo em um shell pongo:
/Applications/checkra1n.app/Contents/MacOS/checkra1n -p
No mesmo diretório onde você compilou o carregador e o módulo, execute
loader/loader module/xnuspy. Após isso, o xnuspy fará sua parte e
em alguns segundos seu dispositivo será iniciado. O loader aguardará mais alguns
segundos após emitir xnuspy-getkernelv caso o SEPROM precise ser explorado.
Às vezes, alguns dos meus telefones travavam em "Booting" após a execução do KPF do checkra1n. Ainda não descobri o que causa isso, mas se acontecer, tente novamente. Além disso, se o dispositivo travar após bootx, tente novamente. Por fim, marcar o código compilado de xnuspy_ctl como executável no meu iPhone X rodando iOS 13.3.1 é um pouco instável, mas funciona 100% das vezes nos meus outros telefones. Se você tiver um panic com um aborto de busca de instrução do kernel ao executar seu programa de hook, tente novamente.
O xnuspy irá corrigir uma chamada de sistema enosys para apontar para xnuspy_ctl_tramp. Este é um pequeno trampolim que marca o código compilado de xnuspy_ctl como executável e desvia para ele. Você pode encontrar a implementação de xnuspy_ctl em module/el1/xnuspy_ctl/xnuspy_ctl.c e exemplos no diretório example.
Dentro de include/xnuspy/ está xnuspy_ctl.h, um cabeçalho que define constantes para xnuspy_ctl. Ele deve ser incluído em todos os programas que interceptam funções do kernel.
Você pode usar sysctlbyname para descobrir qual chamada de sistema foi corrigida:```
size_t oldlen = sizeof(long);
long SYS_xnuspy_ctl = 0;
sysctlbyname("kern.xnuspy_ctl_callnum", &SYS_xnuspy_ctl, &oldlen, NULL, 0);
Esta chamada de sistema recebe quatro argumentos, `flavor`, `arg1`, `arg2` e `arg3`.
O flavor pode ser `XNUSPY_CHECK_IF_PATCHED`, `XNUSPY_INSTALL_HOOK`,
`XNUSPY_REGISTER_DEATH_CALLBACK`, `XNUSPY_CALL_HOOKME`, `XNUSPY_CACHE_READ`,
`XNUSPY_KREAD`, `XNUSPY_KWRITE` ou `XNUSPY_GET_CURRENT_THREAD`.
O significado dos próximos três argumentos depende do flavor.
## `XNUSPY_CHECK_IF_PATCHED`
Isto existe para que você possa verificar se `xnuspy_ctl` está presente. Invocá-lo com este
flavor fará com que retorne `999`. Os valores dos outros argumentos são
ignorados.
## `XNUSPY_INSTALL_HOOK`
Projetei este flavor para corresponder à API do [`MSHookFunction`](http://www.cydiasubstrate.com/api/c/MSHookFunction/).
`arg1` é o endereço *NÃO DESLIZADO* da função do kernel que você deseja hookear. Se você
fornecer um endereço deslizado, muito provavelmente causará um pânico. `arg2` é um ponteiro para sua
função de substituição compatível com ABI. `arg3` é um ponteiro para `xnuspy_ctl`
`copyout` o endereço de um trampolim que representa a função original do
kernel. Isto pode ser `NULL` se você não pretende chamar a original.
## `XNUSPY_REGISTER_DEATH_CALLBACK`
Este flavor permite registrar um "callback de morte" opcional, uma função que xnuspy
chamará quando seu programa de hook for encerrado. Ele dá a você uma chance de limpar qualquer coisa
que você criou a partir dos seus hooks do kernel. Se você criou quaisquer threads do kernel, você deve
dizer a elas para terminarem nesta função.
Seu callback não é invocado de forma assíncrona, portanto, se você bloquear, estará impedindo
a thread de coleta de lixo do xnuspy de executar.
`arg1` é um ponteiro para sua função de callback. Os valores dos outros argumentos
são ignorados.
## `XNUSPY_CALL_HOOKME`
`hookme` é um pequeno stub de montagem que xnuspy exporta através do cache xnuspy
para você hookear. Invocar `xnuspy_ctl` com este flavor fará com que `hookme` seja
chamado, fornecendo uma maneira de você obter facilmente execução de código no kernel sem
precisar hookear uma função real do kernel.
`arg1` é um argumento que será passado para `hookme` quando for invocado.
Isto pode ser `NULL`.
## `XNUSPY_CACHE_READ`
Este flavor oferece uma maneira de ler do cache xnuspy. Ele contém muitas coisas
úteis como `kprintf`, `current_proc`, `kernel_thread_start`, algumas funções libc,
e o slide do kernel para que você não precise encontrá-los por conta própria. Para uma lista completa
de IDs do cache, confira `example/xnuspy_ctl.h`.
`arg1` é um dos IDs de cache definidos em `xnuspy_ctl.h` e `arg2` é um
ponteiro para `xnuspy_ctl` `copyout` o endereço ou valor do que você solicitou.
Os valores dos outros argumentos são ignorados.
## `XNUSPY_KREAD`
Este flavor oferece uma maneira fácil de ler memória do kernel do userspace sem
tfp0.
`arg1` é um endereço virtual do kernel, `arg2` é o endereço de um buffer no userspace,
e `arg3` é o tamanho desse buffer no userspace. `arg3` bytes serão escritos
de `arg1` para `arg2`.
## `XNUSPY_KWRITE`
Este flavor oferece uma maneira fácil de escrever na memória do kernel a partir do userspace sem
tfp0.
`arg1` é um endereço virtual do kernel, `arg2` é o endereço de um buffer no userspace,
e `arg3` é o tamanho desse buffer no userspace. `arg3` bytes serão escritos
de `arg2` para `arg1`.
## `XNUSPY_GET_CURRENT_THREAD`
Este flavor fornece ao userspace o endereço do kernel da thread chamadora.
`arg1` é um ponteiro para `xnuspy_ctl` `copyout` o valor de retorno de
`current_thread`. Os valores dos outros argumentos são ignorados.
### Erros
Para todos os flavors exceto `XNUSPY_CHECK_IF_PATCHED`, `0` é retornado em caso de sucesso.
Em caso de erro, `-1` é retornado e `errno` é definido. `XNUSPY_CHECK_IF_PATCHED`
não retorna nenhum erro. O `mach_to_bsd_errno` do XNU é usado para converter um
`kern_return_t` no `errno` apropriado.
#### Erros Relativos a `XNUSPY_INSTALL_HOOK`
`errno` é definido como...
- `EEXIST` se:
- Já existe um hook para a função do kernel não deslizada indicada por `arg1`.
- `ENOMEM` se:
- `unified_kalloc` retornou `NULL`.
- `ENOSPC` se:
- Não há structs `xnuspy_tramp` livres, uma estrutura de dados interna do
xnuspy. Isto não deve acontecer a menos que você esteja hookeando centenas de funções do kernel
*ao mesmo tempo*. Se precisar de mais hooks de função, confira [Limites](#limits).
- `ENOTSUP` se:
- O chamador não é de um executável Mach-O ou biblioteca dinâmica.
- `ENOENT` se:
- `mh_for_addr` não conseguiu determinar o cabeçalho Mach-O correspondente a
`arg2` dentro do espaço de endereçamento do chamador.
- `EFAULT` se:
- O cabeçalho Mach-O determinado não é realmente um cabeçalho Mach-O. Isto provavelmente
nunca acontecerá.
- `EIO` se:
- `mach_make_memory_entry_64` não retornou uma entrada de memória para a totalidade
dos segmentos `__TEXT` e `__DATA` do cabeçalho Mach-O determinado.
`errno` também depende do valor de retorno de `vm_map_wire_external`,
`mach_vm_map_external`, `mach_make_memory_entry_64`, `copyin`, `copyout` e,
se aplicável, da função de inicialização única.
Se este flavor retornar um erro, a função alvo do kernel não foi hookeada.
Se você passou um ponteiro não-`NULL` para `arg3`, ele pode ou não ter sido
inicializado. É inseguro usá-lo se tiver sido.
#### Erros Relativos a `XNUSPY_REGISTER_DEATH_CALLBACK`
`errno` é definido como...
- `ENOENT` se:
- O processo chamador não hookeou nenhuma função do kernel.
Se este flavor retornar um erro, seu callback de morte não foi registrado.
#### Erros Relativos a `XNUSPY_CALL_HOOKME`
`errno` é definido como...
- `ENOTSUP` se:
- `hookme` está muito distante da memória que contém as estruturas `xnuspy_tramp`.
Isto é determinado dentro do pongoOS, e só pode acontecer se
xnuspy teve que recorrer a código não utilizado já dentro do kernelcache.
Nesse caso, chamar `hookme` quase certamente causaria um pânico do kernel,
e você terá que encontrar outra função do kernel para hookear.
Se este flavor retornar um erro, `hookme` não foi chamado.
#### Erros Relativos a `XNUSPY_CACHE_READ`
`errno` é definido como...
- `EINVAL` se:
- A constante indicada por `arg1` não representa nada no cache.
- `arg1` era `IO_LOCK`, mas o kernel é iOS 14.4.2 ou inferior ou iOS 15.x.
- `arg1` era `IPC_OBJECT_LOCK`, mas o kernel é iOS 15.x.
- `arg1` era `IPC_PORT_RELEASE_SEND`, mas o kernel é iOS 14.5 ou superior.
- `arg1` era `IPC_PORT_RELEASE_SEND_AND_UNLOCK`, mas o kernel é iOS 14.4.2 ou inferior.
- `arg1` era `KALLOC_CANBLOCK`, mas o kernel é iOS 14.x ou superior.
- `arg1` era `KALLOC_EXTERNAL`, mas o kernel é iOS 13.x.
- `arg1` era `KFREE_ADDR`, mas o kernel é iOS 14.x ou superior.
- `arg1` era `KFREE_EXT`, mas o kernel é iOS 13.x.
- `arg1` era `PROC_REF`, mas o kernel é iOS 14.8 ou inferior.
- `arg1` era `PROC_REF_LOCKED`, mas o kernel é iOS 15.x.
- `arg1` era `PROC_RELE`, mas o kernel é iOS 14.8 ou inferior.
- `arg1` era `PROC_RELE_LOCKED`, mas o kernel é iOS 15.x.
- `arg1` era `VM_MAP_UNWIRE`, mas o kernel é iOS 15.x.
- `arg1` era `VM_MAP_UNWIRE_NESTED`, mas o kernel é iOS 14.8 ou inferior.
`errno` também depende do valor de retorno de `copyout` e, se aplicável, do
valor de retorno da função de inicialização única.
Se este flavor retornar um erro, o ponteiro que você passou para `arg2` não foi
inicializado.
#### Erros Relativos a `XNUSPY_KREAD` e `XNUSPY_KWRITE`
`errno` é definido como...
- `EFAULT` se:
- A tradução de endereço falhou para `arg1` ou `arg2`. Se você compilou com
`XNUSPY_DEBUG=1`, uma mensagem sobre isso é impressa no log do kernel.
Se este flavor retornar um erro, a memória do kernel não foi lida/escrita.
#### Erros Relativos a `XNUSPY_GET_CURRENT_THREAD`
Se `copyout` falhar, `errno` é definido como seu valor de retorno.
# Informações Importantes
### Armadilhas Comuns
Ao escrever funções de substituição, era fácil esquecer que estava escrevendo
código do kernel. Aqui estão algumas coisas a ter em mente ao escrever hooks:
- *Você não pode executar nenhum código do userspace que viva fora do segmento
`__TEXT` do seu programa*. Você causará pânico se, por exemplo, acidentalmente chamar `printf`
em vez de `kprintf`. Você precisa reimplementar qualquer função libc que deseje chamar
se essa função já não estiver disponível via `XNUSPY_CACHE_READ`.
No entanto, você pode criar ponteiros de função para outras funções do kernel e chamá-las.
- *Muitas macros comumente usadas no código do userspace são inseguras para o kernel.* Por
exemplo, `PAGE_SIZE` expande para `vm_page_size`, não uma constante. Você precisa
desabilitar PAN (em A10+, o que também não recomendo fazer) antes de ler esta
variável ou você causará pânico.
- *Certifique-se de compilar seu código com `-fno-stack-protector` e `-D_FORTIFY_SOURCE=0`* Em alguns casos,
o dispositivo terá que ler `___stack_chk_guard` desreferenciando outro ponteiro do userspace,
o que causará pânico em A10+.
- *Por segurança, não compile seus programas de hook com otimizações do compilador.*
Também é recomendado dar uma olhada em https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/style/style.html.
### Depurando Pânicos do Kernel
Erros são inevitáveis ao escrever código, então eventualmente você vai causar um
pânico do kernel. Um pânico não significa necessariamente que há um bug no xnuspy, então
antes de abrir uma issue, por favor, certifique-se de que você ainda causa pânico quando não fizer
nada além de chamar a função original e retornar seu valor (se necessário). Se
você ainda causar pânico, então provavelmente é um bug do xnuspy (e por favor abra uma issue),
mas se não, há algo errado com sua substituição.
Como xnuspy na verdade não redireciona a execução para páginas EL0, depurar
um pânico não é tão direto. Abra `module/el1/xnuspy_ctl/xnuspy_ctl.c`,
e logo antes da única chamada para `kwrite_instr` em `xnuspy_install_hook`,
adicione uma chamada para `IOSleep` por alguns segundos. Isso é feito para garantir que haja
tempo suficiente antes de o dispositivo entrar em pânico para que os logs sejam propagados. Recompile o xnuspy com
`XNUSPY_DEBUG=1 make -B` e carregue o módulo novamente. Após carregar o módulo,
se você ainda não tiver feito, compile `klog` de `klog/`. Faça upload para seu dispositivo
e execute `stdbuf -o0 ./klog | grep shared_mapping_kva`. Execute seu programa de hook novamente
e observe uma linha de `klog` que se pareça com esta:
`shared_mapping_kva: dist 0x7af4 uaddr 0x104797af4 umh 0x104790000 kmh 0xfffffff00c90c000`
Se você estiver instalando mais de um hook, haverá mais de uma ocorrência.
Nesse caso, `dist` e `uaddr` variarão, mas `umh` e `kmh` não. `kmh`
aponta para o início do mapeamento do kernel do segmento `__TEXT` do seu programa.
Jogue seu programa de hook em seu desmontador favorito e redefina sua base de modo que seu cabeçalho
Mach-O esteja no endereço de `kmh`. Para IDA Pro, isso é `Edit -> Segments -> Rebase
program...` com `Image base` selecionado. Após seu dispositivo entrar em pânico e reiniciar novamente,
se houver endereços que correspondam ao mapeamento do kernel de sua substituição
no log de pânico, eles coincidirão com a desmontagem. Se não houver nenhum, então
você provavelmente tem algum tipo de corrupção sutil de memória dentro de sua substituição.
xnuspy também não tem como saber se uma thread do kernel ainda está executando (ou executará)
no mapeamento do kernel do segmento `__TEXT` do seu programa após seus hooks serem
desinstalados. Uma das coisas que xnuspy faz para lidar com isso é não
desalocar este mapeamento imediatamente após seu programa de hook morrer. Em vez disso, ele é
adicionado ao final de uma fila. Assim que a thread de coleta de lixo do xnuspy perceber
que um limite definido foi excedido em relação a quantas páginas de mapeamentos estão mantidas
nessa fila, ela começará a desalocar do início da fila e
continuará até que esse limite não seja mais excedido. Por padrão, esse limite é 1 MB,
ou 64 páginas.
Embora isso ajude enormemente, quanto maiores os segmentos `__TEXT` e `__DATA`
do seu programa de hook se tornarem, menor a probabilidade de xnuspy vencer essa corrida. Se você estiver
entrando em pânico regularmente e tiver um programa de hook um tanto grande, tente aumentar
esse limite adicionando `XNUSPY_LEAKED_PAGE_LIMIT=n` antes de `make`. Isso definirá
este limite para `n` páginas em vez de 64.
### Limites
xnuspy reserva uma página de memória estática do kernel antes de o XNU inicializar para suas structs
`xnuspy_tramp`, permitindo que você hookeie simultaneamente cerca de 225 funções do kernel. Se você quiser
mais, pode adicionar `XNUSPY_TRAMP_PAGES=n` antes de `make`. Isso dirá ao xnuspy para
reservar `n` páginas de memória estática para estruturas `xnuspy_tramp`. No entanto, se
xnuspy tiver que recorrer a código não utilizado dentro do kernelcache, então isso
será ignorado. Quando isso acontece é detalhado em [Como Funciona](#how-it-works).
### Registro de Logs
Por alguma razão, logs de `os_log_with_args` não aparecem no fluxo
emitido pela ferramenta de linha de comando `oslog`. Logs de `kprintf` também não
chegam lá, mas eles *podem* ser vistos com `dmesg`. No entanto, `dmesg`
não é um feed ao vivo, então escrevi `klog`, uma ferramenta que mostra logs de `kprintf`
em tempo real. Encontre-a em `klog/`. Recomendo fortemente usar isso em vez
de inundar `dmesg` com suas mensagens `kprintf`.
Se você receber `open: Resource busy` após executar `klog`, execute este comando
`launchctl unload /System/Library/LaunchDaemons/com.apple.syslogd.plist`
e tente novamente.
Infelizmente, você não conseguirá ver nenhum `NSLog` se
`atm_diagnostic_config=0x20000000` estiver definido nos bootargs do XNU. `klog` depende
deste argumento de inicialização estar presente. Se você quiser `NSLog` de volta, remova esse
argumento de inicialização de `pongo_send_command` dentro de `loader.c`.
### Desinstalação de Hook
xnuspy gerenciará isso para você. Assim que um processo for encerrado, todos os hooks do kernel
que foram instalados por esse processo são desinstalados em cerca de um segundo.
### Funções Hookáveis do Kernel
A maioria dos frameworks de hook de função tem algum comprimento mínimo que torna uma determinada
função hookável. xnuspy tem esse limite *apenas* se você planeja chamar a função original
*e* a primeira instrução da função hookeada não for `B`. Neste caso, o comprimento mínimo é de oito bytes. Caso contrário, não há comprimento mínimo.
xnuspy usa `X16` e `X17` para seus trampolins, portanto, funções do kernel que
esperam que esses registradores persistam entre chamadas de função não podem ser hookeadas (não há
muitas que esperam isso). Se a função que você deseja hookear começar com `BL`,
e você pretende chamar a original, só pode fazê-lo se a execução da
função original não modificar `X17`.
### Segurança de Thread
`xnuspy_ctl` realizará uma inicialização única na primeira vez que for chamado
após uma reinicialização recente. Esta é a única parte do xnuspy que está sujeita a condições de corrida,
pois não consigo inicializar estaticamente o bloqueio de leitura/escrita que uso. Após a primeira chamada
retornar, todas as chamadas futuras são garantidas como seguras para threads.
# Como Funciona
Isto é simplificado, mas captura bem a ideia principal. Um hook de função no xnuspy
é uma estrutura que reside em memória do kernel gravável e executável. Na maioria dos casos,
esta é a memória retornada por `alloc_static` dentro do pongoOS. Pode ser resumido
a isto:```
struct {
uint64_t replacement;
uint32_t tramp[2];
uint32_t orig[10];
};
Onde replacement é o endereço virtual do kernel (detalhado posteriormente) da
função de substituição, tramp é um pequeno trampolim que redireciona a execução para
replacement, e orig é um trampolim maior e mais complicado que representa
a função original.
Uma das primeiras coisas que o xnuspy faz é determinar onde a substituição em EL0 reside dentro do espaço de endereçamento do processo chamador. Isso é feito para que as funções do kernel possam ser hookeadas a partir de bibliotecas dinâmicas. O cabeçalho Mach-O que corresponde ao endereço dessa substituição é salvo.
Depois, é criado um mapeamento compartilhado usuário-kernel dos segmentos __TEXT e __DATA desse cabeçalho
(bem como de qualquer segmento entre esses, se houver). __TEXT é compartilhado para que você
possa chamar outras funções a partir dos seus hooks. __DATA é compartilhado para que as alterações nas variáveis
globais sejam vistas tanto pelo EL1 quanto pelo EL0.
Como esse mapeamento é uma cópia um-para-um de __TEXT e __DATA, é fácil
descobrir o endereço da função de substituição do usuário nele. Dado o endereço do
cabeçalho Mach-O do processo chamador u, o endereço do início do
mapeamento compartilhado k, e o endereço da função de substituição do usuário r, nós
aplicamos a seguinte fórmula: replacement = k + (r - u)
Depois disso, replacement é o endereço virtual do kernel da função de substituição
do usuário no mapeamento compartilhado e é escrito na estrutura do hook da função.
O xnuspy não redireciona a execução para o endereço EL0 da função de substituição
porque isso é extremamente inseguro: não apenas nos coloca à mercê do escalonador,
mas também não nos dá controle sobre o cenário em que um processo
com um hook do kernel morre enquanto uma thread do kernel ainda está executando na
substituição.
Finalmente, o mapeamento compartilhado é marcado como executável e um desvio
incondicional imediato (B) é montado. Ele direciona a execução para o início de tramp,
e é o que substitui a primeira instrução da função do kernel agora hookeada.
Infelizmente, isso nos limita a desviar para estruturas de hook a mais de 128 MB de distância
de uma determinada função do kernel. O xnuspy verifica esse cenário antes de inicializar
e, se descobrir que isso pode acontecer, usa código não utilizado já presente no kernelcache para que as estruturas de hook
residam lá.
Faço o meu melhor para garantir que os patchfinders funcionem; portanto, se algo não estiver funcionando, por favor, abra uma issue.