MASTG-TECH-0084: Depuração
Vindo de um ambiente Linux, você esperaria que a chamada de sistema ptrace fosse tão poderosa quanto você está acostumado, mas, por algum motivo, a Apple decidiu deixá-la incompleta. Depuradores iOS como o LLDB a usam para anexar, avançar passo a passo ou continuar o processo, mas não podem usá-la para ler ou escrever na memória (todas as solicitações PT_READ_* e PT_WRITE* estão ausentes). Em vez disso, eles precisam obter uma chamada de porta de tarefa Mach (chamando task_for_pid com o ID do processo de destino) e depois usar as funções da API de interface de IPC Mach para executar ações como suspender o processo de destino e ler/escrever estados de registradores (thread_get_state/thread_set_state) e memória virtual (mach_vm_read/mach_vm_write).
Para mais informações, você pode consultar o projeto LLVM no GitHub, que contém o código-fonte do LLDB, bem como o Capítulo 5 e 13 de "Mac OS X and iOS Internals: To the Apple's Core" [#levin] e o Capítulo 4 "Tracing and Debugging" de "The Mac Hacker's Handbook" [#miller].
Depuração com LLDB¶
O executável padrão do debugserver que o Xcode instala não pode ser usado para anexar a processos arbitrários (geralmente é usado apenas para depurar aplicativos desenvolvidos internamente e implantados com o Xcode). Para habilitar a depuração de aplicativos de terceiros, a permissão task_for_pid-allow deve ser adicionada ao executável do debugserver para que o processo do depurador possa chamar task_for_pid e obter a porta de tarefa Mach de destino, como visto anteriormente. Uma maneira fácil de fazer isso é adicionar a permissão ao binário do debugserver enviado com o Xcode.
Para obter o executável, monte a seguinte imagem DMG:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/<target-iOS-version>/DeveloperDiskImage.dmg
Você encontrará o executável do debugserver no diretório /usr/bin/ no volume montado. Copie-o para um diretório temporário, depois crie um arquivo chamado entitlements.plist com o seguinte conteúdo:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/ PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>run-unsigned-code</key>
<true/>
<key>get-task-allow</key>
<true/>
<key>task_for_pid-allow</key>
<true/>
</dict>
</plist>
Aplique a permissão com codesign:
codesign -s - --entitlements entitlements.plist -f debugserver
Copie o binário modificado para qualquer diretório no dispositivo de teste. Os exemplos a seguir usam usbmuxd para encaminhar uma porta local via USB.
iproxy 2222 22
scp -P 2222 debugserver root@localhost:/tmp/
Nota: No iOS 12 e superior, use o seguinte procedimento para assinar o binário do debugserver obtido da imagem do Xcode.
1) Copie o binário do debugserver para o dispositivo via scp, por exemplo, na pasta /tmp.
2) Conecte-se ao dispositivo via SSH e crie o arquivo, nomeado entitlements.xml, com o seguinte conteúdo:
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>platform-application</key>
<true/>
<key>com.apple.private.security.no-container</key>
<true/>
<key>com.apple.private.skip-library-validation</key>
<true/>
<key>com.apple.backboardd.debugapplications</key>
<true/>
<key>com.apple.backboardd.launchapplications</key>
<true/>
<key>com.apple.diagnosticd.diagnostic</key>
<true/>
<key>com.apple.frontboard.debugapplications</key>
<true/>
<key>com.apple.frontboard.launchapplications</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>com.apple.system-task-ports</key>
<true/>
<key>get-task-allow</key>
<true/>
<key>run-unsigned-code</key>
<true/>
<key>task_for_pid-allow</key>
<true/>
</dict>
</plist>
3) Digite o seguinte comando para assinar o binário do debugserver usando ldid:
ldid -Sentitlements.xml debugserver
4) Verifique se o binário do debugserver pode ser executado via o seguinte comando:
./debugserver
Agora você pode anexar o debugserver a qualquer processo em execução no dispositivo.
VP-iPhone-18:/tmp root# ./debugserver *:1234 -a 2670
debugserver-@(#)PROGRAM:debugserver PROJECT:debugserver-320.2.89
for armv7.
Attaching to process 2670...
Com o seguinte comando, você pode iniciar um aplicativo via debugserver em execução no dispositivo de destino:
debugserver -x backboard *:1234 /Applications/MobileSMS.app/MobileSMS
Anexe a um aplicativo já em execução:
debugserver *:1234 -a "MobileSMS"
Agora você pode conectar-se ao dispositivo iOS a partir do seu computador host:
(lldb) process connect connect://<ip-of-ios-device>:1234
Digitando image list, você obtém uma lista do executável principal e de todas as bibliotecas dependentes.
Depuração de Aplicativos de Release¶
Na seção anterior, aprendemos como configurar um ambiente de depuração em um dispositivo iOS usando o LLDB. Nesta seção, usaremos essas informações e aprenderemos como depurar um aplicativo de release de terceiros. Continuaremos usando iOS UnCrackable Nível 1 e resolveremos usando um depurador.
Em contraste com uma compilação de depuração, o código compilado para uma compilação de release é otimizado para alcançar o máximo desempenho e o mínimo tamanho de binário. Como uma prática recomendada geral, a maioria dos símbolos de depuração é removida em uma compilação de release, adicionando uma camada de complexidade ao engenharia reversa e depurar os binários.
Devido à ausência dos símbolos de depuração, os nomes dos símbolos estão faltando nas saídas de backtrace, e definir breakpoints simplesmente usando nomes de função não é possível. Felizmente, os depuradores também suportam definir breakpoints diretamente em endereços de memória. Mais adiante nesta seção, aprenderemos como fazer isso e, eventualmente, resolver o desafio crackme.
É necessário algum trabalho de base antes de definir um breakpoint usando endereços de memória. Isso requer a determinação de dois offsets:
- Offset do breakpoint: O offset de endereço do código onde queremos definir um breakpoint. Este endereço é obtido realizando análise estática do código em um desmontador como o Ghidra.
- Offset de deslocamento ASLR: O offset de deslocamento ASLR para o processo atual. Como o offset ASLR é gerado aleatoriamente em cada nova instância de um aplicativo, isso deve ser obtido individualmente para cada sessão de depuração. Isso é determinado usando o próprio depurador.
O iOS é um sistema operacional moderno com múltiplas técnicas implementadas para mitigar ataques de execução de código, sendo uma delas o Address Space Randomization Layout (ASLR). A cada nova execução de um aplicativo, um offset de deslocamento ASLR aleatório é gerado, e várias estruturas de dados do processo são deslocadas por este offset.
O endereço final do breakpoint a ser usado no depurador é a soma dos dois endereços acima (Offset do breakpoint + Offset de deslocamento ASLR). Esta abordagem assume que o endereço base da imagem (discutido em breve) usado pelo desmontador e pelo iOS é o mesmo, o que é verdade na maioria das vezes.
Quando um binário é aberto em um desmontador como o Ghidra, ele carrega um binário emulando o carregador do respectivo sistema operacional. O endereço no qual o binário é carregado é chamado de endereço base da imagem. Todo o código e símbolos dentro deste binário podem ser endereçados usando um offset de endereço constante a partir deste endereço base da imagem. No Ghidra, o endereço base da imagem pode ser obtido determinando o endereço do início de um arquivo Mach-O. Neste caso, é 0x100000000.

O valor da string oculta é armazenado em um rótulo com a flag hidden definida. Na desmontagem, o valor de texto deste rótulo é armazenado no registrador X21, armazenado via mov a partir de X0, no offset 0x100004520. Este é o nosso offset do breakpoint.

Para o segundo endereço, precisamos determinar o offset de deslocamento ASLR para um determinado processo. O offset ASLR pode ser determinado usando o comando LLDB image list -o -f. A saída é mostrada na captura de tela abaixo.

Na saída, a primeira coluna contém o número de sequência da imagem ([X]), a segunda coluna contém o offset ASLR gerado aleatoriamente, enquanto a 3ª coluna contém o caminho completo da imagem e, no final, o conteúdo entre colchetes mostra o endereço base da imagem após adicionar o offset ASLR ao endereço base original da imagem (0x100000000 + 0x70000 = 0x100070000). Você notará que o endereço base da imagem de 0x100000000 é o mesmo que no Ghidra. Agora, para obter o endereço de memória efetivo para uma localização de código, só precisamos adicionar o offset ASLR ao endereço identificado no Ghidra. O endereço efetivo para definir o breakpoint será 0x100004520 + 0x70000 = 0x100074520. O breakpoint pode ser definido usando o comando b 0x100074520.
Na saída acima, você também pode notar que muitos dos caminhos listados como imagens não apontam para o sistema de arquivos no dispositivo iOS. Em vez disso, eles apontam para um determinado local no computador host onde o LLDB está sendo executado. Essas imagens são bibliotecas do sistema para as quais os símbolos de depuração estão disponíveis no computador host para auxiliar no desenvolvimento e depuração de aplicativos (como parte do SDK iOS do Xcode). Portanto, você pode definir breakpoints nessas bibliotecas diretamente usando nomes de função.
Após colocar o breakpoint e executar o aplicativo, a execução será interrompida assim que o breakpoint for atingido. Agora você pode acessar e explorar o estado atual do processo. Neste caso, você sabe pela análise estática anterior que o registrador X0 contém a string oculta, então vamos explorá-lo. No LLDB, você pode imprimir objetos Objective-C usando o comando po (print object).

Voilà, o crackme pode ser facilmente resolvido com a ajuda de análise estática e um depurador. Existem inúmeras funcionalidades implementadas no LLDB, incluindo alterar o valor dos registradores, alterar valores na memória do processo e até automatizar tarefas usando scripts Python.
Oficialmente, a Apple recomenda o uso do LLDB para fins de depuração, mas o GDB também pode ser usado no iOS. As técnicas discutidas acima são aplicáveis ao depurar usando o GDB também, desde que os comandos específicos do LLDB sejam alterados para comandos do GDB.