Android App Penetration Testing & Frida Hooks

Mastering Frida: Deep Dive into OkHttp3 Interceptors & Trust Managers for SSL Bypass

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to SSL Pinning and its Bypass

SSL Pinning is a security mechanism implemented within client applications to prevent Man-in-the-Middle (MITM) attacks. Instead of trusting any certificate signed by a trusted Certificate Authority (CA), the application “pins” specific certificates or public keys that it expects from the server. If the server presents a certificate that doesn’t match the pinned one, the connection is terminated, even if it’s signed by a valid CA. While crucial for security, SSL pinning poses a significant challenge for security researchers and penetration testers who need to intercept and analyze application traffic.

Many modern Android applications leverage the OkHttp3 library for network communication due to its efficiency and robust feature set, including built-in SSL pinning capabilities via its CertificatePinner class. This article provides an expert-level guide on how to effectively bypass OkHttp3’s SSL pinning using Frida, focusing on both CertificatePinner and custom X509TrustManager implementations.

Prerequisites for Your Frida SSL Bypass Journey

Before diving into the technicalities, ensure you have the following tools and setup ready:

  • Rooted Android Device or Emulator: Necessary for running frida-server with elevated privileges.
  • ADB (Android Debug Bridge): For interacting with your Android device.
  • Frida-CLI and Frida-Server: Install frida-tools on your host machine (pip install frida-tools) and push the appropriate frida-server binary to your device.
  • Python: For writing and running Frida scripts.
  • Proxy Tool: Such as Burp Suite or OWASP ZAP, configured to intercept traffic from your device.

Setting up Frida-Server on Android

# Push frida-server to deviceadb push /path/to/frida-server /data/local/tmp/frida-server# Grant execute permissionsadb shell "chmod 755 /data/local/tmp/frida-server"# Run frida-server in the backgroundadb shell "/data/local/tmp/frida-server &"

Verify Frida is running by executing frida-ps -U on your host machine. You should see a list of running processes on your Android device.

Understanding OkHttp3’s SSL Pinning Mechanisms

OkHttp3 offers two primary ways to implement SSL pinning:

  1. CertificatePinner: This is OkHttp3’s dedicated class for certificate pinning. It allows developers to specify a set of expected SHA-256 hashes of the public keys (or full certificates) for specific hosts. During the TLS handshake, if the server’s certificate chain does not contain a certificate whose public key matches one of the pinned hashes, the connection fails.
  2. Custom X509TrustManager: Developers can also provide their own implementation of X509TrustManager (or extend existing ones) to perform custom certificate validation logic. This is often used to add more complex pinning rules, integrate with enterprise-specific trust stores, or bypass system-level trust.

Our Frida strategies will target these two mechanisms to achieve a comprehensive SSL bypass.

Strategy 1: Bypassing OkHttp3’s CertificatePinner

The most direct approach to bypass OkHttp3’s built-in pinning is to hook the CertificatePinner class. Specifically, we’ll focus on its check method, which is responsible for verifying the server’s certificate against the pinned hashes.

Frida Script for CertificatePinner Bypass

This script intercepts calls to okhttp3.CertificatePinner.check(String hostname, List<Certificate> peerCertificates) and effectively nullifies its pinning logic, allowing all certificates to pass.

