Android App Penetration Testing & Frida Hooks

Real-World Scenarios: Bypassing OkHttp3 SSL Pinning with Frida in Complex Android Apps

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to SSL Pinning and its Bypass

SSL (Secure Sockets Layer) pinning is a security mechanism employed by client applications to prevent Man-in-the-Middle (MitM) attacks by ensuring that the application only trusts specific, predefined server certificates or public keys. While crucial for enhancing security, it poses a significant challenge for penetration testers and security researchers who need to intercept and analyze application traffic. OkHttp3, a popular HTTP client for Android and Java, offers robust SSL pinning capabilities, making it a frequent target for bypass attempts during security audits.

This article dives deep into practical strategies for bypassing OkHttp3 SSL pinning, particularly in complex Android applications that might employ custom implementations or obfuscation. We’ll leverage Frida, a dynamic instrumentation toolkit, to hook into the application’s runtime and modify its behavior, allowing us to inspect network traffic.

Understanding OkHttp3 SSL Pinning Mechanisms

OkHttp3 implements SSL pinning primarily through the CertificatePinner class. This class takes a list of hostnames and their corresponding SHA-256 hashes of the public key or entire certificate. During a TLS handshake, OkHttp3 verifies that at least one of the pinned certificates/public keys matches the server’s certificate chain. If no match is found, the connection is aborted, preventing unauthorized interception.

A typical OkHttp3 pinning setup looks like this:

OkHttpClient client = new OkHttpClient.Builder()
    .certificatePinner(new CertificatePinner.Builder()
        .add("example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
        .build())
    .build();

More complex applications might also use custom X509TrustManager implementations or modify the default TrustManagerFactory to enforce pinning, adding layers of complexity to the bypass process.

Prerequisites for Frida-Based Bypassing

  • Rooted Android Device or Emulator: Frida requires root access to inject its agent into target processes.
  • Frida-server: The Frida server running on the Android device, matching the device’s architecture (ARM, ARM64, x86).
  • Frida-tools: Installed on your host machine (e.g., via pip install frida-tools).
  • ADB (Android Debug Bridge): For interacting with the device and pushing Frida server.
  • Network Proxy Tool: Such as Burp Suite or OWASP ZAP, configured to intercept traffic.
  • Basic Knowledge: Familiarity with Java/Kotlin, JavaScript, and command-line interfaces.

The Challenge with Generic SSL Unpinning Scripts

Many publicly available Frida SSL unpinning scripts target common system-level TrustManagers or generic OkHttp3 patterns. While effective against straightforward implementations, they often fail against:

  • Custom X509TrustManager: Apps that implement their own logic for certificate validation.
  • Dynamically Loaded Classes: Pinning logic might reside in classes loaded at runtime, after a generic script has already run.
  • Obfuscated Code: Class and method names can be changed, making direct hooking difficult.
  • Multi-layered Pinning: Combining CertificatePinner with custom trust managers.

This necessitates a more targeted and analytical approach.

Targeting OkHttp3’s CertificatePinner with Frida

The most direct way to bypass OkHttp3’s built-in pinning is to hook the check method of the okhttp3.CertificatePinner class. By hooking this method, we can effectively make it always return without performing the actual pinning checks.

Frida Script for CertificatePinner Bypass

Java.perform(function() {
    console.log("[*] Starting OkHttp3 CertificatePinner bypass...");

    try {
        var CertificatePinner = Java.use("okhttp3.CertificatePinner");

        CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function (hostname, peerCertificates) {
            console.log("[*] Bypassing OkHttp3 CertificatePinner.check for hostname: " + hostname);
            // Do not call original implementation, effectively disabling pinning
            // this.check(hostname, peerCertificates); // <--- DO NOT CALL THIS
        };

        // Handle the other overload if it exists and is used
        CertificatePinner.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;').implementation = function (hostname, peerCertificates) {
            console.log("[*] Bypassing OkHttp3 CertificatePinner.check (array overload) for hostname: " + hostname);
        };

        console.log("[*] OkHttp3 CertificatePinner bypass applied successfully.");
    } catch (e) {
        console.error("[-] Error during OkHttp3 CertificatePinner bypass: " + e);
    }
});

This script targets both common overloads of the check method. When either is called, our custom implementation (which does nothing) is executed, effectively neutering the pinning logic.

Advanced Scenarios: Custom X509TrustManager Bypasses

If the above script doesn’t work, the application likely employs a custom X509TrustManager. These custom managers override the default trust evaluation process. Our goal is to find and hook the checkServerTrusted method within these custom implementations.

Finding Custom TrustManagers

This often requires some static analysis (e.g., using JADX or Ghidra) to identify classes that implement javax.net.ssl.X509TrustManager or extend relevant `TrustManager` classes. Look for calls to checkServerTrusted within the application’s code.

