Android MITM Setup
Intercepting a mobile app's HTTPS traffic on a modern Android device requires four things working together: a rootable emulator (so you can write to the system trust store), mitmproxy on the host (decoding and re-issuing the TLS), a CA certificate the device actually trusts (system store, not user store, since Android 7+), and a Frida-injected runtime hook that disables the app's TLS-pinning logic. None of these pieces is hard; together they're the half-day-of-stack-overflow that this skill collapses into a checklist.
This skill is the dynamic counterpart to the static decompilation skill in android-reverse-engineering. Use both: jadx reads the APK, this gets you the wire traffic.
When this skill applies
- Target is an Android app on API 24+ (Android 7+, where user-store CAs stopped being trusted for app traffic by default).
- The app uses HTTPS — most do.
- The app pins certificates (OkHttp
CertificatePinner,TrustManager, or platform default after Android 7). - You have legal grounds to inspect the app's traffic (own software, written permission, disclosure programme, interoperability under applicable law).
If the app doesn't pin, you can skip the Frida step. If it uses gRPC or QUIC, mitmproxy 9+ has experimental support but you may need extra config.
Workflow
Phase 1: Pick a rootable emulator image
Use the Android Studio emulator (avdmanager / emulator) with a system image flavour that allows adb root. The flavour matters:
- ✅ Google APIs (e.g.
system-images;android-34;google_apis;arm64-v8a) — rootable, includes Play Services so most apps work. - ❌ Google Play (
google_apis_playstore) — not rootable; Play Integrity / SafetyNet hardened. Spending an afternoon discovering this is a rite of passage. - ✅ AOSP / default — rootable but no Play Services; some apps refuse to start.
Create the AVD once:
avdmanager create avd \
--name Pixel_API_34_GoogleAPIs \
--package "system-images;android-34;google_apis;arm64-v8a" \
--device pixel_6
Boot with a writable system partition so /system edits stick:
emulator -avd Pixel_API_34_GoogleAPIs -writable-system
If you want to use a physical device, you need a userdebug Android build (custom ROM like LineageOS) or Magisk on a stock build. The Magisk path is documented elsewhere; the emulator is faster to set up and easier to throw away.
Phase 2: Generate the mitmproxy CA and install it into the system store
Start mitmproxy once on the host so it generates ~/.mitmproxy/:
mitmproxy -p 8080
# Press q to quit — the cert files now exist in ~/.mitmproxy/
ls ~/.mitmproxy/
# mitmproxy-ca-cert.cer mitmproxy-ca-cert.pem mitmproxy-ca.p12 ...
Compute the cert's old-style subject hash. Android's system CA directory uses <hash>.0 as the filename:
HASH=$(openssl x509 -inform PEM -subject_hash_old -in ~/.mitmproxy/mitmproxy-ca-cert.cer | head -1)
echo "$HASH"
Push the cert into the system store. On Android 10+ this requires -writable-system (see Phase 1) AND adb remount:
adb root
adb remount
adb push ~/.mitmproxy/mitmproxy-ca-cert.cer /system/etc/security/cacerts/${HASH}.0
adb shell chmod 644 /system/etc/security/cacerts/${HASH}.0
adb reboot
On Android 14+ the system CA directory moved to an APEX (/apex/com.android.conscrypt/cacerts/). mitmproxy documents the current path; check their docs if the steps above don't take.
After reboot, verify:
adb shell ls /system/etc/security/cacerts/ | grep "$HASH"
adb shell settings list global | grep cacerts # sanity
Phase 3: Route emulator traffic through mitmproxy
Set the global proxy on the emulator to the host bridge alias 10.0.2.2:8080:
adb shell settings put global http_proxy 10.0.2.2:8080
Start mitmproxy on the host:
mitmproxy -p 8080
Open Chrome on the emulator and visit https://example.com — you should see the flow in mitmproxy and the page should load (TLS now terminates at mitmproxy with the new CA the device trusts).
To revert proxy:
adb shell settings put global http_proxy :0
Phase 4: Bypass TLS pinning with Frida
For most apps using OkHttp CertificatePinner or platform TrustManager, the default-trust path will now show pinning errors in the app: the app trusts the system CA store (Phase 2) but rejects any cert not matching its hardcoded pin.
The fix is to hook the pinning check at runtime. Steps:
# 1. Download frida-server matching your emulator architecture from
# https://github.com/frida/frida/releases
# (e.g. frida-server-16.x.x-android-arm64.xz)
# 2. Push and launch
adb push frida-server-android-arm64 /data/local/tmp/frida-server
adb shell chmod 755 /data/local/tmp/frida-server
adb shell "/data/local/tmp/frida-server &"
# 3. Install Frida tooling on the host
pipx install frida-tools # or: pip install frida-tools
# 4. Verify connection
frida-ps -U | head
Then inject a pin-bypass script. Don't paste hook source into this skill — pinning bypass scripts evolve fast and Codeshare keeps them up to date. The canonical starting points:
frida-multiple-unpinningon Frida Codeshare: https://codeshare.frida.re/@akabe1/frida-multiple-unpinning/objection: https://github.com/sensepost/objection — wraps Frida with a higher-level CLI;objection -g <package> explorethenandroid sslpinning disablecovers ~80% of cases.
Launch the app with the script:
frida -U -f com.example.app -l unpinning.js --no-pause
If the standard scripts don't take, the app has likely been R8-shrunk and the class/method signatures the script targets are now obfuscated to single-letter names. Typical resolution:
- Decompile the APK (
android-reverse-engineeringplugin) and search forCertificatePinner/checkServerTrusted/okhttp3patterns. - Identify the obfuscated class/method that calls them.
- Write a targeted hook on that specific class. See
references/tls-pinning-bypass.mdfor the structure (not the source — pinning bypass for a specific APK should not be in a public repo).
Phase 5: Capture and document the contract
With traffic flowing through mitmproxy you should now see:
- Auth flows (Auth0, OAuth, SAML) — the IdP host plus PKCE / state / nonce / audience.
- Backend BFF / API gateway calls (Azure APIM, AWS API Gateway, CloudFront).
- Feature-flag / experimentation calls (LaunchDarkly, Optimizely).
- Telemetry (Firebase, Sentry, Datadog RUM).
- WebSocket upgrades (game servers, real-time channels).
Save flows for replay:
mitmdump -p 8080 -w session.flows # record
mitmdump -nr session.flows # replay
mitmdump -nr session.flows -f "~u /api/" # filter
This is the input to mobile-auth-replay if you want to drive the same contract from a desktop tool.
Common pitfalls
- "Google Play" image instead of "Google APIs". Cannot
adb root. Re-create the AVD. - Forgot
-writable-systemat boot./systemis read-only; cert push fails. Re-boot with the flag. - CA installed in user store via Settings → Security. Doesn't work on Android 7+ for app traffic. Must be system store.
- Auth host fronted by a TLS-fingerprint WAF (Akamai, Cloudflare Bot Management). Irrelevant on the emulator itself — the real app's TLS stack passes. It matters in
mobile-auth-replaywhere you replay from desktop. Note it now, address it there. - gRPC traffic shows as binary garbage. mitmproxy can decode gRPC with the
--set http2_force_decode_for_gprc=trueflag and a.proto(if you have one). Otherwise you'll see HTTP/2 envelopes with protobuf bodies — read them withprotoc --decode_rawper message.
References
references/emulator-setup.md— AVD creation, image selection,-writable-systemgotchas.references/ca-cert-install.md— CA install per Android version (7, 10, 11, 14+ APEX).references/tls-pinning-bypass.md— when default scripts work, when you need targeted hooks, how to identify the obfuscated class.
Pairs with
- Simone Avogadro's
android-reverse-engineering— static decompilation. Run that first to read the APK; this captures what it does. mobile-auth-replay— once you've seen the auth flow in mitmproxy, drive it from a desktop tool.find-production-sourcemap— if the app is a webview wrapping a JS bundle, sometimes you can skip mitm entirely.
Scope reminder
These techniques produce traffic captures that can include credentials, tokens, PII, and other sensitive data even from your own account. Treat them like passwords: don't paste into bug reports, don't commit session.flows files to public repos, redact before sharing. If you're disclosing a finding to a vendor, send the redacted reproduction steps, not the raw flows.