Análise estática no Android – parte 4: TLS, pinning e armazenamento
O que revisar no código Kotlin/Java de um app: validação de certificado, certificate pinning, dados gravados sem proteção e chamadas de sistema sensíveis.
Série · Parte 7 de 11 Segurança Mobile
- 01 O mapa da segurança mobile: OWASP MASVS, MASWE e MASTG 6 min
- 02 Arquitetura do Android: do kernel ao app 10 min
- 03 Arquitetura do iOS: do boot ao Secure Enclave 7 min
- 04 Análise estática no Android – parte 1: abrindo o APK 6 min
- 05 Análise estática no Android – parte 2: segredos no binário 4 min
- 06 Análise estática no Android – parte 3: manifesto e superfície de ataque 5 min
- 07 Análise estática no Android – parte 4: TLS, pinning e armazenamento 5 min
- 08 Scanners automáticos: o que eles pegam e o que passa batido 4 min
- 09 Análise estática no iOS: binário, Info.plist e entitlements 5 min
- 10 Análise dinâmica: o app rodando sob instrumentação 5 min
- 11 Do achado ao relatório: confirmar, classificar e registrar 4 min
Neste artigo6 seções
O manifesto, visto no artigo anterior, descreve a configuração declarativa de um app. Mas boa parte das falhas de segurança mobile não está numa flag do XML, está no código: na forma como o app valida (ou deixa de validar) um certificado TLS, no que ele grava em disco sem proteção, ou numa chamada de sistema perigosa usada sem o devido cuidado.
Este artigo cobre quatro frentes dessa análise, voltadas ao código Kotlin/Java de um app Android já decompilado.
TLS quebrado
HTTPS por si só não garante que a conexão é segura: ele garante que ela é criptografada, mas quem decide se o certificado do outro lado é confiável é a lógica de validação do cliente. Quando essa lógica está errada, a criptografia continua ali, só que falando com quem não deveria.
O padrão Java expõe esse ponto de verificação através da interface X509TrustManager e do método checkServerTrusted. Uma implementação correta lança uma exceção quando o certificado não é confiável; uma implementação quebrada, às vezes criada "temporariamente" durante o desenvolvimento e esquecida em produção, simplesmente retorna sem validar nada, aceitando qualquer certificado, inclusive o de um atacante fazendo interceptação. A partir do código já decompilado (jadx), dá para localizar as duas coisas:
cd jadx-out/sources
grep -rlE 'ALLOW_ALL_HOSTNAME_VERIFIER|AllowAllHostnameVerifier' .
grep -rln 'checkServerTrusted' . | grep -vE '^\./(okhttp3|com/android|android|org/conscrypt|com/google)'
O segundo comando já filtra as implementações que vêm de bibliotecas conhecidas (OkHttp, o próprio Android, Conscrypt), para sobrar só o que é código do app. Cada arquivo que aparecer merece ser aberto: o problema é confirmado se o checkServerTrusted estiver vazio ou só devolver sem validar nada.
O mesmo problema aparece com um HostnameVerifier que sempre devolve true (um padrão às vezes chamado de AllowAllHostnameVerifier, já incluído no primeiro comando acima), que desliga a checagem de que o certificado corresponde ao domínio esperado.
Pinning de certificado
Certificate pinning vai um passo além da validação padrão de TLS: em vez de confiar em qualquer Autoridade Certificadora (CA) instalada no sistema, o app verifica se o certificado recebido corresponde a um conjunto específico, fixado (pinado) dentro do próprio app. Isso reduz drasticamente o risco de interceptação via proxy, mesmo que o atacante consiga instalar uma CA falsa no aparelho.
Em apps Android que usam a biblioteca OkHttp (a mais comum do ecossistema, inclusive por baixo de clientes HTTP usados em React Native), o pinning é configurado através da classe CertificatePinner. Uma revisão de pinning costuma conferir três coisas:
- Cobertura: o pin realmente cobre o domínio da API de produção, ou só domínios de teste?
- Redundância: existe um pin de backup, para o dia em que o certificado principal precisar ser trocado sem quebrar o app?
- Comportamento na falha: o que o app faz quando o pin não bate? Essa última pergunta importa porque alguns apps, para evitar travar o usuário numa falha de configuração, decidem seguir a conexão mesmo com o pinning falhando, o que anula o propósito do mecanismo inteiro.
grep -rln 'CertificatePinner' . | grep -v '^\./okhttp3'
grep -rn -i 'pinning\|sha256/' .
Existe também uma forma de automatizar essa busca direto no APK, sem passar pelo jadx: a ferramenta SSLPinDetect procura os padrões mais comuns de pinning (incluindo OkHttp e TrustManager customizado) a partir do próprio pacote.
git clone https://github.com/aancw/SSLPinDetect && cd SSLPinDetect && pip install -r requirements.txt
python sslpindetect.py -f example.apk -a apktool.jar -v
Armazenamento inseguro
Toda informação que um app grava localmente, preferências, banco de dados, cache, pode ser lida por quem tem acesso físico a um aparelho com root ou por um backup mal configurado (ver o allowBackup, no artigo anterior). Um ponto específico do código que vale checar é o uso dos antigos modos MODE_WORLD_READABLE e MODE_WORLD_WRITEABLE, que tornavam um arquivo local legível (ou gravável) por qualquer outro app instalado no aparelho. O Google bane esse uso desde o Android 7, levantando uma exceção de segurança em tempo de execução, mas código legado às vezes ainda carrega a tentativa:
grep -rlE 'MODE_WORLD_(READABLE|WRITEABLE)' .
Outro ponto comum é o uso de armazenamento externo, que, dependendo da versão do Android e da configuração do app, pode ficar acessível a outros apps com a permissão certa, tornando qualquer dado sensível salvo ali um risco maior do que o mesmo dado salvo no diretório privado do app:
grep -rln 'getExternalStorage\|Environment.getExternalStorageDirectory' .
Funções sensíveis
Algumas chamadas de API, embora legítimas em muitos contextos, merecem atenção redobrada quando aparecem no código de um app:
Runtime.getRuntime().exec()eProcessBuilder: executam comandos do sistema. Um app que monta esse comando concatenando dado vindo do usuário ou da rede abre caminho para injeção de comando.sendTextMessage(): envia SMS programaticamente. Em apps que não deveriam ter essa função como parte do produto, a presença da chamada é um indício de comportamento inesperado.System.loadLibrary(): carrega uma biblioteca nativa (.so). Não é perigoso por si só (é como qualquer app React Native carrega sua engine JavaScript), mas indica onde fica o código que a análise de bytecode Java não alcança, e que exigiria engenharia reversa de binário nativo para revisar.
grep -rln 'Runtime.getRuntime().exec\|new ProcessBuilder' . | grep -vE '^\./(com/google|androidx|kotlin|okhttp3)'
grep -rln 'sendTextMessage\|System.loadLibrary' .
Um Log que imprime domínio, token ou erro detalhado também vale a pena procurar à parte, porque esse texto fica legível com adb logcat enquanto o app roda:
grep -rn 'Log\.[dvie](' . | cut -c1-200
Tapjacking e captura de tela
Por fim, duas proteções de interface que às vezes passam despercebidas:
- Tapjacking: um app malicioso pode sobrepor uma camada transparente por cima da tela de outro app, capturando toques que o usuário acredita estar dando na tela de baixo. A flag
setFilterTouchesWhenObscuredfaz uma view ignorar toques quando outra janela a está sobrepondo. - Captura de tela e lista de apps recentes: sem a flag
FLAG_SECURE, qualquer tela do app pode ser capturada por outro processo com a permissão certa, aparece em prints e, mais sutilmente, numa miniatura na lista de apps recentes do sistema. Telas com dado sensível (senha, informação financeira, documentos) costumam usar essa flag para evitar os dois casos.
grep -rl 'setFilterTouchesWhenObscured\|setHideOverlayWindows' . | head
grep -rl 'addFlags(8192)\|setFlags(8192, 8192)' . | head
8192 é o valor numérico da flag FLAG_SECURE: ela costuma aparecer assim, em bytecode decompilado, em vez do nome da constante.
Por fim, vale uma varredura pela pasta descompactada do APK em busca de arquivos que não deveriam estar ali: variáveis de ambiente, certificados privados, código-fonte ou sourcemaps esquecidos dentro de assets/.
cd ../../apk-out
find . -type f \( -iname '*.env*' -o -iname '*.p12' -o -iname '*.jks' -o -iname '*.keystore' -o -iname '*.pem' -o -iname '*.key' -o -iname '*.ts' -o -iname '*.map' \) | grep -v '^./res/'
grep -rl 'PRIVATE KEY' .
O limite da análise estática
Tudo isso continua sendo análise estática: ler código sem rodar o app. Ela mostra o que uma função pode fazer, mas só confirma o comportamento real quando combinada com a análise dinâmica, tema do artigo mais à frente nesta série, onde o app roda de fato sob instrumentação.
Antes disso, o próximo artigo cobre os scanners automáticos: o que eles encontram de verdade, e por que nenhum deles substitui a leitura manual do código.






