Skip to content

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:

  1. 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.
  2. 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.