1. O Que é SSL Pinning?
Em aplicações modernas, o protocolo HTTPS garante que a comunicação entre o cliente e o servidor seja cifrada e autenticada via certificado TLS/SSL. Por padrão, um dispositivo Android confia em qualquer certificado assinado por uma Autoridade Certificadora (CA) reconhecida pelo sistema operacional — o que inclui CAs instaladas pelo usuário.
O SSL Pinning (ou Certificate Pinning) é uma técnica de segurança que vai além desse modelo: a aplicação passa a verificar não apenas se o certificado é válido, mas se ele corresponde a um valor específico (hash, chave pública ou certificado completo) que foi embutido (“pinado”) dentro do próprio código da aplicação.
Esse mecanismo impede que um atacante insira um proxy intermediário (MITM) mesmo que o dispositivo confie no certificado do proxy. É uma das primeiras barreiras que você encontrará ao tentar interceptar o tráfego de apps financeiros, bancários, de saúde ou corporativos.
Como o SSL Pinning funciona na prática
Sem pinning: App → valida certificado do servidor contra as CAs do sistema → conexão aceita → proxy intercepta facilmente.
Com pinning: App → valida certificado do servidor → compara com hash/chave embutida no APK → se não bater → conexão recusada → proxy bloqueado.
Tipos de SSL Pinning: • Certificate Pinning: o hash do certificado completo é comparado. Mais rígido, mas quebra a cada renovação de certificado. • Public Key Pinning (SPKI): apenas a chave pública é fixada. Persiste mesmo após renovação do certificado. • OkHttp / TrustKit / Conscrypt: bibliotecas populares no Android que implementam pinning de forma nativa. |
2. Setup do Ambiente
Para realizar o bypass do SSL Pinning, precisamos montar um ambiente controlado que nos permita injetar código no processo da aplicação em tempo de execução. A ferramenta central desta abordagem é o Frida — um framework de instrumentação dinâmica (DBI).
2.1 Pré-requisito: Dispositivo Android com Root
O root no dispositivo é necessário para que o frida-server seja iniciado com privilégios suficientes para se acoplar a qualquer processo. Este guia assume que o dispositivo já está com root funcional (ex: Magisk, KernelSU).
2.2 Instalação do ADB (Android Debug Bridge)
O ADB é a ponte de comunicação entre sua máquina e o dispositivo Android. Ele permite transferir arquivos, executar comandos remotos e gerenciar o dispositivo via terminal.
Linux / macOS
# Ubuntu / Debian
sudo apt update && sudo apt install adb -y
# macOS (Homebrew)
brew install android-platform-toolsWindows
# Via Chocolatey
choco install adb
# Ou baixe manualmente o SDK Platform Tools em:
# https://developer.android.com/tools/releases/platform-tools
Verificando a instalação
adb version
# Output esperado: Android Debug Bridge version 1.0.x
2.3 Configurações no Android
Antes de conectar, é necessário habilitar as opções corretas no dispositivo.
Habilitar Modo de Desenvolvedor
1. Acesse Configurações → Sobre o telefone
2. Toque 7 vezes em Número de compilação
3. Uma mensagem confirmará: “Você agora é um desenvolvedor!”
Habilitar Depuração USB
4. Acesse Configurações → Opções do desenvolvedor
5. Ative a opção Depuração USB
6. Conecte o cabo USB e confirme a chave RSA no popup do dispositivo
# Verificar se o dispositivo é reconhecido
adb devices
# Saída esperada:
# List of devices attached
# R5CTA1XXXXX device
# Output esperado: Android Debug Bridge version 1.0.x
2.4 Download e Deploy do frida-server
O frida-server é o componente que roda no dispositivo Android e expõe uma interface para que o cliente Frida (rodando na máquina do pentester) possa se comunicar e injetar scripts nos processos.
Passo 1: Identificar a arquitetura do dispositivo
adb shell getprop ro.product.cpu.abi
# Saídas comuns: arm64-v8a, armeabi-v7a, x86, x86_64Passo 2: Baixar o frida-server compatível
pip3 install frida-tools
# Verificar a versão instalada
frida --version
# Ex: 16.3.3
# Baixar o frida-server com a MESMA versão do cliente
# Acesse: https://github.com/frida/frida/releases
# Exemplo para arm64:
wget https://github.com/frida/frida/releases/download/16.3.3/frida-server-16.3.3-android-arm64.xz
# Extrair o binário
unxz frida-server-16.3.3-android-arm64.xzPasso 3: Enviar para o dispositivo e configurar permissões
# Enviar o frida-server para o dispositivo
adb push frida-server-16.3.3-android-arm64 /data/local/tmp/frida-server
# Conceder permissão de execução
adb shell chmod +x /data/local/tmp/frida-server2.5 Script Frida para Bypass de SSL Pinning
Com o ambiente pronto, precisamos do script que será injetado no processo da aplicação alvo. O script mais utilizado pela comunidade é o ssl-kill-switch3, mas também trabalharemos com o script universal do Frida Codeshare.
Opção 1: Script via Frida Codeshare (recomendado para início)
# O Frida Codeshare possui scripts prontos e mantidos pela comunidade
# O script 'universal-android-ssl-pinning-bypass' cobre a maioria dos casos
frida --codeshare pcipolloni/universal-android-ssl-pinning-bypass-with-frida \
-f com.exemplo.app \
--no-pauseOpção 2: Script local customizado (mais controle)
Salve o script abaixo como ssl_bypass.js:
// ssl_bypass.js — SSL Pinning Bypass universal para Android
// Hooks: OkHttp3, TrustManager, SSLContext, Conscrypt
setTimeout(function() {
Java.perform(function() {
// ── 1. Hook no TrustManager customizado ──
var TrustManager = Java.registerClass({
name: 'com.sslbypass.CustomTrustManager',
implements: [Java.use('javax.net.ssl.X509TrustManager')],
methods: {
checkClientTrusted(chain, authType) {},
checkServerTrusted(chain, authType) {},
getAcceptedIssuers() { return []; }
}
});
// ── 2. Hook no SSLContext para substituir TrustManager ──
var SSLContext = Java.use('javax.net.ssl.SSLContext');
SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;',
'[Ljavax.net.ssl.TrustManager;',
'java.security.SecureRandom'
).implementation = function(km, tm, sr) {
console.log('[*] SSLContext.init hooked — injetando TrustManager customizado');
this.init(km, [TrustManager.$new()], sr);
};
// ── 3. Hook no OkHttp3 CertificatePinner ──
try {
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
CertificatePinner.check.overload(
'java.lang.String', 'java.util.List'
).implementation = function(hostname, peerCertificates) {
console.log('[*] OkHttp3 CertificatePinner.check bypassed para: ' + hostname);
return;
};
CertificatePinner.check.overload(
'java.lang.String', '[Ljava.security.cert.Certificate;'
).implementation = function(hostname, certs) {
console.log('[*] OkHttp3 CertificatePinner.check (v2) bypassed para: ' + hostname);
return;
};
} catch(e) { console.log('[-] OkHttp3 não encontrado: ' + e); }
// ── 4. Hook no Conscrypt (Android Network Security) ──
try {
var ConscryptPin = Java.use('com.android.org.conscrypt.TrustManagerImpl');
ConscryptPin.verifyChainAndCheckPins.implementation = function(chain, host) {
console.log('[*] Conscrypt checkPins bypassed para: ' + host);
return this.checkTrustedRecursive(chain[0], host, true, null, null, null);
};
} catch(e) { console.log('[-] Conscrypt hook ignorado: ' + e); }
// ── 5. HostnameVerifier ──
var HostnameVerifier = Java.use('javax.net.ssl.HttpsURLConnection');
HostnameVerifier.setDefaultHostnameVerifier.implementation = function(v) {
console.log('[*] HostnameVerifier substituído');
var bypass = Java.registerClass({
name: 'com.sslbypass.AllHostsVerifier',
implements: [Java.use('javax.net.ssl.HostnameVerifier')],
methods: { verify: function(h, s) { return true; } }
});
this.setDefaultHostnameVerifier(bypass.$new());
};
console.log('[+] SSL Pinning Bypass carregado com sucesso!');
});
}, 0);2.6 Configuração do Burp Suite
O Burp Suite atuará como proxy intermediário para capturar e analisar as requisições HTTPS após o bypass do SSL pinning.
Configurar o listener do Burp
7. Acesse: Proxy → Options → Proxy Listeners
8. Clique em Add ou edite o listener existente
9. Bind to port: 8080 (ou a porta de sua preferência)
10. Bind to address: All interfaces (0.0.0.0) — para aceitar conexões do dispositivo físico
Exportar e instalar o certificado do Burp no dispositivo
# No Burp Suite: Proxy → Options → Import / export CA certificate
# Exporte como: Certificate in DER format → burp_cert.der
# Converter para .cer (Android aceita ambos, mas .cer é mais comum)
openssl x509 -inform DER -in burp_cert.der -out burp_cert.crt
# Enviar para o dispositivo
adb push burp_cert.crt /sdcard/Download/burp_cert.crtInstalar o certificado no Android
11. Acesse Configurações → Segurança → Credenciais → Instalar certificado
12. Selecione o arquivo burp_cert.crt
13. Dê um nome ao certificado (ex: BurpSuiteCA) e selecione uso para VPN e apps
Configurar o proxy Wi-Fi no dispositivo
14. Acesse Configurações → Wi-Fi → segure a rede conectada → Modificar rede
15. Opções avançadas → Proxy: Manual
16. Nome do host proxy: IP da máquina com Burp Suite (ex: 192.168.1.100)
17. Porta: 8080
# Dica: Para descobrir o IP da sua máquina
# Linux / macOS
ip a | grep 'inet ' | grep -v 127.0.0.1
# Windows
ipconfig | findstr IPv43. Interceptando o Tráfego
3.1 Inicialização do App e Attach do Processo com Frida
Com o ambiente configurado, chegou a hora de injetar o script no processo da aplicação alvo.
Listar processos/apps em execução
# Listar todos os processos em execução no dispositivo
frida-ps -U
# Filtrar por nome do app (exemplo)
frida-ps -U | grep -i 'banco'
# Listar apenas aplicações instaladas (com pacote)
frida-ps -UaiMétodo 1: Spawn (recomendado — injeta antes do app iniciar)
O modo spawn inicia o aplicativo e injeta o script antes mesmo que qualquer código do app seja executado — garantindo que o bypass esteja ativo desde o primeiro frame.
# Sintaxe geral
frida -U -f <package_name> -l ssl_bypass.js --no-pause
# Exemplo real com app bancário fictício
frida -U -f com.banco.example -l ssl_bypass.js --no-pause
# Usando script do Codeshare (sem precisar de arquivo local)
frida -U --codeshare pcipolloni/universal-android-ssl-pinning-bypass-with-frida \
-f com.banco.example --no-pauseMétodo 2: Attach (injeta em app já em execução)
Útil quando o app já está rodando ou quando o spawn causa problemas de inicialização.
# Primeiro, descubra o PID do processo
frida-ps -U | grep <nome_do_app>
# Attach por nome do pacote
frida -U -n <package_name> -l ssl_bypass.js
# Attach por PID
frida -U -p 12345 -l ssl_bypass.jsSaída esperada no terminal do Frida
____
/ _ | Frida 16.3.3 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
object? -> Display information about 'object'
exit/quit -> Exit
____
[Android Emulator 5554::com.banco.example ]->
[*] SSLContext.init hooked — injetando TrustManager customizado
[*] OkHttp3 CertificatePinner.check bypassed para: api.banco.example.com
[+] SSL Pinning Bypass carregado com sucesso!
3.2 Analisando as Requisições com Burp Suite
Com o Frida rodando e o bypass ativo, as requisições HTTPS da aplicação começarão a aparecer no Burp Suite como tráfego HTTP descriptografado e interceptável.
Fluxo de análise no Burp Suite
18. Proxy → Intercept: Ative o intercept para pausar cada requisição e analisá-la manualmente.
19. Proxy → HTTP History: Visualize todas as requisições capturadas em ordem cronológica.
20. Proxy → HTTP History → clique em uma requisição → Send to Repeater: Permita reenviar a requisição com modificações.
21. Target → Site Map: O Burp monta automaticamente o mapa da API do app conforme as requisições fluem.
4. Outras Capacidades do Frida
O Frida vai muito além do SSL pinning bypass. Por ser um framework de instrumentação dinâmica completo, ele permite interceptar, modificar e injetar lógica em qualquer ponto de execução de um app Android. Abaixo estão as principais funcionalidades utilizadas em pentests mobile avançados.
4.1 Root Detection Bypass
Aplicações de alta segurança verificam ativamente se o dispositivo está com root antes de operar. Essas verificações precisam ser neutralizadas para que possamos trabalhar.
// root_bypass.js — Bypass de detecção de root (RootBeer, SafetyNet, customizado)
Java.perform(function() {
// ── RootBeer (biblioteca popular de detecção de root) ──
try {
var RootBeer = Java.use('com.scottyab.rootbeer.RootBeer');
RootBeer.isRooted.implementation = function() {
console.log('[*] RootBeer.isRooted() → false (bypass)');
return false;
};
} catch(e) { console.log('[-] RootBeer não encontrado'); }
// ── Hook em Runtime.exec() para bloquear 'which su' ──
var Runtime = Java.use('java.lang.Runtime');
Runtime.exec.overload('java.lang.String').implementation = function(cmd) {
if (cmd.indexOf('su') !== -1 || cmd.indexOf('busybox') !== -1) {
console.log('[*] Runtime.exec bloqueado para: ' + cmd);
return this.exec('echo');
}
return this.exec(cmd);
};
// ── File.exists() para paths de root ──
var File = Java.use('java.io.File');
File.exists.implementation = function() {
var path = this.getAbsolutePath();
var rootPaths = ['/system/bin/su', '/system/xbin/su',
'/sbin/su', '/data/local/xbin/su',
'/system/app/Superuser.apk'];
for (var i = 0; i < rootPaths.length; i++) {
if (path === rootPaths[i]) {
console.log('[*] File.exists() → false para: ' + path);
return false;
}
}
return this.exists();
};
console.log('[+] Root Detection Bypass ativo!');
});4.2 Emulator Detection Bypass
// emulator_bypass.js
Java.perform(function() {
var Build = Java.use('android.os.Build');
// Substituir propriedades de hardware para parecer device real
Build.MANUFACTURER.value = 'Samsung';
Build.MODEL.value = 'SM-G998B';
Build.BRAND.value = 'samsung';
Build.FINGERPRINT.value = 'samsung/o1sxeea/o1s:13/TP1A.220624.014/G998BXXU5EWKB:user/release-keys';
Build.HARDWARE.value = 'exynos2100';
console.log('[+] Emulator Detection Bypass ativo!');
});4.3 Interceptação e Modificação de Dados em Runtime
Com Frida, é possível interceptar o retorno de qualquer método Java, modificar parâmetros e até injetar lógica de negócio — útil para testar validações server-side e client-side.
// intercept_data.js — Interceptar e modificar saldo exibido
Java.perform(function() {
// Supondo que o app use um método getBalance() no ViewModel
var AccountViewModel = Java.use('com.banco.example.viewmodel.AccountViewModel');
AccountViewModel.getBalance.implementation = function() {
var originalBalance = this.getBalance();
console.log('[*] getBalance() original: ' + originalBalance);
// Modificar o retorno (apenas visual — servidor não é afetado)
return 999999.99;
};
});4.4 Bypass de Biometria e Autenticação Local
// biometric_bypass.js — Bypass de autenticação biométrica
Java.perform(function() {
// Hook no BiometricPrompt.AuthenticationCallback
var BiometricPrompt = Java.use('android.hardware.biometrics.BiometricPrompt');
BiometricPrompt.authenticate.overload(
'android.os.CancellationSignal',
'java.util.concurrent.Executor',
'android.hardware.biometrics.BiometricPrompt$AuthenticationCallback'
).implementation = function(cancel, executor, callback) {
console.log('[*] BiometricPrompt interceptado — forçando sucesso');
var AuthResult = Java.use('android.hardware.biometrics.BiometricPrompt$AuthenticationResult');
// Simular callback de sucesso
callback.onAuthenticationSucceeded(AuthResult.$new(null, null, 0));
};
console.log('[+] Biometric Bypass ativo!');
});4.5 Dump de Memória e Strings Sensíveis
// memory_dump.js — Interceptar strings sensíveis em memória
Java.perform(function() {
// Hook em SharedPreferences para capturar dados armazenados
var SharedPreferences = Java.use('android.app.SharedPreferencesImpl');
SharedPreferences.getString.overload(
'java.lang.String', 'java.lang.String'
).implementation = function(key, defValue) {
var result = this.getString(key, defValue);
if (result !== null) {
console.log('[SharedPrefs] ' + key + ' = ' + result);
}
return result;
};
// Hook em SecretKeySpec para capturar chaves criptográficas
var SecretKeySpec = Java.use('javax.crypto.spec.SecretKeySpec');
SecretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function(key, algo) {
console.log('[CRYPTO KEY] Algoritmo: ' + algo);
console.log('[CRYPTO KEY] Chave (hex): ' + bytesToHex(key));
return this.$init(key, algo);
};
function bytesToHex(bytes) {
var hex = [];
for (var i = 0; i < bytes.length; i++) {
hex.push(('0' + (bytes[i] & 0xFF).toString(16)).slice(-2));
}
return hex.join('');
}
});
