// HackTricks · Mobile

Android Anti-Instrumentation & SSL Pinning Bypass (Frida/Objection)

Android Anti-Instrumentation & SSL Pinning Bypass (Frida/Objection)

This page provides a practical workflow to regain dynamic analysis against Android apps that detect/root‑block instrumentation or enforce TLS pinning. It focuses on fast triage, common detections, and copy‑pasteable hooks/tactics to bypass them without repacking when possible.

Detection Surface (what apps check)

  • Root checks: su binary, Magisk paths, getprop values, common root packages
  • Frida/debugger checks (Java): Debug.isDebuggerConnected(), ActivityManager.getRunningAppProcesses(), getRunningServices(), scanning /proc, classpath, loaded libs
  • Native anti‑debug: ptrace(), syscalls, anti‑attach, breakpoints, inline hooks
  • Early init checks: Application.onCreate() or process start hooks that crash if instrumentation is present
  • TLS pinning: custom TrustManager/HostnameVerifier, OkHttp CertificatePinner, Conscrypt pinning, native pins

Bypassing Anti-Frida Detection / Stealth Frida Servers

phantom-frida rebuilds Frida from source and applies ~90 patches so common Frida fingerprints disappear while the stock Frida protocol remains compatible (frida-tools can still connect). Target: apps that grep /proc (cmdline, maps, task comm, fd readlink), D-Bus service names, default ports, or exported symbols.[13]

Phases:

  • Source patches: global rename of frida identifiers (server/agent/helper) and rebuilt helper DEX with a renamed Java package.
  • Targeted build/runtime patches: meson tweaks, memfd label changed to jit-cache, SELinux labels (e.g., frida_file) renamed, libc hooks on exit/signal disabled to avoid hook-detectors.
  • Post-build rename: exported symbol frida_agent_main renamed after the first compile (Vala emits it), requiring a second incremental build.
  • Binary hex patches: thread names (gmain, gdbus, pool-spawner) replaced; optional sweep removes leftover frida/Frida strings.

Detection vectors covered:

  • Base (1–8): process name frida-server, mapped libfrida-agent.so, thread names, memfd label, exported frida_agent_main, SELinux labels, libc hook side-effects, and D-Bus service re.frida.server are renamed/neutralized.
  • Extended (9–16): change listening port (--port), rename D-Bus interfaces/internal C symbols/GType names, temp paths like .frida/frida-, sweep binary strings, rename build-time defines and asset paths (libdir/frida). D-Bus interface names that are part of the wire protocol stay unchanged in base mode to avoid breaking stock clients.

Build/usage (Android arm64 example):

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

Flags: --skip-build (patch only), --skip-clone, --arch, --ndk-path, --temp-fixes; WSL helper: wsl -d Ubuntu bash build-wsl.sh.

Step 1 — Quick win: hide root with Magisk DenyList

  • Enable Zygisk in Magisk
  • Enable DenyList, add the target package
  • Reboot and retest

Many apps only look for obvious indicators (su/Magisk paths/getprop). DenyList often neutralizes naive checks.[1]

References:

Play Integrity / Zygisk detections (post‑SafetyNet)

Newer banking/ID apps tie runtime checks to Google Play Integrity (SafetyNet replacement) and can also crash if Zygisk itself is present.[15] Quick triage tips:

  • Temporarily disable Zygisk (toggle off + reboot) and retry; some apps crash as soon as Zygote injection loads.
  • If attestation blocks login, patch Google Play Services with PlayIntegrityFix/Fork + TrickyStore or use ReZygisk/Zygisk‑Next only when testing. Keep the target in DenyList and avoid LSPosed modules that leak props.
  • For one‑off runs, use KernelSU/APatch (no Zygote injection) to stay under Zygisk heuristics, then attach Frida.

Step 2 — 30‑second Frida Codeshare tests

Try common drop‑in scripts before deep diving:

  • anti-root-bypass.js
  • anti-frida-detection.js
  • hide_frida_gum.js

Example:

frida -U -f com.example.app -l anti-frida-detection.js

These typically stub Java root/debug checks, process/service scans, and native ptrace(). Useful on lightly protected apps; hardened targets may need tailored hooks.[1][2]

Automate with Medusa (Frida framework)

Medusa provides 90+ ready-made modules for SSL unpinning, root/emulator detection bypass, HTTP comms logging, crypto key interception, and more.[10]

git clone https://github.com/Ch0pin/medusa
cd medusa
pip install -r requirements.txt
python medusa.py

