Warmup Guide — Android Trust Boundaries from First Principles
Zero-to-expert primer for Phase 04. It builds the Android security model from the ground up — the boot chain and Verified Boot, the UID sandbox, the ART runtime and APK format, Binder IPC and caller identity, the four component types and exported surfaces, intents and deep links, WebView bridges, storage and the hardware-backed Keystore, SELinux, and the JNI/native boundary. Assumes only basic Android/Java/Kotlin exposure. By the end you should be able to trace an intent from manifest resolution to a receiving component, decide where data may be stored, and explain why certificate pinning and root detection are not authorization boundaries.
Table of Contents
- Chapter 1: Why Android Is a Federation of Distrusting Parties
- Chapter 2: The Boot Chain and Verified Boot
- Chapter 3: The App Sandbox — UID Isolation
- Chapter 4: APK/AAB, Zygote, and the ART Runtime
- Chapter 5: Binder IPC and Caller Identity
- Chapter 6: The Four Components and Exported Surfaces
- Chapter 7: Intents, Pending Intents, and Deep Links
- Chapter 8: WebView — The Hybrid Attack Surface
- Chapter 9: Storage, Keystore, and Hardware-Backed Keys
- Chapter 10: Network Security, Pinning, and Why It Isn't AuthZ
- Chapter 11: Permissions and SELinux on Android
- Chapter 12: JNI and the Native Boundary
- Lab Walkthrough Guidance
- Success Criteria
- Common Mistakes
- Interview Q&A
- References
Chapter 1: Why Android Is a Federation of Distrusting Parties
Zero background. Android's security model starts from one assumption: apps do not trust each other, and the system does not trust apps. A phone runs code from hundreds of developers with no relationship; the OS must let them coexist on one device, sharing hardware, without one app reading another's data or impersonating the user to a third. This is fundamentally a multi-tenant isolation problem — the same shape as cloud multi-tenancy (Phase 07), but on a device the user controls and attackers can physically hold.
The layered trust model — the map for this whole phase:
hardware root of trust → bootloader → verified boot → kernel + SELinux
→ system_server / Binder → app sandbox (per-UID) → app process (ART + native)
Each arrow is a trust transition: a boundary where a less-trusted layer asks a more-trusted layer to do something. Vulnerabilities live at these transitions — an exported component that trusts its caller, a Binder service that doesn't check the calling UID, a JNI function that trusts a length from Kotlin. The entire assessment methodology is "enumerate the transitions and check what each side validates."
Who the parties are (you must always name them in a finding): the app developer, the platform (AOSP/Google), the OEM (device maker), the backend the app talks to, and the user. A real report assigns each finding to the responsible party — "the app fails to validate" vs. "the platform API is misleading" vs. "the backend lacks authorization" are different bugs with different owners.
Misconception to kill now. "Mobile security is about the app." Half of mobile findings are backend authorization bugs reached through the app, or platform trust-boundary misunderstandings. The device is just the client; the trust transitions span app, OS, and server.
Chapter 2: The Boot Chain and Verified Boot
The chain of trust. Security must start somewhere unforgeable. Android anchors it in a hardware root of trust — keys fused into the SoC. Each stage cryptographically verifies the next before executing it: the boot ROM verifies the bootloader, which verifies the boot image, which (via Android Verified Boot, AVB) verifies the system/vendor partitions using a signed hash tree (dm-verity). If any stage's signature or hash doesn't match, boot halts or enters a warning/limited state.
Why this matters for an app security engineer. Verified Boot is what lets higher layers assume the kernel and system image are genuine. It also underpins key attestation (Chapter 9): the device can prove to a backend that it booted a verified OS with a locked bootloader. But — and this is the limit — Verified Boot protects integrity at boot; it does not protect a running app from a user with a rooted device, and it is not a substitute for backend authorization.
Rollback protection. AVB also prevents an attacker from flashing an older, vulnerable signed image (downgrade attack) using rollback indices. This is the same anti-replay logic you saw with tokens in Phase 02 — old-but-validly-signed is still an attack.
Misconception to kill now. "Verified Boot means the device is trustworthy." It means the boot software is genuine on a locked device. An unlocked/rooted device, or post-boot compromise, breaks the assumption — which is exactly why server-side authorization must never rely on client-side integrity alone.
Chapter 3: The App Sandbox — UID Isolation
The core mechanism. Android reuses the Linux kernel's user-ID isolation in a clever way: each installed app gets its own UID, and its files live in a private directory owned by that UID. The kernel's standard file-permission enforcement then automatically prevents app A (UID 10123) from reading app B's (UID 10124) data. The sandbox isn't a new mechanism — it's the Unix permission model applied per-app.
What the sandbox gives you for free, and what it doesn't. Inside its sandbox an app's private storage, memory, and process are isolated by the kernel. But the moment the app talks to anyone — another app via intents/Binder, the system via permissions, the network via sockets — it crosses a boundary the UID sandbox doesn't govern. Those crossings are where you focus.
SELinux on top (preview of Chapter 11). UID isolation is discretionary; Android adds mandatory SELinux policy so that even a process running as root is confined to a domain with a fixed allowlist of what it may do. Defense in depth: UID isolation plus SELinux domains.
Misconception to kill now. "The app is sandboxed, so its data is safe." The sandbox protects data at rest from other apps. It does nothing about data the app itself leaks — into logs, backups, screenshots, the clipboard, or an exported component (Chapters 6, 9).
Chapter 4: APK/AAB, Zygote, and the ART Runtime
APK/AAB — what an app actually is. An APK is a ZIP containing: AndroidManifest.xml (the
app's declaration of components, permissions, and exported surfaces — your primary review target),
compiled code as DEX bytecode (classes.dex), native libraries (lib/), resources, and a
signing block. An AAB (Android App Bundle) is the upload format Google Play splits into
optimized APKs. App signing binds the APK to the developer's key; updates must be signed by the
same key (or via Play App Signing), which is how the OS knows an update is from the same author.
Decompilation vs. source — a critical distinction. Tools like JADX decompile DEX back to readable Java/Kotlin-ish code; apktool unpacks resources and the manifest. This is recovered code, not the original source — naming, structure, and some semantics are inferred. Treating decompiled output as ground truth is a classic mistake (the README flags it). Always corroborate with dynamic analysis.
Zygote — why apps start fast and share an identity model. Android pre-initializes a process called Zygote with the runtime and common libraries loaded. Every new app process is forked from Zygote, inheriting the warm runtime (fast start) via copy-on-write. Security-relevant point: because apps fork from a shared parent, the isolation between them comes from the per-UID assignment at fork time, not from separate runtimes.
ART — the runtime. The Android Runtime executes DEX bytecode, using a mix of ahead-of-time (AOT) compilation at install and just-in-time (JIT) plus profile-guided compilation at runtime. For security you care that ART enforces memory safety for managed code (like the JVM) — which is exactly why the dangerous memory bugs concentrate in the native (JNI) layer (Chapter 12).
Misconception to kill now. "The decompiler shows me the real code, so I found the secret." You found a string in recovered bytecode. Embedded secrets are a finding because they're extractable — but also confirm via runtime behavior, and remember: any secret shipped in an APK is already compromised.
Chapter 5: Binder IPC and Caller Identity
Why Binder exists. Apps are isolated processes (Chapter 3), but they constantly need to call the system and each other (get location, send an intent, bind a service). Binder is Android's high-performance inter-process communication (IPC) mechanism — a kernel driver that passes transactions (method calls with serialized arguments, "parcels") between processes and, crucially, tells the callee who the caller is.
Caller identity — the security heart of Binder. When process A makes a Binder call into service
B, the kernel records A's UID and PID, and B can retrieve them (Binder.getCallingUid()). This
is trustworthy identity provided by the kernel — B does not have to trust a UID that A claims
in the payload; it asks the kernel. The recurring bug is a Binder/AIDL service that performs a
privileged action without checking the calling UID/permission — it implicitly trusts anyone who
can reach it. (AIDL — Android Interface Definition Language — is the code generator that turns a
declared interface into the Binder marshalling boilerplate; an "AIDL service" is just a Binder
service with a typed method contract.) Your Binder lab makes you build an AIDL interface that
validates the caller before acting.
clearCallingIdentity — the confused-deputy trap. A service can temporarily drop the caller's
identity and act as itself (to access its own resources). If it does privileged work in that window
on behalf of attacker-controlled input without re-checking authorization, it becomes a confused
deputy — the same class as Phase 02's "preserve the initiating subject downstream."
Misconception to kill now. "The caller passed me their UID in the request." Never trust an identity in the payload; trust the kernel-provided calling UID. Anything in the parcel is attacker-controlled.
Chapter 6: The Four Components and Exported Surfaces
The four Android components — each a possible entry point:
- Activity — a screen/UI entry point.
- Service — background work (and bound services expose an IPC interface).
- Broadcast Receiver — responds to system/app-wide events.
- Content Provider — a structured data interface (often SQL-backed) other apps can query.
Exported = reachable by other apps. A component is exported if other apps can invoke it.
Export happens either explicitly (android:exported="true") or implicitly (declaring an
intent-filter historically auto-exports — modern Android requires explicit exported). Every
exported component is an entry point into your app from untrusted callers, so each one needs a
caller-validation decision: who may invoke it, and does it validate inputs and permissions?
The manifest-analysis discipline (your flagship lab). The manifest declares every component and
its export state, permissions, and intent-filters. The Manifest Exposure Analyzer lab makes
you build the tool that enumerates components and flags those that are exported without an
adequate permission or validation — producing a component access matrix with a decision per
component and negative tests proving unexported components reject external callers.
Content providers — special danger. A content provider over a SQL store can suffer SQL
injection through its query selection, and path traversal through openFile. Exported
providers also leak data. The README's "secure a content provider" interview question is about
exactly this: lock export, require permissions, parameterize queries, and validate URIs/paths.
The "exported but no impact" nuance. Reporting every exported component as a vulnerability is a common mistake — exported is a surface, not automatically a bug. A finding needs reachable impact: what can an attacker actually do by invoking it? Exported + does-something-sensitive + no-validation = finding.
Chapter 7: Intents, Pending Intents, and Deep Links
Intents — the messaging primitive. An Intent is a message asking the system to start an activity, a service, or deliver a broadcast. Explicit intents name the exact target component (safe). Implicit intents name an action and let the system pick a handler (dangerous if a malicious app registers to handle it, or if sensitive data rides in an implicit intent that any app can receive).
The rules: use explicit intents for anything sensitive; never put secrets in implicit intents; validate everything received in an intent (it's attacker-controlled input).
Pending intents — a delegated capability. A PendingIntent is a token that lets another app (or the system) execute an intent as your app, with your identity and permissions. It is a capability you hand out. If you create a mutable PendingIntent with an implicit base intent, a malicious app can fill in the blanks and redirect it — hijacking your app's authority. The fix: make PendingIntents immutable and base them on explicit intents. This is a delegation bug exactly analogous to OAuth's confused-deputy concerns.
Deep links and App Links — origin and parameter trust. A deep link is a URL that opens a
specific screen in your app (myapp://pay?to=...). A deep-link handler is an exported entry
point that takes attacker-controlled parameters — so it must validate origin, tenant, and every
parameter. Plain custom-scheme deep links can be claimed by any app (no ownership proof);
Android App Links (verified https:// links with a assetlinks.json digital-asset-links file
on your domain) cryptographically prove your app owns the domain, preventing link hijacking. The
Deep-Link Validator lab makes you enforce origin/tenant/parameter checks — e.g. a payment deep
link must not let an attacker set an arbitrary recipient or cross tenants.
Misconception to kill now. "Deep links are just navigation." A deep link is untrusted input from outside the app that can trigger actions; treat its parameters with the same suspicion as an HTTP request body.
Chapter 8: WebView — The Hybrid Attack Surface
What WebView is. A WebView embeds a browser engine inside your native app to render web content. It creates a hybrid boundary where web risks (XSS) and native risks (access to app capabilities) meet — the worst of both worlds if misconfigured.
The three danger knobs:
addJavascriptInterface(the JS bridge). Exposes a native object to JavaScript running in the WebView. If the WebView loads any untrusted content (a remote page, an attacker-influenced URL), that JavaScript can call your native bridge methods — turning web XSS into native code execution / data access. Only bridge to fully trusted content; constrain the exposed API.- Navigation control. If the WebView will follow arbitrary links, attacker content loads inside your trusted app context. Use an allowlist of permitted origins and hand off everything else to the real browser (so untrusted pages don't run in your bridge context).
- Content access / file access.
setAllowFileAccess,loadUrl("file://…"), andfile://cross-origin settings can let web content read local files. Disable what you don't need.
The WebView threat-model question (a README interview item): trace what happens if the loaded page is attacker-controlled — can it reach the JS bridge, navigate to local files, or exfiltrate app data? The defense is isolation: trusted content + minimal bridge + origin allowlist + browser handoff.
Misconception to kill now. "It's just rendering our own web page." If the page pulls third-party scripts, follows redirects, or is reachable via an attacker-influenced URL, "our page" can execute attacker JavaScript against your native bridge.
Chapter 9: Storage, Keystore, and Hardware-Backed Keys
The storage decision — the core skill. For any piece of data, choose the minimum-exposure option, in order of preference:
- Don't store it. The most secure data is data you don't keep on the device.
- Keep it server-side, fetched on demand with authorization (online-only actions).
- Encrypt it at rest with a key in the Android Keystore.
- Plaintext in private storage — only for non-sensitive data, and even then mind the leak surfaces below.
The Keystore and hardware-backed keys. The Android Keystore stores cryptographic keys such that the key material never enters your app's process — you ask the Keystore to encrypt/sign, and on devices with a hardware security module / Trusted Execution Environment (TEE) or StrongBox, the key lives in hardware and can be bound to user authentication (require a fingerprint/PIN to use the key) and to the device via key attestation (the backend verifies the key is hardware-backed on a verified-boot device). This is the same KMS/HSM idea from Phase 02, realized on-device.
The leak surfaces that bypass storage security entirely (the Mobile-Data-Policy lab):
- Logs (
Logcat) — secrets logged in debug builds are readable; never log sensitive data. - Backups (
allowBackup) — auto-backup can copy private data off device; exclude sensitive files / disable for sensitive apps. - Screenshots / recents — the OS snapshots your UI; set
FLAG_SECUREon sensitive screens. - Clipboard — copied secrets are readable by other apps; avoid putting secrets on the clipboard.
The lab makes you write a policy covering offline storage, backup exclusion, screenshot protection, clipboard handling, Keystore use, retention, and which actions are online-only.
Misconception to kill now. "I encrypted the data, so it's safe." If the same secret is logged, backed up, screenshotted, or copied to the clipboard, the encryption is irrelevant. Secure the whole lifecycle, not just the database.
Chapter 10: Network Security, Pinning, and Why It Isn't AuthZ
Network Security Configuration. Android lets you declare TLS trust policy in
network_security_config.xml: which CAs to trust, whether cleartext is allowed, and pinning. The
baseline rules: no cleartext traffic, trust only the system CA set (don't trust user-added CAs in
production), and validate certificates properly.
Certificate pinning — what it does. Pinning hardcodes the expected server certificate/public key so the app rejects even a validly-signed cert from a different (e.g. attacker-controlled or mis-issued) CA. It raises the bar against TLS interception. But it comes with an operational cost: rotation. If you pin and the server key rotates without an app update, the app breaks — so pinning needs a rotation plan (backup pins, overlap).
Why pinning and root detection are NOT authorization boundaries — the load-bearing point of this chapter. Pinning and root/anti-tamper detection run on the client, which the attacker controls. On a rooted device or with dynamic instrumentation — most famously Frida, a toolkit that injects a scripting engine into a running process so an analyst can hook, inspect, and rewrite any function call at runtime (e.g. patch the pinning or root-check method to always return "safe") — the client can be made to skip the check. Therefore:
- Pinning protects against network interception; it does not prove the request is authorized — the server must still authorize every request (Phase 02 object-level authZ).
- Root detection is a speed bump that raises analysis cost; it does not prevent analysis and must never be the thing standing between an attacker and your data.
The security decisions that matter are server-side. Client-side checks are defense-in-depth, not boundaries. (The README's "explain why pinning and root detection are not authorization boundaries" is a direct test of this.)
Misconception to kill now. "We pin and detect root, so the API is protected." Both are client-side and bypassable; the API is protected only by server-side authorization that doesn't trust the client.
Chapter 11: Permissions and SELinux on Android
The permission model. Apps declare permissions in the manifest; dangerous (runtime) permissions (location, camera, contacts) must be granted by the user at runtime and can be revoked. Permissions gate access to sensitive APIs and to permission-protected components. Design principle: request the minimum permissions, and protect your own exported components with custom permissions where appropriate.
SELinux — mandatory access control under everything. Beyond UID isolation, Android runs SELinux in enforcing mode: every process runs in a domain, every object has a type, and a central policy specifies exactly which domain may do what to which type. The point: even a process that gains root is still confined to its domain's allowlist — root is no longer omnipotent. This is the same mandatory-access-control idea you'll study deeply in Phase 05/06. For an app reviewer, SELinux explains why certain exploits that would work on a generic Linux box are contained on Android, and why platform-level findings must reason about the relevant domain's policy.
The interaction you must be able to explain (a README evaluation item): UID isolation gives per-app discretionary separation; Binder provides authenticated cross-process calls; SELinux adds mandatory domain confinement on top. A privileged action is gated by all three.
Misconception to kill now. "Root = game over." On modern Android, root is confined by SELinux domains and Verified Boot; many capabilities still require the right domain and an unlocked bootloader. Defense in depth changes the calculus.
Chapter 12: JNI and the Native Boundary
Why native code reintroduces memory bugs. Managed Kotlin/Java in ART is memory-safe. But apps use JNI (Java Native Interface) to call into C/C++ for performance (codecs, crypto, parsers) — and that native code has all the memory-safety hazards of Phase 01 (OOB, UAF, integer overflow). The most security-critical native code is media/file parsers, historically the source of major Android remote bugs (Stagefright).
The trust boundary at the JNI seam. When Kotlin calls a native function passing a buffer and a
length, the native side must not blindly trust that length or assume the buffer is the size
claimed — exactly the Phase 01 parser discipline (length <= remaining). Conversely, native code
returning data to Kotlin must produce well-formed results. The Kotlin/JNI Native Boundary lab
makes you build a Kotlin→C++ parser boundary, fuzz it under sanitizers, find a crash, minimize it,
fix it safely, and add a regression corpus — the full Phase 01 triage loop applied to Android.
Triage discipline carries over. A JNI crash is crash → reachability → controllability → boundary → impact. Don't overstate impact (a null-deref that needs a local malicious app is not a
remote RCE). The README explicitly tests "triage a JNI crash and assess reachability without
overstating impact."
Misconception to kill now. "Kotlin is memory-safe, so the app is memory-safe." The native libraries the app ships and calls via JNI are C/C++ and carry the full memory-corruption risk; ABI mismatches and trusted lengths at the boundary are where it breaks.
Lab Walkthrough Guidance
Build the labs in this order; each maps to a chapter:
- Lab 01 — Manifest Exposure Analyzer. Enumerate components and flag exported-without-guard surfaces (Chapter 6); produce a component access matrix with negative tests.
- Lab 02 — Deep-Link Security Validator. Enforce origin/tenant/parameter validation on deep links (Chapter 7); break a payment-link abuse case, then fix it.
- Lab 03 — Mobile Data Protection Policy. Encode the storage decision plus log/backup/ screenshot/clipboard/Keystore/retention/online-only policy (Chapter 9).
- Lab 04 — Kotlin/JNI Native Boundary. Fuzz the native parser under sanitizers, minimize a crash, fix it, add a regression corpus (Chapters 1, 12).
Across the phase, maintain vulnerable and hardened variants so every issue has a reviewable fix
and regression test. Use Frida only against the owned lab app. Run:
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v
# JNI: build with clang sanitizers and run the fuzz smoke target
Success Criteria
You are ready for Phase 05 when you can, without notes:
- Explain Android's UID sandbox, Binder caller identity, and SELinux domains, and how they interact to gate a privileged action.
- Trace an intent from manifest resolution to the receiving component's input validation.
- Decide, for a given datum, between Keystore-protected, encrypted, server-held, and not-stored.
- Explain why certificate pinning and root detection are not authorization boundaries.
- Identify an exported component with reachable impact (vs. merely exported) and write the fix.
- Explain why a mutable, implicit PendingIntent is a delegated-capability hijack.
- Triage a JNI crash through reachability and controllability without overstating impact.
Common Mistakes
- Reporting exported components without demonstrating reachable impact.
- Treating decompiled output as original source.
- Embedding secrets in the APK ("it's obfuscated" is not protection).
- Disabling TLS validation as a test shortcut (and leaving it disabled).
- Claiming root detection or pinning prevents analysis or authorizes requests.
- Ignoring backend authorization and blaming only the app.
- Trusting a UID claimed in a Binder payload instead of the kernel-provided calling UID.
Interview Q&A
Q: Review a payment deep link myapp://pay?to=X&amount=Y. What's wrong and how do you fix it?
A: The deep-link handler is an exported entry point taking attacker-controlled parameters, so a
malicious app or web page can launch it with an arbitrary recipient/amount, and a custom-scheme link
can be claimed by any app. Fixes: never let the deep link initiate a payment without in-app
confirmation and server-side authorization tied to the authenticated user/tenant; validate every
parameter; use verified Android App Links (assetlinks.json) so the domain ownership is proven; and
treat the parameters as untrusted input.
Q: How do you secure a content provider?
A: Default it to exported="false"; if it must be shared, require a permission and grant per-URI
access narrowly. Parameterize all SQL (no string-built selections) to prevent injection, validate
and canonicalize any path in openFile to prevent traversal, and scope returned columns/rows to the
caller's authorization. Add negative tests proving an unauthorized caller is rejected.
Q: Explain Binder caller identity.
A: When a process makes a Binder transaction into a service, the kernel records the caller's UID/PID,
which the service retrieves via Binder.getCallingUid(). This identity is kernel-provided and
trustworthy, so the service validates the caller against it before privileged work — it must never
trust a UID passed in the payload. Beware clearCallingIdentity: if the service does
attacker-influenced privileged work while running as itself, it's a confused deputy.
Q: Threat-model a WebView with a JavaScript bridge.
A: The risk is web content reaching native capabilities. If the WebView loads any untrusted or
redirectable content, XSS in that page can invoke the addJavascriptInterface bridge and act with
the app's authority. Mitigate by loading only fully trusted content, exposing the smallest possible
bridge API, allowlisting navigable origins and handing other links to the system browser, and
disabling file/content access that isn't needed.
Q: Compare app signing, Verified Boot, and hardware-backed keys. A: App signing binds an APK and its updates to a developer key (author continuity). Verified Boot ensures the OS/boot images are genuine and not downgraded on a locked device (platform integrity). Hardware-backed keys keep cryptographic material in the TEE/StrongBox, usable only via the Keystore and optionally bound to user auth, with attestation proving they're hardware-backed on a verified device. They protect different layers — author, platform, and key material respectively — and none replaces server-side authorization.
References
Official documentation
- Android Developers — Security & Privacy guidance; App security best practices.
- AOSP — Platform architecture, Application sandbox, Verified Boot, SELinux/SEAndroid, Binder/AIDL, Keystore and key attestation documentation.
- Android Security Bulletins (study patches/CVEs; reproduce only in owned builds).
Standards and methodology
- OWASP MASVS (Mobile Application Security Verification Standard) and MASTG (testing guide).
- NIST SP 800-163 (vetting mobile apps).
Tooling and research
- JADX, apktool, MobSF, Frida, Ghidra documentation.
- "Stagefright" (Joshua Drake) — Android media-parser memory-corruption case study.
- Nick Kralevich / AOSP talks on the Android sandbox and SELinux evolution.