Android App Penetration Testing & Frida Hooks

From Zero to Bypass: Universal Android SSL Pinning with Frida Scripts Explained

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: The SSL Pinning Challenge in Android Security

SSL Pinning, or Certificate Pinning, is a critical security measure implemented in many Android applications to prevent Man-in-the-Middle (MiTM) attacks. While traditional SSL/TLS relies on a chain of trust to widely trusted Certificate Authorities (CAs), SSL Pinning hardcodes or ‘pins’ specific certificates or public keys within the application itself. This means the app will only trust those pre-defined certificates, rejecting any others, even if they are signed by a trusted CA. For penetration testers and security researchers, this presents a significant hurdle when attempting to intercept and analyze network traffic using tools like Burp Suite or OWASP ZAP.

This article will guide you through understanding SSL Pinning on Android and, more importantly, demonstrate how to effectively bypass it using Frida, a dynamic instrumentation toolkit. We’ll cover the setup, common pinning mechanisms, and powerful universal Frida scripts to overcome these defenses, enabling you to inspect application network communications.

Why Bypass SSL Pinning?

For legitimate security testing, bypassing SSL Pinning is essential for:

  • Traffic Analysis: Intercepting and inspecting encrypted traffic to understand API calls, data formats, and potential vulnerabilities.
  • Business Logic Testing: Manipulating requests and responses to identify flaws in the application’s business logic.
  • Vulnerability Discovery: Uncovering sensitive data leakage, improper authentication, or authorization issues that might only be visible at the network layer.

Frida: Your Dynamic Instrumentation Powerhouse

Frida is a cross-platform toolkit that allows you to inject custom scripts into running processes. It’s incredibly powerful for dynamic analysis, reverse engineering, and, as we’ll see, bypassing security controls like SSL Pinning. Frida works by injecting a JavaScript engine into target processes, enabling you to hook into functions, modify arguments, and even rewrite application logic on the fly.

Setting Up Frida for Android

Before we dive into bypass scripts, ensure you have Frida set up on your Android testing environment. You’ll need:

  1. A rooted Android device or emulator: Frida requires root privileges to inject into system-level processes or other applications effectively.
  2. Frida client on your workstation: Install with pip:pip install frida-tools
  3. Frida server on your Android device: Download the appropriate frida-server binary for your device’s architecture (e.g., arm64, x86) from Frida Releases.

Frida Server Setup Steps:

# On your workstation:Download frida-server-*-android-<arch>.xz# Extract the binary:unxz frida-server-*-android-<arch>.xz# Push to device:adb push frida-server-*-android-<arch> /data/local/tmp/frida-server# Make executable:adb shell "chmod 755 /data/local/tmp/frida-server"# Run in the background:adb shell "/data/local/tmp/frida-server &"

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

Common SSL Pinning Implementations

Android applications implement SSL Pinning in various ways. Understanding these common methods helps in choosing the right bypass strategy:

  • OkHttp / Retrofit: Many apps use OkHttp’s CertificatePinner or custom X509TrustManager implementations.
  • Android’s TrustManager: Directly overriding the default X509TrustManager to perform certificate validation.
  • Custom Network Libraries: Apps might use their own network stacks with bespoke pinning logic.
  • WebView Pinning: Less common, but possible for web content loaded within a WebView.

Universal SSL Pinning Bypass with Frida Scripts

The beauty of Frida lies in its ability to hook into core Java methods and native functions, often allowing for universal bypass scripts that target common pinning mechanisms without needing to know the app’s specific implementation details.

Script 1: Universal Android SSL Pinning Bypass (Java/OkHttp/TrustManager)

This script attempts to hook into various common Java classes and methods responsible for certificate validation, effectively disabling or overriding their pinning logic. It targets OkHttpClient‘s CertificatePinner, WebView‘s SslErrorHandler, and several X509TrustManager implementations.