# Example interactive workflow
show categories
use http_communications/multiple_unpinner
use root_detection/universal_root_detection_bypass
run com.target.app

Tip: Medusa is great for quick wins before writing custom hooks. You can also cherry-pick modules and combine them with your own scripts.

Automate with Auto-Frida (spawn-mode + consolidated hooks)

Auto-Frida is a Frida automation toolkit that focuses on repeatable setup plus auto-detection of protections and consolidated bypass script generation. It is useful when apps run checks very early or when multiple bypass modules would otherwise double-hook the same APIs.[11]

Key automation ideas:

  • Spawn-mode analysis to install hooks before Application.onCreate() so early SSL pinning, root, emulator, or anti-Frida checks are caught.
  • Protection detection + auto-bypass: detection results drive the generation of a single consolidated script that hooks each Java method/native symbol once, reducing crashes from overlapping hooks.
  • Frida server lifecycle checks: validate server health (process + port 27042 + frida-ps handshake) before downloading/restarting to keep runs stable.

Quick start:

git clone https://github.com/ommirkute/Auto-Frida.git
cd Auto-Frida
pip install -r requirements.txt
python auto_frida.py

Notes

  • Auto-Frida can auto-install frida/frida-tools if missing and supports multi-device selection.
  • Generated scripts can be executed immediately or merged with your custom hooks after analysis.

Step 3 — Bypass init-time detectors by attaching late

Many detections only run during process spawn/onCreate(). Spawn‑time injection (-f) or gadgets get caught; attaching after UI loads can slip past.

# Launch the app normally (launcher/adb), wait for UI, then attach
frida -U -n com.example.app
# Or with Objection to attach to running process
aobjection --gadget com.example.app explore  # if using gadget

If this works, keep the session stable and proceed to map and stub checks.

Step 4 — Map detection logic via Jadx and string hunting

Static triage keywords in Jadx:[1][5]

  • “frida”, “gum”, “root”, “magisk”, “ptrace”, “su”, “getprop”, “debugger”

Typical Java patterns:

public boolean isFridaDetected() {
    return getRunningServices().contains("frida");
}

Common APIs to review/hook:

  • android.os.Debug.isDebuggerConnected
  • android.app.ActivityManager.getRunningAppProcesses / getRunningServices
  • java.lang.System.loadLibrary / System.load (native bridge)
  • java.lang.Runtime.exec / ProcessBuilder (probing commands)
  • android.os.SystemProperties.get (root/emulator heuristics)

Step 5 — Runtime stubbing with Frida (Java)

Override custom guards to return safe values without repacking:[1]

Java.perform(() => {
  const Checks = Java.use('com.example.security.Checks');
  Checks.isFridaDetected.implementation = function () { return false; };

  // Neutralize debugger checks
  const Debug = Java.use('android.os.Debug');
  Debug.isDebuggerConnected.implementation = function () { return false; };

  // Example: kill ActivityManager scans
  const AM = Java.use('android.app.ActivityManager');
  AM.getRunningAppProcesses.implementation = function () { return java.util.Collections.emptyList(); };
});

Triaging early crashes? Dump classes just before it dies to spot likely detection namespaces:

Java.perform(() => {
  Java.enumerateLoadedClasses({
    onMatch: n => console.log(n),
    onComplete: () => console.log('Done')
  });
});

Quick root detection stub example (adapt to target package/class names):[12]

Java.perform(() => {
  try {
    const RootChecker = Java.use('com.target.security.RootCheck');
    RootChecker.isDeviceRooted.implementation = function () { return false; };
  } catch (e) {}
});

Log and neuter suspicious methods to confirm execution flow:

Java.perform(() => {
  const Det = Java.use('com.example.security.DetectionManager');
  Det.checkFrida.implementation = function () {
    console.log('checkFrida() called');
    return false;
  };
});

Bypass emulator/VM detection (Java stubs)

Common heuristics: Build.FINGERPRINT/MODEL/MANUFACTURER/HARDWARE containing generic/goldfish/ranchu/sdk; QEMU artifacts like /dev/qemu_pipe, /dev/socket/qemud; default MAC 02:00:00:00:00:00; 10.0.2.x NAT; missing telephony/sensors.[12]

Quick spoof of 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';
});

Complement with stubs for file existence checks and identifiers (TelephonyManager.getDeviceId/SubscriberId, WifiInfo.getMacAddress, SensorManager.getSensorList) to return realistic values.

