Android Anti-Instrumentation & SSL Pinning Bypass
A practical workflow to bypass Android app protections that detect instrumentation, root, or enforce TLS pinning. Focuses on fast triage, common detections, and copy-pasteable hooks.
Quick Start
# Attach to running app (avoids spawn-time detection)
frida -U -n com.example.app
# Spawn with anti-detection script
frida -U -f com.example.app -l anti-frida-detection.js
# List processes
frida-ps -Uai
Detection Surface (What Apps Check)
| Category | Common Checks |
|---|---|
| Root | su binary, Magisk paths, getprop values, root packages |
| Frida/Debugger | Debug.isDebuggerConnected(), /proc scanning, classpath, loaded libs |
| Native Anti-Debug | ptrace(), syscalls, anti-attach, breakpoints |
| Early Init | Application.onCreate() hooks that crash on instrumentation |
| TLS Pinning | Custom TrustManager, OkHttp CertificatePinner, Conscrypt, native pins |
Step 1: Quick Wins (30 Seconds)
Hide Root with Magisk DenyList
# In Magisk app:
# 1. Enable Zygisk
# 2. Enable DenyList
# 3. Add target package
# 4. Reboot and retest
Many apps only check for obvious indicators. DenyList often neutralizes naive checks.
Try Frida Codeshare Scripts
# Common drop-in scripts
frida -U -f com.example.app -l anti-frida-detection.js
frida -U -f com.example.app -l anti-root-bypass.js
frida -U -f com.example.app -l hide_frida_gum.js
Codeshare: https://codeshare.frida.re/
Use Medusa Framework
Medusa provides 90+ ready-made modules:
git clone https://github.com/Ch0pin/medusa
cd medusa
pip install -r requirements.txt
python medusa.py
# Interactive workflow
show categories
use http_communications/multiple_unpinner
use root_detection/universal_root_detection_bypass
run com.target.app
Step 2: Attach Late to Avoid Init-Time Detection
Many detections only run during process spawn. Attach after UI loads:
# Launch app normally, wait for UI, then attach
frida -U -n com.example.app
# Or with Objection gadget
objection --gadget com.example.app explore
Step 3: Map Detection Logic (Static Analysis)
Search Jadx for keywords: frida, gum, root, magisk, ptrace, su, getprop, debugger
Common Java APIs to hook:
android.os.Debug.isDebuggerConnectedandroid.app.ActivityManager.getRunningAppProcessesandroid.app.ActivityManager.getRunningServicesjava.lang.System.loadLibraryandroid.os.SystemProperties.get
Step 4: Runtime Stubbing with Frida
Basic Anti-Frida Bypass
Java.perform(() => {
// Neutralize debugger checks
const Debug = Java.use('android.os.Debug');
Debug.isDebuggerConnected.implementation = function() { return false; };
// Kill ActivityManager scans
const AM = Java.use('android.app.ActivityManager');
AM.getRunningAppProcesses.implementation = function() {
return java.util.Collections.emptyList();
};
AM.getRunningServices.implementation = function() {
return java.util.Collections.emptyList();
};
});
Custom Detection Class Stubbing
Java.perform(() => {
try {
const Checks = Java.use('com.example.security.Checks');
Checks.isFridaDetected.implementation = function() { return false; };
Checks.isRooted.implementation = function() { return false; };
} catch(e) {
console.log('Custom check class not found');
}
});
Log Suspicious Methods
Java.perform(() => {
const Det = Java.use('com.example.security.DetectionManager');
Det.checkFrida.implementation = function() {
console.log('[+] checkFrida() called');
return false;
};
});
Dump Classes on Crash
Java.perform(() => {
Java.enumerateLoadedClasses({
onMatch: n => console.log(n),
onComplete: () => console.log('Done')
});
});
Step 5: Bypass Emulator/VM Detection
Spoof Build Fields
Java.perform(function() {
var Build = Java.use('android.os.Build');
Build.MODEL.value = 'Pixel 7 Pro';
Build.MANUFACTURER.value = 'Google';
Build.BRAND.value = 'google';
Build.FINGERPRINT.value = 'google/panther/panther:14/UP1A.231105.003/1234567:user/release-keys';
Build.HARDWARE.value = 'panther';
});
Spoof Device Identifiers
Java.perform(function() {
// TelephonyManager
var TelephonyManager = Java.use('android.telephony.TelephonyManager');
TelephonyManager.getDeviceId.implementation = function() { return '123456789012345'; };
TelephonyManager.getSubscriberId.implementation = function() { return '123456789012345'; };
// WiFi MAC
var WifiInfo = Java.use('android.net.wifi.WifiInfo');
WifiInfo.getMacAddress.implementation = function() { return '02:00:00:12:34:56'; };
});
Step 6: SSL Pinning Bypass
Basic SSLContext Hook
Java.perform(function() {
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
var SSLContext = Java.use('javax.net.ssl.SSLContext');
// No-op validations
X509TrustManager.checkClientTrusted.implementation = function() {};
X509TrustManager.checkServerTrusted.implementation = function() {};
// Force permissive TrustManagers
var TrustManagers = [X509TrustManager.$new()];
var SSLContextInit = SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager',
'[Ljavax.net.ssl.TrustManager',
'java.security.SecureRandom'
);
SSLContextInit.implementation = function(km, tm, sr) {
return SSLContextInit.call(this, km, TrustManagers, sr);
};
});
OkHttp4 / Cronet Pinning (2024+)
Java.perform(() => {
try {
const Pinner = Java.use('okhttp3.CertificatePinner');
Pinner.check.overload('java.lang.String', 'java.util.List').implementation = function() {};
Pinner.check$okhttp.implementation = function() {};
} catch(e) {}
try {
const CronetB = Java.use('org.chromium.net.CronetEngine$Builder');
CronetB.enablePublicKeyPinningBypassForLocalTrustAnchors.overload('boolean').implementation = function() { return this; };
CronetB.setPublicKeyPins.overload('java.lang.String', 'java.lang.Set', 'boolean').implementation = function() { return this; };
} catch(e) {}
});
Native BoringSSL Hook (for gRPC/Cronet)
const customVerify = Module.findExportByName(null, 'SSL_CTX_set_custom_verify');
if (customVerify) {
Interceptor.attach(customVerify, {
onEnter(args) {
args[1] = ptr(0); // SSL_VERIFY_NONE
args[2] = NULL; // disable callback
}
});
}
Step 7: Native Detection Bypass
Trace JNI Entry Points
frida-trace -n com.example.app -i "JNI_OnLoad"
Neuter ptrace (Anti-Debug)
const ptrace = Module.findExportByName(null, 'ptrace');
if (ptrace) {
Interceptor.replace(ptrace, new NativeCallback(function() {
return -1; // pretend failure
}, 'int', ['int', 'int', 'pointer', 'pointer']));
}
Scan Native Libraries
# List exported symbols & JNI
nm -D libfoo.so | head
objdump -T libfoo.so | grep Java_
strings -n 6 libfoo.so | egrep -i 'frida|ptrace|gum|magisk|su|root'
Step 8: Stealth Frida Server (phantom-frida)
For apps that detect Frida fingerprints, use phantom-frida:
python3 build.py --version 17.7.2 --name myserver --port 27142 --extended --verify
adb push output/myserver-server-17.7.2-android-arm64 /data/local/tmp/myserver-server
adb shell chmod 755 /data/local/tmp/myserver-server
adb shell /data/local/tmp/myserver-server -D &
adb forward tcp:27142 tcp:27142
frida -H 127.0.0.1:27142 -f com.example.app
phantom-frida: https://github.com/TheQmaks/phantom-frida
Step 9: Universal Proxy + TLS Unpinning
Use HTTP Toolkit hooks for comprehensive unpinning + proxy forcing:
# Download hooks from https://github.com/httptoolkit/frida-interception-and-unpinning
frida -U -f com.vendor.app \
-l ./android-unpinning-with-proxy.js \
--no-pause
# mitmproxy listening locally
mitmproxy -p 8080
Step 10: Static TLS Pinning Removal (Fallback)
apk-mitm app.apk
# Install patched APK and proxy via Burp/mitmproxy
apk-mitm: https://github.com/shroudedcode/apk-mitm
Objection Patching
objection patchapk --source app.apk
Requires apktool. Gadget injection enables instrumentation without root.
Command Cheat Sheet
# List processes
frida-ps -Uai
# Attach to running app
frida -U -n com.example.app
# Spawn with script
frida -U -f com.example.app -l script.js
# Trace native
frida-trace -n com.example.app -i "JNI_OnLoad"
# Objection runtime
objection --gadget com.example.app explore
# Static TLS removal
apk-mitm app.apk
Troubleshooting
| Problem | Solution |
|---|---|
| App crashes on spawn | Attach late with -n instead of -f |
| Zygisk detection | Disable Zygisk temporarily, use KernelSU/APatch |
| Play Integrity blocks | Use PlayIntegrityFix, ReZygisk, or Zygisk-Next |
| TLS still pinned | Add OkHttp4/Cronet hooks, try native BoringSSL hook |
| Native detection | Use ptrace stub, trace JNI_OnLoad, scan .so files |
| Frida detected | Use phantom-frida, try hide_frida_gum.js |
References
- Frida Codeshare: https://codeshare.frida.re/
- Objection: https://github.com/sensepost/objection
- Medusa: https://github.com/Ch0pin/medusa
- phantom-frida: https://github.com/TheQmaks/phantom-frida
- HTTP Toolkit Hooks: https://github.com/httptoolkit/frida-interception-and-unpinning
- apk-mitm: https://github.com/shroudedcode/apk-mitm
- Jadx: https://github.com/skylot/jadx
- Ghidra: https://ghidra-sre.org/
- Magisk: https://github.com/topjohnwu/Magisk