Java.perform(function() {    console.log("[*] Attempting to hook 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 called for hostname: " + hostname);            // Bypass pinning by not calling the original method            console.log("[-] Bypassing CertificatePinner for " + hostname);            // No return value needed, as this is a void method.            // If it returned a boolean, we'd return true.            // OkHttp3's check method throws an exception on failure, so simply not throwing means success.        };        console.log("[+] okhttp3.CertificatePinner.check hooked successfully!");    } catch (e) {        console.log("[-] okhttp3.CertificatePinner.check hook failed: " + e.message);    }});

When this script is loaded, any attempt by the application to use CertificatePinner.check will hit our custom implementation, which does nothing, effectively allowing the connection to proceed without certificate validation errors from the pinner.

Strategy 2: Bypassing Custom X509TrustManager Implementations

Even if CertificatePinner is bypassed, an application might still use a custom X509TrustManager to enforce additional pinning. This is a more general approach that can bypass various forms of trust validation.

The core methods to target in an X509TrustManager are:

  • checkClientTrusted(X509Certificate[] chain, String authType)
  • checkServerTrusted(X509Certificate[] chain, String authType)
  • getAcceptedIssuers()

For SSL pinning bypass, we are primarily interested in checkServerTrusted. Our goal is to make this method always accept any server certificate.

Frida Script for X509TrustManager Bypass

This script attempts to enumerate and hook all instances of X509TrustManager, specifically targeting checkServerTrusted to make it always trust the server’s certificate.

Java.perform(function() {    console.log("[*] Attempting to hook X509TrustManager.checkServerTrusted");    try {        var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");        var SSLContext = Java.use("javax.net.ssl.SSLContext");        // Hook TrustManagerFactory to get all TrustManagers        TrustManagerFactory.getTrustManagers.implementation = function() {            var trustManagers = this.getTrustManagers();            for (var i = 0; i < trustManagers.length; i++) {                if (Java.instance(trustManagers[i]).$className.indexOf("X509TrustManager") != -1) {                    hookTrustManager(trustManagers[i]);                }            }            return trustManagers;        };        // Also hook SSLContext.init to catch dynamically created TrustManagers        SSLContext.init.overload('[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom').implementation = function(keyManagers, trustManagers, secureRandom) {            if (trustManagers != null) {                for (var i = 0; i < trustManagers.length; i++) {                    if (Java.instance(trustManagers[i]).$className.indexOf("X509TrustManager") != -1) {                        hookTrustManager(trustManagers[i]);                    }                }            }            this.init(keyManagers, trustManagers, secureRandom);        };        function hookTrustManager(trustManager) {            try {                // Ensure we're dealing with an X509TrustManager                var X509TrustManager = Java.cast(trustManager, Java.use("javax.net.ssl.X509TrustManager"));                X509TrustManager.checkClientTrusted.overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String').implementation = function(chain, authType) {                    console.log("[+] checkClientTrusted called. Bypassing...");                    // Do nothing, effectively trusting all clients                };                X509TrustManager.checkServerTrusted.overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String').implementation = function(chain, authType) {                    console.log("[+] checkServerTrusted called for authType: " + authType + ". Bypassing...");                    // Do nothing, effectively trusting all servers                };                console.log("[+] Successfully hooked X509TrustManager: " + Java.instance(trustManager).$className);            } catch (e) {                console.log("[-] Failed to hook TrustManager: " + Java.instance(trustManager).$className + " - " + e.message);            }        }        console.log("[+] X509TrustManager hook setup complete. Waiting for app to load TrustManagers.");    } catch (e) {        console.log("[-] Global TrustManager hook setup failed: " + e.message);    }});

This script is more comprehensive as it attempts to hook X509TrustManager instances wherever they might be initialized, including those created by TrustManagerFactory or passed directly to SSLContext.init. This makes it more robust against various implementations of SSL pinning.

Putting It All Together: Executing the Bypass

To use these scripts, follow these steps:

  1. Identify the target application: Determine the package name of the Android application you want to test (e.g., com.example.app).
  2. Choose your script: Depending on your initial analysis, you might start with the CertificatePinner bypass, or go straight for the more general X509TrustManager bypass. For maximum compatibility, combining both in a single script is often recommended.
  3. Run Frida:
    # For the CertificatePinner bypass (save script as okhttp3_pinning.js)frida -U -f com.example.app -l okhttp3_pinning.js --no-pause# For the X509TrustManager bypass (save script as trustmanager_bypass.js)frida -U -f com.example.app -l trustmanager_bypass.js --no-pause

    The -U flag targets a USB-connected device, -f spawns the application, -l loads the Frida script, and --no-pause immediately resumes the app after injection.

  4. Configure your proxy: Ensure your proxy (e.g., Burp Suite) is correctly set up to capture traffic from your Android device. This typically involves configuring Wi-Fi proxy settings on the device and installing the proxy’s CA certificate.
  5. Verify the bypass: Interact with the application. If the bypass is successful, you should see the application’s network traffic flowing through your proxy without SSL handshake errors.

Conclusion

Bypassing SSL pinning is a critical skill for any Android application penetration tester. By understanding how OkHttp3 implements its pinning mechanisms—through CertificatePinner and custom X509TrustManagers—and by leveraging the dynamic instrumentation capabilities of Frida, we can effectively disable these protections. The techniques outlined in this guide provide robust methods to intercept and analyze application traffic, enabling thorough security assessments. Remember to use these powerful techniques responsibly and only on applications for which you have explicit authorization to test.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner