Hitchhiker's Guide — Android Assessment and Hardening
The operational companion to the Phase 04 WARMUP. The WARMUP builds the Android trust model (sandbox, Binder, components, WebView, Keystore, JNI); this guide is the how — the authorized assessment workflow, the commands, the two-application range, cross-layer tracing, and the per-surface hardening and evidence. Use Frida and adb only against the owned lab app; never build reusable bypass scripts for third-party apps.
Table of Contents
- Authorized Workflow
- Build the Two-Application Range
- Useful Commands
- Trace a Cross-Layer Action
- Attack Fixtures
- Binder Service Review
- WebView Experiment
- Native (JNI) Campaign
- Hardening Checklist
- Release and Runtime Evidence
- Worked Examples
- Interview Drills
- References
Authorized Workflow
- Build the included vulnerable app and local API with synthetic accounts.
- Record APK hash, version, signing certificate, device/emulator version, and scope (Phase 00).
- Map manifest components and network endpoints.
- Review JADX output as an approximation — confirm behavior dynamically and against source (decompiled code is not source; Phase 04 WARMUP, Chapter 4).
- Use adb/Frida only on the owned app to observe calls and state.
- Patch source/manifest/backend, add instrumentation/API tests, and retest.
Build the Two-Application Range
The key methodological point: test exported surfaces from a real unrelated app, not from adb shell (which has a special identity). Stand up:
- a vulnerable target app;
- an unrelated caller app signed with a different key (no shared UID/signature permission);
- a local API with cross-account authorization fixtures.
The caller exercises exported components, URI grants, deep links, and Binder under an ordinary app
UID — proving (or disproving) caller validation. Maintain vulnerable and hardened product
flavors so every proof has a source diff and an instrumentation regression.
Useful Commands
adb shell pm path com.example.lab # locate the installed APK
adb shell dumpsys package com.example.lab # components, permissions, exported flags
apkanalyzer manifest print app-debug.apk # manifest without unpacking
jadx -d out app-debug.apk # decompile DEX (approximation)
apktool d app-debug.apk -o decoded # unpack resources + smali
# Dynamic (owned app only):
frida -U -n com.example.lab -l observe.js # observe method args / cert validation
Never interpret tool output as exploitability. For each finding trace: caller → input → code path → privilege → data → backend enforcement. Exported is a surface; a finding needs reachable impact (Phase 04 WARMUP, Chapter 6).
Trace a Cross-Layer Action
external URI/intent → component resolution → caller/input validation
→ local state or Keystore → HTTP request/token → backend object authorization
→ audit event and user result
Test every boundary. Hiding an object ID in the app does not protect the API (backend must authorize). Backend authorization does not excuse an exported component leaking local files. The bug often lives at the seam where one layer assumes another validated.
Attack Fixtures
- an exported service called by the unrelated caller app;
- a deep link carrying another tenant's object ID (payment-link abuse);
- a content-provider query with path/selection edge cases (traversal, SQL injection);
- WebView navigation to an untrusted local fixture (bridge reach);
- a synthetic secret in logs/backups/screenshots/clipboard;
- proxy observation of API traffic in the emulator;
- malformed JNI parser inputs under sanitizer/fuzzer.
Binder Service Review
Inspect: AIDL/Parcel schemas, thread safety, caller identity (Binder.getCallingUid() — never
trust a UID in the payload), permission checks, clearCallingIdentity windows (confused-deputy
risk), callback registration, death recipients, and deferred work. Add tests from the unrelated
app for every method and verify denial under ordinary app UIDs (Phase 04 WARMUP, Chapter 5).
WebView Experiment
Host controlled pages on two local origins. Test: navigation, redirect, popup, mixed content, the
JavaScript bridge (addJavascriptInterface), file chooser, download, custom scheme, and intent-URL
behavior. Harden with: origin allowlists, browser handoff for untrusted links, disabled unnecessary
file/content access, minimal JavaScript, and no privileged bridge for externally-influenced
pages (Phase 04 WARMUP, Chapter 8).
Native (JNI) Campaign
Extract the parser logic from JNI so it runs under ASan/UBSan/libFuzzer on the host (fast, clean attribution — Phase 01 discipline). Then:
- test the thin JNI adapter separately for array/reference/exception handling;
- build every shipped ABI (arm64-v8a, etc.); preserve symbols outside the APK;
- minimize faults, patch the invariant (not the crashing input), and run the corpus against both host and Android builds;
- triage to root cause without overstating impact (Phase 04 WARMUP, Chapter 12).
Hardening Checklist
android:exported="false"unless cross-app use is genuinely required;- signature permission plus runtime caller validation for trusted peers;
- explicit intents and immutable PendingIntents (Phase 04 WARMUP, Chapter 7);
- network security config with cleartext disabled; pinning with a rotation plan;
- scoped storage and explicit backup-extraction rules (
fullBackupContent/dataExtractionRules); FLAG_SECUREon sensitive screens; keep secrets off the clipboard;- backend object/function/tenant authorization (the real boundary, Phase 04 WARMUP, Chapter 10);
- release build non-debuggable and signed through a protected pipeline (Phase 03).
Release and Runtime Evidence
- signing lineage and release-key custody;
- debug/release manifest and configuration diff;
- component/Binder negative tests from another UID;
- API tenant-authorization tests;
- native sanitizer/fuzz report per ABI;
- proof synthetic secrets avoid logs, backups, screenshots, clipboard, and crash telemetry;
- update, logout, lost-device, token-compromise, and key-rotation runbooks.
Worked Examples
Reading dumpsys package for exposure
adb shell dumpsys package com.example.lab | sed -n '/Activity Resolver/,/Service Resolver/p'
com.example.lab/.PaymentActivity filter ...
Action: "android.intent.action.VIEW"
Scheme: "myapp" ← custom scheme, claimable by ANY app (no ownership proof)
exported=true ← reachable by other apps
permission=null ← NO permission guard
Verdict: PaymentActivity is an exported entry point with no guard, reachable via a hijackable
custom scheme. That's a surface; whether it's a finding depends on reachable impact (Phase 04
WARMUP, Chapter 6) — so trace what it does.
A deep-link abuse case and its fix
# From the unrelated caller app (different signing key), not adb:
startActivity(Intent(ACTION_VIEW, Uri.parse("myapp://pay?to=attacker&amount=5000")))
→ PaymentActivity reads to/amount from the Uri and pre-fills a transfer ← BUG
Three independent fixes (defense in depth):
- Origin proof: replace the custom scheme with Android App Links (
https://+ a verifiedassetlinks.jsonon your domain) so only your verified app handles the link. - No silent action: the deep link may navigate, never initiate a payment without in-app confirmation and server-side authorization tied to the authenticated user/tenant.
- Input validation: treat every Uri parameter as untrusted (Phase 04 WARMUP, Chapter 7).
Trusting the kernel-provided caller UID in a Binder service
override fun doPrivileged(req: Request) {
val uid = Binder.getCallingUid() // kernel-provided — trustworthy
if (!allowedUids.contains(uid)) throw SecurityException() // validate BEFORE acting
// NEVER: if (req.claimedUid in allowedUids) ← payload is attacker-controlled
}
Interview Drills
- "Review
myapp://pay?to=X&amount=Y." It's an exported entry point taking attacker-controlled params; custom schemes are claimable by any app. Fix: App Links for origin proof, no payment without in-app confirm + server-side authZ, validate all params. (Worked above.) - "Secure a content provider." Default
exported=false; if shared, require a permission + per-URI grants; parameterize all SQL (no string-built selections); canonicalize/validate paths inopenFile(traversal); scope returned rows to the caller's authZ; add a negative test from another UID. - "Explain Binder caller identity." The kernel records the caller's UID/PID; the service reads it
via
getCallingUid()and validates before privileged work — never trust a UID in the payload. WatchclearCallingIdentity: privileged work on attacker input while running as self is a confused deputy. - "Why aren't pinning and root detection authorization boundaries?" Both run on the client, which the attacker controls (root/Frida can skip them). Pinning protects the network; root detection is a speed bump. The boundary is server-side authorization that doesn't trust the client.
- "Where do you store an auth token on Android?" Prefer not storing it (online-only); else
server-held; else encrypted via the Keystore (key in TEE/StrongBox, optionally user-auth-bound).
And secure the whole lifecycle — keep it out of logs, backups (
dataExtractionRules), screenshots (FLAG_SECURE), and the clipboard, or the encryption is moot.
References
- Android Developers — App security best practices; AOSP — Application Sandbox, Verified Boot, SELinux, Binder/AIDL, Keystore/key attestation.
- OWASP MASVS and MASTG (mobile verification standard and testing guide).
- JADX, apktool, MobSF, Frida, Ghidra documentation; Android Security Bulletins.
- "Stagefright" (Joshua Drake) — Android native media-parser case study.
- Phase 01 references (sanitizers, libFuzzer, ABI) for the native campaign.