I've been spending a lot of time going deep on mobile application security, specifically around how Android and iOS apps get attacked and defended at every layer. This post covers the full picture: platform architecture, attack tooling, reverse engineering workflows, runtime protection, and the ongoing cat-and-mouse game between offense and defense.
1. Threat Landscape
Before getting into specifics, here's the high-level map of what mobile apps are up against:
| Threat Category | Examples |
|---|---|
| Device Compromise | Rooting (Android), jailbreaking (iOS), emulators, virtual environments |
| Reverse Engineering | Decompiling APK/IPA, extracting source code, analyzing business logic |
| Runtime Manipulation | Frida hooking, Xposed modules, method swizzling, code injection |
| Network Attacks | MITM, SSL pinning bypass, API abuse |
| Overlay / Phishing | Screen overlay attacks, fake login screens, accessibility service abuse |
| Data Theft | Extracting tokens, keys, credentials from device storage |
| Transaction Fraud | OTP interception, SMS stealing, clipboard hijacking |
Apps that handle authentication, transaction integrity, and sensitive data -- PII, tokens, session keys -- are the highest-value targets. A compromised app directly enables fraud at scale.
2. Platform Architecture
Understanding what you're defending against starts with understanding what the platforms actually guarantee.
Android
Android is built on a modified Linux kernel:
- UID Isolation: Every app gets a unique Linux UID and its own ART VM instance. The kernel enforces process separation.
- SELinux: Enforcing mode since Android 5.0. Apps are confined to SELinux domains restricting system calls and file paths.
- Permissions: Post-6.0, runtime permissions require individual user grants. Weakness: users routinely grant everything.
- Sideloading: APKs installable by toggling "Install from unknown sources." Major attack surface.
- APK Structure: ZIP archive containing DEX bytecode. Decompiles cleanly to near-original Java/Kotlin via JADX.
- Dynamic Code Loading:
DexClassLoaderallows loading new DEX files from disk or network at runtime.
iOS
iOS is built on the XNU kernel (Mach + BSD hybrid):
- App Sandbox (Seatbelt): Each app gets its own container directory. Kernel-level enforcement prevents cross-app data access.
- Mandatory Code Signing: Every binary, dylib, and executable memory page must be signed. Enforced by AMFI at kernel level.
- No Sideloading: App Store only (or TestFlight). Enterprise cert workarounds exist but are actively revoked.
- Entitlements: Capabilities baked into code signature. Cannot modify without re-signing.
- Mach-O Binaries: Compiled native ARM64. Harder to reverse engineer than DEX. Symbols stripped in release.
- No Dynamic Code Loading: JIT blocked (except Safari's JSC). Code signing enforced on all executable pages.
From an Attacker's Perspective
| Aspect | Android | iOS |
|---|---|---|
| Reverse engineering | Lower difficulty (DEX to Java/Kotlin) | Higher (compiled ARM, stripped symbols) |
| Sideloading | One toggle | Requires jailbreak or enterprise cert |
| Runtime hooking | Xposed (system-wide) + Frida | Frida (jailbreak needed for full access) |
| Root/jailbreak prevalence | Higher | Lower (exploits rarer, patched faster) |
| Dynamic code loading | Allowed | Blocked |
3. Rooting and Jailbreaking
Android Rooting
Stock Android has no su binary -- no process can escalate to UID 0. Rooting installs su plus a management app.
Magisk (Systemless Root): Patches boot.img rather than /system. System stays untouched, bypassing some integrity checks. Provides Zygisk + DenyList to hide root from target apps.
KernelSU: Operates entirely within the kernel. No su binary on the filesystem. Grants root via a kernel mechanism. Significantly harder to detect from userspace.
What root enables:
- Read any app's
/data/data/<package>/-- preferences, databases, tokens - Modify app APKs or runtime memory
- Install Xposed/LSPosed for system-wide hooking
- Disable SELinux (
setenforce 0) - Intercept and modify network traffic at the OS level
iOS Jailbreaking
A jailbreak exploits vulnerabilities to disable three mechanisms:
- AMFI -- code signing enforcement bypassed, unsigned code executes
- Sandbox -- apps access files outside their container
- Root filesystem -- remounted read-write
The types matter because detection strategies differ:
- Untethered: persists across reboots (very rare now)
- Semi-tethered: requires a computer to re-jailbreak after reboot
- Semi-untethered: on-device app re-jailbreaks (unc0ver, Dopamine)
- Rootless (Dopamine 2.0): operates within
/varand preboot. Does not remount root filesystem. Evades traditional filesystem-based detection entirely.
What Becomes Possible on a Compromised Device
| Risk | Description |
|---|---|
| Credential theft | Malware reads target app's sandbox data |
| Runtime hooking | Intercept transferMoney(), getBalance(), SSL pinning |
| API abuse | Extract auth logic, script direct backend calls bypassing client controls |
| Transaction manipulation | Modify in-memory values (amount, recipient) before server request |
| Biometric bypass | Hook auth callback to return true |
| Key extraction | Software-backed keys extractable. Hardware-backed keys safe but oracle attacks possible. |
4. Reverse Engineering Android APKs
Obtaining the APK
adb shell pm path com.target.app
adb pull <path>
For App Bundles (AAB): use bundletool to merge split APKs first.
Static Analysis
apktool d target.apk -o output_dir # Extract manifest, resources
apksigner verify --print-certs target.apk # Signing certificate
unzip -l target.apk | grep "\.so" # Native libraries (need Ghidra, not JADX)
AndroidManifest.xml reveals the real attack surface:
- Exported components (
exported=true= attack surface) debuggable=truein production -- critical vulnerabilityallowBackup=true--adb backupextracts data without root- Deep link intent filters -- potential hijacking
networkSecurityConfig-- pinning configuration
Decompilation with JADX
jadx target.apk -d jadx_output
What to look for:
| Target | Search Patterns |
|---|---|
| Hardcoded secrets | api_key, secret, Bearer, AIza (Google), AKIA (AWS), BuildConfig.java |
| Auth flow | OAuth2/JWT implementation, token storage, refresh handling |
| API surface | Base URLs, Retrofit interfaces, OkHttp interceptors, debug endpoints |
| Pinning | OkHttp CertificatePinner, Network Security Config, custom TrustManager |
| Crypto | AES-ECB (insecure), CBC without HMAC, hardcoded keys, java.util.Random |
| Business logic | Client-side-only validation (transfer limits, price checks) |
Dynamic Analysis with Frida
objection -g com.target.app explore
> android sslpinning disable
// Hook a function at runtime
Java.perform(function() {
var cls = Java.use("com.target.app.TransactionManager");
cls.transferMoney.implementation = function(amount, recipient) {
console.log("transferMoney: " + amount + " to " + recipient);
return this.transferMoney(amount, recipient);
};
});
Protections and Bypasses
| Protection | Bypass |
|---|---|
| ProGuard/R8 (renamed identifiers) | Trace from entry points, rename as understood |
| String encryption | Hook decrypt function with Frida |
| Control flow obfuscation | Dynamic analysis bypasses static obfuscation |
Native code in .so | Ghidra/IDA for static, Interceptor.attach(Module.findExportByName(...)) |
| Root/emulator detection | Magisk DenyList or hook detection functions |
| Integrity/signature checks | Hook verification to return expected value |
5. Reverse Engineering iOS IPAs
Why iOS Is Harder
- Native ARM64 Mach-O vs. DEX bytecode. Ghidra/IDA output pseudocode, not original source.
- Stripped symbols:
TransactionManager.transferMoney()becomessub_100004A3C(). Exception: ObjC selectors survive becauseobjc_msgSenddispatch requires them. - FairPlay DRM: App Store binaries are encrypted. Must decrypt on a jailbroken device (
frida-ios-dump,CrackerXI,bfdecrypt) before analysis.
Tool Mapping
| Android | iOS | Purpose |
|---|---|---|
| apktool | unzip + plutil + jtool2 | Manifest/binary inspection |
| JADX | Ghidra / IDA Pro / Hopper | Decompilation |
| apksigner | codesign / ldid | Signature verification |
Workflow
python dump.py com.target.app # Decrypt IPA (frida-ios-dump)
unzip target.ipa -d output
plutil -p Payload/App.app/Info.plist # URL schemes, ATS exceptions
otool -L Payload/App.app/App # Linked libraries
jtool2 --objc Payload/App.app/App # ObjC class/method dump
Frida on iOS uses the ObjC runtime directly:
var handler = ObjC.classes.PaymentHandler;
Interceptor.attach(handler['- processPayment:'].implementation, {
onEnter: function(args) { console.log("payment: " + ObjC.Object(args[2])); }
});
6. Frida: Dynamic Instrumentation
Architecture
- frida-server -- root daemon on target device
- frida-client -- runs on your machine, sends commands via USB/network
- frida-agent --
.so/.dylibinjected into target process, runs V8 JavaScript engine
How Injection Works
Android (rooted): frida-server uses ptrace() to attach to the target process, forces it to dlopen() frida-agent.so, then the agent starts a V8 runtime inside the target's address space.
Gadget mode (no root): Repackage the APK with frida-gadget.so included. Loads on launch. Requires re-signing.
iOS: Uses task_for_pid() mach trap instead of ptrace(). The jailbreak patches the kernel to allow this.
Interceptor.attach() overwrites the first bytes of a target function with a trampoline (a jump to Frida's handler). Original bytes are saved for calling through. The function's machine code is modified in process memory.
Detection Techniques
| Method | Approach | Bypass |
|---|---|---|
| Port scan | Check TCP 27042 (default) | Change port |
| Process scan | /proc for frida-server | Rename binary |
| Library scan | /proc/self/maps for frida .so | Custom library names |
| Memory scan (robust) | Scan for "LIBFRIDA", "gum-js-loop", "frida_agent_main" | Harder -- strings baked into compiled binary |
| ptrace self-claim | ptrace(PTRACE_TRACEME) blocks Frida's ptrace | Non-ptrace injection methods |
| Prologue integrity | Compare function first bytes against known-good | Hook the check first |
| Thread count | Monitor for unexpected threads | Harder to hide |
| FD scan | /proc/self/fd for Frida pipes/sockets | Rename descriptors |
7. Xposed/LSPosed: System-Wide Hooking
Xposed is more dangerous than Frida for a specific reason: it hooks before your app code runs at all.
How It Works
Android's Zygote process is the parent of all apps. It preloads framework classes, then fork()s for each app launch. Xposed modifies Zygote itself:
- Replaces
/system/bin/app_processwith a modified version - Loads
XposedBridge.jarinto Zygote before any app launches - Provides
findAndHookMethod()for intercepting any Java method - Hooks are inherited by every app process forked from Zygote
LSPosed: Uses Magisk's Zygisk for injection (systemless). Modules scoped per-app. DenyList covers hiding.
Why This Is a Different Problem Than Frida
| Factor | Implication |
|---|---|
Hooks before onCreate() | RASP init runs after hooks are in place. All detection returns fake "safe" results. |
| No external footprint | No process, no port, no injected .so in maps |
| Persistent | Install once, works forever, no computer needed |
| Module ecosystem | SSLUnpinning, RootCloak, Lucky Patcher, Inspeckage require zero scripting |
Detection
- Class check:
Class.forName("de.robv.android.xposed.XposedBridge")-- bypass: hookClass.forName() - Stack trace: Check for Xposed frames -- bypass: hook
getStackTrace() - Native maps scan:
/proc/self/mapsfor XposedBridge/lspd -- bypass: Zygisk manipulates layout - ART internals: Check
entry_point_from_quick_compiled_code_in method metadata. Most robust, requires per-version ART knowledge. - Behavioral (most resilient): Call functions that should fail and verify they fail. Perform crypto and verify server-side. If "should fail" succeeds, something is hooking.
8. SSL/TLS Pinning and Bypass
Why Pinning Exists
Devices trust 150+ root CAs. Any compromised or attacker-installed CA can issue valid certificates for any domain, enabling MITM. Pinning restricts trust to a specific certificate or public key regardless of the device's CA store.
Public key pinning (recommended) survives certificate rotation. Certificate pinning breaks when the cert renews.
Android Implementations
Network Security Config (platform-level, Android 7+):
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.target.com</domain>
<pin-set><pin digest="SHA-256">hash=</pin></pin-set>
</domain-config>
</network-security-config>
OkHttp CertificatePinner (app-level): Only applies to connections through that specific client instance.
Custom TrustManager: Full control. Common vulnerability: empty checkServerTrusted() that accepts everything.
iOS Implementations
URLSession delegate: Implement didReceive challenge, extract server public key, compare against pinned hash.
TrustKit: Swizzles NSURLSession delegates. Can be un-swizzled by an attacker.
Bypass per Implementation
| Target | Hook |
|---|---|
| Network Security Config | NetworkSecurityTrustManager.checkPins(), or install CA as system cert (root) |
| OkHttp | CertificatePinner.check() / check$okhttp() |
| Custom TrustManager | checkServerTrusted() returns without throwing |
| iOS URLSession | Force completionHandler(.useCredential) |
| TrustKit | Hook verifyPublicKeyPin: or re-swizzle |
| Any | objection: android sslpinning disable / ios sslpinning disable |
Defense Beyond Pinning
Pinning alone is insufficient on compromised devices. Stack these:
- Mutual TLS (mTLS): Client cert required. Private key in Keystore/Keychain.
- Request signing: HMAC/signature on each request. Intercepted requests unforgeable without the key.
- Proxy detection: Check system proxy settings.
- Server-side anomaly detection: Flag proxy-like traffic patterns.
9. Screen Overlay Attacks
Android's SYSTEM_ALERT_WINDOW permission allows floating windows on top of all apps.
Tapjacking: Transparent overlay intercepts or redirects taps on confirm buttons.
Credential harvesting: Malicious app monitors foreground via UsageStatsManager/AccessibilityService, draws fake login screen over target app when launched.
Partial overlay: Covers specific fields (recipient account number, for example) with different values.
Platform Mitigations Over Time
| Version | Mitigation |
|---|---|
| 4.0.3 | filterTouchesWhenObscured: views reject touches if overlay detected |
| 6.0 | SYSTEM_ALERT_WINDOW requires manual Settings toggle |
| 10 | Overlays non-clickable for API 29+ apps |
| 12 | setHideOverlayWindows(true) forces overlay dismissal |
In-App Defenses
<Button android:filterTouchesWhenObscured="true" />
window.setHideOverlayWindows(true) // Hide all overlays (API 31+)
window.setFlags(FLAG_SECURE, FLAG_SECURE) // Block screenshots/recording
10. OTP and SMS Interception
| Method | Mechanism |
|---|---|
READ_SMS abuse | Pre-Android 10: any app with permission reads all SMS. Rooted devices bypass post-10 restrictions. |
| Notification listener | NotificationListenerService reads ALL notifications including OTP previews |
| Accessibility abuse | AccessibilityService reads all screen content, performs taps. Most dangerous permission. |
| SIM swap | Social-engineer carrier to port number to attacker's SIM. Outside app control. |
| SS7 exploitation | Telecom protocol vulnerabilities allow SMS interception in transit. Outside app control. |
| Clipboard hijacking | Apps auto-copying OTP to clipboard. Android 12+ shows toast on read. |
Mitigations
| Mitigation | Description |
|---|---|
| Replace SMS OTP | Use TOTP, push notifications, or FIDO2/WebAuthn |
| SMS Retriever API | Hash-based delivery to matching app only, no READ_SMS needed |
| Transaction-bound OTP | OTP encodes transaction details, server verifies match |
| Detect listeners | Enumerate active NotificationListenerService / AccessibilityService instances |
| Aggressive expiry | 30-60 seconds, single use, invalidate on error |
| Velocity checks | Flag multiple OTP requests, failed attempts, new device/location |
11. Root and Jailbreak Detection
Every individual check has a known bypass. That's the key insight. The only real approach is combining multiple methods, running them at unpredictable intervals, obfuscating detection code, and verifying results server-side.
Android
| Method | Check | Known Bypass |
|---|---|---|
| Filesystem | su at /system/bin/su, /sbin/su, Magisk paths, root manager packages | DenyList hides paths. KernelSU has no su. Package name randomization. |
| System properties | ro.debuggable=1, ro.secure=0, test-keys, SELinux permissive | Magisk resetprop |
su execution | Runtime.exec("su") | DenyList |
| Mount analysis | /system mounted rw, Magisk overlays | Systemless root avoids rw mounts |
| Play Integrity API | Server-side attestation of boot integrity and device certification | "Play Integrity Fix" module |
| Process/package scan | Frida-server in /proc, hooking framework packages | Rename binaries, randomize package names |
iOS
| Method | Check | Known Bypass |
|---|---|---|
| Filesystem | /Applications/Cydia.app, /usr/bin/ssh, /usr/bin/dpkg | Rootless jailbreaks avoid these paths |
| URL schemes | canOpenURL("cydia://") | Remove Cydia |
| Dylib enumeration | _dyld_get_image_name() for substrate/frida/libhooker | Hook to return filtered list |
| Fork test | fork() restricted on stock iOS | Hook fork() to return failure |
| Sandbox write test | Write outside sandbox; success = broken | Hook file operations |
12. RASP Architecture
RASP (Runtime Application Self-Protection) is code shipped inside the app binary that continuously collects signals, computes risk, and enforces security decisions.
Signal Collectors
| Collector | Trigger | Signals |
|---|---|---|
| Environment | Launch + periodic | Root, emulator, debugger, developer options |
| Integrity | Launch + pre-critical-ops | Signature, binary hash, resource checksum |
| Hook Detector | Continuous | Frida, Xposed, Substrate, function prologue integrity |
| Network | Every API call | Pinning status, proxy, certificate chain |
| Behavioral | Continuous | Thread anomalies, timing, memory patterns, screen capture |
Risk Engine
risk_score = w1*root + w2*hook + w3*emulator + w4*debugger + w5*integrity + w6*behavioral
- 0-30: normal, log telemetry
- 31-60: block transactions, require step-up auth
- 61-85: block sensitive ops, alert backend
- 86-100: kill app, wipe tokens, server-side lock
The enforcement actions span a spectrum: silent telemetry, feature gating, step-up auth, token invalidation, app termination, server-side lockout. Proportional response matters -- hard-killing the app on any anomaly creates a bad user experience and gets bypassed the same way.
Self-Protection
RASP code is itself an attack target. Mitigations:
- Obfuscated via R8/LLVM
- Self-checksumming
- Critical logic in native C/C++
- Detection code scattered across the codebase (not one
SecurityCheckerclass) - Decoy functions waste the reverser's time
The server side makes final decisions using device fingerprint, behavioral analytics, attestation, and historical risk data. Never trust the client.
Commercial vs. Custom
| Approach | Examples | Trade-off |
|---|---|---|
| Commercial | Promon SHIELD, Guardsquare, Zimperium, Appdome, Talsec freeRASP | Fast, battle-tested, but attackers research public SDK bypasses |
| Custom | Built in-house | Full control, unknown to attackers, requires deep expertise |
| Hybrid | Commercial + custom layers | Best of both |
13. Code Obfuscation, Hardening, and Integrity
Three distinct disciplines: obfuscation hides what code does, hardening makes code fight back, and integrity checking verifies code hasn't been modified.
Obfuscation
| Technique | Tool | Bypass |
|---|---|---|
| Identifier renaming | R8/ProGuard | Patient manual analysis |
| String encryption | DexGuard, StringCare | Hook decrypt function |
| Control flow flattening | DexGuard, DashO, O-LLVM | Dynamic analysis |
| Opaque predicates | DexGuard | Dynamic analysis |
| Dead code injection | Various | Ignore unreachable code |
| Native LLVM obfuscation | O-LLVM, Hikari | Ghidra + Frida (significantly harder) |
iOS: symbol stripping (Xcode default), O-LLVM/Hikari, iXGuard (renames ObjC selectors).
Hardening
Anti-debugging:
// Android native
if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) { exit(1); }
// Android Java
if (Debug.isDebuggerConnected() || (applicationInfo.flags and FLAG_DEBUGGABLE != 0)) { /* kill */ }
// iOS
ptrace(PT_DENY_ATTACH, 0, 0, 0);
Anti-emulator: Check Build.FINGERPRINT for "generic", Build.HARDWARE for "goldfish"/"ranchu", sensor availability, OpenGL renderer, telephony data.
Anti-hooking: Verify function prologue bytes match compile-time expected values.
Environment binding: Bind keys to APK signature hash. Bind to installer (com.android.vending). Bind to device ID.
Integrity
- Signature verification: Compare runtime signing cert hash against expected value
- DEX checksumming: Hash
classes.dexat runtime, store expected hash in native code - Resource integrity: Hash assets,
.sofiles, manifest - Continuous verification: Periodic re-checks, guards guarding guards
- Server-side: Send hashes + Play Integrity attestation to backend
14. Cryptographic Key Storage
Android Keystore
Three backing tiers:
| Tier | Mechanism | Extractable on Root? |
|---|---|---|
| Software | Encrypted blob in filesystem | Yes |
| TEE (TrustZone) | Isolated processor area, own OS (Trusty/QSEE). Keys never leave TEE. | No (oracle attacks possible) |
| StrongBox (Android 9+) | Dedicated secure element chip (Titan M, Samsung eSE) | No (nation-state physical attacks only) |
KeyGenParameterSpec.Builder("key", PURPOSE_SIGN)
.setIsStrongBoxBacked(true)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(0, AUTH_BIOMETRIC_STRONG)
.setInvalidatedByBiometricEnrollment(true)
.build()
Key Attestation: Device generates a certificate chain proving hardware backing. Server verifies against Google root cert. Contains hardware type, OS version, boot state.
iOS Keychain
Encrypted database. Key derived from device UID (hardware-fused) + user passcode.
| Access Class | When Accessible | In Backups? |
|---|---|---|
kSecAttrAccessibleAlways (deprecated) | Always | Yes |
kSecAttrAccessibleAfterFirstUnlock | After first unlock | Yes |
kSecAttrAccessibleWhenUnlocked | While unlocked | Yes |
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly | Unlocked + passcode set | No |
Secure Enclave
Dedicated coprocessor since A7 (iPhone 5S). Own boot ROM, encrypted memory, AES engine. No access from the application processor even at the kernel level. NIST P-256 EC keys only.
Attack Scenarios
| Target | Attack | Feasibility |
|---|---|---|
| Software Keystore | Extract encrypted keyblob, hook Keystore API | Rooted device |
| TEE | Oracle attacks, firmware exploits (QSEE CVEs), side-channel | Oracle: rooted. Physical: expensive. |
| StrongBox/SEP | Physical attacks (decapping, microprobing) | Nation-state only. No public success. |
| Keychain (weak class) | keychain-dumper on jailbroken device | Trivial |
| Keychain (strong class) | Protected until user authenticates | Requires active user session |
| Backup extraction | Items without ThisDeviceOnly in unencrypted backups | If backup password known |
15. Secure Data Storage
Android (Least to Most Secure)
| Storage | Encryption | Root Risk | Use For |
|---|---|---|---|
External storage (/sdcard/) | None, world-readable | N/A | Never sensitive data |
| SharedPreferences | None (plaintext XML) | Trivially readable | UI preferences only |
| SQLite | None by default | Trivially readable | Non-sensitive data |
| EncryptedSharedPreferences | AES-256-GCM, Keystore master key | Encrypted on disk | Session tokens, cached data |
| Keystore (TEE/StrongBox) | Hardware-backed | Key not extractable | Encryption/signing keys |
iOS (Least to Most Secure)
| Storage | Encryption | Jailbreak Risk | Use For |
|---|---|---|---|
| UserDefaults | None (plaintext plist) | Trivially readable | UI preferences only |
| Sandbox files | None by default | Readable | Non-sensitive, or use NSFileProtection |
| NSFileProtectionComplete | Hardware + passcode derived | Protected when locked | Sensitive files |
| Keychain (strong class) | UID + passcode derived | Protected with ThisDeviceOnly + biometric | Tokens, credentials |
| Secure Enclave | Hardware isolated | Not extractable | Signing keys |
What Goes Where
| Data | Android | iOS |
|---|---|---|
| Session token | EncryptedSharedPreferences | Keychain (WhenPasscodeSetThisDeviceOnly) |
| Signing key | Keystore (StrongBox, biometric per-use) | Secure Enclave (biometryCurrentSet) |
| Cached data | SQLCipher (key in Keystore) | Core Data + NSFileProtectionComplete |
| PIN/password | Never store. Server-derived token. | Never store. Server-derived token. |
Common Mistakes
| Mistake | Fix |
|---|---|
| Tokens in plain SharedPreferences/UserDefaults | EncryptedSharedPreferences / Keychain |
| Hardcoded keys in source | Generate in Keystore/Secure Enclave at runtime |
allowBackup=true | Set false or exclude sensitive paths |
| Logging tokens to Logcat | Strip logging in release (R8 rules / Timber no-op) |
| Sensitive data on clipboard | Never copy tokens. Use password input types. |
iOS kSecAttrAccessibleAlways | kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly |
16. Device Fingerprinting and Risk Scoring
Fingerprinting
Combine multiple signals into a composite hash for persistent device identification:
Hardware (stable): CPU info, GPU renderer, display metrics, RAM, sensor list (model strings, ranges, vendor), camera config.
Software (less stable): Android ID / iOS IDFA, OS version, build number, fonts. Wi-Fi/BT MAC addresses are randomized since Android 10/iOS 14 -- unusable for identification now.
Behavioral (hardest to spoof): Typing cadence, touch pressure, accelerometer patterns, usage patterns, network/location history.
Risk Scoring
Signals evaluated in real-time:
- Environment: root, emulator, dev options, bootloader, SELinux, attestation
- Runtime: debugger, hooks, prologue tampering, unexpected libraries, thread count
- App integrity: signature, checksum, resource hashes, install source
- Network: VPN, proxy, TOR, IP-GPS mismatch, IP reputation
- Temporal: time since install, device first seen, attestation age, signal change frequency
Backend Intelligence
Context matters a lot. A device that's been clean for 6 months then suddenly shows root + Frida is a different risk profile than a device that's always been rooted (power user, lower risk).
- Cross-device: Same account on a new device from a different country = high risk
- Binding anomalies: 50+ accounts on one device = fraud farm. 10+ devices for one account in 24h = credential compromise.
- Velocity: Transaction/OTP request spikes vs. baseline
- ML models: Trained on labeled data learning complex signal correlations
Signal Integrity
| Protection | Purpose |
|---|---|
| Signed payloads | Device-bound key signs signals, server verifies |
| Server nonce | Prevents replay of old clean signals |
| Attestation anchoring | Tied to Play Integrity / App Attest |
| Consistency checks | Flag impossible claims (StrongBox on device without it) |
17. OWASP MASVS and MASTG
MASVS is the standard framework for mobile security requirements. If you're doing security work on a mobile app, this is the checklist.
Categories
| Category | Key Requirements |
|---|---|
| MASVS-STORAGE | Encrypted credentials, excluded from backups/logs/clipboard/screenshots |
| MASVS-CRYPTO | No MD5/SHA1/DES/RC4, no hardcoded keys, secure PRNG, adequate key lengths |
| MASVS-AUTH | Backend auth, session lifecycle, Keystore-backed biometrics (not local boolean) |
| MASVS-NETWORK | TLS 1.2+, certificate pinning, proper validation, no cleartext |
| MASVS-PLATFORM | Minimal permissions, secure WebViews, validated deep links, protected exports |
| MASVS-CODE | PIE/canaries/ARC, debuggable=false, no vulnerable libs, safe error handling |
| MASVS-RESILIENCE | Root/debug/hook/emulator detection, integrity checks, obfuscation, active response |
Verification Profiles
- MAS-L1: Baseline for all apps
- MAS-L2: Defense-in-depth for apps handling sensitive data
- MAS-R: Resilience against reverse engineering
High-security apps target L2 + R.
Assessment Phases
- Static: Decompile, review manifest, map crypto/storage/auth issues, identify API surface
- Dynamic: MITM testing, session analysis, storage inspection on rooted device
- Resilience: Test each detection (root, Frida, Xposed, emulator, debugger, integrity) with known bypasses. Verify active response, not just logging.
- Backend: Replay modified requests, expired tokens, stripped attestation headers, rate limit testing
- Report: Each finding mapped to MASVS requirement + severity + reproduction + remediation
The throughline across all of this: every client-side control is a speed bump, not a wall. The goal isn't to build an impenetrable client -- that's not achievable. The goal is to raise the cost of attack, generate signals, and make the server-side trust decisions. Defense in depth across all these layers is what makes the economics of attacking a specific app unattractive.