Todos os artigos
Segurança · 6 min de leitura

Análise estática no Android – parte 1: abrindo o APK

Como descompactar um APK em formatos diferentes, o que cada um revela e o que muda quando o app é feito em React Native.

Série · Parte 4 de 11 Segurança Mobile
  1. 01 O mapa da segurança mobile: OWASP MASVS, MASWE e MASTG 6 min
  2. 02 Arquitetura do Android: do kernel ao app 10 min
  3. 03 Arquitetura do iOS: do boot ao Secure Enclave 7 min
  4. 04 Análise estática no Android – parte 1: abrindo o APK 6 min
  5. 05 Análise estática no Android – parte 2: segredos no binário 4 min
  6. 06 Análise estática no Android – parte 3: manifesto e superfície de ataque 5 min
  7. 07 Análise estática no Android – parte 4: TLS, pinning e armazenamento 5 min
  8. 08 Scanners automáticos: o que eles pegam e o que passa batido 4 min
  9. 09 Análise estática no iOS: binário, Info.plist e entitlements 5 min
  10. 10 Análise dinâmica: o app rodando sob instrumentação 5 min
  11. 11 Do achado ao relatório: confirmar, classificar e registrar 4 min
Sobre a série
Neste artigo4 seções
  1. Três formatos, três perguntas
  2. Se o pacote for um AAB
  3. O caso React Native: o bundle JavaScript
  4. Limites das ferramentas

Um APK é, na prática, um arquivo ZIP com uma extensão diferente e algumas regras a mais. Isso significa que boa parte do trabalho de análise estática, olhar o app sem executá-lo, começa simplesmente descompactando esse arquivo e escolhendo a ferramenta certa para cada pergunta.

Este artigo cobre a primeira fase prática da série: como abrir um APK em formatos diferentes, por que cada um serve para um tipo de busca, e o que muda quando o app é feito em React Native.

Três formatos, três perguntas

Um APK sozinho não é muito legível: o AndroidManifest.xml vem em binário, o código Kotlin/Java vem em bytecode DEX, e os recursos (strings.xml, layouts) também vêm compilados. Por isso o primeiro passo de qualquer análise é converter o pacote em pelo menos três formas diferentes, cada uma útil para um tipo de busca:

Formato Como obter Para que serve
Arquivos crus unzip Ver o assets/, bibliotecas nativas (.so), bundle JavaScript
Java decompilado jadx Ler código Kotlin/Java quase original, BuildConfig, lógica de negócio
Manifest e recursos em XML apktool AndroidManifest.xml e strings.xml legíveis, smali

O jadx converte bytecode DEX de volta para algo muito próximo do código-fonte original, porque esse bytecode preserva nomes de classes, métodos e variáveis (ao contrário de um binário nativo compilado, que perde quase tudo isso). É esse detalhe que torna a engenharia reversa de um APK relativamente acessível, e por isso ferramentas de ofuscação como o R8 existem: elas embaralham esses nomes antes de publicar o app.

O apktool já olha para outro lado do pacote: ele desmonta o DEX em smali (uma representação textual do bytecode, mais próxima de assembly do que de Java) e, principalmente, decodifica o manifesto e os recursos compilados de volta para XML legível. É ali que ficam as permissões, os componentes exportados e a configuração de rede do app, assunto do próximo artigo.

Na prática, os três formatos saem de três comandos, rodados a partir do APK (o jadx e o apktool se instalam com um gerenciador de pacotes comum, como o Homebrew no macOS: brew install jadx apktool):

mkdir -p pentest && cd pentest

unzip -q -o example.apk -d apk-out
jadx -q -d jadx-out example.apk
apktool d -f -q example.apk -o apktool-out
Pasta O que tem Para que serve
apk-out/ Arquivos crus (dex, assets, libs nativas) Bundle JS, arquivos soltos em assets/
jadx-out/ Java decompilado BuildConfig, código nativo do app
apktool-out/ Manifest e res/ em XML legível Manifest, strings.xml, configuração de rede

O jadx costuma ser o mais lento dos três (minutos, não segundos, em apps grandes); nada impede seguir explorando as outras duas pastas enquanto ele termina.

Se o pacote for um AAB

Hoje a Google Play exige o envio no formato Android App Bundle (.aab), não mais o APK direto. O AAB não é instalável sozinho: ele contém os recursos separados por configuração (idioma, densidade de tela, arquitetura de CPU) e é a própria Play Store quem gera, na hora da instalação, o APK otimizado para cada aparelho.

Para analisar um AAB como analisaria um APK, a ferramenta oficial do Google, o bundletool, gera um APK universal, que contém todas as configurações juntas:

java -jar bundletool.jar build-apks --bundle=example.aab --output=example.apks --mode=universal --overwrite
unzip -p example.apks universal.apk > universal.apk

