Network stacks: end-to-end capture
One section per Android HTTP client / network stack, each a
complete walkthrough: which layer reads the plaintext (system
proxy, native in-process tap, or an app-layer hook), the exact
rutken capture --embed-trust or
rutken run --bespoke command, the pinning-bypass flag
if that stack pins, which libssl the native tap
targets, the output you should see, and the gotchas. For the
mechanism behind each pin, this page
cross-links the
TLS pinning bypass companion; for
the start-to-finish runbook of a single capture, see
Dynamic & network capture.
How to read this page
Every HTTPS connection can be read at one of four layers; the first that fits the app wins. Which layer applies is decided by the stack, so each stack section below names its layer up front.
-
Proxy MITM — the app honours the system
HTTP proxy and uses a Java TLS stack, so after a
pinning-bypass hook the traffic decrypts through Rutken's
proxy. This is the path for every Java stack that respects
http_proxy(HttpURLConnection, OkHttp, Volley, Apache, Retrofit, and the OkHttp wrappers). -
Native in-process tap —
SSL_read/SSL_writeinside the process's BoringSSL. It sees cleartext before encryption and after decryption, so it is immune to pinning and needs no proxy at all. This is the path for the system-proxy-blind native stacks (Cronet, Flutter), and the universal fallback for anything the Java unpin misses. Add--bespoke-raw-bytesto surface the actual request / response bytes (the session otherwise digests them to{len, sha256}). - App-layer hook — the app's own decrypt / secret methods, located by static analysis.
-
TLS keylog — a
CLIENT_RANDOMkeyfile for offline decryption.
Two engines deliver the hooks. unpin (Community) drives a bundled Frida gadget — turnkey, broad Java coverage, ideal for a first look. capture / run --bespoke (paid editions) run Rutken's own in-house agent — no Frida fingerprint, and it is the only engine with the native in-process tap. Reach for unpin first; switch to the bespoke agent when the target detects Frida or when the stack is native (Cronet, Flutter) and the proxy never sees the bytes.
Only capture traffic from an app you own or are explicitly authorized to test. The behaviour-changing hooks take their consent acknowledgements verbatim and fail closed without them.
Prerequisites (all stacks)
These are common to every walkthrough below; each stack section only adds what is specific to it.
-
Device connected:
adb devicesshows it asdevice. Note the serial (e.g.emulator-5554) — it is the--device/--serialvalue everywhere. -
The target APK on disk. For a third-party app,
adb pullits installed path first (pm path <pkg>finds it). -
A signing keystore to resign the repacked APK
on the embed route:
keytool -genkeypair -keystore signing.p12 -storetype PKCS12 -storepass <pw> -alias k -keyalg RSA -keysize 2048 -validity 3650 -dname "CN=Test". -
An owned-allowlist file for the bespoke
capture route:
echo "<pkg>" > owned-allowlist.txt. -
A current arm64 agent for
run --bespoke/capture --agent-so— build it fresh, never reuse a fixture.so(stale ones are rejected with a version mismatch):cargo build -p agent --features bespoke-agent --target aarch64-linux-android --release. Or pass--bespoke-attach(no path) to let Rutken auto-resolve its own bundled agent for the device ABI. -
The consent phrases, supplied verbatim:
--i-am-authorized-bespokefor observation and--i-am-authorized-bespoke-activefor any behaviour change (pinning bypass is a return-override, so it needs the active ack). Undercapture --embed-trustthe active ack auto-armsdebug.rutken.return_overrides; underrun --bespokeset it by hand first (adb shell setprop debug.rutken.return_overrides 1).
Replace <sha256> everywhere with the SHA-256
of the exact target APK, and
<pool-resident-name> with a string already in
the target's DEX string pool (the embed step is resolve-only).
Coverage at a glance
The read layer and pin-bypass flag for each stack. "Proxy"
means the stack honours the system proxy and decrypts there after
the pin is defeated; "Native tap" means the in-process
SSL_read / SSL_write reads it regardless
of proxy or pinning.
| Stack | Read layer | Honours system proxy? | If it pins |
|---|---|---|---|
HttpURLConnection / javax.net.ssl |
Proxy MITM (native tap fallback) | yes | Conscrypt / NSC force-void |
| OkHttp3 / Retrofit | Proxy MITM (native tap fallback) | yes | CertificatePinner return-override |
| OkHttp2 | Proxy MITM (native tap fallback) | yes | CertificatePinner return-override |
| Volley | Proxy MITM | yes | rides HttpURLConnection → Conscrypt force-void |
| Apache HttpClient | Proxy MITM (native tap fallback) | yes | Conscrypt force-void (native-trust path) |
| Cronet | Native tap (proxy-blind) | no | addPublicKeyPins no-op |
| WebView | Proxy MITM + native tap | yes | native tap reads plaintext around an onReceivedSslError pin |
| Flutter (dart:io) | Native tap (proxy-blind) | no | native force-accept on CertificateCallback |
| Xamarin / .NET MAUI | Native tap (rides Conscrypt) | yes (via Conscrypt) | native tap reads plaintext around a managed pin |
| Ktor / gRPC / React Native | collapse to OkHttp3 — see that section | ||
Per network stack
Each card gives the read layer, the exact end-to-end command,
the pinning-bypass flag, the libssl the native tap
targets, the expected output, and the gotchas. Where two stacks
have an identical capture procedure, they share one heading.
HttpURLConnection / javax.net.ssl (platform Conscrypt)
Read layer: Proxy MITM · native tap fallback
# Decrypt over the proxy with Rutken's in-house agent (paid)
RUTKEN_KEYSTORE_PASS=<pw> rutken <app>.apk capture \
--device <serial> --owned-allowlist owned-allowlist.txt \
--embed-trust \
--agent-so target/aarch64-linux-android/release/libagent.so \
--library-name <pool-resident-name> \
--keystore signing.p12 --key-alias k \
--i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" \
--duration 90 --format json
# Community first look (Frida gadget, proxy only -- no native tap)
rutken app-gadget.apk unpin --gadget --gadget-port 27042 --ca-store none \
--serial <serial> --duration 120 --record out.rcap \
--authorize <sha256> \
--i-am-authorized "I am authorized to intercept traffic for this target" \
--i-am-authorized-subvert "I am authorized to subvert this target's defenses"
OkHttp3 / Retrofit
Read layer: Proxy MITM · native tap fallback
# By-name: force CertificatePinner.check -> Void (un-obfuscated)
rutken <app>.apk run --bespoke --bespoke-attach-agent <arm64 libagent.so> \
--bespoke-pack java.tls.certificatepinner_check \
--bespoke-authorize <sha256> \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
# Structural (name-independent) route for an R8-renamed okhttp
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
--bespoke-derived --bespoke-force-void-cert-pinner \
--bespoke-authorize <sha256> \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
# Or decrypt over the proxy end-to-end with the embed route
rutken <app>.apk capture --device <serial> --embed-trust \
--owned-allowlist owned-allowlist.txt \
--agent-so target/aarch64-linux-android/release/libagent.so \
--library-name <pool-resident-name> \
--keystore signing.p12 --key-alias k \
--i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" --duration 90
OkHttp2
Read layer: Proxy MITM · native tap fallback
# OkHttp2 CertificatePinner -- same by-name return-override as okhttp3
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
--bespoke-pack java.tls.certificatepinner_check \
--bespoke-authorize <sha256> \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
Volley
Read layer: Proxy MITM
# Volley rides HttpURLConnection -> decrypt over the proxy (embed route)
rutken <app>.apk capture --device <serial> --embed-trust \
--owned-allowlist owned-allowlist.txt \
--agent-so target/aarch64-linux-android/release/libagent.so \
--library-name <pool-resident-name> \
--keystore signing.p12 --key-alias k \
--i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" --duration 90
Apache HttpClient
Read layer: Proxy MITM · native tap fallback
# Apache rides Conscrypt -- force the platform trust check void, then decrypt
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
--bespoke-pack java.tls.trustmanagerimpl_check_server_trusted \
--bespoke-authorize <sha256> \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
# Non-debuggable release: embed the agent + CA (Route 1)
rutken <app>.apk capture --device <serial> --embed-trust \
--owned-allowlist owned-allowlist.txt \
--agent-so target/aarch64-linux-android/release/libagent.so \
--library-name <pool-resident-name> --keystore signing.p12 --key-alias k \
--i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" --duration 90
Cronet
Read layer: Native in-process tap — system-proxy-blind
# Native plaintext tap -- no proxy, immune to pinning
rutken <app>.apk run --bespoke \
--bespoke-attach-agent <arm64 libagent.so> \
--bespoke-hooks native-tls.json \ # SSL_read / SSL_write / keylog symbols
--bespoke-raw-bytes \ # surface the actual bytes, not a digest
--bespoke-authorize <sha256> \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
# Also neutralise addPublicKeyPins on the real CronetEngine.Builder
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
--bespoke-pack java.tls.cronet_add_public_key_pins_noop \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
WebView
Read layer: Proxy MITM · native tap
# Unpinned WebView: decrypt through the proxy (embed route)
rutken <app>.apk capture --device <serial> --embed-trust \
--owned-allowlist owned-allowlist.txt \
--agent-so target/aarch64-linux-android/release/libagent.so \
--library-name <pool-resident-name> --keystore signing.p12 --key-alias k \
--i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" --duration 90
# Pinned via onReceivedSslError: read the BoringSSL
# plaintext in-process instead
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
--bespoke-hooks native-tls.json --bespoke-raw-bytes \
--bespoke-authorize <sha256> \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
Flutter (dart:io)
Read layer: Native in-process tap — system-proxy-blind
# Native force-accept on the dart:io certificate callback + plaintext tap
rutken <app>.apk run --bespoke --bespoke-attach-agent <arm64 libagent.so> \
--bespoke-hooks flutter-tls.json \
--bespoke-raw-bytes \
--bespoke-authorize <sha256> \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
Xamarin / .NET MAUI
Read layer: Native in-process tap (rides Conscrypt)
# Passive native tap -- reads plaintext regardless of a managed pin
rutken <app>.apk run --bespoke \
--bespoke-attach-agent <arm64 libagent.so> \
--bespoke-hooks native-tls.json \
--bespoke-raw-bytes \
--bespoke-authorize <sha256> \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
Ktor / gRPC / React Native (they ride OkHttp)
Read layer: collapses to OkHttp3
-
Ktor on Android (the OkHttp engine) →
OkHttp3. Proxy MITM;
CertificatePinnerreturn-override if it pins. - gRPC-Java with the OkHttp transport → OkHttp3. Proxy MITM over its Conscrypt TLS. (A gRPC build configured on the Cronet transport instead collapses to Cronet — native tap, proxy-blind.)
-
React Native (the default networking
module) → OkHttp3. Proxy MITM;
CertificatePinner/ NSC bypass as for OkHttp.
# Ktor / gRPC-OkHttp / React Native all ride OkHttp -> same as OkHttp3
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
--bespoke-pack java.tls.certificatepinner_check \
--bespoke-authorize <sha256> \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
Command cheat-sheet
The three shapes every stack above reduces to. Replace
<sha256> with the SHA-256 of the exact target
APK and supply the consent phrases verbatim.
# 1. Proxy MITM, end-to-end, non-debuggable release (Route 1 embed)
# -> HttpURLConnection, OkHttp3/Retrofit, OkHttp2, Volley, Apache, unpinned WebView
RUTKEN_KEYSTORE_PASS=<pw> rutken <app>.apk capture \
--device <serial> --owned-allowlist owned-allowlist.txt --embed-trust \
--agent-so target/aarch64-linux-android/release/libagent.so \
--library-name <pool-resident-name> --keystore signing.p12 --key-alias k \
--i-am-authorized-repack "I am authorized to repack and reinstall this target on this device" --i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" \
--i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent" --duration 90 --format json
# 2. Native in-process tap, no proxy, immune to pinning
# -> Cronet, Flutter, Xamarin, WebView fallback, any obfuscated miss
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
--bespoke-hooks native-tls.json --bespoke-raw-bytes \
--bespoke-authorize <sha256> \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
# 3. Defeat a specific pin (by-name pack or derived/structural)
rutken <app>.apk run --bespoke --bespoke-attach-agent <agent> \
--bespoke-pack java.tls.certificatepinner_check \ # okhttp
# --bespoke-pack java.tls.cronet_add_public_key_pins_noop # cronet
# --bespoke-derived --bespoke-force-void-cert-pinner # R8 okhttp
# --bespoke-derived --bespoke-force-true-hostname # hostname verifier
--bespoke-authorize <sha256> \
--i-am-authorized-bespoke "I am authorized to observe this target via the bespoke agent" --i-am-authorized-bespoke-active "I am authorized to MODIFY this target's behaviour via the bespoke agent"
The pinning-bypass flag for every
flavour lives on the
TLS pinning bypass page; the
full flag surface is
always current via rutken capture --help /
rutken run --help.
Troubleshooting
-
"flows: 0" but pinning says bypassed, or
opaque flows only. You triggered traffic before the
proxy / gate was live, or the app's launch pings raced the
setup. Wait until
http_proxyis set, then exercise the app — new connections pick up the proxy, launch-time ones may not. -
Nothing on the proxy for Cronet or Flutter.
Expected — both are system-proxy-blind. Use the native
tap (shape 2 above) with
--bespoke-raw-bytes. -
Bespoke hooks all
JavaRefused(ReturnOverrideDisabled). The return-override gate was not armed. The active ack auto-arms it undercapture --embed-trust; underrun --bespoke,adb shell setprop debug.rutken.return_overrides 1before launch (only"1"enables it). -
unsupported protocol versionon bespoke attach. You used a stale / fixture agent.so. Rebuild the agent from current source. -
The by-name okhttp pack fires 0× on a release
build. R8 renamed
okhttp3.CertificatePinner. Use--bespoke-derived --bespoke-force-void-cert-pinner, or read the plaintext off the native tap, which is name-independent. -
Agent-lib name collision. Route 1 embeds
the agent as
lib<library-name>.so; if that name collides with a real app lib (e.g. one of the app's own bundled.sofiles), the agent shadows it and the app's own native function breaks (UnsatisfiedLinkError). Pick a non-colliding--library-name.
Only capture traffic from an app you own or are explicitly authorized to test. The behaviour-changing hooks require authorization acknowledgements and fail closed without them.