SSL pinning bypass quick hook (Java)

Neutralize custom TrustManagers and force permissive SSL contexts:[12]

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);
  };
});

Notes

  • Extend for OkHttp: hook okhttp3.CertificatePinner and HostnameVerifier as needed, or use a universal unpinning script from CodeShare.
  • Run example: frida -U -f com.target.app -l ssl-bypass.js --no-pause

LSPosed layered unpinning and pre-load Flutter patching

When LSPosed is already available on the test device, SSL Kill Switch can apply persistent hooks without repacking the APK or attaching Frida. Install and activate the module, restrict its LSPosed scope to the authorized packages, restart those processes, install/trust the interception CA as required, and route traffic through the proxy. Only its optional transparent-routing feature invokes su for iptables.[20]

The Java bypass is applied twice: from initZygote() for boot-classpath implementations and again from handleLoadPackage() with the target application’s class loader. This lets one module cover the following layers while preserving each hooked method’s return contract.[20]

  • Replace the TrustManager[] passed to SSLContext.init() while preserving KeyManager[]; suppress Conscrypt and Network Security Configuration checks. Void validators can return immediately, but methods returning a cleaned chain must return the unverified input as the expected array or List<X509Certificate> instead of null.
  • Force named hostname verifiers to succeed and replace verifier arguments passed to HttpsURLConnection.
  • No-op OkHttp/TrustKit pinners, then also replace the finished OkHttp client’s verifier and pinner fields. This second layer survives internal method-signature changes that prevent a direct CertificatePinner hook from matching.
  • Continue WebView TLS failures and suppress later error callbacks. Redirecting process-wide SslErrorHandler.cancel() calls to proceed() also catches overrides that explicitly cancel without calling super; equivalent hooks can use Cordova or Tencent X5 handler types.

Flutter needs a different path because BoringSSL is statically linked into libflutter.so and its verifier is normally not exported. The original NVISO technique locates ssl_verify_peer_cert with byte signatures; SSL Kill Switch moves that idea to a pre-load file-patching flow.[19][20]

  1. Intercept every available Runtime.loadLibrary0 overload, falling back to System.loadLibrary() and also covering absolute-path System.load() calls.
  2. Resolve libflutter.so through the supplied class loader, nativeLibraryDir, or base/split APK ZIP entries; extract it to app cache when extractNativeLibs=false.
  3. Pattern-scan a copy with byte/nibble wildcards, overwrite the verifier prologue with an architecture-specific immediate-return stub, and verify the written bytes. Use the target function’s semantic success value (0 for ssl_verify_peer_cert, but 1 for Boolean chain-verification routines).
  4. Load the patched copy in the original class-loader namespace and suppress the original load. If no signature matches, allow the original library load rather than corrupting an unknown Flutter build.

Current implementation caveats are important during testing:[20]

  • Java hooks and the Kotlin Flutter patch affect every process selected in LSPosed module scope; the module UI’s per-category/domain selections do not gate these active paths. The Flutter mode selector is also ignored and the alternative native C++ engine is disabled.
  • The UI’s nominal per-application redirect does not emit -m owner --uid-owner UID, so it actually redirects device-wide TCP 80/443 traffic. It also does not cover QUIC/HTTP/3 over UDP 443 or exclude traffic already destined for the proxy.
  • Global removal and flushAll() delete/flush OUTPUT entries but leave the added POSTROUTING MASQUERADE rules. Inspect the complete NAT table and remove residual test rules manually.

mTLS interception: bypass server pinning without breaking client auth

For mTLS apps, SSLContext.init(KeyManager[], TrustManager[], SecureRandom) controls two different trust decisions:

  • TrustManager[] validates the server certificate.
  • KeyManager[] presents the client certificate/private key.

If you replace both arrays with a generic “trust all” hook, the app may accept Burp’s certificate but stop sending its client certificate, so the handshake still fails. In mTLS scenarios, keep the original KeyManager[] and replace only TrustManager[].[17][18]

Java.perform(function () {
  var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
  var SSLContext = Java.use('javax.net.ssl.SSLContext');
  var TrustAll = Java.registerClass({
    name: 'com.ht.TrustAll', implements: [X509TrustManager], methods: {
      checkClientTrusted: function () {}, checkServerTrusted: function () {},
      getAcceptedIssuers: function () { return []; }
    }
  });
  var init = SSLContext.init.overload('[Ljavax.net.ssl.KeyManager;','[Ljavax.net.ssl.TrustManager;','java.security.SecureRandom');
  init.implementation = function (km, tm, sr) {
    return init.call(this, km, Java.array('javax.net.ssl.TrustManager', [TrustAll.$new()]), sr);
  };
});

mTLS client certificate extraction from live keystore reloads

A common Android mTLS pattern is:

  1. generate an app keypair,
  2. store the private key + issued client cert in PKCS12 (.p12), often with a runtime-derived password,
  3. reload that keystore on every request to build a KeyManager.

That password can be strong at rest and still be useless during runtime: the app must eventually call KeyStore.load(...), getCertificate(...), and getKey(alias, password) in-process. Hook the method/constructor that receives the decrypted KeyStore, alias, and password (often a custom KeyManager wrapper) and dump the live material instead of brute-forcing the .p12 offline.[17][18]

Quick triage:

  • privateKey.getEncoded() returns bytes → software/JCE key, usually exportable.
  • privateKey.getEncoded() returns null → likely AndroidKeyStore/TEE-backed, so direct key export is blocked and you need a different approach.
Frida example: dump client cert/private key from a decrypted PKCS12-backed KeyStore
Java.perform(function () {
  var CKM = Java.use('com.example.app.ClientKeyManager');
  var Base64 = Java.use('android.util.Base64');
  var X509Certificate = Java.use('java.security.cert.X509Certificate');

  CKM.$init.implementation = function (ks, alias, password) {
    this.$init(ks, alias, password);
    var cert = Java.cast(ks.getCertificate(alias), X509Certificate);
    var certPem = Base64.encodeToString(cert.getEncoded(), 0);
    var key = ks.getKey(alias, password);
    var raw = key.getEncoded();
    console.log('alias=' + alias + ' password=' + password);
    console.log('CERT=' + certPem);
    console.log('KEY=' + (raw ? Base64.encodeToString(raw, 0) : 'null'));
  };
});

If the key is exportable, convert the dumped PEM key + certificate into a Burp-compatible client bundle:

openssl pkcs12 -export -out client-cert.pfx -inkey privateKey.key -in cert.pem

Useful extra hook points when Frida is attached before enrollment:

  • KeyPairGenerator.generateKeyPair()
  • KeyStore.setKeyEntry()
  • custom registration code that signs a nonce before the server issues the client certificate

OkHttp4 / gRPC / Cronet pinning (2024+)

Modern stacks pin inside newer APIs (OkHttp4+, gRPC over Cronet/BoringSSL). Add these hooks when the basic SSLContext hook hangs:[14]

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.util.Set', 'boolean').implementation = function(){ return this; };
  } catch (e) {}
});

If TLS still fails, drop to native and patch BoringSSL verification entry points used by Cronet/gRPC:

const customVerify = Module.findExportByName(null, 'SSL_CTX_set_custom_verify');
if (customVerify) {
  Interceptor.attach(customVerify, {
    onEnter(args){
      // arg0 = SSL_CTX*, arg1 = mode, arg2 = callback
      args[1] = ptr(0); // SSL_VERIFY_NONE
      args[2] = NULL;  // disable callback
    }
  });
}

Step 6 — Follow the JNI/native trail when Java hooks fail

Trace JNI entry points to locate native loaders and detection init:[1]

frida-trace -n com.example.app -i "JNI_OnLoad"

Quick native triage of bundled .so files:

# 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'

Interactive/native reversing:

Example: neuter ptrace to defeat simple anti‑debug in libc:

const ptrace = Module.findExportByName(null, 'ptrace');
if (ptrace) {
  Interceptor.replace(ptrace, new NativeCallback(function () {
    return -1; // pretend failure
  }, 'int', ['int', 'int', 'pointer', 'pointer']));
}

See also: Reversing Native Libraries

Step 7 — Objection patching (embed gadget / strip basics)

When you prefer repacking to runtime hooks, try:

objection patchapk --source app.apk

Notes:

  • Requires apktool; ensure a current version from the official guide to avoid build issues: https://apktool.org/docs/install[8]
  • Gadget injection enables instrumentation without root but can still be caught by stronger init‑time checks.

Optionally, add LSPosed modules and Shamiko for stronger root hiding in Zygisk environments, and curate DenyList to cover child processes.[12]

For a complete workflow including script-mode Gadget configuration and bundling your Frida 17+ agent into the APK, see:

Frida Tutorial — Self-contained agent + Gadget embedding