A partir desse universal.apk, bastam as mesmas técnicas de descompactação, jadx e apktool descritas acima.

O caso React Native: o bundle JavaScript

Apps feitos com React Native acrescentam uma camada ao que o jadx e o apktool mostram. A lógica de produto não fica (só) em Kotlin: ela fica num bundle JavaScript, um único arquivo que concentra todo o código JS da aplicação, normalmente em assets/index.android.bundle dentro do APK.

Esse bundle existe em duas formas possíveis, dependendo de como o projeto foi configurado:

  • Texto puro (minificado): ainda é JavaScript, só que compactado numa linha. Dá para ler direto ou formatar com uma ferramenta como o Prettier.
  • Bytecode Hermes: desde que o Hermes, o motor JavaScript criado pela Meta para React Native, se tornou o padrão do framework (a partir da versão 0.70), o bundle passou a ser compilado para um bytecode próprio, não mais texto. Ferramentas como o hermes-dec fazem o caminho inverso: extraem a tabela de strings embutida no bytecode e, com limitações, produzem um pseudo-JavaScript aproximado do código original.

Dá para confirmar qual dos dois formatos um bundle usa com o utilitário file, do Unix, e localizar o próprio bundle dentro do pacote descompactado:

find apk-out -name "*.bundle" -o -name "*.jsbundle" -o -name "*.hbc"
file apk-out/assets/index.android.bundle
find apk-out -iname "libhermes*"
Resposta do file O que fazer
Hermes JavaScript bytecode, version <n> Extrair as strings do bytecode (próximo passo)
ASCII text / UTF-8 text É JS minificado. Dá para ler direto ou formatar com o Prettier

A presença de uma biblioteca libhermes.so no APK também é um indício forte de que o app usa Hermes, mesmo antes de olhar o bundle.

Com um bundle em bytecode Hermes, o primeiro passo é extrair a tabela de strings embutida, a fonte mais confiável de texto legível dentro do binário (uv tool install hermes-dec instala o pacote):

hbc-file-parser apk-out/assets/index.android.bundle > hbc-strings.txt
strings -n 5 apk-out/assets/index.android.bundle > bundle.txt

Para ir além da tabela de strings e tentar reconstruir a lógica em si, o próprio hermes-dec traz um desassemblador e um decompilador (pseudo-JS, que não compila: ele não reconstrói laços nem if):

hbc-disassembler apk-out/assets/index.android.bundle saida.hasm
hbc-decompiler apk-out/assets/index.android.bundle saida.js

O strings funciona em qualquer um dos dois formatos (bytecode ou texto) e é mais rápido, mas corta trechos de texto muito longos; o hbc-strings.txt, gerado pelo hermes-dec, é a leitura completa.

Dentro desse texto, o que se procura é sempre parecido: endereços de servidor que não deveriam estar na versão pública (ambientes de desenvolvimento ou homologação), nomes de variáveis de segredo, tokens no formato JWT, chaves de provedores de nuvem e qualquer lógica de validação que um atacante possa decifrar lendo o próprio código:

grep -oE 'https?://[A-Za-z0-9._-]+' bundle.txt | sort -u
grep -oE '[^ |]*(dev|develop|hml|homolog|staging|test)[^ |]*' hbc-strings.txt | sort -u
grep -oE '[^ |]*(secret|SECRET|password|token|TOKEN)[^ |]*' hbc-strings.txt | sort -u
grep -oE 'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*' hbc-strings.txt | sort -u
grep -oE "AIza[0-9A-Za-z_-]{35}" hbc-strings.txt
grep -oE "AKIA[0-9A-Z]{16}" hbc-strings.txt

Na ordem: todos os hosts chamados pelo app; endereços de ambientes que não são de produção; nomes de variável que sugerem segredo; tokens JWT; e dois padrões conhecidos de chave (API do Google e credencial da AWS).

Encontrar uma dessas strings é só metade do trabalho: o passo seguinte, quando se tem acesso ao repositório do app, é localizar a origem exata no código-fonte com uma busca pelo trecho encontrado (git grep -n -F '<trecho>' -- src), para entender se aquilo é intencional (um valor de exemplo) ou um vazamento real.

Limites das ferramentas

Nem toda ferramenta de decompilação acompanha a evolução do Hermes. O hbctool, por exemplo, só sabe ler as versões 59 e 62 do bytecode, o que o deixa defasado em apps mais novos. Vale sempre conferir, antes de montar um fluxo de análise em cima de uma ferramenta de terceiros, se ela cobre a versão do Hermes usada pelo app analisado.

No próximo artigo da série: o que dá para aprender olhando só para as variáveis de ambiente que acabam compiladas dentro do binário, e onde cada plataforma esconde esse dado.

Leia também