Frida Script Snippet for Custom TrustManager

Java.perform(function() {
    console.log("[*] Looking for custom X509TrustManager implementations...");

    Java.enumerateLoadedClasses({
        onMatch: function(className) {
            if (className.includes("TrustManager") && !className.startsWith("android.") && !className.startsWith("com.android.") && !className.startsWith("org.conscrypt.")) {
                try {
                    var targetClass = Java.use(className);
                    if (targetClass.$interfaces.includes("javax.net.ssl.X509TrustManager")) {
                        console.log("[*] Found custom X509TrustManager: " + className);

                        targetClass.checkServerTrusted.overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String').implementation = function (chain, authType) {
                            console.log("[*] Bypassing custom checkServerTrusted in: " + className);
                            // Do nothing, effectively trusting all certificates
                        };
                        // Add more overloads if necessary based on analysis
                    }
                } catch (e) {
                    // console.error("[-] Error processing class " + className + ": " + e);
                }
            }
        },
        onComplete: function() {
            console.log("[*] X509TrustManager enumeration complete.");
        }
    });
});

This script attempts to enumerate loaded classes and identify those that implement X509TrustManager, then hooks their checkServerTrusted methods. Remember that the `includes` part might need refinement based on the specific app’s package name or class structure to avoid false positives.

Dynamic Class Loading and Obfuscation Considerations

Obfuscation renames classes and methods, making direct string matching (e.g., "okhttp3.CertificatePinner") unreliable. Dynamic class loading means the class might not be available when Java.perform first runs.

  • Enumerating Loaded Classes: As shown in the custom TrustManager example, Java.enumerateLoadedClasses is vital for finding dynamically loaded or obfuscated classes at runtime.
  • Stalker/Interceptor: For highly dynamic or native code scenarios, Frida’s Stalker or Interceptor can be used to monitor memory access or function calls, but this is significantly more complex.
  • Delayed Hooks: If a class is loaded later, you might need to use techniques like hooking java.lang.ClassLoader.loadClass to catch the moment a specific class is loaded and then apply your hook.

Step-by-Step Bypass Methodology

Step 1: Setup Frida Environment

Push the Frida server to your Android device, make it executable, and run it in the background:

adb push frida-server /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &"

Verify it’s running: adb shell "ps -ef | grep frida"

Step 2: Identify Target Application Package Name

List all installed applications and find your target:

frida-ps -Uai

Note down the package name (e.g., com.example.targetapp).

Step 3: Initial Generic Bypass Attempt (Optional but Recommended)

Try a universal SSL unpinning script first. Many are available online (e.g., from codeshare.frida.re). This helps quickly identify if the app uses a simple pinning mechanism.

frida -U -f com.example.targetapp -l universal-unpinning.js --no-pause

Monitor Burp/ZAP. If traffic flows, you’re done. If not, proceed to more targeted approaches.

Step 4: Decompile and Analyze (Crucial for Complex Apps)

Use JADX or Ghidra to decompile the APK. Search for:

  • okhttp3.CertificatePinner: To find direct OkHttp3 pinning.
  • X509TrustManager: To find custom trust managers.
  • TrustManagerFactory: To see if the default factory is being manipulated.
  • Keywords like `pinning`, `certificate`, `trust`, `ssl` within the source code.

Identify the exact class and method names responsible for certificate validation.

Step 5: Craft Specific Frida Script

Based on your analysis from Step 4, write a targeted Frida script. If you found `CertificatePinner` usage, use the first script provided. If you found a custom `X509TrustManager`, use or adapt the second script. For obfuscated apps, you might need to combine enumeration with string matching on method signatures if direct class names are not clear.

Step 6: Execute Targeted Frida Script

Run your custom script against the application. The -f flag spawns the app, and --no-pause ensures it runs immediately.

frida -U -f com.example.targetapp -l my_okhttp3_bypass.js --no-pause

Observe the Frida output for any errors or confirmation messages from your script.

Step 7: Verify Bypass and Intercept Traffic

With your proxy (Burp/ZAP) configured and its CA certificate installed on the Android device, attempt to use the application. If the bypass is successful, you should now see the application’s network traffic flowing through your proxy, allowing for detailed inspection and modification.

Conclusion

Bypassing SSL pinning in Android applications, especially those utilizing robust frameworks like OkHttp3 with custom implementations, requires a systematic and often iterative approach. While generic scripts can be a starting point, understanding the underlying mechanisms and employing targeted Frida hooks based on static analysis is key to success in real-world scenarios. Frida’s dynamic instrumentation capabilities empower security researchers to overcome sophisticated protections and gain crucial insights into application behavior.

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