The Android 14 CA move that broke every '/system/etc' pinning tutorial

Ninety percent of mobile testing is web testing you already know — REST, tokens, IDOR, injection. The other ten percent is a wall you hit before you can do any of it: getting the app’s HTTPS traffic to land in your proxy as plaintext. And most write-ups get that ten percent wrong, because they were written for an Android that no longer exists. You follow the steps, the cert goes into /system, the app still throws handshake errors, and you conclude the app has exotic pinning. It doesn’t. Google moved the trust store out from under you and the tutorial never noticed.

Before you reach for Frida, answer one question: is the CA actually trusted by the system store this app reads? On Android 14+, the store you’re editing may not be the store it checks.

Get that right and most apps just decrypt — because most apps don’t pin. Here’s the decision tree, Android-first, with the specific walls that actually stop people in 2026.

Wall 1 — the app ignores the proxy (and the detection trap)

Put the device and your laptop on one network, set the Wi-Fi proxy to your laptop’s IP and your proxy’s port, and browse. Two outcomes, and the second is the one that wastes an afternoon:

  • Traffic appears (TLS errors are fine for now) — the app respects the system proxy. Go to Wall 2.
  • Nothing appears. The app is on a networking stack that ignores the device HTTP proxy. This is not rare and it is not only Flutter — plenty of gRPC and native HTTP stacks never read the proxy setting. Don’t fight it. Route the whole device through transparent/VPN-mode capture (an iptables redirect of outbound 443 to your listener) so the app’s opt-out is irrelevant. You capture what it sends regardless of what it wanted to do.

The detection trap that sends people down the wrong path: if a plain browser on the device shows up in your proxy but the target app doesn’t, that’s an app-level proxy-ignore, not a network problem. People see “nothing from the app” and immediately start writing pinning bypasses — before they’ve even established the traffic could reach them. Confirm the browser reaches your proxy, confirm the app doesn’t, and switch to transparent capture first. You may find the app wasn’t pinning at all; it just never spoke to your proxy.

Wall 2 — the CA store, and the Android 14 change that breaks the old playbook

Since Android 7 (Nougat), apps trust only the system CA store, not user-installed certs, unless their network_security_config explicitly opts in:

<certificates src="user"/>

So a user-installed cert only works on an app that both (a) doesn’t pin and (b) trusts user certs. Miss either and you get nothing. That part is old news. Here is what changed and what most guides still get wrong:

Since Android 14, the system CA trust store moved to an updatable APEX module. It now lives at:

/apex/com.android.conscrypt/cacerts/

The classic move — remount /system read-write, drop your CA as a <hash>.0 file into /system/etc/security/cacerts/, fix perms — no longer changes trust on Android 14+. That old path is not what the runtime consults anymore. You can do the whole ritual perfectly, ls your cert sitting there, and every pinning-free app still rejects you. This is exactly why the “copy your cert to /system” tutorials silently fail on new devices: nothing errors, the cert is present, and trust just… doesn’t happen.

To system-trust your CA now you have to target the conscrypt APEX itself — mount your cert into /apex/com.android.conscrypt/cacerts/ — or use a Magisk module built to mount into that APEX (the current AlwaysTrustUserCerts-style modules do this for you). Get that right and you’re back to the good news:

System-trusting the CA clears every app that doesn’t pin — which is most of them. If traffic decrypts cleanly here, you’re done. Don’t touch Frida. Instrumentation is for the apps that still fail after the CA is genuinely trusted, and not a step before.

If you can’t or won’t touch the system store, the fallback is to patch the target: decompile, add a network_security_config with <certificates src="user"/>, re-sign, reinstall. Heavier, but scoped to the one app.

Wall 3 — pinning, and where exactly to hook it

Pinning means the app carries the exact cert or public key it expects and rejects everything else, including your trusted-CA interception. Symptom: handshakes fail even though the CA is system-trusted. Now — and only now — you disable the check at runtime. The turnkey path is Objection:

android sslpinning disable

When the universal scripts miss, you need to know the actual hook targets, because “it didn’t work” usually means you hooked the wrong layer. The ones that matter:

  • OkHttpokhttp3.CertificatePinner.check. The most common Java pin by far.
  • The TrustManager pathjavax.net.ssl.X509TrustManager and android.net.http.X509TrustManagerExtensions.checkServerTrusted. This is what a lot of “custom pinning” actually routes through.
  • Conscrypt — the Platform class, for apps that verify below the standard TrustManager.
  • Native pinning (BoringSSL in libssl.so)SSL_CTX_set_custom_verify and SSL_set_verify. When the app pins in C, no amount of Java hooking touches it; you hook these symbols directly.

