# 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.

- Autor: Marcelo Silva
- Publicado em: 2026-10-12
- Tema: Segurança
- Série: Segurança Mobile, parte 7 de 11 (https://marcelxsilva.dev/series/seguranca-mobile/)
- URL: https://marcelxsilva.dev/artigos/tls-pinning-e-armazenamento/

O manifesto, visto no [artigo anterior](https://marcelxsilva.dev/artigos/manifest-e-superficie-de-ataque/), 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`](https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/X509TrustManager.html) 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:

```bash
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](https://developer.android.com/privacy-and-security/security-config#CertificatePinning) 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](https://square.github.io/okhttp/) (a mais comum do ecossistema, inclusive por baixo de clientes HTTP usados em React Native), o pinning é configurado através da classe [`CertificatePinner`](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-certificate-pinner/). 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.

```bash
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](https://github.com/aancw/SSLPinDetect) procura os padrões mais comuns de pinning (incluindo OkHttp e TrustManager customizado) a partir do próprio pacote.

```bash
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](https://source.android.com/docs/security/app-sandbox) 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`](https://developer.android.com/reference/android/content/Context#MODE_WORLD_READABLE), 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](https://developer.android.com/about/versions/nougat/android-7.0-changes#fso), levantando uma exceção de segurança em tempo de execução, mas código legado às vezes ainda carrega a tentativa:

```bash
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:

```bash
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()` e `ProcessBuilder`: 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](https://owasp.org/www-community/attacks/Command_Injection).
- `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.

```bash
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:

```bash
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 [`setFilterTouchesWhenObscured`](https://developer.android.com/reference/android/view/View#setFilterTouchesWhenObscured(boolean)) faz uma view ignorar toques quando outra janela a está sobrepondo.
- **Captura de tela e lista de apps recentes**: sem a flag [`FLAG_SECURE`](https://developer.android.com/reference/android/view/WindowManager.LayoutParams#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.

```bash
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/`.

```bash
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.