Java.perform(function () {    console.log("[*] Starting Android Universal SSL Pinning Bypass...");    var CertificatePinner = Java.use("okhttp3.CertificatePinner");    CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function (str, list) {        console.log("[+] Bypassing OkHttp3 CertificatePinner: " + str);        return;    };    var TrustManager = Java.use('javax.net.ssl.X509TrustManager');    var HostnameVerifier = Java.use('javax.net.ssl.HostnameVerifier');    var SSLContext = Java.use('javax.net.ssl.SSLContext');    var array_list = Java.use("java.util.ArrayList");    var TrustManagers = Java.use('com.android.org.conscrypt.TrustManagerImpl');    // Bypass TrustManagerImpl (Android N/O/P/Q/R)    TrustManagers.verifyChain.implementation = function (chain, authType, host, managers) {        console.log("[+] Bypassing TrustManagerImpl verifyChain");        return chain;    };    // Create a custom TrustManager that trusts all certificates    var bypassTrustManager = Java.registerClass({        name: 'dev.frida.SSLTrustAllManager',        implements: [TrustManager],        methods: {            checkClientTrusted: function (chain, authType) {            },            checkServerTrusted: function (chain, authType) {            },            getAcceptedIssuers: function () {                return array_list.$new().toArray();            }        }    });    // Hook into SSLContext to use our custom TrustManager    var Initializer = SSLContext.$init.overload('java.lang.String');    Initializer.implementation = function (protocol) {        Initializer.call(this, protocol);        try {            var context = this;            var trustManagers = [bypassTrustManager.$new()];            context.init(null, trustManagers, null);            console.log("[+] SSLContext initialized with custom TrustManager");        } catch (e) {            console.log("[-] Error initializing SSLContext: " + e.message);        }    };    // For some older apps, direct hook on WebViewClient's onReceivedSslError    var WebViewClient = Java.use("android.webkit.WebViewClient");    WebViewClient.onReceivedSslError.implementation = function (view, handler, error) {        console.log("[+] Bypassing WebViewClient onReceivedSslError");        handler.proceed();    };    // For HostnameVerifier    HostnameVerifier.verify.implementation = function (hostname, session) {        console.log("[+] Bypassing HostnameVerifier for: " + hostname);        return true;    };    console.log("[*] Universal SSL Pinning Bypass script loaded!");});

How to Use the Script:

# Save the script as `universal-bypass.js`frida -U -f <package_name> -l universal-bypass.js --no-pause

Replace <package_name> with the target application’s package name (e.g., com.example.app). The --no-pause flag immediately attaches and injects the script.

Script 2: Native Hooking for OpenSSL/BoringSSL (for more stubborn cases)

Some applications might implement pinning at a lower, native level, often using OpenSSL or Google’s BoringSSL directly. For these scenarios, we might need to hook into native functions. This script targets common certificate verification functions in native libraries.

if (ObjC.available) {    // iOS specific, not applicable here. Keeping for completeness if discussing cross-platform.    // console.log("iOS specific bypass not covered in this Android guide.");} else if (Java.available) {    Java.perform(function () {        console.log("[*] Attempting native SSL bypass...");        var moduleName = "libssl.so"; // Or libboringssl.so, or libcryptoboring.so etc.        var address = Module.findExportByName(moduleName, "SSL_set_verify");        if (address) {            Interceptor.replace(address, new NativeCallback(function () {                console.log("[+] Hooked SSL_set_verify to disable verification.");                return;            }, 'void', ['pointer', 'int', 'pointer']));            console.log("[+] SSL_set_verify hooked.");        } else {            console.log("[-] SSL_set_verify not found in " + moduleName);        }        address = Module.findExportByName(moduleName, "SSL_CTX_set_verify");        if (address) {            Interceptor.replace(address, new NativeCallback(function () {                console.log("[+] Hooked SSL_CTX_set_verify to disable verification.");                return;            }, 'void', ['pointer', 'int', 'pointer']));            console.log("[+] SSL_CTX_set_verify hooked.");        } else {            console.log("[-] SSL_CTX_set_verify not found in " + moduleName);        }        // Another common target: BoringSSL's SSL_do_handshake        var SSL_do_handshake_address = Module.findExportByName(moduleName, "SSL_do_handshake");        if (SSL_do_handshake_address) {            Interceptor.attach(SSL_do_handshake_address, {                onEnter: function (args) {                    // Optionally examine args here                },                onLeave: function (retval) {                    console.log("[+] Bypassed SSL_do_handshake result. Returning success.");                    retval.replace(0); // Force SSL_do_handshake to return success (0)                }            });            console.log("[+] SSL_do_handshake hooked.");        } else {            console.log("[-] SSL_do_handshake not found in " + moduleName);        }        console.log("[*] Native SSL bypass script loaded!");    });}

Note on Native Hooks: Identifying the correct library (libssl.so, libboringssl.so, etc.) and function names is crucial. You might need to perform some initial reverse engineering (e.g., using Ghidra or IDA Pro) to find the relevant functions. The `retval.replace(0)` in `SSL_do_handshake` forces the handshake to succeed, bypassing certificate checks.

Applying the Bypass and Intercepting Traffic

Once you’ve successfully injected a Frida bypass script, you can now configure your proxy tool (e.g., Burp Suite) to intercept traffic. Ensure your Burp Suite proxy listener is set up correctly (usually on port 8080 or 8081). Then, configure your Android device to proxy all traffic through your workstation’s IP and Burp’s port.

# Example adb command to set global proxy (requires root on some devices/versions)adb shell settings put global http_proxy <YOUR_WORKSTATION_IP>:8080

With Frida active and the proxy configured, launch the target application. You should now observe encrypted HTTPS traffic flowing through Burp Suite, allowing you to inspect requests and responses in plaintext.

Conclusion

SSL Pinning, while a robust security feature, is not insurmountable for dedicated penetration testers armed with the right tools. Frida provides an incredibly versatile and powerful platform for dynamic instrumentation, enabling us to bypass various pinning implementations, from high-level Java code to lower-level native functions. By understanding the underlying mechanisms of SSL Pinning and mastering Frida’s capabilities, you can effectively analyze the network behavior of even the most secure Android applications, uncovering critical vulnerabilities and bolstering app security.

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