Skip to content

Mobile Application Security

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.

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.

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, plist

Static 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.

TechniqueWhat it doesWhere it failsNotes
Manifest reviewPermissions, exported componentsIgnoring IPC surfaceAndroid-first check
Secrets extractionStrings/keys in the binaryAssuming obscurity worksApp = public artifact
Storage auditPrefs/SQLite/keychain reviewDevice-encryption complacencyData-at-rest class
Pinning validationTLS pinning present + bypass-testedPinning as authFrida-class hooks
Repackaging testsTampered APK accepted by backend?No attestation server-sidePlay Integrity join
Posture enforcementRoot/jailbreak/patch gatesEnroll-only MDMFleet truth
ContainerizationCorporate data in managed containerCopy-out paths openDLP on device
Rogue-app monitoringStore watch for impersonationVolume across storesBT18 join
Attestation integrationServer verifies client integrityTrust-client-code habitsBackend control
Store-page hygienePermission minimization, changelogsPermission creepPrivacy-era risk

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 argparse
import json
import re
import sys
import zipfile
from 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()

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.

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.

  1. Acquire — the genuine app from any store (public artifact); or target the fleet via phishing link
  2. Static — mobitriage/jadx: endpoints, secrets, IPC surface; server API re-implemented client-side
  3. Instrument — Frida-class hooks strip pinning, dump crypto, trace auth flows in an emulator
  4. Repackage — tampered APK with injected logic (tracker, overlay, credential capture) re-signed
  5. Distribute — third-party store or direct sideload lure; rogue-app listing (BT18 watch fires)
  6. Harvest — credentials/tokens captured; attestation failures enumerate attempts server-side
  7. Analyze — S2 pipeline diffs repackaged vs genuine; BT10 sighting; victim enumeration via integrity logs
  8. Respond — BT15 revocation for harvested sessions, takedown via BT18, attestation enforcement tightened

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.

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).

#!/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 argparse
import json
import struct
import sys
import zipfile
from pathlib import Path
RES_XML_START_ELEMENT = 0x0102
RES_STRING_POOL_TYPE = 0x0001
RES_XML_TYPE = 0x0003
COMPONENT_TAGS = {
0x0102: "activity", # android name for start element resolved via pool
}
# attribute resource IDs of interest (android:name=0x01010003, exported=0x01010010)
ATTR_NAME = 0x01010003
ATTR_EXPORTED = 0x01010010
ATTR_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()

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.

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

$ 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”

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.

  1. Add iOS IPA support: plist parsing (stdlib biplist-free via plutil-compatible XML), fair-play status check, binary string pass.
  2. Add a diff mode: two APKs, report component/permission/endpoint deltas (release-over-release creep).
  3. Parse the network-security-config XML fully (pinning present, cleartext domains listed).
  4. Cross-reference exported deep-link activities with their URL parsers for the sensitive-IPC shortlist.
  5. Emit attestation-integration checklists per platform from the findings (server-side work items).
SignalSourceIDNotes
Static findings per releasemobitriage gateper releaseAppsec health
Hardcoded credential hitsmobitriageper buildImmediate block
Attestation failuresBackend logsper sessionRepackaged clients
Posture-gate blocksMDM/UEMper deviceFleet hygiene
Root/jailbreak detectionsMDM/EDRper deviceContainer gating
Container DLP eventsMAM policyper deviceCopy-out attempts
Rogue-app listingsBT18 watchweeklyEcosystem layer
Pinning-bypass attemptsBackend TLS telemetryper clientFrida-class probes
Exported-IPC probesAndroid telemetryper deviceComponent abuse
Deep-link abuseApp telemetry + serverper eventSensitive surface

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.

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?

  • 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 ipctrace encode 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 mobitriage do you fix now, which do you ticket, and which do you accept — with what evidence?

H9 (IoT Cloud & Mobile Attack Surface) · BT8 (Malware Analysis) · BT16 (Cloud Detection & Response) · BT18 (Digital Risk & Brand Protection) · W14 (OAuth & OIDC Attacks)