Como Quebrar SSL Pinning em Aplicativos Android

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

⚠  AVISO LEGAL: Realize estes procedimentos apenas em dispositivos e aplicações para os quais você tem autorização explícita. O uso não autorizado é crime.

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-tools

Windows

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

Passo 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.xz

Passo 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-server

2.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-pause

Opçã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.crt

Instalar 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 IPv4

3. 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 -Uai

Mé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-pause

Mé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.js

Saí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('');
  }

});