Android App Penetration Testing & Frida Hooks

Real-World Case Study: Bypassing SSL Pinning in Android Apps with Frida & OkHttp3 Hooks

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: The Challenge of SSL Pinning

SSL Pinning is a security mechanism implemented by developers to prevent man-in-the-middle (MITM) attacks. Instead of relying on the system’s trust store for Certificate Authorities (CAs), applications with SSL pinning explicitly ‘pin’ or hardcode specific server certificates or public keys within their code. This means the application will only trust connections to servers presenting one of these pre-approved certificates, even if a valid CA-signed certificate is presented by an attacker’s proxy.

Why Bypass SSL Pinning?

For penetration testers and security researchers, bypassing SSL pinning is often a critical step in understanding an application’s network communication. Without it, intercepting traffic using tools like Burp Suite or OWASP ZAP is impossible, hindering the ability to analyze API calls, data transmission, and potential vulnerabilities. This tutorial focuses on a common scenario: bypassing SSL pinning implemented using the OkHttp3 library, a widely adopted HTTP client for Android.

Tools of the Trade: Frida & OkHttp3

Frida: The Dynamic Instrumentation Toolkit

Frida is an open-source dynamic instrumentation toolkit that allows you to inject snippets of JavaScript or your own library into native apps on Windows, macOS, GNU/Linux, iOS, Android, and QNX. It’s incredibly powerful for security research, reverse engineering, and app analysis because it operates at runtime, enabling modifications to application logic and observation of internal behavior without needing to decompile, modify, and recompile the application.

OkHttp3: A Common Target

OkHttp3 is a popular, open-source HTTP client developed by Square, widely used in Android applications for making network requests. It offers features like connection pooling, GZIP compression, and response caching. Crucially for our purpose, OkHttp3 provides robust mechanisms for implementing SSL pinning, primarily through its CertificatePinner class, making it a frequent target for bypass attempts.

Prerequisites for Your Lab

Before we dive into the bypass, ensure you have the following setup:

  • Rooted Android Device or Emulator: Necessary for running Frida server with root privileges.
  • ADB (Android Debug Bridge): For interacting with your Android device.
  • Frida Server: Installed and running on your Android device.
  • Frida Client: Installed on your host machine (usually via pip install frida-tools).
  • Proxy Tool: Such as Burp Suite or mitmproxy, configured to intercept traffic from your Android device. Ensure your proxy’s CA certificate is installed on the Android device (even though pinning bypasses it, it’s good practice for other traffic).
  • Target Application: An Android application that uses OkHttp3 and implements SSL pinning.

Step-by-Step Guide: Bypassing OkHttp3 SSL Pinning

Step 1: Setting Up Your Environment

First, push the Frida server to your device and run it. Replace `[frida-server-version]` with the appropriate version for your device’s architecture (e.g., `frida-server-16.1.4-android-arm64`).

adb push frida-server-[frida-server-version]-android-arm64 /data/local/tmp/frida-serveradb shell 'chmod 755 /data/local/tmp/frida-server'adb shell '/data/local/tmp/frida-server &'

Next, configure your Android device to proxy all traffic through your host machine’s Burp Suite (or similar tool). Go to Wi-Fi settings, modify your network, and set up a manual proxy pointing to your host machine’s IP address and Burp Suite’s listening port (e.g., 8080).

Step 2: Identifying the Pinning Mechanism (Optional but Recommended)

While we’re targeting OkHttp3’s CertificatePinner, a quick check can confirm if the app indeed uses it. You can decompile the APK using tools like Apktool or MobSF and search for strings like `CertificatePinner`, `okhttp3`, or `pinning`. This step helps confirm the attack vector.

Step 3: Crafting the Frida Script for OkHttp3

The core of our bypass lies in a Frida script that hooks into OkHttp3’s SSL pinning logic. Specifically, we will target the check method of okhttp3.CertificatePinner and force it to return without performing any checks. Additionally, it’s good practice to try to disable pinning at the X509TrustManager level, as some applications might implement custom trust logic there.

Create a file named okhttp3_bypass.js with the following content:

