Author: admin

  • Frida for Android Pentesters: Intercepting JNI_OnLoad & Private Native Functions Step-by-Step

    Introduction

    Android applications often leverage Java Native Interface (JNI) to interact with native C/C++ libraries. These native components are critical areas for security analysis, as they frequently contain performance-sensitive code, cryptographic operations, or obfuscated logic that can bypass Java-level protections. Frida, a dynamic instrumentation toolkit, is an indispensable tool for Android penetration testers looking to peer into these native layers. This guide will walk you through advanced Frida techniques to intercept the crucial JNI_OnLoad function and hook private (non-exported) native functions, providing a deeper understanding and control over an application’s native behavior.

    Understanding JNI_OnLoad

    When an Android application loads a native library (e.g., System.loadLibrary("mylib")), the Android system looks for and executes a function named JNI_OnLoad within that library. This function is typically where the library performs its initialization, including registering native methods with the Java Virtual Machine (JVM) using RegisterNatives. Intercepting JNI_OnLoad provides an early and powerful vantage point to observe which native methods are being registered and even to tamper with their registration process or arguments.

    Prerequisites

    • An Android device or emulator with root access.
    • Frida server running on the Android device/emulator.
    • Frida client installed on your host machine (pip install frida-tools).
    • ADB (Android Debug Bridge) installed and configured.
    • Basic familiarity with C/C++, JNI, and Android development concepts.
    • Optional: Static analysis tools like Ghidra or IDA Pro for analyzing native libraries.

    Hooking JNI_OnLoad with Frida

    To hook JNI_OnLoad, we first need to identify the target native library. For this example, let’s assume our target library is named libnative-lib.so. We can find the address of JNI_OnLoad using Frida’s Module.findExportByName function.

    Frida Script for JNI_OnLoad

    Java.perform(function () {
        var libraryName = "libnative-lib.so"; // Replace with your target library name
    
        // Wait for the library to be loaded
        Interceptor.attach(Module.findExportByName(null, 'android_dlopen_ext'), {
            onEnter: function (args) {
                this.library_path = Memory.readUtf8String(args[0]);
            },
            onLeave: function (retval) {
                if (this.library_path.indexOf(libraryName) !== -1) {
                    console.log("[+] Target library loaded: " + this.library_path);
                    hookJniOnLoad(libraryName);
                }
            }
        });
    
        function hookJniOnLoad(libName) {
            var jniOnLoadPtr = Module.findExportByName(libName, "JNI_OnLoad");
    
            if (jniOnLoadPtr) {
                console.log("[+] Found JNI_OnLoad at: " + jniOnLoadPtr);
                Interceptor.attach(jniOnLoadPtr, {
                    onEnter: function (args) {
                        console.log("[*] JNI_OnLoad called!");
                        console.log("    -> JavaVM pointer: " + args[0]);
                        console.log("    -> Reserved (JNIEnv): " + args[1]);
                        // Optionally, peek into the JNIEnv structure to find RegisterNatives
                        // This is more advanced and requires parsing C structures in JS
                    },
                    onLeave: function (retval) {
                        console.log("[*] JNI_OnLoad finished.");
                        console.log("    -> Returned JNI version: " + retval.toInt32());
                    }
                });
            } else {
                console.log("[-] JNI_OnLoad not found in " + libName);
            }
        }
    });
    

    Explanation:

    1. We first use a hook on android_dlopen_ext (or __loader_dlopen for older Android versions) to detect when our target library libnative-lib.so is loaded. This ensures our JNI_OnLoad hook is applied at the correct time.
    2. Once loaded, hookJniOnLoad is called.
    3. Module.findExportByName(libName, "JNI_OnLoad") locates the entry point of the JNI_OnLoad function.
    4. Interceptor.attach is then used to intercept its execution, logging the JavaVM* and reserved arguments, and its return value (the JNI version).

    Hooking Private Native Functions

    Private native functions are those C/C++ functions within a native library that are not explicitly exported or registered with the JVM via RegisterNatives. They might be internal helper functions, complex algorithms, or core logic called by other exported JNI methods. Hooking these functions requires a different approach, often involving static analysis to find their exact memory offsets.

    Method 1: Hooking Exported but Unregistered Functions

    Sometimes, a function is exported from the shared library but not directly registered as a JNI method. You can find these using tools like nm -D on the library or readelf -s. If found, you can use Module.findExportByName similarly to JNI_OnLoad.

    # On your host machine, pull the library from the device
    adb pull /data/app/com.example.app/lib/arm64/libnative-lib.so
    
    # Analyze exports
    nm -D libnative-lib.so | grep "_Z" # For C++ mangled names
    nm -D libnative-lib.so | grep "my_secret_func"
    

    Once identified, the Frida script is straightforward:

    // ... inside Java.perform ...
    var secretExportedFuncPtr = Module.findExportByName("libnative-lib.so", "my_secret_exported_function");
    if (secretExportedFuncPtr) {
        Interceptor.attach(secretExportedFuncPtr, {
            onEnter: function (args) {
                console.log("[+] secret_exported_function called with arg0: " + args[0].toInt32());
            },
            onLeave: function (retval) {
                console.log("[+] secret_exported_function returned: " + retval.toInt32());
            }
        });
    }
    

    Method 2: Hooking Non-Exported/Private Functions by Offset

    This is the most common scenario for truly private functions. You will typically need to use static analysis tools like Ghidra or IDA Pro to find the relative offset of the function from the base address of the library.

    Steps to Find Offset (using Ghidra as an example):

    1. Obtain the Native Library: Pull the target .so file from the Android device using ADB.adb pull /data/app/com.example.app/lib/arm64/libnative-lib.so .
    2. Open in Ghidra: Launch Ghidra, create a new project, and import the .so file. Analyze it with default options.
    3. Locate the Function: Navigate through the symbol tree or search for function names (e.g., in the Listing view, look for calls to your target function from exported JNI methods). Identify the function you want to hook.
    4. Determine Offset: Once you’ve located your target function (e.g., calculate_private_hash), observe its memory address in Ghidra. This address is relative to the start of the memory segment in the Ghidra view. The crucial part is to get the *relative offset* from the library’s base address. In Ghidra, this is usually the address shown for the function, minus the `0x100000000` (or similar, depending on load address) base of the ELF section. A simpler way is to look at the ‘Entry Point’ in the function’s properties or locate its address in the `Symbols` window. Let’s say Ghidra shows the function at `0x12345678`. If the base address of the `.text` segment starts at `0x100000000`, the offset would be `0x12345678 – 0x100000000`. More reliably, identify the function’s address in Ghidra and calculate the difference between that address and the *base address of the module as loaded by Ghidra*. Often, the base address of the `.text` segment in Ghidra starts at a fixed offset (e.g., 0x0 or 0x100000000 for PIE binaries), so the function’s address shown directly represents its offset from the library’s load address. For example, if a function is at `0x2120` in Ghidra’s disassembly of a PIE binary, `0x2120` is its offset.

    Frida Script for Offset Hooking

    Once you have the offset (e.g., 0x2120), you can combine Frida’s ability to find the base address of the loaded library with this offset.

    Java.perform(function () {
        var libraryName = "libnative-lib.so";
        var privateFunctionOffset = 0x2120; // Replace with the actual offset found via static analysis
    
        // Wait for the library to be loaded (same as JNI_OnLoad example)
        Interceptor.attach(Module.findExportByName(null, 'android_dlopen_ext'), {
            onEnter: function (args) {
                this.library_path = Memory.readUtf8String(args[0]);
            },
            onLeave: function (retval) {
                if (this.library_path.indexOf(libraryName) !== -1) {
                    console.log("[+] Target library loaded: " + this.library_path);
                    hookPrivateNativeFunction(libraryName, privateFunctionOffset);
                }
            }
        });
    
        function hookPrivateNativeFunction(libName, offset) {
            var baseAddress = Module.findBaseAddress(libName);
            if (baseAddress) {
                var privateFuncPtr = baseAddress.add(offset);
                console.log("[+] Hooking private function at calculated address: " + privateFuncPtr);
    
                Interceptor.attach(privateFuncPtr, {
                    onEnter: function (args) {
                        console.log("[*] Private function called!");
                        console.log("    -> Argument 0: " + args[0].toInt32());
                        console.log("    -> Argument 1: " + Memory.readUtf8String(args[1]));
                        // Modify arguments if needed: args[0] = new NativePointer(123);
                    },
                    onLeave: function (retval) {
                        console.log("[*] Private function finished.");
                        console.log("    -> Original return value: " + retval.toInt32());
                        // Modify return value if needed: retval.replace(new NativePointer(456));
                    }
                });
            } else {
                console.log("[-] Could not find base address for " + libName);
            }
        }
    });
    

    Explanation:

    1. Similar to JNI_OnLoad, we wait for the library to be loaded.
    2. Module.findBaseAddress(libName) retrieves the actual runtime base address where the library is loaded into memory.
    3. By adding our statically determined offset to this base address, we get the precise runtime address of the private function: baseAddress.add(offset).
    4. Interceptor.attach is then used to hook this specific address, allowing inspection and modification of arguments and return values.

    Example C++ Native Library (native-lib.cpp)

    #include <jni.h>
    #include <string>
    #include <android/log.h>
    
    #define TAG "NativeLib"
    #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
    
    // A private helper function not exported or directly registered
    int calculate_secret_value(int a, const char* msg) {
        LOGD("[%s] calculate_secret_value called with %d and %s", TAG, a, msg);
        return a * 2 + strlen(msg);
    }
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_example_fridanatives_MainActivity_stringFromJNI(
            JNIEnv* env, jobject /* this */) {
        std::string hello = "Hello from C++";
        int result = calculate_secret_value(10, "FridaTest");
        hello += ", Secret Result: " + std::to_string(result);
        return env->NewStringUTF(hello.c_str());
    }
    
    // JNI_OnLoad is called when the library is loaded
    extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
        JNIEnv* env;
        if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
            return JNI_ERR;
        }
    
        // Example: Registering a native method (though we use JNI_OnLoad for logging)
        // This example focuses on hooking JNI_OnLoad itself rather than specific registrations
    
        LOGD("[%s] JNI_OnLoad called! JVM: %p, Reserved: %p", TAG, vm, reserved);
        return JNI_VERSION_1_6;
    }
    

    This C++ code defines a JNI_OnLoad function and a private function calculate_secret_value that is called by stringFromJNI. After compiling this into libnative-lib.so, you would use Ghidra to find the offset of calculate_secret_value.

    Conclusion

    Intercepting JNI_OnLoad and private native functions are powerful techniques for any Android penetration tester or reverse engineer. By understanding how native libraries initialize and how to locate functions not explicitly exposed, you gain unparalleled control and visibility into the deepest layers of an Android application’s logic. Frida’s dynamic instrumentation capabilities, combined with static analysis tools, provide a comprehensive toolkit for uncovering hidden behaviors and vulnerabilities within native code.

  • Reverse Engineering Lab: Bypassing Android Native Anti-Tampering via Frida JNI_OnLoad Hooks

    Introduction to Native Anti-Tampering and Frida

    Android applications, especially those handling sensitive data or intellectual property, often implement robust anti-tampering mechanisms to deter reverse engineering, piracy, and unauthorized modifications. While Java-level checks can be straightforward to bypass, native anti-tampering implemented in C/C++ within shared libraries (.so files) presents a more significant challenge. These native checks can include integrity verification, debugger detection, root detection, and signature validation, often executed early in the app’s lifecycle.

    Frida, a dynamic instrumentation toolkit, is an invaluable tool for Android penetration testers and reverse engineers. It allows us to inject custom scripts into running processes, enabling us to inspect, modify, and even redirect function calls in real-time. This article delves into an advanced technique for bypassing native anti-tampering: hooking the JNI_OnLoad function using Frida.

    Understanding Native Anti-Tampering in Android

    Native anti-tampering techniques leverage the power and low-level access of C/C++ to implement checks that are harder to observe and manipulate from the Java layer. Common techniques include:

    • Integrity Checks: Verifying the integrity of the APK, DEX files, or native libraries themselves (e.g., checksums, cryptographic hashes) to detect modifications.
    • Debugger Detection: Checking for the presence of debuggers (e.g., tracing PIDs, inspecting /proc/self/status or /proc/self/task).
    • Root Detection: Looking for common root indicators (e.g., su binaries, known root paths, sensitive files).
    • Signature Verification: Ensuring the app’s signing certificate matches an expected value.
    • Anti-Emulation/Virtualization: Detecting execution within emulators or virtual machines.

    Many of these critical checks are often initialized or performed very early, sometimes even before the application’s Java code has fully loaded. This is where JNI_OnLoad becomes a prime target.

    JNI_OnLoad: The Early Execution Hook

    In Android, when a native shared library is loaded by the Java Virtual Machine (JVM), the JVM looks for an exported function named JNI_OnLoad. If found, this function is executed immediately after the library is loaded and before any other native methods (e.g., Java_com_package_Class_method) are called. Its primary purpose is to allow the native library to perform initialization tasks, such as registering native methods dynamically and setting the JNI version.

    This early execution makes JNI_OnLoad a strategic point for both developers to implement robust anti-tampering and for reverse engineers to bypass it. By hooking JNI_OnLoad, we gain the ability to:

    1. Observe the initial setup routines of the native library.
    2. Intercept and potentially modify the behavior of functions called within JNI_OnLoad.
    3. Establish other hooks on critical anti-tampering functions *before* they execute or fully initialize their checks.

    Identifying and Hooking JNI_OnLoad

    Before we can hook JNI_OnLoad, we need to locate it within the target native library. We can use tools like readelf, objdump, or disassemblers like IDA Pro/Ghidra.

    Step 1: Locate the Native Library and JNI_OnLoad

    First, identify the native library (e.g., libnative-lib.so) that contains the anti-tampering logic. Then, use a disassembler to find the JNI_OnLoad function. Its signature is typically jint JNI_OnLoad(JavaVM* vm, void* reserved).

    $ adb shell pm path com.example.myapp # Get APK path$ adb pull /data/app/~~.../com.example.myapp-XYZ==/base.apk$ unzip base.apk lib/armeabi-v7a/libnative-lib.so # Extract .so$ readelf -s libnative-lib.so | grep JNI_OnLoad   # Find symbol offset

    Alternatively, Frida can discover loaded modules and their exports:

    // frida_find_onload.jsProcess.enumerateModules().forEach(function(module){    if(module.name.indexOf('libnative-lib.so') !== -1) {        console.log("Found libnative-lib.so at base address: " + module.base);        module.enumerateExports().forEach(function(exp){            if(exp.name === 'JNI_OnLoad'){                console.log("JNI_OnLoad found at: " + exp.address);            }        });    }});
    $ frida -U -l frida_find_onload.js com.example.myapp

    Step 2: Understanding the Anti-Tampering Logic

    Once JNI_OnLoad is located, the next crucial step is static analysis using IDA Pro or Ghidra to understand what functions it calls, especially those related to security checks. Look for calls to functions like check_debugger, verify_integrity, is_rooted, or similar named functions that return a boolean or status code. For instance, a function native_security_check() might be called, returning 0 for pass and 1 for fail.

    Bypassing with Frida: A Practical Example

    Let’s assume our target native library libnative-lib.so has a JNI_OnLoad function that calls a critical anti-tampering function, let’s say native_integrity_check(), which returns 0 on success and 1 on failure. If this function returns 1, the app might exit or disable critical features. Our goal is to force native_integrity_check() to always return 0.

    Simulated Native Code Structure

    // native-lib.c#include <jni.h>#include <string.h>#include <stdio.h>jboolean native_integrity_check() {    // Simulate a complex integrity check    // For demonstration, let's assume it always fails for now    printf("[*] Performing native integrity check...n");    // In a real scenario, this would check CRC, debugger, etc.    return JNI_TRUE; // Assume check fails (returns true for failure condition)}jint JNI_OnLoad(JavaVM* vm, void* reserved) {    JNIEnv* env;    if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {        return JNI_ERR;    }    // Perform early anti-tampering check    if (native_integrity_check()) {        printf("[!] JNI_OnLoad: Integrity check FAILED. Exiting or limiting features.n");        // In a real app, this might crash or call exit()    } else {        printf("[+] JNI_OnLoad: Integrity check PASSED.n");    }    // Register native methods (if any)    return JNI_VERSION_1_6;}JNIEXPORT jstring JNICALL Java_com_example_myapp_NativeLib_stringFromJNI(        JNIEnv* env,        jobject /* this */) {    return (*env)->NewStringUTF(env, "Hello from JNI!");}

    Frida Script to Bypass native_integrity_check

    Our Frida script will find libnative-lib.so, locate native_integrity_check, and then hook it to always return JNI_FALSE (which corresponds to 0, indicating success in our simulated scenario).

    // bypass_native_tampering.jsfunction bypassNativeIntegrityCheck() {    var moduleName = "libnative-lib.so";    var targetModule = null;    Process.enumerateModules().forEach(function(module){        if(module.name === moduleName){            targetModule = module;            console.log("[+] Found " + moduleName + " at base address: " + module.base);        }    });    if(!targetModule){        console.error("[-] " + moduleName + " not found. Ensure the app is running and the library is loaded.");        return;    }    // Locate the native_integrity_check function relative to the module's base address    // This offset needs to be determined via static analysis (IDA Pro/Ghidra) or by finding its export    // For this example, let's assume native_integrity_check is exported or we know its offset.    // If it's not exported, you'll need to calculate the offset from JNI_OnLoad or module base.    // For simplicity, let's assume it's exported for now, or use a known offset if not.    var integrityCheckFunction = targetModule.findExportByName("native_integrity_check");    if (!integrityCheckFunction) {        // If not exported, you might need to find it by pattern or offset within JNI_OnLoad        // Example (hypothetical offset): var integrityCheckFunction = targetModule.base.add(0x1234);        console.error("[-] native_integrity_check function not found in " + moduleName + ".");        return;    }    console.log("[+] Found native_integrity_check at: " + integrityCheckFunction);    // Hook the native_integrity_check function    Interceptor.attach(integrityCheckFunction, {        onEnter: function(args) {            console.log("[*] Hooked native_integrity_check called!");            // Optionally, log arguments or context        },        onLeave: function(retval) {            console.log("[*] Original native_integrity_check returned: " + retval);            // Modify the return value to always be JNI_FALSE (0) for success            // JNI_TRUE is 1, JNI_FALSE is 0            retval.replace(0); // Force return value to 0            console.log("[+] Forced native_integrity_check return to JNI_FALSE (0).");        }    });    console.log("[+] Frida script loaded and native_integrity_check hooked successfully.");}setImmediate(bypassNativeIntegrityCheck);

    To run this script:

    $ frida -U -l bypass_native_tampering.js com.example.myapp

    When the application loads libnative-lib.so, JNI_OnLoad will be called. Inside JNI_OnLoad, native_integrity_check() will be invoked. However, because our Frida script has already hooked native_integrity_check(), its original logic will execute (or we could prevent it entirely with `onEnter` and `return`), but its return value will be forcibly changed to 0 (JNI_FALSE), successfully bypassing the anti-tampering measure.

    Why JNI_OnLoad is Key for Hooking

    The beauty of targeting JNI_OnLoad (or functions called within it) is that it executes very early in the native library’s lifecycle. This means we can establish our hooks before critical anti-tampering logic has a chance to execute and detect our presence, or before it sets up irreversible states (like exiting the application). If we tried to hook these functions later, the app might have already performed its checks and potentially exited or altered its behavior.

    Advanced Considerations

    • Obfuscation: Native anti-tampering functions are often heavily obfuscated, making static analysis challenging. Symbol stripping is common, requiring analysis based on function prologue/epilogue or call graphs.
    • Anti-Frida Techniques: Some sophisticated anti-tampering solutions might try to detect Frida’s presence (e.g., checking for specific process names, memory regions, or system calls used by Frida). Bypassing these might require more advanced Frida techniques or custom agents.
    • Timing Attacks: Anti-tampering checks might be time-sensitive or run in separate threads, requiring careful synchronization of hooks.
    • Registering New Native Methods: Instead of bypassing, sometimes you might want to register your own custom native method within JNI_OnLoad to gain a backdoor into the application.

    Conclusion

    Bypassing native anti-tampering in Android applications is a critical skill for security researchers and penetration testers. By understanding the lifecycle of native libraries and strategically leveraging JNI_OnLoad with Frida, we can intercept and manipulate low-level security checks before they can affect the application’s execution. This approach provides a powerful method for gaining control over applications that employ even advanced native protections, paving the way for further analysis and vulnerability discovery.

  • The Ultimate Frida Hook: Defeating Android Custom Cert Pinning on Any App

    Introduction: The Challenge of Custom Cert Pinning

    Certificate pinning is a security mechanism implemented by applications to prevent Man-in-the-Middle (MitM) attacks. Instead of relying on the device’s trust store, applications ‘pin’ specific server certificates or public keys, trusting only those. While generic Frida scripts (like those from Frida-codeshare or Universal Android SSL Pinning Bypass) often succeed against standard implementations, custom certificate pinning poses a significant hurdle. These advanced implementations bypass common hooking points, requiring a more targeted and informed approach.

    This article provides an expert-level guide to defeating custom certificate pinning on Android applications using Frida. We’ll delve into reconnaissance, identify common custom pinning patterns, and craft highly specific Frida hooks to ensure success.

    Understanding Custom Certificate Pinning

    How Apps Implement Custom Pinning

    Custom certificate pinning typically involves developers writing their own logic to validate server certificates, bypassing or extending standard Android APIs. Common patterns include:

    • Custom X509TrustManager implementations: Apps create their own class that implements javax.net.ssl.X509TrustManager and override methods like checkServerTrusted() to perform custom validation.
    • Direct SSLContext manipulation: Instead of using default TrustManagerFactory, they might explicitly provide an array of custom TrustManagers when initializing SSLContext.
    • Framework-specific pinning: Libraries like OkHttp provide their own pinning mechanisms (e.g., okhttp3.CertificatePinner), which, while not custom code, are distinct from the standard Android APIs.
    • Native code pinning: Less common but more challenging, where pinning logic resides in native libraries (C/C++). This guide primarily focuses on Java/Kotlin layer bypasses.

    The Reconnaissance Phase: Identifying the Pinning Mechanism

    The key to defeating custom pinning is understanding *how* the app implements it. This requires reverse engineering.

    Step 1: Decompilation and Code Analysis

    Your primary tools here will be apktool for resources and manifest, and jadx (or similar decompilers like Ghidra, JEB) for Java source code analysis.

    1. Decompile the APK:
      apktool d myapp.apk
    2. Analyze with jadx: Open the APK directly in the jadx-gui or decompile it to source files:
      jadx -d output_dir myapp.apk
    3. Search for keywords: Look for classes or methods related to SSL/TLS and trust management. Key terms to search for include:
      • TrustManager, X509TrustManager
      • SSLContext, SSLSocketFactory
      • certificate, pinning, checkServerTrusted
      • hostnameverifier
      • Specific network libraries like okhttp, retrofit (e.g., CertificatePinner, pinSet)
    4. Identify custom implementations: Look for classes that extend or implement X509TrustManager or instantiate SSLContext with non-default trust managers. Pay close attention to the checkServerTrusted method in these custom classes.

    Step 2: Runtime Analysis (Optional but Recommended)

    Frida can also aid in dynamic reconnaissance. If static analysis doesn’t yield clear results, you can use Frida to enumerate loaded classes or trace method calls.

    • Enumerate loaded classes: To find custom TrustManagers at runtime:
      Java.perform(function() {Java.enumerateLoadedClassesSync().forEach(function(className) {if (className.includes("TrustManager")) {console.log(className);}});});

      Attach this script with frida -U -f com.package.name -l script.js --no-pause and observe the output.

    • Use frida-trace: Trace methods like javax.net.ssl.TrustManagerFactory.init or javax.net.ssl.SSLContext.init to see which TrustManagers are being passed.

    Crafting Your Targeted Frida Hook

    Once you’ve identified the custom pinning logic, you can write a specific Frida script to bypass it.

    Strategy 1: Hooking checkServerTrusted in Custom X509TrustManager

    This is often the most direct approach if a custom X509TrustManager is found. You simply override its checkServerTrusted method to do nothing, effectively trusting all certificates.

    Example Custom Java TrustManager:

    package com.example.app.security;import javax.net.ssl.X509TrustManager;import java.security.cert.CertificateException;import java.security.cert.X509Certificate;public class CustomPinningTrustManager implements X509TrustManager {    @Override    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { /* ... */ }    @Override    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {        // This is where the custom pinning logic would reside        if (!verifyCertificateAgainstPins(chain[0])) {            throw new CertificateException("Server certificate not pinned!");        }    }    @Override    public X509Certificate[] getAcceptedIssuers() {        return new X509Certificate[0];    }}

    Frida Hook for CustomPinningTrustManager.checkServerTrusted:

    Java.perform(function() {    try {        var CustomTrustManager = Java.use("com.example.app.security.CustomPinningTrustManager"); // Replace with actual class name        CustomTrustManager.checkServerTrusted.implementation = function(chain, authType) {            console.log("[*] Bypassing custom checkServerTrusted in CustomPinningTrustManager!");            // Simply return without calling the original method to bypass the check            // If needed, you could optionally call the original with try/catch            // this.checkServerTrusted(chain, authType);        };        console.log("[+] Custom TrustManager checkServerTrusted hook applied successfully.");    } catch (e) {        console.log("[-] Failed to hook CustomPinningTrustManager: " + e);    }});

    Strategy 2: Hooking SSLContext.init

    If the app dynamically provides custom TrustManagers to SSLContext.init(), you can intercept this call and replace them with a dummy TrustManager that trusts everything.

    Java.perform(function() {    try {        var SSLContext = Java.use("javax.net.ssl.SSLContext");        SSLContext.init.overload('[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom').implementation = function(keyManagers, trustManagers, secureRandom) {            console.log("[*] Intercepted SSLContext.init!");            var newTrustManagers = [];            // Create a dummy X509TrustManager that accepts all certificates            var BypassX509TrustManager = Java.registerClass({                name: 'org.frida.BypassX509TrustManager',                implements: [Java.use('javax.net.ssl.X509TrustManager')],                methods: {                    checkClientTrusted: function(chain, authType) {                        console.log("[+] BypassX509TrustManager: checkClientTrusted called.");                    },                    checkServerTrusted: function(chain, authType) {                        console.log("[+] BypassX509TrustManager: checkServerTrusted called.");                        // Do nothing, trust all server certificates                    },                    getAcceptedIssuers: function() {                        console.log("[+] BypassX509TrustManager: getAcceptedIssuers called.");                        return Java.array('Ljava.security.cert.X509Certificate;', []);                    }                }            });            // Replace all original TrustManagers with our dummy one            for (var i = 0; i < trustManagers.length; i++) {                newTrustManagers.push(BypassX509TrustManager.$new());            }            // Call the original init method with our new TrustManagers            this.init(keyManagers, newTrustManagers, secureRandom);            console.log("[+] SSLContext.init successfully bypassed with dummy TrustManager!");        };        console.log("[+] SSLContext.init hook applied successfully.");    } catch (e) {        console.log("[-] Failed to hook SSLContext.init: " + e);    }});

    Strategy 3: Bypassing okhttp3.CertificatePinner (If Applicable)

    If the app uses OkHttp and its built-in CertificatePinner, you can specifically target its check method. Decompilation will reveal if okhttp3.CertificatePinner is used.

    Java.perform(function() {    try {        var CertificatePinner = Java.use('okhttp3.CertificatePinner');        // Hook the overload with List        CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(hostname, peerCertificates) {            console.log('[*] Bypassing OkHttp CertificatePinner.check for: ' + hostname);            // Do nothing, effectively bypassing the pinning check            return;        };        console.log('[+] okhttp3.CertificatePinner.check (List overload) hook applied!');    } catch (e) {        console.log('[-] okhttp3.CertificatePinner (List overload) not found or hook failed: ' + e);    }    try {        // Hook the overload with Certificate[]        var CertificatePinner2 = Java.use('okhttp3.CertificatePinner');        CertificatePinner2.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;').implementation = function(hostname, peerCertificates) {            console.log('[*] Bypassing OkHttp CertificatePinner.check for: ' + hostname);            // Do nothing            return;        };        console.log('[+] okhttp3.CertificatePinner.check (Array overload) hook applied!');    } catch (e) {        console.log('[-] okhttp3.CertificatePinner (Array overload) not found or hook failed: ' + e);    }});

    Deployment and Verification

    Setting up Frida and Proxy

    Before running your hook, ensure your environment is set up:

    1. Rooted Android device/emulator: With frida-server running.
    2. Proxy tool: Burp Suite or OWASP ZAP configured and listening, with its CA certificate installed on the Android device.
    3. ADB port forwarding: If Burp Suite is on your host machine:
      adb forward tcp:8080 tcp:8080

      (Replace 8080 with your proxy’s listening port.)

    Running Your Custom Frida Hook

    Save your crafted JavaScript code (e.g., as custom_cert_bypass.js) and run it:

    frida -U -f com.package.name -l custom_cert_bypass.js --no-pause
    • -U: Connect to a USB device.
    • -f com.package.name: Spawn the application (replace com.package.name with the target app’s package).
    • -l custom_cert_bypass.js: Load your Frida script.
    • --no-pause: Start the application immediately after injection.

    Testing the Bypass

    Interact with the application, specifically features that make network requests. Monitor your proxy tool (e.g., Burp Suite) for intercepted traffic. If successful, you should see requests flowing through your proxy, allowing you to inspect and manipulate them.

    Conclusion: Empowering Your Android Pentesting Toolkit

    Defeating custom certificate pinning requires a blend of static and dynamic analysis, coupled with precise Frida scripting. By systematically reverse-engineering the application’s unique implementation, you can craft a targeted hook that bypasses even the most robust pinning mechanisms. This advanced technique significantly enhances your Android penetration testing capabilities, allowing you to gain full visibility into application network communications and uncover hidden vulnerabilities. Remember, understanding the underlying mechanism is always the ultimate bypass.

  • Understanding & Evading Android Custom Certificate Pinning with Frida’s Power

    Introduction to Certificate Pinning and Its Purpose

    Certificate pinning is a security mechanism employed by applications to prevent Man-in-the-Middle (MITM) attacks. Instead of relying solely on the device’s pre-installed trusted root certificates, applications embed or ‘pin’ specific server certificates or public keys within their code. This ensures that the app will only communicate with servers presenting one of these pre-approved certificates, effectively preventing an attacker from intercepting traffic by issuing a fake certificate from an untrusted Certificate Authority (CA).

    While essential for enhancing application security, certificate pinning poses a significant challenge for security researchers and penetration testers who need to intercept and analyze application traffic. Bypassing this mechanism is crucial for identifying vulnerabilities in API interactions, data handling, and other server-side components.

    Differentiating System and Custom Pinning

    System-Wide Pinning and Its Bypass

    Many applications rely on the Android system’s default certificate validation process. In these cases, installing a trusted user certificate (like Burp Suite’s CA certificate) into the Android system’s trust store often suffices to intercept traffic. Tools like MagiskTrustUserCerts or Xposed modules like TrustMeAlready can automate this process by moving user-installed certificates into the system trust store.

    The Challenge of Custom Certificate Pinning

    Custom certificate pinning, however, is far more robust and challenging to bypass. Instead of using the default system validation, applications implement their own certificate validation logic. This often involves:

    • Using custom implementations of javax.net.ssl.X509TrustManager.
    • Leveraging network libraries like OkHttp with its CertificatePinner class.
    • Implementing checks directly in native code (JNI).
    • Storing pins as raw public keys or hashes, not just full certificates.

    Because the app bypasses the standard Android trust store, simply installing a proxy’s CA certificate won’t work. The application performs its own checks, failing if the presented server certificate doesn’t match its internal pins, regardless of whether the system trusts the proxy’s CA.

    Introducing Frida: Your Dynamic Instrumentation Toolkit

    Frida is a dynamic instrumentation toolkit that allows you to inject JavaScript (or your own library) into native apps on Windows, macOS, Linux, iOS, Android, and QNX. It provides a powerful API to hook into application functions at runtime, modify their behavior, inspect memory, and even inject custom code.

    For bypassing custom certificate pinning, Frida is indispensable. It allows us to intercept and modify the application’s internal certificate validation logic without altering the application binary. This is particularly effective against custom Java/Kotlin-based pinning implementations, even when the code is obfuscated.

    Setting Up Your Android Penetration Testing Environment

    Prerequisites

    • A rooted Android device (physical or emulator).
    • Android Debug Bridge (ADB) installed on your host machine.
    • Python 3 installed on your host machine.
    • Burp Suite or OWASP ZAP for traffic interception.

    Installing Frida Server on Android

    1. Download the appropriate Frida server binary for your Android device’s architecture (e.g., frida-server-x.x.x-android-arm64) from Frida’s GitHub releases page.

    2. Push the server binary to your device:

    adb push frida-server-x.x.x-android-arm64 /data/local/tmp/frida-server

    3. Make the server executable:

    adb shell

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

    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.

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

    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.

  • Beyond Standard: Attacking Android Apps with Frida Custom TrustManager Bypass

    Introduction: The Pinning Paradox for Penetration Testers

    Certificate pinning is a crucial security mechanism in modern Android applications, designed to prevent Man-in-the-Middle (MitM) attacks by ensuring that an app only communicates with servers presenting specific, pre-defined certificates or public keys. While incredibly effective for enhancing security, it poses a significant challenge for penetration testers and security researchers who rely on intercepting network traffic to analyze application behavior, identify vulnerabilities, and understand data flows. Tools like Burp Suite or OWASP ZAP, which typically inject their own Root CA certificates, often fail when an app implements robust certificate pinning.

    Frida, a dynamic instrumentation toolkit, is the go-to solution for bypassing many Android security controls, including standard SSL pinning. However, not all pinning implementations are created equal. When developers opt for custom `X509TrustManager` implementations, the standard Frida scripts often fall short, demanding a more targeted and nuanced approach. This article delves into the intricacies of bypassing such custom pinning mechanisms using Frida, equipping you with the expert-level knowledge to tackle even the most resilient Android app security.

    Limitations of Standard Frida SSL Unpinning Scripts

    Generic Frida scripts for SSL unpinning are highly effective because they target common, well-known locations where certificate checks occur within Android’s framework or popular networking libraries (e.g., OkHttp, Volley, Android’s `HttpsURLConnection`). These scripts typically hook methods related to `SSLContext`, `TrustManagerFactory`, or directly within popular HTTP client libraries, forcing them to trust any certificate or return a pre-defined set of trusted issuers.

    Java.perform(function () {    var certificateFactory = Java.use("java.security.cert.CertificateFactory");    var FileInputStream = Java.use("java.io.FileInputStream");    var BufferedInputStream = Java.use("java.io.BufferedInputStream");    var KeyStore = Java.use("java.security.KeyStore");    var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");    var SSLContext = Java.use("javax.net.ssl.SSLContext");    // Hooking various places...    // ... this approach often misses custom TrustManager implementations.});

    The limitation arises when an application implements its own `X509TrustManager` from scratch. Instead of relying on the system’s `TrustManagerFactory` to provide trust decisions, the app’s developers write their own logic to validate server certificates. This custom logic can be deeply embedded within the application’s codebase, making it invisible to generic hooks that expect to find certificate validation at standard framework locations. Therefore, to bypass these custom implementations, we must first understand how they work and then specifically target their custom validation methods.

    Demystifying Custom TrustManagers in Android

    What is a TrustManager?

    In Java’s security architecture, a `TrustManager` is a component responsible for deciding whether the credentials of a remote host (e.g., a server’s SSL certificate) should be trusted. The `X509TrustManager` interface, specifically, deals with X.509 certificates, which are the standard for SSL/TLS. It defines three key methods:

    • checkClientTrusted(X509Certificate[] chain, String authType): Verifies the client’s certificate chain. Rarely used for typical server pinning bypass.
    • checkServerTrusted(X509Certificate[] chain, String authType): The most critical method for server certificate pinning. It’s invoked by the TLS handshake process to determine if the server’s certificate chain should be trusted. If this method throws a `CertificateException`, the connection is aborted.
    • getAcceptedIssuers(): Returns an array of acceptable CAs for authenticating peers.

    When an Android app connects to an HTTPS server, the `checkServerTrusted` method of the configured `X509TrustManager` is called with the server’s certificate chain (`chain`) and the authentication type (`authType`). A standard `TrustManager` would validate the chain against the device’s pre-installed trusted CAs. A custom `TrustManager`, however, might implement additional or entirely different validation rules.

    Why Custom Implementations?

    Developers implement custom `X509TrustManager` interfaces to enforce stricter certificate pinning policies that go beyond what the operating system or default libraries provide. This allows them to:

    • **Pin specific certificates**: The app trusts only an exact certificate.
    • **Pin public keys**: The app trusts any certificate whose public key matches a predefined one.
    • **Pin subject public key information (SPKI) hashes**: A common and robust method where the hash of the certificate’s public key is hardcoded.
    • **Implement custom logic**: Add expiration checks, blacklist specific CAs, or integrate with internal security services.

    The core idea is that even if an attacker manages to install a rogue CA certificate on the device, the custom `TrustManager` will ignore the system’s trust store and rely solely on its internal, hardcoded validation logic, leading to a `CertificateException` if the server’s certificate doesn’t match the pinned one.

    // Example: A hypothetical custom TrustManager enforcing public key hash pinningpackage com.example.app;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;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 = "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; // A base64 SHA-256 hash    @Override    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {        // Client certificates are not typically pinned this way    }    @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]; // Get the leaf certificate        try {            byte[] publicKeyEncoded = serverCert.getPublicKey().getEncoded();            MessageDigest md = MessageDigest.getInstance("SHA-256");            byte[] publicKeyHash = md.digest(publicKeyEncoded);            String calculatedHash = java.util.Base64.getEncoder().encodeToString(publicKeyHash);            if (!PINNED_PUBLIC_KEY_HASH.equals("sha256/" + calculatedHash)) {                throw new CertificateException("Server certificate public key hash mismatch!");            }        } catch (NoSuchAlgorithmException e) {            throw new CertificateException("Algorithm not found", e);        }        // If we reach here, the certificate is considered trusted. No exception thrown.    }    @Override    public X509Certificate[] getAcceptedIssuers() {        // Return an empty array or specific pinned certs if applicable        return new X509Certificate[0];    }}

    Identifying the Custom TrustManager in a Target App

    The first critical step in bypassing custom pinning is to locate the `X509TrustManager` implementation within the target application. This typically involves reverse engineering the APK:

    1. Decompile the APK:

      Use tools like Jadx-GUI, APKTool, or Ghidra to decompile the application’s APK file into Java source code or Smali. Jadx-GUI is particularly user-friendly for navigating the codebase.

    2. Search for Keywords:

      Once decompiled, search the entire codebase for common keywords that indicate custom trust management. Key terms include:

      • TrustManager
      • X509TrustManager
      • checkServerTrusted
      • getAcceptedIssuers
      • pinning
      • SSLContext (look for instantiations that might pass a custom `TrustManager` array)

      You can use `grep -r

  • Advanced Frida Hooking: Reverse Engineering Android Custom SSL Pinning Techniques

    Introduction to SSL Pinning and Its Challenges

    SSL/TLS pinning is a crucial security mechanism employed by mobile applications to prevent Man-in-the-Middle (MITM) attacks. By hardcoding a server’s legitimate certificate or public key within the client application, an app can verify that it is communicating with the expected backend server, even if the device’s trust store has been compromised or a rogue certificate authority issues a malicious certificate. While standard SSL pinning often relies on Android’s `X509TrustManager` APIs, developers sometimes implement custom pinning logic that bypasses or augments these standard frameworks. These custom implementations pose a significant challenge for penetration testers and reverse engineers.

    This article delves into advanced Frida hooking techniques to identify and bypass custom SSL pinning implementations in Android applications. We’ll explore how to analyze applications for non-standard pinning, develop targeted Frida scripts, and ultimately, intercept traffic that would otherwise be protected.

    Understanding Custom SSL Pinning Implementations

    Standard SSL pinning typically involves providing a custom `TrustManager` that overrides the `checkServerTrusted` method. This method then validates the server’s certificate chain against pre-defined pins. However, custom pinning often deviates in several ways:

    • Direct Certificate/Public Key Comparison: Instead of using `TrustManager`, some applications directly extract the public key or hash from the server’s certificate and compare it against a hardcoded value.
    • Custom `SSLSocketFactory` or `HostnameVerifier`: Applications might implement their own `SSLSocketFactory` to control the SSL handshake or a custom `HostnameVerifier` to validate the hostname during the handshake, often incorporating pinning logic here.
    • Proprietary Network Libraries: Some apps use custom-built networking layers or heavily modified third-party libraries where pinning logic is deeply embedded and not exposed through typical Java APIs.
    • Native Code Pinning: In highly secure applications, pinning logic might be implemented in native C/C++ code, making it significantly harder to analyze and bypass using Java-level Frida hooks alone.

    The key to bypassing custom pinning lies in detailed reverse engineering to identify the exact code path where the pinning check occurs.

    Methodology: Decompilation and Analysis

    The first step in any custom pinning bypass is thorough analysis of the application’s bytecode.

    Tools for Analysis:

    • apktool: For decompiling APKs into Smali code and re-packaging.
    • JADX-GUI or Ghidra/IDA Pro: For converting Smali to Java code (JADX-GUI) or analyzing native libraries (Ghidra/IDA Pro).

    Steps for Identification:

    1. Decompile the APK: Start by decompiling the target APK using `apktool`:

      apktool d target.apk -o target_dir
    2. Static Code Analysis: Open the decompiled project in JADX-GUI and search for common keywords associated with SSL/TLS and cryptography:

      • `X509TrustManager`, `checkServerTrusted` (even if custom, this method name is often reused).
      • `PublicKey`, `Certificate`, `KeyStore`, `TrustStore`.
      • `SSLSocketFactory`, `HostnameVerifier`, `verify`.
      • Cryptographic hashing algorithms like `SHA256`, `MD5`, `Base64` (often used for certificate hashes).
      • Custom class names that might indicate security implementations (e.g., `SecureHttpClient`, `PinningClient`, `CertVerifier`).
    3. Trace Network Operations: Identify where network requests are initiated (e.g., `OkHttpClient`, `HttpURLConnection`, custom network wrappers). Trace how `SSLSocketFactory` or `TrustManager` instances are initialized and passed to these networking components.

    4. Look for Direct Comparisons: Pay close attention to methods that compare byte arrays, strings, or `PublicKey` objects. A common custom pinning technique is to hardcode a certificate’s public key hash and compare it directly during the handshake.

    Example Smali Snippet (Hypothetical Custom Pinning):

    .method public checkServerTrusted([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V throws Ljava/security/cert/CertificateException; .registers 6 .param p1, "chain" .param p2, "authType" .annotation system Ldalvik/annotation/Throws; value = { Ljava/security/cert/CertificateException; } .end annotation .line 20 .local v0, "serverCert":Ljava/security/cert/X509Certificate; invoke-virtual {p1, v2}, [Ljava/security/cert/X509Certificate;->aget(I)Ljava/lang/Object; move-result-object v0 .line 21 invoke-virtual {v0}, Ljava/security/cert/X509Certificate;->getPublicKey()Ljava/security/PublicKey; move-result-object v1 .line 22 invoke-virtual {v1}, Ljava/lang/Object;->hashCode()I move-result v2 .line 23 sget v3, Lcom/example/secureapp/MyCustomTrustManager;->EXPECTED_PUBLIC_KEY_HASH:I .line 24 if-eq v2, v3, :cond_0 .line 25 new-instance v4, Ljava/security/cert/CertificateException; invoke-direct {v4, v5}, Ljava/security/cert/CertificateException;->(Ljava/lang/String;)V throw v4 .line 27 :cond_0 return-void .end method

    In this hypothetical Smali, `MyCustomTrustManager` retrieves the public key, calculates its hash, and compares it against a static `EXPECTED_PUBLIC_KEY_HASH`. If they don’t match, a `CertificateException` is thrown.

    Frida Hooking for Custom Pinning Bypass

    Once the pinning logic is identified, Frida can be used to dynamically modify the application’s behavior at runtime.

    Prerequisites:

    • Rooted Android device or emulator.
    • Frida server running on the device.
    • Frida-tools installed on your host machine (`pip install frida-tools`).

    Techniques for Advanced Bypass:

    1. Overriding Custom `TrustManager.checkServerTrusted`

    If the custom pinning still uses a `TrustManager`-like interface, but with a different class name or additional checks, you can hook its specific `checkServerTrusted` method.

    // frida_custom_trustmanager_bypass.js Java.perform(function () { try { var CustomTrustManager = Java.use('com.example.secureapp.MyCustomTrustManager'); CustomTrustManager.checkServerTrusted.overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String').implementation = function (chain, authType) { console.log('Bypassing custom checkServerTrusted in MyCustomTrustManager!'); // Simply return without throwing an exception, effectively trusting all certificates return; }; console.log('MyCustomTrustManager.checkServerTrusted hook applied!'); } catch (e) { console.error('Failed to hook MyCustomTrustManager:', e); } });

    Execute with: `frida -U -f com.example.secureapp -l frida_custom_trustmanager_bypass.js –no-pause`

    2. Bypassing Direct Certificate/Public Key Comparison

    When the app directly compares public keys or their hashes, you need to target the specific comparison method. For example, if it’s comparing `PublicKey` objects, you can hook the `equals` method.

    // frida_public_key_equals_bypass.js Java.perform(function () { try { var PublicKey = Java.use('java.security.PublicKey'); PublicKey.equals.implementation = function (obj) { var result = this.equals(obj); if (!result && obj && obj.$className && obj.$className.includes('PublicKey')) { console.log('Forcing PublicKey.equals to true for custom pinning bypass!'); return true; } return result; }; console.log('PublicKey.equals hook applied!'); } catch (e) { console.error('Failed to hook PublicKey.equals:', e); } // More specific: Hooking a custom comparison method if found during analysis try { var CustomCertVerifier = Java.use('com.example.secureapp.CertVerifier'); // Assuming this class has a method like 'comparePublicKeyHashes' CustomCertVerifier.comparePublicKeyHashes.implementation = function (hash1, hash2) { console.log('Bypassing custom public key hash comparison!'); return true; // Always return true }; console.log('CustomCertVerifier.comparePublicKeyHashes hook applied!'); } catch (e) { console.error('Failed to hook CustomCertVerifier:', e); } });

    This `PublicKey.equals` hook is a more general approach but can be effective if the custom pinning relies on `PublicKey` object equality. A more targeted approach is to hook the specific method that performs the hash comparison (e.g., `comparePublicKeyHashes` in the example). If the comparison is done via a static field, you might need to modify that field’s value at runtime or hook the method that uses it.

    3. Manipulating `HostnameVerifier`

    If pinning logic is embedded within a custom `HostnameVerifier`, you can override its `verify` method.

    // frida_hostname_verifier_bypass.js Java.perform(function () { try { var HostnameVerifier = Java.use('javax.net.ssl.HostnameVerifier'); HostnameVerifier.verify.implementation = function (hostname, session) { console.log('Bypassing HostnameVerifier.verify for: ' + hostname); return true; // Always return true }; console.log('javax.net.ssl.HostnameVerifier hook applied!'); } catch (e) { console.error('Failed to hook HostnameVerifier:', e); } // If a custom implementation of HostnameVerifier is found during analysis var CustomHostnameVerifier = Java.use('com.example.secureapp.MyHostnameVerifier'); if (CustomHostnameVerifier) { CustomHostnameVerifier.verify.implementation = function (hostname, session) { console.log('Bypassing custom MyHostnameVerifier.verify for: ' + hostname); return true; // Always return true }; console.log('Custom MyHostnameVerifier.verify hook applied!'); } });

    4. Runtime Instance Manipulation with `Java.choose`

    Sometimes, the target object (e.g., a custom `TrustManager` or `HostnameVerifier`) is instantiated early in the application lifecycle. `Java.choose` allows you to find existing instances of a class and manipulate them.

    // frida_java_choose_bypass.js Java.perform(function () { Java.choose('com.example.secureapp.MyCustomTrustManager', { onMatch: function (instance) { console.log('Found an instance of MyCustomTrustManager: ' + instance); // You can now interact with the instance instance.checkServerTrusted.overload('[Ljava.security.cert.X509Certificate;', 'java.lang.String').implementation = function (chain, authType) { console.log('Bypassing custom checkServerTrusted on chosen instance!'); return; }; console.log('Hooked instance of MyCustomTrustManager!'); }, onComplete: function () { console.log('Java.choose for MyCustomTrustManager completed.'); } }); });

    Combining Techniques and Final Execution

    Often, a combination of these techniques is required. You might need to hook `checkServerTrusted` on a custom `TrustManager` AND also force `PublicKey.equals` to true if the custom manager internally performs direct key comparisons.

    Remember to always start Frida with the `–no-pause` flag when dealing with pinning, as some apps perform checks very early during startup.

    frida -U -f com.example.secureapp -l frida_combined_bypass.js --no-pause

    Conclusion

    Bypassing custom SSL pinning on Android requires a deep understanding of the application’s internal workings, meticulous static analysis, and dynamic instrumentation using powerful tools like Frida. Unlike standard pinning, where generic scripts often suffice, custom implementations demand a targeted approach tailored to the unique logic found within the app. By identifying the exact points of certificate validation – be it through custom `TrustManager` implementations, direct public key comparisons, or specialized `HostnameVerifier`s – and crafting precise Frida hooks, penetration testers can effectively neutralize even sophisticated pinning mechanisms, enabling comprehensive security assessments of network communications.

    While challenging, the process of reverse engineering custom pinning serves as an excellent exercise in Android application security analysis, highlighting the importance of defense-in-depth strategies for developers and the persistent ingenuity required for modern penetration testing.

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

    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.

  • Troubleshooting Frida Cert Pinning Bypass: Fixing Common Android Hooking Failures

    Introduction to Certificate Pinning and Frida’s Role

    Certificate pinning is a crucial security mechanism implemented by developers to prevent man-in-the-middle (MiTM) attacks on mobile applications. Instead of trusting any certificate signed by a trusted CA, an app configured with pinning will only trust specific, pre-defined certificates or their public keys. While excellent for security, this poses a significant challenge for penetration testers who need to intercept and analyze app traffic. This is where Frida, a dynamic instrumentation toolkit, becomes indispensable. Frida allows us to inject custom scripts into running processes, enabling runtime modification of application logic, including the ability to bypass certificate pinning.

    However, bypassing certificate pinning with Frida is rarely a ‘set it and forget it’ task. Many factors can lead to an unsuccessful bypass, from subtle misconfigurations to sophisticated anti-Frida or custom pinning implementations. This article delves into common issues encountered during Frida certificate pinning bypass attempts on Android and provides expert-level troubleshooting steps to overcome them.

    The Basics: Ensuring Your Frida Setup is Correct

    Before diving into complex bypass techniques, it’s vital to ensure your foundational Frida setup is robust. A faulty setup is the source of many failed hooking attempts.

    Verify Frida Server Status

    The Frida server must be running on the Android device and accessible from your host machine. Ensure it has the correct permissions and is the right architecture (e.g., `arm64`, `x86_64`).

    # On your host machine:Push frida-server to the device
    adb push /path/to/frida-server /data/local/tmp/
    
    # On the Android device (via adb shell):
    adb shell
    su
    cd /data/local/tmp/
    chmod 755 frida-server
    ./frida-server &
    
    # On your host machine, verify connection:
    frida-ps -U
    

    If `frida-ps -U` doesn’t list processes, double-check server execution, device connectivity, and permissions.

    Device Compatibility and Root Access

    Most advanced pinning bypasses require a rooted Android device. Root access grants Frida the necessary permissions to inject into system-level processes or modify sensitive application data. While some bypasses might work on non-rooted debuggable apps, for a comprehensive approach, root is almost always preferred.

    Common Frida Certificate Pinning Bypass Techniques

    Understanding the common bypass techniques helps in troubleshooting when they fail.

    Using Objection for Automated Bypass

    Objection, built on top of Frida, offers a quick and often effective automated bypass. It targets common pinning frameworks.

    objection -g com.example.app explore --startup-command