Android App Penetration Testing & Frida Hooks

Penetration Tester’s Playbook: Frida for Android Custom SSL Pinning Exploitation

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to SSL Pinning and Its Variants

SSL pinning is a security mechanism implemented by applications to prevent man-in-the-middle (MITM) attacks. Instead of relying solely on the device’s trust store, applications “pin” specific server certificates or public keys, only trusting connections to servers presenting those exact credentials. While standard SSL pinning can often be bypassed with generic Frida scripts that hook common Android security APIs (like okhttp3, conscrypt, etc.), custom implementations pose a more significant challenge for penetration testers.

Understanding Custom SSL Pinning

Custom SSL pinning refers to scenarios where developers implement their own certificate validation logic, often by:

  • Implementing a custom X509TrustManager.
  • Directly comparing certificate hashes or public keys against hardcoded values.
  • Using third-party networking libraries with unique pinning mechanisms that aren’t covered by standard bypass scripts.
  • Performing validation at the native layer (JNI).

These custom implementations bypass the common hooking points, requiring a more targeted and often application-specific approach.

The Penetration Tester’s Methodology: Identifying Custom Pinning

Step 1: Initial Assessment and Generic Bypass Attempt

Always start by trying generic Frida SSL pinning bypass scripts. If these fail, it strongly indicates a custom implementation.

frida -U -f com.example.app -l universal-android-ssl-bypass.js --no-pause

If the application still fails to connect through a proxy like Burp Suite, it’s time for deeper analysis.

Step 2: Static Analysis with Decompilers (Jadx/Ghidra)

Decompile the APK using tools like Jadx-GUI. Search for keywords related to certificate validation:

  • X509TrustManager
  • TrustManagerFactory
  • checkServerTrusted
  • verify (especially with HostnameVerifier)
  • PublicKey, Certificate, MessageDigest (for hashing)
  • Hardcoded strings resembling certificate hashes or public keys (e.g., base64 encoded strings, hex values).
  • Third-party networking libraries (e.g., Volley, Retrofit, custom HTTP clients).

Pay close attention to methods that take X509Certificate[] as arguments or return boolean values after performing checks. Look for custom classes implementing X509TrustManager or methods that manipulate SSLSocketFactory.

Step 3: Dynamic Analysis with Frida’s JavaScript API

Once static analysis provides potential areas of interest, use Frida to dynamically inspect the application’s runtime behavior.

Enumerating Classes and Methods

A powerful technique is to enumerate loaded classes and look for patterns. For instance, if you suspect a custom TrustManager, you can search for classes containing “Trust” or “SSL”.

Java.perform(function() {    Java.enumerateLoadedClasses({        onMatch: function(className) {            if (className.includes("Trust") || className.includes("SSL")) {                console.log("[+] Found class: " + className);            }        },        onComplete: function() {            console.log("[+] Class enumeration complete!");        }    });});

This script provides a starting point. Once interesting classes are identified, you can enumerate their methods:

