Certificate pinning is routinely mischaracterized as a robust security boundary, but on a device you control, local verification logic is ultimately just a set of conditional jumps waiting to be bypassed. Mobile developers add pinning to prevent man-in-the-middle inspection, yet because the validation code runs inside an environment completely exposed to runtime instrumentation, it only slows down dynamic analysis rather than preventing it.
If you are reverse engineering an Android application to inspect its network traffic, relying on off-the-shelf automated scripts often leads to dead ends when developers move away from stock frameworks. Understanding how to locate the exact validation layer—and writing targeted Frida scripts to disable it—is an essential skill for mobile device security testing.
Locating the Intercept Point Across App Layers
Before firing up Frida, you need to determine where the app handles TLS verification. Android apps typically enforce certificate pinning at one of three levels: standard framework components, popular HTTP client libraries, or compiled native code.
Begin by decompiling the target APK using jadx or apktool:
jadx-gui target_app.apk
Perform a global text search across the decompiled Java source for common pinning indicators:
- Framework TrustManagers: Search for
X509TrustManager,checkServerTrusted, orX509TrustManagerExtensions. Custom implementations often throw aCertificateExceptionwhen the certificate chain does not match an embedded hash. - OkHttp Framework: Search for
okhttp3.CertificatePinnerorCertificatePinner$Builder. Look for code adding hashes formatted assha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=. - Network Security Config: Check
res/xml/network_security_config.xmlfor<pin-set>declarations. If an app relies purely on the system network security config without custom code checks, overriding the trust anchors via a patched APK or universal script is straightforward.
If a search yields no hits in Java, or if traffic remains unencrypted despite overriding Java classes, the application likely handles networking at the native layer using libraries like BoringSSL, libcurl, or custom C/C++ networking engines common in Flutter and React Native apps.
Neutralizing Java-Level Verification Hooks
When the target uses standard Java networking or OkHttp, runtime method hooks can replace the verification logic with a no-op implementation.
For generic X509TrustManager implementations, the goal is to hook checkServerTrusted so that it returns cleanly without throwing a CertificateException.
// java_trustmanager_bypass.js
Java.perform(function () {
const ArrayElement = Java.use('java.lang.reflect.Array');
const X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
const SSLContext = Java.use('javax.net.ssl.SSLContext');
// Define a custom TrustManager that trusts all certificates
const TrustManager = Java.registerClass({
name: 'com.research.CustomTrustManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function (chain, authType) {},
checkServerTrusted: function (chain, authType) {},
getAcceptedIssuers: function () {
return [];
}
}
});
// Override SSLContext.init to force our custom TrustManager
SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;',
'[Ljavax.net.ssl.TrustManager;',
'java.security.SecureRandom'
).implementation = function (keyManager, trustManager, secureRandom) {
const customTM = TrustManager.$new();
const tmArray = Java.array('javax.net.ssl.TrustManager', [customTM]);
this.init(keyManager, tmArray, secureRandom);
};
});
To target OkHttp directly, hook CertificatePinner.check to suppress validation exceptions:
// okhttp_bypass.js
Java.perform(function () {
try {
const CertificatePinner = Java.use('okhttp3.CertificatePinner');
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function (hostname, peerCertificates) {
// Return cleanly without executing the original check logic
return;
};
} catch (err) {
console.log("OkHttp3 class not found in classloader context");
}
});
Execute the script against the target application process:
frida -U -f com.example.targetapp -l java_trustmanager_bypass.js
Digging Into Native Code When Java Hooks Fail
When Java-level hooks yield no decrypted traffic, the pinning logic resides in native shared libraries (.so files). Applications built with frameworks like Flutter embed their own copy of BoringSSL (e.g., inside libflutter.so), completely bypassing the Android Java runtime and system trust stores.
To confirm native TLS activity, inspect the open file descriptors or loaded shared libraries of the running process:
adb shell
su
ls -l /proc/$(pidof com.example.targetapp)/map | grep -E "libcrypto|libssl|libflutter"
In BoringSSL and OpenSSL, certificate verification during the TLS handshake relies on internal functions such as SSL_CTX_set_custom_verify or ssl_verify_peer_cert. Because native symbols are often stripped in production release builds, you cannot always hook these functions by export name. You must instead locate the function offset by analyzing the binary in disassembly tools like Ghidra or IDA Pro.
Search the native library for string references commonly associated with handshake failures:
ssl_clientCERTIFICATE_VERIFY_FAILEDHandshake message sequence error
Once you locate the basic block referencing the verification check, trace backwards to find the function prologue and calculate its relative offset from the module base address.
Patching BoringSSL and Custom Native Transport Chains
If the symbols are exported (or if you calculate the exact function offset), you can attach an Interceptor to override the return value of the verification call. In BoringSSL, a custom verification function callback typically returns enum ssl_verify_result_t, where ssl_verify_retry or ssl_verify_ok (value 0) signals a successful validation.
The following Frida script searches for exported BoringSSL/OpenSSL symbols or hooks known offsets inside native modules:
// native_boringssl_bypass.js
function hookNativeSSL() {
const targetModule = "libflutter.so"; // Replace with actual module name if different
const module = Process.findModuleByName(targetModule);
if (!module) {
console.log(`[*] Module ${targetModule} not loaded yet.`);
return;
}
// Example 1: Exported symbol hook
const sslSetCustomVerify = Module.findExportByName(targetModule, "SSL_CTX_set_custom_verify");
if (sslSetCustomVerify) {
Interceptor.attach(sslSetCustomVerify, {
onEnter: function (args) {
// Argument 2 is typically the mode, Argument 3 is the callback function pointer
// We can replace the callback pointer with a dummy function that returns 0 (ssl_verify_ok)
console.log("[*] Hooked SSL_CTX_set_custom_verify");
}
});
}
// Example 2: Direct offset hook when symbols are stripped
// Offset must be determined via disassembly (e.g., Ghidra)
const verificationFunctionOffset = 0x3a4b20;
const targetAddress = module.base.add(verificationFunctionOffset);
Interceptor.attach(targetAddress, {
onLeave: function (retval) {
// Force the verification result to return 0 (Success)
retval.replace(ptr(0x0));
}
});
}
setImmediate(hookNativeSSL);
When dealing with complex native implementations where function signature tracking is difficult, an alternative approach is patching memory instructions directly. By locating the conditional jump (CBZ, CBNZ, or B.NE in ARM64) that branches to the error handling block after a failed certificate check, you can overwrite the instruction bytes with NOP (0x1F 0x20 0x03 0xD5 in ARM64 assembly) at runtime:
// Memory patch example for ARM64
function patchInstruction(address) {
Memory.protect(address, 4, 'rwx');
// ARM64 NOP instruction
address.writeByteArray([0x1f, 0x20, 0x03, 0xd5]);
Memory.protect(address, 4, 'rx');
console.log("[*] Instruction patched successfully at " + address);
}
By systematically isolating the transport layer—moving from high-level Java frameworks down to compiled C functions—you can bypass any client-side certificate pinning implementation. Modern client integrity controls simply change the depth at which you must instrument the runtime environment.
Related content
Want a second set of eyes on your security posture?
Let's talk about where your real exposure is.
Book an advisory call