Android App Penetration Testing & Frida Hooks

Crafting Custom Frida Scripts for Android Certificate Pinning Defeat (A Deep Dive)

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: The Battle Against Certificate Pinning

Certificate pinning is a robust security mechanism implemented in mobile applications, particularly on Android, to prevent man-in-the-middle (MITM) attacks. It ensures that an application only trusts a specific server certificate or a public key, rather than any certificate signed by a trusted root CA. While essential for security, this often presents a significant hurdle for penetration testers and security researchers who need to intercept and analyze application traffic using tools like Burp Suite or OWASP ZAP.

Generic certificate pinning bypass scripts for Frida, while powerful, often fall short against highly customized or obfuscated pinning implementations. This article delves into the advanced techniques required to craft custom Frida scripts, empowering you to defeat even the most resilient Android certificate pinning mechanisms.

Understanding Android Certificate Pinning Implementations

Before bypassing, it’s crucial to understand how pinning is typically implemented on Android. Common methods include:

  • Java TrustManager API: Custom implementations of `X509TrustManager`, specifically overriding `checkServerTrusted`.
  • OkHttp Library: Leveraging `CertificatePinner` or custom `HostnameVerifier` within `OkHttpClient.Builder`.
  • WebView: Handling SSL errors via `onReceivedSslError` or `onPageStarted`.
  • Custom SSL Libraries: Such as OpenSSL, Conscrypt, or even native C/C++ implementations.

Generic scripts usually target the `TrustManager` API. When these fail, it signifies a more sophisticated, possibly custom or library-specific, pinning logic.

Setting Up Your Frida Environment

Ensure you have Frida installed on your host machine and the Frida server running on your Android device.

Host Machine Setup

pip install frida-tools

Device Setup

# Download the correct frida-server for your device's architecture (e.g., arm64) from GitHub releasesfrida-server-16.1.4-android-arm64adb push frida-server /data/local/tmp/adb shell "chmod 755 /data/local/tmp/frida-server"adb shell "/data/local/tmp/frida-server &"

Identifying the Pinning Mechanism: Static & Dynamic Analysis

When generic Frida scripts fail, a more targeted approach is needed. This involves a combination of static and dynamic analysis.

1. Static Analysis (Decompilation)

Use tools like Jadx-GUI, Ghidra, or APKTool to decompile the Android application (APK). Look for keywords associated with certificate pinning:

  • `X509TrustManager`
  • `checkServerTrusted`
  • `CertificatePinner`
  • `HostnameVerifier`
  • `onReceivedSslError`
  • `okhttp3`
  • `ssl`
  • `trust`

Pay close attention to custom classes implementing `X509TrustManager` or custom methods handling SSL certificates. For instance, you might find a class like `com.example.app.security.CustomTrustManager` extending `X509TrustManager` and overriding `checkServerTrusted` with specific logic.

2. Dynamic Analysis (Frida Enumeration & Tracing)

If static analysis doesn’t immediately reveal the mechanism, use Frida to enumerate loaded classes and trace method calls related to SSL/TLS.

// script.js - Enumerate TrustManagersJava.perform(function() {    Java.enumerateClasses({        onMatch: function(className) {            if (className.includes('Trust') || className.includes('SSL')) {                // console.log(className); // Uncomment for broad enumeration                try {                    var clazz = Java.use(className);                    if (clazz.$interface && clazz.$interface.includes('X509TrustManager')) {                        console.log('Found X509TrustManager implementation: ' + className);                    }                } catch (e) {                    // Handle cases where a class cannot be loaded                }            }        },        onComplete: function() {            console.log('Enumeration complete.');        }    });});
frida -U -f com.example.app -l script.js --no-pause

Once you identify a potential `TrustManager` or SSL-related class, use `frida-trace` to monitor its methods:

frida-trace -U -f com.example.app -i "*checkServerTrusted*" -i "*verify*"

This will generate a handler file (e.g., `_checkServerTrusted.js`) where you can insert custom logic.

Crafting Custom Frida Scripts for Targeted Bypasses

The core of defeating custom pinning lies in understanding the application’s unique implementation and crafting a script to specifically nullify its validation logic.

