Todos os artigos
Segurança · 4 min de leitura

Análise estática no Android – parte 2: segredos no binário

Para onde vai o .env quando o app é publicado, onde cada plataforma guarda esse valor e a linha entre configuração pública e segredo de verdade.

Série · Parte 5 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 artigo5 seções
  1. Como o valor sai do .env e entra no app
  2. Onde esse valor aparece no Android
  3. Onde esse valor aparece no iOS
  4. O que é aceitável e o que não é
  5. Fechando o ciclo: da string até a linha de código

Todo app precisa de alguma configuração que muda entre ambientes: o endereço da API, a chave pública de um serviço de terceiro, o identificador do projeto no Firebase. Esses valores costumam viver num arquivo .env durante o desenvolvimento, e a pergunta deste artigo é simples: para onde eles vão quando o app vira um binário publicado na loja?

A resposta curta é: para dentro do binário, lado a lado com o código. E "dentro do binário" significa que qualquer pessoa que baixe o APK ou o IPA pode lê-los.

Como o valor sai do .env e entra no app

Em projetos React Native, é comum usar uma biblioteca como o react-native-config para ler variáveis de um arquivo .env em tempo de build e torná-las acessíveis ao código JavaScript e nativo. O problema de segurança não está na biblioteca em si, está no que ela faz por definição: ela copia todas as variáveis do .env para o binário, sem distinguir o que é público do que é secreto. Se uma chave estiver no .env no momento do build, ela vai para o APK e para o IPA, do mesmo jeito.

Isso significa que a responsabilidade de separar "configuração pública" de "segredo" não é da ferramenta, é de quem decide o que entra no .env de produção.

Onde esse valor aparece no Android

No Android, cada variável de ambiente lida em tempo de build normalmente vira uma constante na classe BuildConfig, gerada automaticamente pelo Gradle para cada módulo do app. Essa classe, depois de decompilada (veja o artigo anterior), fica dentro de jadx-out/sources, no caminho que espelha o nome do pacote do app:

find jadx-out -name BuildConfig.java
grep 'public static final' jadx-out/sources/com/exemplo/app/BuildConfig.java

A saída é uma lista simples de campos public static final, com nome e valor em texto puro. Para conferir só o nome de cada campo e o começo do valor, sem imprimir o segredo inteiro na tela:

grep 'public static final String' jadx-out/sources/com/exemplo/app/BuildConfig.java \
  | sed -E 's/= "(.{0,6})[^"]*"/= "\1…"/'

O mesmo valor também pode aparecer duplicado nos recursos de string, se for referenciado por algum SDK nativo que espera configuração via meta-data no AndroidManifest.xml, prática comum em SDKs do Google (Firebase, Maps) e de terceiros:

grep -oE '<string name="[A-Z_]{4,}">' apktool-out/res/values/strings.xml
grep -i 'meta-data' apktool-out/AndroidManifest.xml
ls apk-out/assets

Onde esse valor aparece no iOS

No iOS a mecânica muda: as variáveis de ambiente não ficam expostas num arquivo de configuração separado como o Info.plist. Elas são compiladas diretamente dentro do binário executável do app, misturadas ao código de máquina. Isso não as esconde, só muda a ferramenta usada para encontrá-las: em vez de ler uma classe decompilada, basta procurar o texto dentro do binário com uma ferramenta como o strings, do Unix:

strings Payload/Example.app/Example | grep -iE 'secret|client_id|api_key|https://' | sort -u | head -50

Na prática, isso quer dizer que um valor que vaza no Android (visível no BuildConfig) normalmente também está presente no binário iOS, só que num formato menos óbvio de inspecionar. Dá até para confirmar isso sem nunca imprimir o valor em texto puro: basta ler os nomes e valores do lado Android e procurar cada valor, em bytes, dentro do binário iOS.

import re
bc = open('jadx-out/sources/com/exemplo/app/BuildConfig.java').read()
vals = dict(re.findall(r'String (\w+) = "([^"]{8,})"', bc))
blob = open('Payload/Example.app/Example', 'rb').read()
for k, v in vals.items():
    print(f'{k:32} {"SIM" if v.encode() in blob else "-"}')

O que é aceitável e o que não é

Nem todo valor encontrado dentro de um binário é um problema. A linha entre "aceitável" e "vazamento" depende do que aquele valor permite fazer sozinho:

Tipo Exemplo Pode ficar no app?
Identificador público URL base da API, chave de API com restrição de domínio/app, DSN de um serviço de monitoramento de erros Sim, foi feito para isso
Segredo de cliente confidencial Client secret de OAuth, chave privada, token de serviço com privilégio amplo Não. Quem extrai o valor consegue se passar pelo próprio app perante o backend
Metadado de build Caminho interno de CI, identificador de time de assinatura Idealmente não. Não compromete o app sozinho, mas revela detalhes do pipeline interno para quem for mapear a infraestrutura

A categoria mais arriscada é a do meio: um "client secret" existe, por definição, para autenticar um cliente de software perante um servidor, mas um app distribuído numa loja pública não consegue guardar segredo de verdade, porque o próprio usuário (ou quem analisa o APK) tem acesso físico ao binário inteiro. É esse paradoxo que leva protocolos como o OAuth 2.0 para apps nativos a recomendar fluxos que não dependem de um client secret fixo embutido no app, como o PKCE.

Fechando o ciclo: da string até a linha de código

Encontrar um valor suspeito no BuildConfig ou no binário é só a primeira metade do trabalho. A segunda metade, quando há acesso ao código-fonte do app, é confirmar de onde aquele valor vem: uma busca pelo nome da variável no repositório costuma levar direto ao arquivo de configuração que a define, e aí dá para avaliar se aquilo foi intencional ou um descuido.

No próximo artigo: o AndroidManifest.xml depois de decodificado revela muito mais do que variáveis, ele define boa parte da superfície de ataque de um app, dos componentes exportados até a configuração de rede.

Leia também