References:

Step 8 — Fallback: Patch TLS pinning for network visibility

If instrumentation is blocked, you can still inspect traffic by removing pinning statically:[1]

apk-mitm app.apk
# Then install the patched APK and proxy via Burp/mitmproxy

Make Apk Accept Ca Certificate

Install Burp Certificate

LSPosed/Xposed Hooking Abuse (Telephony/SMS)

On rooted devices, LSPosed/Xposed modules can hook Java telephony/SMS APIs at runtime, keeping the APK unmodified on disk while fully controlling what the app sees. This is commonly abused to bypass SIM‑binding flows that trust local telephony APIs or local SMS provider state.[16]

Key primitives

  • Suppress outgoing verification SMS while exfiltrating the token by short‑circuiting SmsManager.sendTextMessage in beforeHookedMethod.
  • Spoof MSISDN/line number by forcing TelephonyManager.getLine1Number() and SubscriptionInfo.getNumber() to return an attacker‑controlled value.
  • Plant a fake “Sent” record in the SMS provider so apps that check local SMS history see a successful send even if the carrier never received it.

Example: block SMS dispatch and capture content

XposedHelpers.findAndHookMethod(
  "android.telephony.SmsManager",
  lpparam.classLoader,
  "sendTextMessage",
  String.class, String.class, String.class, PendingIntent.class, PendingIntent.class,
  new XC_MethodHook() {
    protected void beforeHookedMethod(MethodHookParam param) {
      String body = (String) param.args[2];
      // exfiltrate body to operator channel
      param.setResult(null); // suppress real SMS send
    }
  }
);

Example: spoof device phone number

XposedHelpers.findAndHookMethod(
  "android.telephony.TelephonyManager",
  lpparam.classLoader,
  "getLine1Number",
  new XC_MethodHook() {
    protected void afterHookedMethod(MethodHookParam param) {
      param.setResult(spoofedMsisdn);
    }
  }
);
XposedHelpers.findAndHookMethod(
  "android.telephony.SubscriptionInfo",
  lpparam.classLoader,
  "getNumber",
  new XC_MethodHook() {
    protected void afterHookedMethod(MethodHookParam param) {
      param.setResult(spoofedMsisdn);
    }
  }
);

Example: inject a fake “Sent” SMS record

ContentValues v = new ContentValues();
v.put("address", dest);
v.put("body", body);
v.put("type", 2);   // sent
v.put("status", 0); // success
context.getContentResolver().insert(Uri.parse("content://sms/sent"), v);

Handy command cheat‑sheet

# List processes and attach
frida-ps -Uai
frida -U -n com.example.app

# Spawn with a script (may trigger detectors)
frida -U -f com.example.app -l anti-frida-detection.js

# Trace native init
frida-trace -n com.example.app -i "JNI_OnLoad"

# Objection runtime
objection --gadget com.example.app explore

# Static TLS pinning removal
apk-mitm app.apk

Universal proxy forcing + TLS unpinning (HTTP Toolkit Frida hooks)

Modern apps often ignore system proxies and enforce multiple layers of pinning (Java + native), making traffic capture painful even with user/system CAs installed. A practical approach is to combine universal TLS unpinning with proxy forcing via ready-made Frida hooks, and route everything through mitmproxy/Burp.

Workflow

  • Run mitmproxy on your host (or Burp). Ensure the device can reach the host IP/port.
  • Load HTTP Toolkit’s consolidated Frida hooks to both unpin TLS and force proxy usage across common stacks (OkHttp/OkHttp3, HttpsURLConnection, Conscrypt, WebView, etc.). This bypasses CertificatePinner/TrustManager checks and overrides proxy selectors, so traffic is always sent via your proxy even if the app explicitly disables proxies.
  • Start the target app with Frida and the hook script, and capture requests in mitmproxy.

Example

# Device connected via ADB or over network (-U)
# See the repo for the exact script names & options
frida -U -f com.vendor.app \
  -l ./android-unpinning-with-proxy.js \
  --no-pause

# mitmproxy listening locally
mitmproxy -p 8080

Notes

  • Combine with a system-wide proxy via adb shell settings put global http_proxy <host>:<port> when possible. The Frida hooks will enforce proxy use even when apps bypass global settings.
  • This technique is ideal when you need to MITM mobile-to-IoT onboarding flows where pinning/proxy avoidance is common.
  • Hooks: https://github.com/httptoolkit/frida-interception-and-unpinning

References