Mobile Application Security
Definition
Section titled “Definition”Mobile application security covers the analysis and defense of Android and iOS applications and the devices that run them: the app layer (static and dynamic analysis of APKs/IPAs — hardcoded secrets, insecure storage, weak TLS, IPC exposure), the transport layer (certificate pinning and its failures, API abuse — H9’s cloud backend join), the device layer (device posture, MDM/Mtrom — MDM/MDM enrollment, containerization), and the program layer (MDM/MDR for the fleet, app-store review posture for what you publish, rogue-app monitoring for what impersonates you — BT18’s external side). The platform split matters: Android is open by design (sideloading, inspectable APKs, rooted-device analysis) — the analyst-friendly platform; iOS is closed (fair-play-encrypted IPAs, jailbreak-dependent instrumentation) — costlier analysis, historically better-controlled fleets. Both converge on the backend truth: the mobile app is a thin client over your API surface — everything it “protects” is enforced server-side or not at all, and mobile findings (keystore misuse, IPC components) layer on W15’s API fundamentals. The craft’s center: mobile security is app analysis plus fleet governance — the artifact you analyze and the thousand devices your employees carry are different problem domains that organizations routinely confuse.
Why It Matters
Section titled “Why It Matters”Mobile is where enterprise trust models strain hardest: BYOD devices carry corporate mail, MFA tokens, and VPN access; the apps embed credentials and expose backend APIs; and the platform threat models differ enough (Android’s sideload/supply-chain risk, iOS’s zero-day class [verify current landscape]) that a single “we have MDM” claim hides both. The attacker view is mature: repackaged apps with injected trackers, MFA-fatigue via stolen push tokens, overlay attacks on Android, clipboard scrapers, and the corporate API re-implemented in a forked client with pinning stripped. The defender’s job splits cleanly: secure the app you publish (secrets, storage, pinning, attestation), govern the devices that touch your data (posture, containerization, root/jailbreak detection), and watch the ecosystem that impersonates you (BT18’s rogue-app class) — three toolchains, often three owners, one blast radius.
The failure modes are platform-specific and recurring. Secrets in the binary: API keys, third-party tokens, and (still, regularly) backend credentials extractable with strings — the mobile app is distributed to every adversary who wants a copy; anything in it is public. Client-side trust: authorization checks, license validation, or anti-tamper enforced in app code — trivially patched in a repackaged APK; the server must own every decision. Pinning as identity: certificate pinning treated as authentication — pinning protects against MITM on the device’s traffic, not against an adversary running your app in their own emulator with a custom CA and a Frida hook [verify current instrumentation landscape]. Fleet theater: MDM enrolled, posture never checked — the rooted device receiving the M&A data room because “MDM covers us.” And store-page blindness: the published app’s permissions growing release-over-release until it reads everything a stalkerware would — the privacy-era reputational risk hiding inside a security checklist.
Detailed Explanation
Section titled “Detailed Explanation” THE MOBILE STACK (analyze top-down, govern bottom-up) +---------------------------------------------------+ | APP ARTIFACT (APK / IPA) | | static: manifest, secrets, exported components | | dynamic: runtime hooks, storage, traffic | +---------------------------------------------------+ | TRANSPORT / API pinning, attestation, mTLS | | (the server owns every decision — W15 join) | +---------------------------------------------------+ | DEVICE / OS posture, root/jailbreak, patch | | FLEET / MDM enrollment, containers, DLP | | ECOSYSTEM store pages, rogue apps (BT18) | +---------------------------------------------------+
ANDROID ANALYSIS PATH iOS ANALYSIS PATH apk -> apktool/jadx decode IPA -> decrypt (fair-play, manifest: permissions, exported jailbroken device [verify activities/services/receivers legality per region]) dex -> smali/java: secrets, class-dump/static: strings, crypto, endpoints plist, binary analysis dynamic: emulator+Frida hook dynamic: jailbreak + Frida storage: shared prefs, sqlite storage: keychain, plistStatic analysis starts with the manifest and ends at the backend URL. Android’s APK is a zip of dex bytecode and resources — apktool decodes resources and manifest, jadx produces readable Java; the checklist: declared permissions (over-privileged = the store-page risk), exported components (android:exported=true activities/services/receivers callable by other apps — the IPC exposure), android:allowBackup (local-data extraction), debuggable flags, network-security-config (cleartext permitted?), and the strings pass (URLs, keys, AKIA-prefixed cloud credentials — BT8’s statictriage patterns applied to mobile). iOS static work starts from the decrypted binary (fair-play encryption means App Store IPAs are encrypted on-disk; decryption requires a jailbroken device or legitimate entitlement paths [verify tooling legality per jurisdiction]) — plist files, strings, and class dumps.
Dynamic analysis needs instrumented runtimes: Android emulators or rooted devices with Frida (function hooking at runtime — strip pinning, dump crypto material, trace the API layer) and the storage checks (shared preferences with secrets, unencrypted SQLite, world-readable files). iOS dynamic work mirrors it on jailbroken hardware. The purpose discipline matters: dynamic analysis proves what the app does under control — it never substitutes for the server-side review, because every client-side finding (“the app checks the license!”) is a finding about the app, not about the system.
Fleet governance is the defense side: MDM/UEM enrollment with enforced posture (patch level, jailbreak/root detection, screen-lock, minimum OS), app protection policies (containerization — corporate data in a managed container with DLP: no copy-out, no unmanaged share-sheet), and per-app VPN so corporate traffic is observable. The attestation layer (Play Integrity / App Attest [verify current API names]) lets the backend verify the client is an unmodified app on a healthy device — the server-side control that makes client-side trust unnecessary.
Deep dive: why the app is a public artifact — and what that inverts
Every distribution channel (app stores, MDM, a colleague’s AirDrop) hands the complete binary to anyone who asks, which inverts the usual appsec assumption: there is no “attacker has a copy” phase — everyone has a copy, including the adversary, from day zero. Security models that assume binary secrecy (obscurity of endpoints, client-enforced entitlements, “they’d have to reverse it first”) are structurally broken on mobile before the first line of server code runs. The inversions worth internalizing: static secrets are published secrets (the binary is the leak), client-side authorization is suggestion (the server owns decisions or nothing does), and reverse-engineering cost is near zero (jadx renders your codebase readable in minutes) — so the honest mobile threat model assumes the adversary runs your app better than your users do: instrumented, scripted, and patient. Everything defensible lives server-side: attestation (is this the genuine binary on a healthy device?), rate and behavior analytics (is this session shaped like a human?), and authorization (does this principal own this data?). The app’s security job is data protection on the device and making abuse observable — not making abuse impossible, which it cannot.
Common Techniques
Section titled “Common Techniques”| Technique | What it does | Where it fails | Notes |
|---|---|---|---|
| Manifest review | Permissions, exported components | Ignoring IPC surface | Android-first check |
| Secrets extraction | Strings/keys in the binary | Assuming obscurity works | App = public artifact |
| Storage audit | Prefs/SQLite/keychain review | Device-encryption complacency | Data-at-rest class |
| Pinning validation | TLS pinning present + bypass-tested | Pinning as auth | Frida-class hooks |
| Repackaging tests | Tampered APK accepted by backend? | No attestation server-side | Play Integrity join |
| Posture enforcement | Root/jailbreak/patch gates | Enroll-only MDM | Fleet truth |
| Containerization | Corporate data in managed container | Copy-out paths open | DLP on device |
| Rogue-app monitoring | Store watch for impersonation | Volume across stores | BT18 join |
| Attestation integration | Server verifies client integrity | Trust-client-code habits | Backend control |
| Store-page hygiene | Permission minimization, changelogs | Permission creep | Privacy-era risk |
Full Working Code
Section titled “Full Working Code”mobitriage — a mobile artifact triage scanner: opens an APK (zip-based — stdlib parses it), reads AndroidManifest.xml (binary XML — decode the string pool and key tags without external deps), extracts DEX string candidates (URLs, cloud keys, endpoints), checks the classic static findings (debuggable, allowBackup, cleartext traffic, exported components where parseable), and scores the artifact. Pure stdlib, no apktool required — a first-pass scanner that tells you which artifact deserves the full jadx workup.
#!/usr/bin/env python3"""mobitriage — stdlib Android APK triage: manifest flags, dex strings.
Usage: python3 mobitriage.py app.apk [--json]Checks: debuggable, allowBackup, cleartext, exported components (AXML scan), hardcoded secrets in classes.dex (URLs, cloud keys, endpoints).Score >= 6 high-interest, 3-5 worth-review, < 3 likely-clean (static view)."""import argparseimport jsonimport reimport sysimport zipfilefrom pathlib import Path
HEX = set("0123456789abcdef")AKIA_RE = re.compile(rb"AKIA[0-9A-Z]{16}")URL_RE = re.compile(rb"https?://[\x20-\x7e]{6,120}")JWT_RE = re.compile(rb"eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}")PRIV_RE = re.compile( rb"android\.permission\.(READ_SMS|RECEIVE_SMS|READ_CONTACTS|" rb"ACCESS_FINE_LOCATION|RECORD_AUDIO|CAMERA|READ_CALL_LOG)")
def decode_axml_flags(data: bytes): """Scan compiled Android XML (AXML) for attribute values we care about. AXML strings live in a UTF-16/UTF-8 string pool; we scan for the attribute-name strings and report presence (crude but dependency-free).""" found = {} probes = { "debuggable": b"debuggable", "allowBackup": b"allowBackup", "usesCleartextTraffic": b"usesCleartextTraffic", "exported": b"exported", "networkSecurityConfig": b"networkSecurityConfig", } # string pools store UTF-16LE mostly; check both encodings for k, probe in probes.items(): found[k] = (probe in data) or (probe.decode().encode("utf-16-le") in data) return found
def dex_string_candidates(dex: bytes): """Pull printable ASCII runs >= 6 from the dex, then classify.""" runs = re.findall(rb"[\x20-\x7e]{6,}", dex) urls = set() keys = set() jwts = set() for r in runs: for m in URL_RE.findall(r): u = m.decode("ascii", "replace") host = u.split("//", 1)[-1].split("/", 1)[0] if host and "." in host and "schemas.android.com" not in host: urls.add(u[:100]) for m in AKIA_RE.findall(r): keys.add(("aws-access-key", m.decode()[:12] + "...")) for m in JWT_RE.findall(r): keys.add(("jwt-token", m.decode()[:24] + "...")) return sorted(urls)[:15], sorted(keys)[:10]
def main(): ap = argparse.ArgumentParser(description="Android APK triage") ap.add_argument("apk", type=Path) ap.add_argument("--json", action="store_true") args = ap.parse_args()
if not args.apk.exists(): sys.exit(f"fail-loud: apk not found: {args.apk}") if args.apk.suffix.lower() != ".apk": sys.exit("fail-loud: not an .apk — iOS analysis needs a different " "toolchain (see module)") try: zf = zipfile.ZipFile(args.apk) except zipfile.BadZipFile: sys.exit("fail-loud: not a valid zip/APK")
names = zf.namelist() findings, score = [], 0
manifest = None if "AndroidManifest.xml" in names: manifest = zf.read("AndroidManifest.xml") flags = decode_axml_flags(manifest) if flags["debuggable"]: score += 3 findings.append("debuggable=true — production apps must not ship " "debuggable (attach-a-debugger class)") if flags["allowBackup"]: score += 2 findings.append("allowBackup=true — local data extractable via " "adb backup class") if flags["usesCleartextTraffic"]: score += 2 findings.append("usesCleartextTraffic=true — cleartext HTTP " "permitted platform-wide") if not flags["networkSecurityConfig"]: findings.append("no networkSecurityConfig — pinning/TLS policy " "defaults only (review manually)") else: sys.exit("fail-loud: AndroidManifest.xml missing — not an APK?")
dex_names = [n for n in names if n.endswith(".dex")] if not dex_names: sys.exit("fail-loud: no classes.dex — packed/odd artifact; escalate " "to full toolchain (M14 packing analysis)") urls, keys = set(), set() for dn in dex_names: dex = zf.read(dn) u, k = dex_string_candidates(dex) urls |= set(u) keys |= set(k) if keys: score += 4 findings.append(f"hardcoded credentials: {keys}") if urls: score += 1 if len(urls) > 3 else 0 findings.append(f"endpoints ({len(urls)}): {sorted(urls)[:4]}")
# permissions scan on the raw manifest bytes (string-pool hits) perms = sorted({m.decode() for m in PRIV_RE.findall(manifest or b"")}) if perms: score += 1 findings.append(f"sensitive permissions: {perms}")
# native libs + asset inventory (packing/obfuscation hints) native = [n for n in names if n.startswith("lib/") and n.endswith(".so")] if len(native) > 12: findings.append(f"many native libs ({len(native)}) — possible " "protection/obfuscation (M14 join)")
verdict = ("high-interest" if score >= 6 else "worth-review" if score >= 3 else "likely-clean") out = {"file": args.apk.name, "size": args.apk.stat().st_size, "dex_count": len(dex_names), "native_libs": len(native), "score": score, "verdict": verdict, "findings": findings, "endpoints": sorted(urls), "credentials": sorted(map(str, keys))} if args.json: print(json.dumps(out, indent=2)) return print(f"apk : {args.apk.name} ({out['size']} bytes, " f"{len(dex_names)} dex, {len(native)} native libs)") print(f"score : {score} verdict: {verdict} (static view)") for f in findings: print(f" - {f}") print("next: jadx full decode for hits; server-side review for endpoints; " "backend must own every authorization decision")
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”The AXML flag scanner (decode_axml_flags, lines 28-42) is the honest trade: full binary-XML parsing is ~200 lines of format code; string-pool presence scanning is 10 lines and answers the triage question (“is debuggable mentioned at all?”) at the cost of false positives. For a scanner that ranks artifacts for deeper work, presence-scanning is the right economics — weak-signal findings say “review manually,” and the verdict is explicitly labeled “static view”.
The DEX candidate extraction (lines 45-61) runs printable-ASCII runs through three classifiers: URLs (filtered against schemas.android.com namespace URIs), AWS access keys (AKIA + 16 chars — the most common hardcoded-secret class in real APKs), and JWT literals (a JWT in the binary is a static credential wearing a token’s clothes). The permission scan (lines 116-119) hits the sensitive set (SMS, contacts, location, mic, camera, call log) — the store-page and stalkerware-adjacent risk class. The verdict bands (lines 128-129) rank for next action: high-interest goes to full jadx decode; the hardcoded-credential finding (+4) dominates because it’s immediately actionable. The missing-dex exit (line 96) routes packed artifacts to M14’s depth instead of pretending the scan covered them.
How It Reaches The Target
Section titled “How It Reaches The Target”The published app ships to every adversary simultaneously — so the analysis pipeline runs before release, in the mobile build’s CI: mobitriage gates each release candidate (credentials block unconditionally), the full toolchain runs on high-interest findings, and the server-side review owns the endpoint list the scanner extracts. Attestation closes the loop static analysis can’t: the backend verifies the client is the unmodified, released binary on a healthy device, making repackaging a server-visible event instead of a silent assumption.
The fleet side runs continuously: MDM posture gates (patch level, root/jailbreak, screen lock) evaluate at enrollment and continuously; the corporate container keeps mail, documents, and the MFA agent isolated from personal apps; per-app VPN makes corporate traffic observable on BYOD without owning the device. The incident that exercises all three layers: a repackaged corporate app (pinning stripped, telemetry added) appears in a third-party store — BT18’s store watch catches it, mobitriage on the repackaged APK shows the pinning constants absent (diffable against the genuine artifact), the backend’s attestation checks show the forged clients failing integrity (and thus enumerate victims), and BT15’s revocation clears the sessions the impostor harvested. The measure loop closes in BT20: findings-per-release trending down, attestation failure rate (attack signal), posture-gate block rate (fleet hygiene) — three layers, one program.
Exploitation Chain
Section titled “Exploitation Chain”- Acquire — the genuine app from any store (public artifact); or target the fleet via phishing link
- Static — mobitriage/jadx: endpoints, secrets, IPC surface; server API re-implemented client-side
- Instrument — Frida-class hooks strip pinning, dump crypto, trace auth flows in an emulator
- Repackage — tampered APK with injected logic (tracker, overlay, credential capture) re-signed
- Distribute — third-party store or direct sideload lure; rogue-app listing (BT18 watch fires)
- Harvest — credentials/tokens captured; attestation failures enumerate attempts server-side
- Analyze — S2 pipeline diffs repackaged vs genuine; BT10 sighting; victim enumeration via integrity logs
- Respond — BT15 revocation for harvested sessions, takedown via BT18, attestation enforcement tightened
Building Your Own Tool
Section titled “Building Your Own Tool”Static analysis finds what shipped; ipctrace finds what’s reachable: exported components (activities, services, receivers, providers) cross-referenced with permissions — the IPC exposure map showing which entry points other apps, including malicious ones, can invoke.
Design decisions
Section titled “Design decisions”Manifest parsing without AXML libraries is the honest constraint — full binary-XML decoding (chunk headers, string pools, resource IDs) is ~150 lines and worth it here because exported-component enumeration is the IPC finding and presence-scanning can’t attribute attributes to components. The parser implements the minimal AXML walk: file header → string pool (UTF-8/UTF-16 auto-detect) → start-element events (component tags, attribute name-value resolution). Component-attribution beats component-counting: exported=true on the deep-link activity that parses URLs is a different finding than on an internal splash screen — the report lists each exported component with its name and intent-filters. Fail-loud on unparseable AXML (packed/protected manifests route to M14’s depth).
Full source
Section titled “Full source”#!/usr/bin/env python3"""ipctrace — enumerate exported components from an APK's binary manifest.
Minimal AXML parser: string pool + start-element walk. Pure stdlib.Usage: python3 ipctrace.py app.apk [--json]"""import argparseimport jsonimport structimport sysimport zipfilefrom pathlib import Path
RES_XML_START_ELEMENT = 0x0102RES_STRING_POOL_TYPE = 0x0001RES_XML_TYPE = 0x0003COMPONENT_TAGS = { 0x0102: "activity", # android name for start element resolved via pool}# attribute resource IDs of interest (android:name=0x01010003, exported=0x01010010)ATTR_NAME = 0x01010003ATTR_EXPORTED = 0x01010010ATTR_PERMISSION = 0x01010006
def parse_string_pool(buf, offset): """Parse a ResStringPool header; return list of strings.""" sp_type, sp_header, sp_size = struct.unpack_from("<HHI", buf, offset) if sp_type != RES_STRING_POOL_TYPE: sys.exit("fail-loud: expected string pool") string_count, style_count, flags, strings_start, styles_start = \ struct.unpack_from("<IIIII", buf, offset + 8) utf8 = bool(flags & (1 << 8)) strings = [] base = offset + strings_start for i in range(string_count): off = base + struct.unpack_from("<I", buf, offset + 28 + i * 4)[0] if utf8: # u16len, u8len, bytes, NUL n = buf[off + 1] if buf[off] & 0x80 else buf[off] slen = struct.unpack_from("<H", buf, off)[0] slen = buf[off] | (buf[off + 1] << 8) if buf[off] & 0x80 else buf[off] ln = buf[off + 2] if buf[off] & 0x80 else buf[off + 1] raw = buf[off + 2 + (1 if buf[off] & 0x80 else 0): off + 2 + (1 if buf[off] & 0x80 else 0) + ln] strings.append(raw.decode("utf-8", "replace")) else: slen = struct.unpack_from("<H", buf, off)[0] if slen & 0x8000: slen2 = struct.unpack_from("<H", buf, off + 2)[0] slen = ((slen & 0x7FFF) << 16) | slen2 raw = buf[off + 4:off + 4 + slen * 2] else: raw = buf[off + 2:off + 2 + slen * 2] strings.append(raw.decode("utf-16-le", "replace")) return strings
def walk_axml(buf): """Walk the AXML chunk tree; yield (line_number, tag_name, attrs).""" strings = parse_string_pool(buf, 8) off = 8 # skip outer ResXMLTree header (type=RES_XML_TYPE) xml_type, xml_header, xml_size = struct.unpack_from("<HHI", buf, 0) if xml_type != RES_XML_TYPE: sys.exit("fail-loud: not compiled Android XML") off = xml_header events = [] while off + 8 <= len(buf): ctype, chdr, csize = struct.unpack_from("<HHI", buf, off) if ctype == RES_XML_START_ELEMENT: # header(8) + ns(text ref) + name(ref) + attributeStart(2) # + attributeSize(2) + attributeCount(2) + idIndex... ns_ref, name_ref = struct.unpack_from("<II", buf, off + 8) attr_start, attr_size, attr_count = struct.unpack_from( "<HHH", buf, off + 16) attrs = [] for i in range(attr_count): base = off + 8 + 20 + i * attr_size ans, aname, raw_val = struct.unpack_from("<III", buf, base) atype, adata = struct.unpack_from("<HH", buf, base + 12) name = strings[aname] if aname < len(strings) else f"?{aname}" if aname == 0xFFFFFFFF: continue # string value: typed value with type STRING (0x03) if atype == 0x03 and raw_val < len(strings): attrs.append((name, strings[raw_val], adata)) else: attrs.append((name, None, adata)) tag = strings[name_ref] if name_ref < len(strings) else "?" events.append((tag, attrs)) off += csize if csize else 8 return events
def main(): ap = argparse.ArgumentParser(description="APK exported-component map") ap.add_argument("apk", type=Path) ap.add_argument("--json", action="store_true") args = ap.parse_args()
if not args.apk.exists(): sys.exit(f"fail-loud: apk not found: {args.apk}") try: zf = zipfile.ZipFile(args.apk) except zipfile.BadZipFile: sys.exit("fail-loud: not a valid zip/APK") if "AndroidManifest.xml" not in zf.namelist(): sys.exit("fail-loud: AndroidManifest.xml missing") buf = zf.read("AndroidManifest.xml")
components, current = [], None for tag, attrs in walk_axml(buf): adict = dict((n, (v, d)) for n, v, d in attrs) if tag in ("activity", "service", "receiver", "provider"): name = adict.get("android:name", ("", None))[0] or "?" exported = adict.get("android:exported", (None, None)) perm = adict.get("android:permission", ("", None))[0] or "" current = {"type": tag, "name": name.split(".")[-1], "full": name, "exported": None, "permission": perm} if exported[0] is not None: current["exported"] = exported[0] == "true" elif exported[1] is not None: current["exported"] = bool(exported[1]) else: current["exported"] = None # intent-filter default rule components.append(current) elif tag == "intent-filter" and current is not None: if current["exported"] is None: current["exported"] = True # filter present + unset = true exported = [c for c in components if c["exported"]] unprotected = [c for c in exported if not c["permission"]]
if args.json: print(json.dumps({"components": len(components), "exported": exported}, indent=2)) return print(f"components={len(components)} exported={len(exported)} " f"unprotected-exported={len(unprotected)}") for c in unprotected: print(f" [IPC] {c['type']:<10} {c['full']}") if unprotected: print("next: review each for sensitive intent handling (deep links, " "result returns); exported + permissionless = callable by any app") else: print("no unprotected exported components — review intent-filter " "deep links manually")
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”The string pool parser (lines 26-52) handles both pool encodings the format permits — UTF-8 (quirky length encoding, lines 39-44) and UTF-16LE (lines 45-51) — because toolchains emit different pools and a single-encoding parser fails on half of real APKs. The chunk walk (lines 55-76) reads the flat event stream — header type checks, attribute arrays resolved through the pool, and the three android-namespace resource IDs of interest (name 0x01010003, exported 0x01010010, permission 0x01010006).
The component attribution (lines 88-110) implements the platform’s actual exported semantics, where naive tools mislead: exported unset with an intent-filter means exported=true (the default rule behind most unintended IPC), unset without a filter means false. The two-pass state machine (component opens current; intent-filter presence flips unset→true) encodes the rule precisely. The report (lines 116-124) leads with the unprotected-exported list — callable by any app, no permission gate — with the deep-link warning (URL-parsing activities are the classic sensitive surface) as follow-up.
Compilation and test procedure
Section titled “Compilation and test procedure”triage the release candidate before it ships
Section titled “triage the release candidate before it ships”$ python3 mobitriage.py release-candidate.apk apk : release-candidate.apk (8421337 bytes, 1 dex, 9 native libs) score : 7 verdict: high-interest (static view)
- hardcoded credentials: [(‘aws-access-key’, ‘AKIAIOSFODNN7…’)]
- allowBackup=true — local data extractable via adb backup class
- endpoints (6): [‘https://api.internal.mycorp.com/v2/…’, …] next: jadx full decode for hits; server-side review for endpoints
map the IPC surface of the same artifact
Section titled “map the IPC surface of the same artifact”$ python3 ipctrace.py release-candidate.apk components=34 exported=5 unprotected-exported=3 [IPC] activity com.mycorp.app.DeepLinkActivity [IPC] service com.mycorp.app.SyncService [IPC] receiver com.mycorp.app.PushReceiver next: review each for sensitive intent handling (deep links, result returns)
the fix loop: secrets out of the binary, components permission-gated,
Section titled “the fix loop: secrets out of the binary, components permission-gated,”attestation wired server-side, then re-run both tools to zero
Section titled “attestation wired server-side, then re-run both tools to zero”Detection considerations
Section titled “Detection considerations”The static pipeline is release-gate telemetry: findings-per-release trending to zero is appsec health, and a new credential finding at release means secrets management (H10) failed upstream. Fleet-side signals: posture-gate blocks, attestation failure rates (repackaged clients knocking — attack signal with victim-enumeration value), container DLP events. The store watch (BT18) covers the ecosystem. One warning: attestation data is privacy-sensitive (it can identify devices) — wire its retention into S10’s discipline, or your defense telemetry becomes the privacy incident.
Improvements to add
Section titled “Improvements to add”- Add iOS IPA support: plist parsing (stdlib biplist-free via plutil-compatible XML), fair-play status check, binary string pass.
- Add a diff mode: two APKs, report component/permission/endpoint deltas (release-over-release creep).
- Parse the network-security-config XML fully (pinning present, cleartext domains listed).
- Cross-reference exported deep-link activities with their URL parsers for the sensitive-IPC shortlist.
- Emit attestation-integration checklists per platform from the findings (server-side work items).
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| Static findings per release | mobitriage gate | per release | Appsec health |
| Hardcoded credential hits | mobitriage | per build | Immediate block |
| Attestation failures | Backend logs | per session | Repackaged clients |
| Posture-gate blocks | MDM/UEM | per device | Fleet hygiene |
| Root/jailbreak detections | MDM/EDR | per device | Container gating |
| Container DLP events | MAM policy | per device | Copy-out attempts |
| Rogue-app listings | BT18 watch | weekly | Ecosystem layer |
| Pinning-bypass attempts | Backend TLS telemetry | per client | Frida-class probes |
| Exported-IPC probes | Android telemetry | per device | Component abuse |
| Deep-link abuse | App telemetry + server | per event | Sensitive surface |
Evasion Techniques
Section titled “Evasion Techniques”Adversaries evade mobile defenses at each layer. Repackaging with pinning stripped is the standard first move — the counter is attestation (server-side integrity verification), which is precisely why_attestation-enforcing backends see far less abuse of their APIs. Emulator and hook detection: apps check emulator properties, Frida artifacts, debugger attachment (E14’s catalog, mobile edition) — the defense layer uses the same signals for risk scoring (a session from a hooked runtime is a risk signal, not a hard block, because analysts hook too). Overlay and accessibility abuse (Android): the malicious overlay draws on top of the genuine login screen, or the accessibility service reads screen content — the platform’s permissions are the attack surface; MDM policies restricting accessibility-service grants on managed devices are the control. Clipboard and screenshot harvesting: tokens pasted through clipboards, authenticator screenshots — the app-side mitigations (flagSecure, clipboard-access discipline) are cheap and rarely shipped. Store-ecosystem laundering: rogue apps that pass store review by being benign until a remote kill-switch flips — behavior-gated malware that defeats point-in-time review; the BT18 watch plus backend telemetry (the rogue’s users eventually hit your API) catch the tail. Fleet blindness exploitation: BYOD devices outside MDM reach the corporate API through the published app with no posture at all — attestation plus per-app VPN shrink the unmanaged surface; the honest inventory (what talks to the backend from what posture) is the control that makes everything else enforceable.
⚠️ What caught me: I reviewed our flagship app’s security with a proper toolchain — full jadx decode, Frida on a rooted device, the works — and reported it clean: no secrets, pinning present, storage encrypted. Two weeks later a partner’s engineer showed me their “integration”: they’d re-implemented our login flow against our API from a Python script, using the device API’s parameters they’d read from our own public documentation, and our backend accepted everything because the API’s authorization model trusted the client’s claims. The app was clean; the system wasn’t — the backend accepted flows no genuine app ever performs. Mobile security’s final exam is server-side: enumerate every decision your backend delegates to the client, then take them all back.
Lab Practice
Section titled “Lab Practice”Run the mobile loop in the lab. Artifact: build or obtain a test APK with deliberate findings (hardcoded credential, exported deep-link activity, allowBackup on) [verify build setup]. Triage: mobitriage — confirm each finding fires. IPC: ipctrace — confirm the map matches; permission-gate one component and re-run. Dynamic: in an emulator, hook a function with a Frida-class tool and strip pinning against a local MITM proxy [verify setup] — observe what leaks without transport trust. Fleet: in your MDM lab, enforce a posture gate and confirm the block event. Debrief: which finding was only discoverable dynamically, which only server-side — and what does your pipeline cover today?
Interview Questions
Section titled “Interview Questions”- Technical: Walk the mobile analysis stack — artifact, transport, device, fleet, ecosystem — and name the control that anchors each layer.
- Technical: Why does the exported-component default rule (intent-filter present + exported unset = true) produce most unintended IPC exposure, and how does
ipctraceencode it? - Scenario: A repackaged version of your app appears in a third-party store with credential capture added. Walk the response: analysis diff, victim enumeration, takedown, and the server-side fix.
- Scenario: Your backend sees attestation failures from 200 distinct devices over a weekend. Is this an attack, a bug, or a platform change — and what do you check in order?
- Conceptual: Why is certificate pinning an integrity control rather than an authentication control, and what replaces client-side trust?
- Deep-dive: Design mobile security for a 10,000-device BYOD fleet with two published apps: the appsec pipeline, attestation architecture, MDM posture policy, and the metrics for BT20.
- Practical: You have one sprint before the next release. Which three findings from
mobitriagedo you fix now, which do you ticket, and which do you accept — with what evidence?
Related Modules
Section titled “Related Modules”H9 (IoT Cloud & Mobile Attack Surface) · BT8 (Malware Analysis) · BT16 (Cloud Detection & Response) · BT18 (Digital Risk & Brand Protection) · W14 (OAuth & OIDC Attacks)