Scenario 1: Custom `checkServerTrusted` Implementation

Many applications implement their own `X509TrustManager` or extend a default one to add custom validation logic within `checkServerTrusted`. Our goal is to make this method return without performing any checks.

Example Static Analysis Discovery

You find a class `com.example.app.security.PinningTrustManager` in Jadx:

public class PinningTrustManager implements X509TrustManager {    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { ... }    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {        // CUSTOM PINNING LOGIC HERE        if (!isCertificateValid(chain[0])) {            throw new CertificateException("Server certificate not pinned!");        }    }    public X509Certificate[] getAcceptedIssuers() { ... }    private boolean isCertificateValid(X509Certificate cert) { ... }}

Custom Frida Script

Java.perform(function() {    try {        // Target the specific custom TrustManager        var PinningTrustManager = Java.use('com.example.app.security.PinningTrustManager');        PinningTrustManager.checkServerTrusted.implementation = function(chain, authType) {            console.log('Bypassing custom checkServerTrusted in PinningTrustManager!');            // Do nothing, effectively bypassing the pinning logic            // You can optionally log the certificates for debugging            // for (var i = 0; i < chain.length; i++) {            //     console.log('  Certificate ' + i + ': ' + chain[i].getSubjectDN().getName());            // }        };        console.log('Custom PinningTrustManager checkServerTrusted hook installed!');    } catch (e) {        console.error('Failed to hook PinningTrustManager: ' + e);    }    // Also include a generic TrustManager bypass for good measure,    // in case another TrustManager is used or the app has fallback.    try {        var TrustManager = Java.use('javax.net.ssl.X509TrustManager');        var SSLContext = Java.use('javax.net.ssl.SSLContext');        var TrustManagerFactory = Java.use('javax.net.ssl.TrustManagerFactory');        var certs = Java.array('java.security.cert.X509Certificate', []);        var ByteArrayInputStream = Java.use('java.io.ByteArrayInputStream');        var CertificateFactory = Java.use('java.security.cert.CertificateFactory');        var KeyStore = Java.use('java.security.KeyStore');        var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');        // --- Generic checkServerTrusted bypass ---        TrustManager.checkServerTrusted.implementation = function(chain, authType) {            console.log('Bypassing generic checkServerTrusted in X509TrustManager!');        };        // --- For apps using Conscrypt TrustManagerImpl ---        TrustManagerImpl.checkServerTrusted.implementation = function(chain, authType, host) {            console.log('Bypassing Conscrypt checkServerTrusted for host: ' + host);        };        // --- Custom SSLContext initialization bypass ---        SSLContext.init.overload('[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom').implementation = function(km, tm, sr) {            console.log('Bypassing SSLContext.init');            // Provide a dummy TrustManager that accepts all certs            var CustomTrustManager = Java.registerClass({                name: 'com.example.CustomTrustManager',                implements: [TrustManager],                methods: {                    checkClientTrusted: function(chain, authType) {},                    checkServerTrusted: function(chain, authType) {},                    getAcceptedIssuers: function() { return certs; }                }            });            this.init(km, [CustomTrustManager.$new()], sr);        };        console.log('Generic TrustManager hooks installed!');    } catch (e) {        console.error('Failed to install generic TrustManager hooks: ' + e);    }});

Scenario 2: OkHttp `CertificatePinner` Bypass

If the application uses OkHttp, it might employ `CertificatePinner` or a custom `HostnameVerifier`.

Example Static Analysis Discovery

You find code like:

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

Custom Frida Script

Java.perform(function() {    try {        var CertificatePinner = Java.use('okhttp3.CertificatePinner');        CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(hostname, peerCertificates) {            console.log('Bypassing CertificatePinner.check for host: ' + hostname);            // Do nothing, effectively bypassing the pinning            // original method: this.check(hostname, peerCertificates);        };        // For older versions or alternative check methods        CertificatePinner.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;').implementation = function(hostname, peerCertificates) {            console.log('Bypassing CertificatePinner.check (old overload) for host: ' + hostname);        };        console.log('OkHttp CertificatePinner hooks installed!');    } catch (e) {        console.error('Failed to hook OkHttp CertificatePinner: ' + e);    }    // Also bypass HostnameVerifier if present    try {        var HostnameVerifier = Java.use('javax.net.ssl.HostnameVerifier');        var CustomHostnameVerifier = Java.registerClass({            name: 'com.example.CustomHostnameVerifier',            implements: [HostnameVerifier],            methods: {                verify: function(hostname, session) {                    console.log('Bypassing Custom HostnameVerifier for host: ' + hostname);                    return true; // Always return true                }            }        });        // Find all OkHttpClient.Builder instances and replace their HostnameVerifier        var OkHttpClientBuilder = Java.use('okhttp3.OkHttpClient$Builder');        OkHttpClientBuilder.hostnameVerifier.implementation = function(verifier) {            console.log('Replacing HostnameVerifier with custom one.');            return this.hostnameVerifier(CustomHostnameVerifier.$new());        };        console.log('HostnameVerifier bypass installed!');    } catch (e) {        console.error('Failed to hook HostnameVerifier: ' + e);    }});

Scenario 3: WebView `onReceivedSslError` Bypass

For applications using `WebView` to display web content, certificate errors might be handled in `onReceivedSslError`.

Example Static Analysis Discovery

public class MyWebViewClient extends WebViewClient {    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {        // Custom handling, often cancels by default        // handler.cancel();    }}

Custom Frida Script

Java.perform(function() {    try {        var WebViewClient = Java.use('android.webkit.WebViewClient');        // Look for custom WebViewClient implementations        Java.enumerateClasses({            onMatch: function(className) {                try {                    var clazz = Java.use(className);                    if (clazz.$interface && clazz.$interface.includes('android.webkit.WebViewClient')) {                        console.log('Found WebViewClient implementation: ' + className);                        var CustomWebViewClient = Java.use(className);                        if (CustomWebViewClient.onReceivedSslError) {                            CustomWebViewClient.onReceivedSslError.implementation = function(view, handler, error) {                                console.log('Bypassing onReceivedSslError for WebView: ' + error.getUrl());                                handler.proceed(); // Crucially call proceed()                                // this.onReceivedSslError(view, handler, error); // Avoid calling original to prevent issues                            };                            console.log('Hooked onReceivedSslError in ' + className);                        }                    }                } catch (e) {                    // console.error('Error processing class ' + className + ': ' + e);                }            },            onComplete: function() {                console.log('WebViewClient enumeration complete.');            }        });    } catch (e) {        console.error('Failed to hook WebViewClient: ' + e);    }});

Running Your Custom Script

Save your script (e.g., `bypass.js`) and run it with Frida:

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

Observe the Frida output for your `console.log` messages, indicating successful hooks and bypasses. Simultaneously, attempt to intercept the application’s traffic using your proxy (e.g., Burp Suite) to verify the bypass.

Advanced Techniques and Debugging Tips

  • Logging Arguments: Use `JSON.stringify` on complex objects passed to methods to understand their structure: `console.log(‘Args: ‘ + JSON.stringify(args));`
  • Handling Overloads: Remember to use `.overload()` when hooking overloaded methods, specifying argument types.
  • Dynamic Class Discovery: If a class name is obfuscated or dynamically loaded, use `Java.enumerateLoadedClasses` or `frida-trace` without specific method filters to find relevant classes during runtime.
  • Method Invocation: If you need to call the original method within your hook, use `this.methodName.call(this, arg1, arg2, …)` or `this.methodName.originalMethod.call(this, arg1, arg2, …)` for specific scenarios.
  • Handling Native Libraries: For pinning implemented in native code (JNI, C/C++), you’ll need to use Frida’s `Module.findExportByName` and `Interceptor.attach` to hook native functions. This is a more advanced topic beyond the scope of this deep dive but an important consideration.

Conclusion

Defeating Android certificate pinning, especially highly customized implementations, requires a systematic approach combining static analysis, dynamic enumeration, and the precise crafting of custom Frida scripts. By understanding the underlying pinning mechanisms and leveraging Frida’s powerful JavaScript API, penetration testers can effectively bypass these security controls, enabling thorough security assessments. Always remember to conduct such activities ethically and with proper authorization.

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