Java.perform(function() {    var TargetClass = Java.use("com.example.app.CustomTrustManager"); // Replace with actual class    var methods = TargetClass.class.getDeclaredMethods();    for (var i = 0; i < methods.length; i++) {        console.log("[+] Method: " + methods[i].getName());    }});

Tracing Key Methods

If static analysis pointed to specific methods, use Frida’s `Interceptor` or `Java.use` and `implementation` to trace their execution and parameters. This helps confirm if they are indeed responsible for pinning and how they function.

Java.perform(function() {    var X509Certificate = Java.use("java.security.cert.X509Certificate");    var CustomTrustManager = Java.use("com.example.app.CustomTrustManager"); // Example custom TrustManager    CustomTrustManager.checkServerTrusted.implementation = function(chain, authType) {        console.log("[*] CustomTrustManager.checkServerTrusted called!");        for (var i = 0; i < chain.length; i++) {            console.log("    Certificate " + i + " Subject: " + chain[i].getSubjectDN().getName());            console.log("    Certificate " + i + " Issuer: " + chain[i].getIssuerDN().getName());        }        console.log("    AuthType: " + authType);        // Call original to see the actual behavior or block it.        // this.checkServerTrusted(chain, authType);    };});

Crafting the Custom Pinning Bypass Script

Once the specific custom validation method is identified, the bypass script can be written. The goal is typically to make the validation method always return void (if it’s a check method that throws an exception on failure) or true (if it’s a boolean-returning verify method).

Example Scenario: Overriding a Custom X509TrustManager.checkServerTrusted

Suppose our static and dynamic analysis reveals a class com.example.app.security.MyCustomTrustManager which overrides checkServerTrusted and performs custom logic.

// Simplified example of custom pinning logic in Javapackage com.example.app.security;import java.security.cert.CertificateException;import java.security.cert.X509Certificate;import javax.net.ssl.X509TrustManager;public class MyCustomTrustManager implements X509TrustManager {    private static final String PINNED_PUBLIC_KEY_HASH = "SOME_HARDCODED_HASH"; // Example    @Override    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {        // Not relevant for server pinning bypass in most cases    }    @Override    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {        if (chain == null || chain.length == 0) {            throw new IllegalArgumentException("Certificate chain is empty or null.");        }        X509Certificate serverCert = chain[0];        // Custom logic: e.g., verify public key hash        String serverPublicKeyHash = calculateHash(serverCert.getPublicKey()); // Hypothetical method        if (!PINNED_PUBLIC_KEY_HASH.equals(serverPublicKeyHash)) {            throw new CertificateException("Custom pinning failed: Public key mismatch!");        }        System.out.println("Custom pinning successful!");    }    @Override    public X509Certificate[] getAcceptedIssuers() {        return new X509Certificate[0];    }    private String calculateHash(java.security.PublicKey publicKey) {        // ... actual hashing logic ...        return "DYNAMICALLY_CALCULATED_HASH";    }}

Frida Bypass for the Above Custom TrustManager

The bypass script will directly hook checkServerTrusted and ensure it doesn’t throw an exception, effectively bypassing the custom logic.

Java.perform(function() {    console.log("[+] Attempting to bypass custom SSL pinning...");    try {        var MyCustomTrustManager = Java.use("com.example.app.security.MyCustomTrustManager");        if (MyCustomTrustManager) {            MyCustomTrustManager.checkServerTrusted.implementation = function(chain, authType) {                console.log("[*] Hooked MyCustomTrustManager.checkServerTrusted!");                // Do nothing, effectively bypassing the validation                // You can also log information here if needed                console.log("    Certificate chain length: " + chain.length);                console.log("    Auth Type: " + authType);            };            console.log("[+] MyCustomTrustManager.checkServerTrusted hooked successfully!");        } else {            console.log("[-] MyCustomTrustManager not found. Pinning might be elsewhere.");        }    } catch (e) {        console.error("[-] Error hooking custom TrustManager: " + e.message);    }    // You might also need to hook HostnameVerifier if it's used    try {        var HostnameVerifier = Java.use("javax.net.ssl.HostnameVerifier");        HostnameVerifier.verify.implementation = function(hostname, session) {            console.log("[*] Hooked HostnameVerifier.verify!");            return true; // Always return true        };        console.log("[+] HostnameVerifier.verify hooked successfully!");    } catch (e) {        console.log("[-] HostnameVerifier not found or not applicable: " + e.message);    }});

Execution

Save the above JavaScript code as custom-pinning-bypass.js and execute it with Frida:

frida -U -f com.example.app -l custom-pinning-bypass.js --no-pause

Ensure your proxy (e.g., Burp Suite) is configured correctly, and the device’s proxy settings are pointing to your Burp Listener. You should now be able to intercept traffic.

Conclusion

Bypassing custom SSL pinning on Android applications requires a blend of static and dynamic analysis. While more challenging than generic pinning, a methodical approach involving decompilation and targeted Frida hooks can effectively disarm even sophisticated custom implementations. The key lies in identifying the precise methods responsible for validation and then overriding or nullifying their security checks. This expert-level technique is indispensable for comprehensive Android application penetration testing.

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