Double pinning is the trap here. An app can pin at both the OkHttp layer and natively. The tell is specific: you unpin OkHttp, most calls start flowing, and a few handshakes still fail. That’s not a broken script — that’s a second pin at the native layer you haven’t hooked yet. Add the libssl.so hooks and the stragglers clear.

For apps with anti-Frida checks, skip runtime hooking entirely: patch the pinning method out of the smali (or use apk-mitm), re-sign, reinstall.

The Flutter wall

Flutter deserves its own section because it defeats both of the earlier fixes at once, and people burn hours not realizing they’re fighting two problems:

  1. It ignores the system proxy. Dart’s HttpClient never reads the platform HTTP proxy. Your proxy setting does nothing — the app was never going to talk to you that way. Fix: transparent/VPN-mode capture (the iptables redirect), so the app’s proxy-obliviousness stops mattering.
  2. It ships its own TLS with its own CA store. Flutter bundles BoringSSL inside libflutter.so, with its own embedded trust store. System-trusting your CA — even correctly into the conscrypt APEX — is invisible to it, because it isn’t asking the Android store. You have to reach into the engine.

Two ways in. Pattern-scan libflutter.so for the certificate-verification function and hook it — the target is:

ssl_crypto_x509_session_verify_cert_chain

(the symbol is stripped in release builds, so you’re scanning for the byte pattern / offset, not a named export). Or patch the engine wholesale with reFlutter, which rebuilds libflutter.so to disable verification and reroute through your proxy. Either way: transparent capture handles how the bytes reach you, the engine hook handles whether it accepts your cert. You need both. Fix one and the other still stops you cold, which is exactly why Flutter feels unbeatable until you name the two problems separately.

Once you’re through, it’s just HTTP

The moment plaintext lands, the mobile-specific part is over. Map endpoints, capture two identities and test BOLA/IDOR, fuzz parameters, check token handling. Mobile apps are often softer than their web siblings — developers assume nobody watches the wire, so you find hard-coded keys, debug endpoints, and auth that only ran client-side. If you haven’t already pulled the app apart on disk, the endpoints and secrets you find in reading an APK statically tell you what to expect on the wire, which turns interception from fishing into confirmation.

iOS, briefly: same shape, different ceremony. Trust the CA explicitly in Settings → General → About → Certificate Trust Settings (installed is not trusted), then Frida/Objection on a jailbroken device for pinning. The walls are the same; only the paths change.

The point

The reason mobile interception feels harder than it is: the trust model keeps moving, and the tutorials lag it by a full Android release. The Android 14 APEX change turned a five-minute cert install into a silent failure that looks like exotic pinning, and thousands of write-ups still send you to /system. Name the layer you’re actually stuck at — proxy-ignore, CA store, Java pin, native pin, or Flutter’s private stack — and each has a precise, cheap fix. Guess, and you’ll write a Frida script to solve a proxy problem.

Crusader is the HTTP/2 intercepting proxy on the other end of all this. Point the device at it for standard capture once your CA is trusted, or use its transparent/VPN-mode capture for the apps — Flutter, gRPC, anything native — that ignore the proxy entirely, so the app’s opt-out never gets a vote. Get the traffic flowing and you get the same identity, replay, and history workflow you’d use on any web target. download Crusader free and turn the mobile app into just another set of requests.

Frequently asked questions

Why does copying my CA to /system/etc/security/cacerts no longer work on Android 14?
Because Android 14 moved the system CA trust store into an updatable APEX module at /apex/com.android.conscrypt/cacerts/. The old /system/etc/security/cacerts path is no longer the source of truth for trust decisions, so remounting it and dropping your hashed cert there changes nothing. You now have to target the conscrypt APEX directly, or use a Magisk module that mounts your cert into it.
Why can't I intercept a Flutter app even after system-trusting my CA?
Two reasons stacked. First, Dart’s HttpClient never reads the system HTTP proxy, so proxy settings are ignored outright — fix that with transparent/VPN-mode capture. Second, Flutter bundles its own BoringSSL inside libflutter.so with its own embedded CA store, so trusting your CA in the Android system store is irrelevant to it. You have to hook the native verification function in libflutter.so (or patch the engine with reFlutter).
What does 'double pinning' mean and how do I get past it?
An app can pin at more than one layer — for example OkHttp’s CertificatePinner in Java and again in native code via BoringSSL. If you unpin OkHttp and most calls start flowing but a handful of handshakes still fail, you’re hitting a second native pin. Hook the native layer (SSL_CTX_set_custom_verify / SSL_set_verify in libssl.so) in addition to the Java one.
Try Crusader free →