Skip to content

MASTG-TECH-0088: Análise Baseada em Emulação

Simulador iOS

A Apple fornece um aplicativo simulador no Xcode que oferece uma interface de usuário com aparência de dispositivo iOS real para iPhone, iPad ou Apple Watch. Ele permite prototipar e testar rapidamente builds de depuração de seus aplicativos durante o processo de desenvolvimento, mas na verdade não é um emulador. A diferença entre um simulador e um emulador foi discutida anteriormente na seção "Análise Dinâmica Baseada em Emulação)".

Durante o desenvolvimento e depuração de um aplicativo, a toolchain do Xcode gera código x86, que pode ser executado no simulador iOS. No entanto, para um build de release, apenas código ARM é gerado (incompatível com o simulador iOS). É por isso que aplicativos baixados da Apple App Store não podem ser usados para qualquer tipo de análise de aplicativo no simulador iOS.

Corellium

O Corellium é uma ferramenta comercial que oferece dispositivos iOS virtuais executando o firmware real do iOS, sendo o único emulador iOS publicamente disponível. Por ser um produto proprietário, não há muita informação disponível sobre a implementação. O Corellium não possui licenças comunitárias disponíveis, portanto não entraremos em muitos detalhes sobre seu uso.

O Corellium permite lançar múltiplas instâncias de um dispositivo (com jailbreak ou não) que são acessíveis como dispositivos locais (com uma simples configuração de VPN). Ele tem a capacidade de criar e restaurar snapshots do estado do dispositivo, e também oferece um shell conveniente baseado na web para o dispositivo. Finalmente e mais importante, devido à sua natureza de "emulador", você pode executar aplicativos baixados da Apple App Store, permitindo qualquer tipo de análise de aplicativo como você conhece em dispositivos iOS reais (com jailbreak).

Note que para instalar um IPA em dispositivos Corellium, ele deve estar descriptografado e assinado com um certificado válido de desenvolvedor Apple. Veja mais informações aqui.

Unicorn

Unicorn é um framework leve de emulação de CPU multi-arquitetura baseado em QEMU e vai além dele ao adicionar recursos úteis especialmente feitos para emulação de CPU. O Unicorn fornece a infraestrutura básica necessária para executar instruções do processador. Nesta seção usaremos os bindings Python do Unicorn para resolver o desafio iOS UnCrackable Nível 1.

Para usar o poder total do Unicorn, precisaríamos implementar toda a infraestrutura necessária que geralmente está prontamente disponível do sistema operacional, por exemplo, carregador de binários, linker e outras dependências, ou usar outros frameworks de nível superior como Qiling que aproveita o Unicorn para emular instruções da CPU, mas entende o contexto do SO. No entanto, isso é supérfluo para este desafio muito localizado onde apenas executar uma pequena parte do binário será suficiente.

Ao realizar análise manual em Análise de Código Native Desmontado, determinamos que a função no endereço 0x1000080d4 é responsável por gerar dinamicamente a string secreta. Como estamos prestes a ver, todo o código necessário está praticamente auto-contido no binário, tornando este um cenário perfeito para usar um emulador de CPU como o Unicorn.

Se analisarmos essa função e as chamadas de função subsequentes, observaremos que não há dependência rígida de nenhuma biblioteca externa e também não está realizando nenhuma chamada de sistema. O único acesso externo às funções ocorre, por exemplo, no endereço 0x1000080f4, onde um valor está sendo armazenado no endereço 0x10000dbf0, que mapeia para a seção __data.

Portanto, para emular corretamente esta seção do código, além da seção __text (que contém as instruções), também precisamos carregar a seção __data.

Para resolver o desafio usando Unicorn, realizaremos os seguintes passos:

  • Obter a versão ARM64 do binário executando lipo -thin arm64 <app_binary> -output uncrackable.arm64 (ARMv7 também pode ser usado).
  • Extrair as seções __text e __data do binário.
  • Criar e mapear a memória a ser usada como memória de stack.
  • Criar memória e carregar as seções __text e __data.
  • Executar o binário fornecendo os endereços de início e fim.
  • Finalmente, despejar o valor de retorno da função, que neste caso é nossa string secreta.

Para extrair o conteúdo das seções __text e __data do binário Mach-O, usaremos LIEF, que fornece uma abstração conveniente para manipular múltiplos formatos de arquivo executável. Antes de carregar essas seções na memória, precisamos determinar seus endereços base, por exemplo, usando Ghidra, Radare2 ou IDA Pro.

A partir da tabela acima, usaremos o endereço base 0x10000432c para __text e 0x10000d3e8 para a seção __data para carregá-los na memória.

Ao alocar memória para o Unicorn, os endereços de memória devem ser alinhados em página de 4k e também o tamanho alocado deve ser múltiplo de 1024.

O seguinte script emula a função em 0x1000080d4 e despeja a string secreta:

import lief
from unicorn import *
from unicorn.arm64_const import *

# --- Extrair conteúdo das seções __text e __data do binário ---
binary = lief.parse("uncrackable.arm64")
text_section = binary.get_section("__text")
text_content = text_section.content

data_section = binary.get_section("__data")
data_content = data_section.content

# --- Configurar Unicorn para execução ARM64 ---
arch = "arm64le"
emu = Uc(UC_ARCH_ARM64, UC_MODE_ARM)

# --- Criar memória de Stack ---
addr = 0x40000000
size = 1024*1024
emu.mem_map(addr, size)
emu.reg_write(UC_ARM64_REG_SP, addr + size - 1)

# --- Carregar seção text --
base_addr = 0x100000000
tmp_len = 1024*1024
text_section_load_addr = 0x10000432c
emu.mem_map(base_addr, tmp_len)
emu.mem_write(text_section_load_addr, bytes(text_content))

# --- Carregar seção data ---
data_section_load_addr = 0x10000d3e8
emu.mem_write(data_section_load_addr, bytes(data_content))

# --- Hack para stack_chk_guard ---
# sem isso será lançado erro de leitura de memória inválida em 0x0
emu.mem_map(0x0, 1024)
emu.mem_write(0x0, b"00")


# --- Executar de 0x1000080d4 a 0x100008154 ---
emu.emu_start(0x1000080d4, 0x100008154)
ret_value = emu.reg_read(UC_ARM64_REG_X0)

# --- Despejar valor de retorno ---
print(emu.mem_read(ret_value, 11))

Você pode notar que há uma alocação de memória adicional no endereço 0x0, este é um hack simples para contornar a verificação de stack_chk_guard. Sem isso, haverá um erro de leitura de memória inválida e o binário não pode ser executado. Com este hack, o programa acessará o valor em 0x0 e o usará para a verificação de stack_chk_guard.

Para resumir, usar o Unicorn requer alguma configuração adicional antes de executar o binário, mas uma vez feito, esta ferramenta pode ajudar a fornecer insights profundos sobre o binário. Ele fornece a flexibilidade para executar o binário completo ou uma parte limitada dele. O Unicorn também expõe APIs para anexar hooks à execução. Usando esses hooks, você pode observar o estado do programa em qualquer ponto durante a execução ou até mesmo manipular valores de registradores ou variáveis e forçar a exploração de outros ramos de execução em um programa. Outra vantagem ao executar um binário no Unicorn é que você não precisa se preocupar com várias verificações como detecção de root/jailbreak ou detecção de debugger, etc.