Java.perform(function() {    console.log('--- OkHttp3 SSL Pinning Bypass Script Loaded ---');    try {        // Hook CertificatePinner.check()        var CertificatePinner = Java.use('okhttp3.CertificatePinner');        if (CertificatePinner) {            CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(hostname, peerCertificates) {                console.log('Bypassing CertificatePinner.check for hostname: ' + hostname);                // Do nothing, effectively bypassing the pinning check                return;            };            console.log('okhttp3.CertificatePinner.check() hook applied successfully.');        }    } catch (e) {        console.log('Failed to hook okhttp3.CertificatePinner.check(): ' + e.message);    }    try {        // Attempt to hook other common pinning mechanisms if they exist        // This targets the TrustManager that performs actual certificate validation.        var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');        var SSLContext = Java.use('javax.net.ssl.SSLContext');        var TrustManagerFactory = Java.use('javax.net.ssl.TrustManagerFactory');        var KeyStore = Java.use('java.security.KeyStore');        var TrustManager = Java.use('javax.net.ssl.TrustManager');        var ArrayList = Java.use('java.util.ArrayList');        var Arrays = Java.use('java.util.Arrays');        var X509Certificate = Java.use('java.security.cert.X509Certificate');        // Custom TrustManager that accepts any certificate        var CustomTrustManager = Java.registerClass({            name: 'com.example.ssl.CustomTrustManager',            implements: [X509TrustManager],            methods: {                checkClientTrusted: function(chain, authType) {                    console.log('CustomTrustManager: checkClientTrusted - ALL TRUSTED');                },                checkServerTrusted: function(chain, authType) {                    console.log('CustomTrustManager: checkServerTrusted - ALL TRUSTED');                },                getAcceptedIssuers: function() {                    console.log('CustomTrustManager: getAcceptedIssuers');                    return [];                }            }        });        // Replace default TrustManagers with our custom one        SSLContext.init.overload('[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom').implementation = function(keyManagers, trustManagers, secureRandom) {            console.log('SSLContext.init hook: Replacing TrustManagers');            var customTrustManagers = ArrayList.$new();            customTrustManagers.add(CustomTrustManager.$new());            this.init(keyManagers, customTrustManagers.toArray(), secureRandom);        };        TrustManagerFactory.init.overload('java.security.KeyStore').implementation = function(keyStore) {            console.log('TrustManagerFactory.init hook: Bypassing KeyStore init');            // We don't want the factory to initialize with a specific keystore,            // so we'll just call the original method with null or let it fail silently            // if the original is bypassed. For now, we'll try to let it pass            // to ensure other components aren't broken, but our SSLContext hook should win.            return this.init(keyStore);        };        TrustManagerFactory.getTrustManagers.implementation = function() {            console.log('TrustManagerFactory.getTrustManagers hook: Injecting CustomTrustManager');            var originalManagers = this.getTrustManagers();            var newManagers = [];            for (var i = 0; i < originalManagers.length; i++) {                if (Java.instance(originalManagers[i]).$className.indexOf('X509TrustManager') !== -1) {                    newManagers.push(CustomTrustManager.$new());                } else {                    newManagers.push(originalManagers[i]);                }            }            return newManagers;        };        console.log('Attempted to hook X509TrustManager and SSLContext for broader bypass.');    } catch (e) {        console.log('Failed to apply broader TrustManager/SSLContext hooks: ' + e.message);    }    console.log('--- OkHttp3 SSL Pinning Bypass Script Finished Loading ---');});

This script has two main parts: first, it directly hooks okhttp3.CertificatePinner.check, which is the most common way OkHttp3 implements pinning. By overriding this method to do nothing, we effectively tell the app to trust any certificate presented. Second, it attempts a broader bypass by replacing the system’s X509TrustManager and hooking SSLContext.init to inject our own TrustManager that accepts all certificates. This provides a more robust bypass for various SSL/TLS implementations.

Step 4: Executing the Bypass

Now, execute the Frida script against your target application. Replace `[package_name]` with the actual package name of the Android application you’re testing (e.g., `com.example.app`).

frida -U -f [package_name] -l okhttp3_bypass.js --no-pause
  • -U: Targets a USB-connected device.
  • -f [package_name]: Spawns the specified package name.
  • -l okhttp3_bypass.js: Loads our bypass script.
  • --no-pause: Prevents Frida from pausing the spawned application, allowing it to start immediately.

Once the command runs, Frida will inject the script into the app process as it launches. You should see output from the script’s `console.log` statements in your terminal, indicating that the hooks have been applied.

Step 5: Verifying the Bypass

With the script running, interact with the application. Attempt to perform actions that involve network communication. Simultaneously, monitor your Burp Suite (or other proxy tool). If the bypass is successful, you should now see the application’s network traffic flowing through your proxy, allowing you to intercept, inspect, and modify requests and responses.

Understanding the Frida Script Logic

The primary hook `CertificatePinner.check.overload(‘java.lang.String’, ‘java.util.List’).implementation = function(…)` is crucial. The `CertificatePinner` class in OkHttp3 is responsible for verifying that the server’s certificate matches one of the expected pins. By overriding its `check` method and providing an empty implementation, we essentially tell the application:

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