Documentation

Proving Grounds

A set of intentionally-vulnerable practice apps you download and run on your own device or emulator, each with a full end-to-end CLI walkthrough. Work through them to build real skill with Rutken: static analysis, dynamic capture, and defeating TLS certificate pinning across every major Android network stack. Each app hides a fake secret behind its defenses — your job is to capture it.

Overview

The Proving Grounds are small Android apps we built on purpose to be defeated. Each one implements a real defense — a specific TLS pinning style, a custom trust check, a hardened network stack — and hides a fake API key behind it. You download the app, install it on a device or emulator you control, and use Rutken to find the defense statically and then defeat it at runtime to read the plaintext traffic and capture the key.

Nothing here talks to a real service you don't own: the apps call a public echo endpoint, and the “secret” is a throwaway string that only exists to prove your bypass worked.

Authorization — read this first.

Run these only on a device or emulator you own or are explicitly authorized to test. The dynamic commands repack, reinstall, and instrument the target, and Rutken makes you type a verbatim authorization phrase before it will touch a device — that phrase is you asserting you have permission. The same techniques against an app or device you do not control may be illegal. These practice apps exist so you never need to point Rutken at anything you aren't allowed to.

Get set up

Everything below assumes Rutken is installed (see Download). Static analysis needs nothing else. Dynamic capture additionally needs a device and a signing key.

What you need

  • Static analysis — just Rutken and the downloaded .apk. Free, no account.
  • Dynamic capture — an Android device or emulator with USB debugging on (visible to adb), a PKCS#12 signing keystore to re-sign the practice app, and a paid tier (the bespoke hooking engine).

One-time keystore

Any keystore works. Generate a throwaway one with the JDK's keytool:

keytool -genkeypair -storetype PKCS12 -keystore practice.p12 \
  -alias practice -keyalg RSA -validity 10000 \
  -dname "CN=Practice" -storepass changeit

The loop you'll repeat

Every range follows the same four moves:

  1. Scan — rutken <app>.apk rules finds the defense statically.
  2. Identify — read which pinning style it flagged, and on which network stack.
  3. Capture — rutken <app>.apk capture … instruments the app and reads the plaintext.
  4. Confirm — the captured flow contains the fake key. That's the win.

Find the package id and declare it owned. Both capture routes require an allowlist naming the app you're authorized to touch. Get the id from the scan, then:

rutken multipin-debug.apk info          # prints the application id
echo <application.id> > owned.txt       # e.g. the id the line above printed

The ranges

Start with MultiPin — it's the grand tour. Each card links the app; the walkthrough below it is the full procedure. Debug builds are the friendliest to start on; the release builds add R8 obfuscation (the class and method names are stripped), which is where Rutken's name-independent matching earns its keep.

MultiPin Beginner → Advanced

Seven network stacks pinned in one app — OkHttp, Retrofit, Cronet, Apache, HttpURLConnection and more. The best place to start.

Flutter Intermediate

Pinning inside Flutter's dart:io / BoringSSL — below the Java layer, with native libraries mapped straight from the APK.

Certificate Transparency Intermediate

An SCT-verifying interceptor that rejects certificates with no valid transparency proof — a stricter check than plain pinning.

Custom HostnameVerifier Intermediate

A hand-rolled HostnameVerifier plus a transparency check — defeated by forcing the verifier's result instead of swapping a trust store.

Split / AAB Advanced

An App-Bundle-shaped install: base APK plus per-ABI split configs, native libs not extracted to disk. The hardest delivery shape.

WebView Expert

A WebView that pins by overriding onReceivedSslError. The toughest surface — push the tool and your static reading here.

Download links resolve once the app files are published to the community storage. If a link 404s, the range isn't published yet.

MultiPin — the grand tour

MultiPin pins the same request across seven network stacks. Beat one, you've learned that stack; beat them all, you understand the whole Android TLS surface. Each stack is a “flavour” you can trigger independently.

Part 1 — Static free, no device

Scan it

rutken multipin-debug.apk rules

Rutken reports the pinning findings and names the stack behind each. On the release build the class names are stripped by R8 — Rutken matches the pinning code by its shape, not its name, so the findings still land.

Read the app

rutken multipin-debug.apk info
rutken multipin-debug.apk concepts --list     # the catalog of security concepts
rutken multipin-debug.apk concepts            # where each concept appears in THIS app

Note the application id from info, then echo <id> > owned.txt.

Part 2 — Dynamic paid tier, device required

