Comprehensive guide to Android security architecture, ART memory layout, JNI execution, Frida dynamic instrumentation, and multi-layer SSL pinning bypass.
Master Android 14/15 SSL pinning bypass using Frida, Objection, and native BoringSSL hooks. Learn how to intercept HTTPS traffic in enterprise applications with modern dynamic instrumentation
25 min read
Basic understanding of Android OS, Java/Kotlin, C/C++, and Linux terminal commands.
Understand Android kernel & ART security architecture, trace JNI native execution, write Frida hooks for Java & C layers, bypass SSL pinning and root detection.
Modern Android application security relies on a defense-in-depth architecture spanning Linux kernel User ID (UID) process isolation, the Android Runtime (ART) execution environment, Java Native Interface (JNI) boundaries, and hardware-backed keystores. However, security controls like SSL pinning and root detection operating inside client-side process memory can be bypassed using dynamic binary instrumentation. This masterclass provides a complete first-principles analysis of Android internals and presents production-grade Frida hooks to trace JNI invocations, bypass Network Security Configuration (NSC) & custom OkHttp3 SSL pinning, and defeat anti-analysis protections.
To perform security analysis or reverse engineer an Android application, you must view the operating system as a multi-layered sandwich:
app_a123) to every single installed app. Linux process boundaries prevent App A from reading App B's filesystem or memory space..dex) files into native Ahead-Of-Time (AOT) or Just-In-Time (JIT) machine code via ART.Intents, Services, and Content Providers..so files) using the Java Native Interface (JNI).+-------------------------------------------------------------------+
| Android Application Layer |
| App A (UID: u0_a142) App B (UID: u0_a143) |
| [ Activities | Services ] [ Activities | Services ]|
+-------------------------------------------------------------------+
| Android Runtime (ART) |
| - DEX Bytecode Interpreter - JIT / AOT Compiler |
| - Garbage Collection (GC) - ART Heap Allocation |
+-------------------------------------------------------------------+
| Java Native Interface (JNI) Bridge |
| Java Method Call (Java_com_example_app_NativeLib_verifyLicense) |
| ------------------> C/C++ Shared Library (`libnative-lib.so`) |
+-------------------------------------------------------------------+
| Linux Kernel Layer |
| - Process Sandboxing (UID / GID isolation per application) |
| - SELinux Mandatory Access Control (MAC) Policies |
| - Linux Capabilities & cgroups |
+-------------------------------------------------------------------+
When an Android application runs, .dex bytecode is executed within ART. Understanding how ART manages memory and method dispatching is critical for dynamic instrumentation:
| Component | Architecture Role | Security & Reverse Engineering Significance |
|---|---|---|
| Zygote Process | Warm parent process pre-loading Android framework classes. | Forked to create all app processes; inherits global hooks if injected early. |
| DEX Bytecode | Compiled Dalvik instructions stored in classes.dex. | Decompiled back to Java via jadx or Smali via apktool. |
| OAT / ELF Files | Native machine code produced by ART's AOT compiler (dex2oat). | Contains compiled native pointers for Java methods. |
| JNIEnv Pointer | Thread-local pointer to JNI function table (JNINativeInterface). | Primary targets for hooking native bridge function calls (RegisterNatives). |
| ART vtable / ArtMethod | Internal C++ object representing a Java method inside ART memory. | Modified dynamically by Frida to redirect execution flow. |
When a Java method declared as public native String getAuthToken() is called, ART looks up the native function address either via dynamic symbol lookup (Java_package_class_method) or explicit RegisterNatives calls inside JNI_OnLoad.
Java Code: [NativeLib.getAuthToken()]
|
v
ART Execution Engine (ArtMethod structure)
| (Pointers to native function struct)
v
JNI Native Struct (`JNINativeInterface`)
[0x00] Reserved [0x08] FindClass [0x10] RegisterNatives
|
v
Native C/C++ Function: `Java_com_example_app_NativeLib_getAuthToken` in `libnative.so`
To perform hands-on analysis, assemble the following toolchain on your workstation and root target device/emulator:
apktool, jadx-gui, Frida CLI (pip install frida-tools).frida-server binary for your target architecture (e.g., frida-server-16.x.x-android-x86_64.xz)./data/local/tmp/:# Decompress frida-server
xz -d frida-server-16.x.x-android-x86_64.xz
mv frida-server-16.x.x-android-x86_64 frida-server
# Push to device via ADB
adb push frida-server /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server"
# Execute frida-server as root in background
adb shell "su -c '/data/local/tmp/frida-server &'"
# Verify workstation connection
frida-ps -U
Below is a production-grade Frida script (trace_auth.js) that attaches to an Android app, intercepts a target class method com.example.app.security.AuthManager.verifyToken, prints its parameters, and modifies its return value to true.
// trace_auth.js - Java Method Hooking Harness
Java.perform(function () {
console.log("[+] Frida Agent Loaded successfully.");
try {
var AuthManager = Java.use("com.example.app.security.AuthManager");
// Overload handling for verifyToken(String token, String deviceId)
AuthManager.verifyToken.overload("java.lang.String", "java.lang.String").implementation = function (token, deviceId) {
console.log("\n[======== INTERCEPTED METHOD CALL ========]");
console.log("[+] Target Class : com.example.app.security.AuthManager");
console.log("[+] Target Method : verifyToken");
console.log("[+] Param 1 (Token) : " + token);
console.log("[+] Param 2 (DeviceID): " + deviceId);
// Execute original method to observe true output
var originalResult = this.verifyToken(token, deviceId);
console.log("[+] Original Return Value: " + originalResult);
// Force return value true (Bypassing client-side verification check)
var coercedResult = true;
console.log("[*] Coercing Return Value to: " + coercedResult);
console.log("[===========================================]\n");
return coercedResult;
};
} catch (err) {
console.log("[-] Error attaching hook: " + err.stack);
}
});
To run this script against a target package:
frida -U -f com.example.app -l trace_auth.js
Modern mobile applications enforce Transport Layer Security (TLS) pinning via Network Security Config (NSC), OkHttp CertificatePinner, or native libssl.so / BoringSSL routines.
The script below targets both Java-level TrustManagers (OkHttp3, Conscrypt) and native C-level BoringSSL verification routines in one unified hook (universal_ssl_pinning_bypass.js).
/*
* universal_ssl_pinning_bypass.js
* Author: Syed Zada Abrar (Andrax Pentester)
* Comprehensive Multi-Layer TLS Pinning Bypass for Android 7.0 - 15.0
*/
Java.perform(function () {
console.log("[+] Starting Universal TLS / SSL Pinning Bypass Engine...");
// 1. Hook X509TrustManager (Generic Java TLS Bypass)
try {
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
var SSLContext = Java.use('javax.net.ssl.SSLContext');
var CustomTrustManager = Java.registerClass({
name: 'com.andrax.CustomTrustManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function (chain, authType) {},
checkServerTrusted: function (chain, authType) {},
getAcceptedIssuers: function () { return []; }
}
});
var TrustManagerArray = [CustomTrustManager.$new()];
var SSLContext_init = SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;',
'[Ljavax.net.ssl.TrustManager;',
'java.security.SecureRandom'
);
SSLContext_init.implementation = function (keyManager, trustManager, secureRandom) {
console.log("[+] SSLContext.init() intercepted. Injecting permissive TrustManager.");
SSLContext_init.call(this, keyManager, TrustManagerArray, secureRandom);
};
console.log("[+] [SUCCESS] Generic X509TrustManager Hooked.");
} catch (e) {
console.log("[-] X509TrustManager hook failed: " + e.message);
}
// 2. Hook OkHttp v3 / v4 CertificatePinner
try {
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function (hostname, peerCertificates) {
console.log("[+] OkHttp3 CertificatePinner.check() intercepted for host: " + hostname + " -> Pinning Bypassed!");
return;
};
console.log("[+] [SUCCESS] OkHttp3 CertificatePinner Hooked.");
} catch (e) {
console.log("[*] OkHttp3 CertificatePinner not found or obfuscated.");
}
// 3. Hook Trustkit SSL Pinning Library
try {
var TrustKit = Java.use('com.datatheorem.android.trustkit.pinning.OkHostnameVerifier');
TrustKit.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (hostname, session) {
console.log("[+] TrustKit HostnameVerifier intercepted for: " + hostname);
return true;
};
} catch (e) {}
});
// 4. Native BoringSSL Pinning Bypass (C Layer)
Interceptor.attach(Module.findExportByName(null, "SSL_CTX_set_custom_verify"), {
onEnter: function (args) {
console.log("[+] Native BoringSSL SSL_CTX_set_custom_verify() hooked.");
// Change verification mode parameter to SSL_VERIFY_NONE (0)
args[1] = ptr(0);
}
});
Security features like Root Detection (/system/xbin/su checks) and Frida Detection (/proc/net/tcp scanning) frequently reside in native C++ libraries (libsecurity.so).
open / stat File Access InterceptorThis native C hook intercepts system calls querying /system/bin/su, /system/xbin/su, or /proc/self/maps to hide root binaries and debugging harnesses.
// native_root_bypass.js
console.log("[+] Attaching Native Syscall Interceptor...");
var pOpen = Module.findExportByName(null, "open");
if (pOpen) {
Interceptor.attach(pOpen, {
onEnter: function (args) {
var path = Memory.readUtf8String(args[0]);
if (path) {
if (path.indexOf("su") !== -1 || path.indexOf("magisk") !== -1 || path.indexOf("frida") !== -1) {
console.log("[*] Intercepted native file access to sensitive path: " + path);
// Redirect path pointer to non-existent safe path
this.fakePath = Memory.allocUtf8String("/nonexistent_path_bypassed");
args[0] = this.fakePath;
}
}
}
});
}
To defend production Android applications against dynamic instrumentation and dynamic reverse engineering, implement the following architectural mitigations:
| Attack Vector | Vulnerability Mechanism | Enterprise Mitigation Strategy | Implementation Code Reference |
|---|---|---|---|
| Frida Hooking | Dynamic injection into ART ArtMethod structs. | Implement native dual-process ptrace anti-debugging (PTRACE_TRACEME). | Compile native C layer with ptrace(PTRACE_TRACEME, 0, 1, 0). |
| TLS Pinning Bypass | Java TrustManager / OkHttp Pinner dynamic replacement. | Enforce Network Security Config + Native BoringSSL Custom Verification. | Utilize <network-security-config> with pin-set expiration. |
| Code Tampering | Decompilation & APK repackaging via apktool. | Enable Play Integrity API + DexGuard obfuscation & signature verification. | Check Google Play Integrity API token server-side. |
| Shared Lib Inspection | Exported JNI symbols easily hookable via Interceptor.attach. | Strip debug symbols, obfuscate control flow (OLLVM), and dynamic JNI registration. | Use RegisterNatives inside native initialization without exported names. |
JNINativeInterface). Both can be dynamically instrumented using Frida.Share this tutorial
Sign in to leave a comment.