The debug build is the friendliest first run. The command below embeds Rutken's own agent into the app, re-signs it with your practice key, installs it, and drives the pinning-bypass hooks so the app trusts the capture proxy. Rutken auto-selects the right agent for your device's CPU, so you don't pass one.

Capture

rutken multipin-debug.apk capture --embed-trust \
  --device <adb-serial> \
  --owned-allowlist owned.txt \
  --keystore practice.p12 --key-alias practice \
  --duration 90 \
  --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"

Find <adb-serial> with rutken devices (or adb devices). While it runs, exercise the app's flavours; Rutken records each decrypted flow.

Beat the obfuscated release

The release build strips names and specializes method signatures, so the OkHttp pinner can't be found by name. Add the derived-bypass flags: Rutken finds the pinner structurally and neutralizes it, and because this modifies the app's behaviour it requires the active-modification phrase too.

rutken multipin-release.apk capture --embed-trust \
  --device <adb-serial> --owned-allowlist owned.txt \
  --keystore practice.p12 --key-alias practice --duration 90 \
  --embed-derived --embed-force-void-cert-pinner \
  --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"

Confirm the win

The captured flows are plaintext HTTP over what the app believed was a pinned TLS connection. The response body carries the fake API key. Seeing it means every pin you triggered is defeated.

Prefer not to re-sign? The pinning bypass reference covers the alternate route (patch the app to trust user CAs and run a proxy), and Dynamic capture covers routes, agent delivery, and teardown in full.

Flutter (dart:io)

Flutter apps pin below the Java layer, inside the engine's own TLS (BoringSSL), and ship their native libraries mapped directly from the APK rather than unpacked to disk. That combination defeats naive Java-only tools. Rutken locates the in-APK library and hooks the certificate callback.

# Static: confirm it's a Flutter app and see the pinning surface
rutken flutterpin-debug.apk rules
rutken flutterpin-debug.apk info

# Dynamic: same embed-trust capture as MultiPin
echo <application.id> > owned.txt
rutken flutterpin-debug.apk capture --embed-trust \
  --device <adb-serial> --owned-allowlist owned.txt \
  --keystore practice.p12 --key-alias practice --duration 90 \
  --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"

The app sends two probes — one it expects to succeed and one it expects the pin to block. When the capture is working, both come back 200: the pin is bypassed.

Certificate Transparency

This range doesn't just pin a key — it demands a valid Signed Certificate Timestamp, rejecting any certificate without a transparency proof. Rutken defeats it by forcing the interceptor's verification result rather than trying to forge an SCT.

rutken ctfail-debug.apk rules          # flags the CT interceptor
rutken ctfail-debug.apk info
echo <application.id> > owned.txt

rutken ctfail-debug.apk capture --embed-trust \
  --device <adb-serial> --owned-allowlist owned.txt \
  --keystore practice.p12 --key-alias practice --duration 90 \
  --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"

Try the ctfail-release.apk next — same procedure, names stripped.

Custom HostnameVerifier

Here the app rolls its own HostnameVerifier and layers a transparency check on top. The lesson: you don't always swap a trust store — sometimes the cleanest bypass is to force a single verification method to return the answer you want. Rutken does exactly that, even when the verifier class is loaded lazily or renamed by R8.

rutken hvct-debug.apk rules
rutken hvct-debug.apk info
echo <application.id> > owned.txt

rutken hvct-debug.apk capture --embed-trust \
  --device <adb-serial> --owned-allowlist owned.txt \
  --keystore practice.p12 --key-alias practice --duration 90 \
  --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"

Split / AAB delivery

Modern apps ship as an App Bundle: a base APK carrying the code plus per-ABI and per-density split APKs, installed together, with native libraries left compressed inside the base rather than extracted to disk. This is the hardest delivery shape — a tool has to re-sign the whole set consistently and find libraries that were never written to the filesystem. Scan the base APK (it holds the code):

# The download is a zip of base.apk + split_config.*.apk. Scan the base:
rutken base.apk rules
rutken base.apk info
echo <application.id> > owned.txt

The capture flow is the same --embed-trust procedure, pointed at the base APK; Rutken handles the split re-sign and the in-base native libraries. See Dynamic capture for the split specifics.

WebView Expert

The WebView range pins by overriding onReceivedSslError on a WebViewClient and refusing to proceed on a bad certificate. It's the toughest range in the set and the best test of your static reading: scan it, find the overriding client, and understand exactly where the decision is made.

rutken webviewpin-debug.apk rules
rutken webviewpin-debug.apk concepts
rutken webviewpin-debug.apk info

Use this range to practice reading an app precisely before you reach for dynamic tooling — a skill that pays off on every hard target.

Where to next