Inside the Attack: How an iOS App Gets Reversed
Part 2 of the iOS secrets series. A step-by-step walkthrough of how an attacker extracts secrets from a real iOS binary — FairPlay decryption, static analysis, Frida injection, memory scanning, and where Apple's defences get addressed along the way.
Previous article covered what secrets are, why they matter, and every protection Apple ships. Every mechanism on that list rests on one assumption: the device is running an unmodified OS.
This post maps what happens when that assumption breaks. Not as a theoretical exercise — as a concrete walkthrough of the actual tools and steps involved, in roughly the order an attacker would use them.
The scenario assumes ideal conditions for an attacker: a jailbroken test device, time, and a copy of your app from the App Store. This isn't far-fetched. Security researchers operate this way routinely. So do motivated competitors and pirates.
The Toolbox
Before getting into the sequence, it's worth understanding what tools exist and what each one does. They fall into three categories.
Static Analysis
Static analysis means examining the binary and its resources without running the app. No device required, no jailbreak required — just the file.
| Tool | What it does |
|---|---|
| Hopper, IDA Pro, Ghidra | Disassemble and decompile the Mach-O binary. Show functions, strings, cross-references, call graphs. |
| class-dump | Extracts Objective-C class and method declarations from the binary. Less useful for pure Swift, but mixed codebases expose a lot. |
| otool | Inspect Mach-O structure, load commands, linked libraries, and raw disassembly. Ships with Xcode. |
| swift-demangle | Converts mangled Swift symbol names back to readable signatures. |
| strings | Dumps all printable character sequences from the binary. Finds hardcoded values in seconds. |
| PlistBuddy, iExplorer | Read .plist, .json, and other resource files from the unpacked .ipa. |
The strings tool deserves special attention. It requires no expertise, no jailbreak, and no binary analysis knowledge. Running it on your app binary and grepping for anything that looks like a key, token, or URL takes about thirty seconds. If your secrets survive that test, you've cleared the lowest bar.
Dynamic Analysis
Dynamic analysis means attaching to the running process — hooking functions, reading memory, intercepting calls, patching return values.
| Tool | What it does |
|---|---|
| Frida | A dynamic instrumentation toolkit. Injects JavaScript into any running process and lets you hook any function, log arguments, modify return values, and dump memory. Works on jailbroken devices without recompiling anything. |
| Objection | A runtime mobile exploration toolkit built on Frida. Automates common tasks: bypass jailbreak detection, disable SSL pinning, enumerate classes, list keychain entries. |
| LLDB | The standard Xcode debugger. On a jailbroken device, it can attach to any process — including App Store apps — without entitlements. |
| Clutch, frida-ios-dump | Strip the FairPlay DRM encryption layer off a downloaded App Store binary. More on this in the attack chain below. |
| memdump, Frida-trace | Scan process memory at runtime. Can locate decrypted strings that were obfuscated at rest. |
Network Analysis
Network analysis intercepts traffic between the app and its servers. If the app sends a secret to authenticate with a backend, this is where it appears.
| Tool | What it does |
|---|---|
| Charles Proxy, mitmproxy, Burp Suite | Man-in-the-middle HTTPS proxies. Install a CA certificate on the target device and all HTTPS traffic passes through them in plaintext. |
| SSL Kill Switch 3, Frida scripts | Disable the app's certificate validation — including SSL pinning — from the outside. No recompilation required. |
| Wireshark | Packet-level analysis. Useful for non-HTTP protocols or to understand connection patterns before attempting interception. |
The Attack Chain
Here's the sequence in practice.
Step 1: Jailbreak the Device
The attacker starts with a dedicated test device — not their daily phone — and installs a jailbreak. Tools like checkra1n, unc0ver, palera1n, and Dopamine target different iOS versions and chip generations.
What a jailbreak actually does:
- Removes the sandbox restriction — any process can access any other process's files
- Enables root access — full filesystem read/write
- Allows debuggers to attach to any process, including App Store apps
- Makes code signing bypassable — modified binaries can run after re-signing with
ldid - Allows system daemons to be patched — including
amfid, the daemon that enforces App Store code signing
ASLR still runs on a jailbroken device, but it becomes transparent once a debugger is attached: LLDB shows real addresses after rebasing, so the "random" layout is effectively visible.
Step 2: Obtain the .ipa
The attacker downloads your app from the App Store normally. On a jailbroken device, the installed .ipa can be extracted from the filesystem. Alternatively, it can be downloaded via iTunes (for older iOS versions) or Apple Configurator.
The result is your full app bundle: the Mach-O binary, all resource files, .plists, .jsons, databases, and anything else you shipped.
At this point, static analysis on resources is already possible — PlistBuddy and iExplorer can read everything in the bundle without decrypting anything.
Step 3: Decrypt the Binary (FairPlay)
Apps downloaded from the App Store have a DRM layer called FairPlay applied to the binary. Before static analysis can proceed, it needs to be removed.
On a jailbroken device, the app runs normally and the OS decrypts the binary in memory. Tools like Clutch or frida-ios-dump take a snapshot of the decrypted binary from memory and write it back to disk. This is called a "memory dump" of the binary.
After this step, the attacker has the real Mach-O — the same binary that would exist if they built the app from source. Code signing protected it from modification; it said nothing about reading.
Step 4: Static Analysis
With the decrypted Mach-O, the attacker opens it in Hopper, IDA Pro, or Ghidra.
What they're looking for first:
Strings. The simplest pass. Running strings on the binary and filtering for anything resembling an API key, URL, or token. A hardcoded let apiKey = "..." survives as a literal string in the binary's data section. It's visible immediately.
Resource files. Any secrets.json, .plist, or .bundle in the unpacked app is readable as-is. If your entire credential set is in a config file, this step ends the investigation.
Code structure. Hopper and IDA reconstruct the disassembly into pseudo-code. The attacker searches for calls to known frameworks — CryptoKit, URLSession, SecItemAdd — and traces back through the call graph to find where arguments originate. This reveals how secrets are loaded, even if they're not visible as plain strings.
Swift symbol names. swift-demangle converts mangled names like _$s7MyFramework14SecretProviderC6apiKeySSvg into readable declarations. The structure of the code is visible even in a release build.
Step 5: Code Injection and Runtime Logging
If the secret isn't immediately visible statically — because it's obfuscated, derived at runtime, or fetched from the network — the attacker moves to dynamic analysis.
Using Frida or Objection, they inject JavaScript into the running process. A typical script might:
- Hook a specific method and log its arguments every time it's called
- Hook
URLSessionto log request headers and bodies before encryption - Hook the deobfuscation function to log its output — the plaintext secret — as it's computed
- Enumerate all strings in memory that match a pattern (length, character set, format of an API key)
The key insight: it doesn't matter how carefully you stored or derived the secret on disk. When your app calls URLSession.dataTask(with: request) with an Authorization header containing your key, Frida can log that header. The value exists in process memory in plaintext at that moment. No amount of storage-time encryption changes this.
swift// What you write let encryptedKey = bundle.loadEncryptedKey() let key = decrypt(encryptedKey, using: derivedKey) request.addValue("Bearer \(key)", forHTTPHeaderField: "Authorization") // Frida hooks here — key is a plaintext string in the argument
LLDB gives more surgical control. The attacker sets a breakpoint on the decryption function, runs the app to that point, and reads the register or stack containing the plaintext value. If the attacker has done their static analysis well, ASLR is already transparent — LLDB shows real addresses after rebasing, so the breakpoint can be placed precisely.
Step 6: Memory Scanning
After running the app through its normal flows, the attacker uses memdump or Frida scripts to scan the process's memory pages for strings matching the expected format of a secret.
This catches cases where a secret is decrypted into a string variable early in the app's lifecycle and never explicitly zeroed. The value sits in heap memory for the duration of the session. A scan finds it.
This is why secrets that can't be avoided on the client should be zeroed from memory immediately after use — not left in a variable for the lifetime of the object that holds them.
Step 7: Network Interception
If the app fetches secrets from a server at runtime, or if the attacker wants to see exactly what's transmitted, they set up a MITM proxy.
Without SSL pinning, this is trivial: install a trusted CA certificate on the device, route traffic through Charles or mitmproxy, and all HTTPS traffic is visible in plaintext.
With SSL pinning, the attacker uses SSL Kill Switch 3 or a Frida script that hooks the certificate validation call and forces it to return success regardless of what certificate the proxy presented. This doesn't break the TLS encryption — it bypasses the client's refusal to connect. The app happily sends its traffic through the proxy.
Where Apple's Protections Land in This Sequence
Part 1 covered what Apple provides and where the limits are. It's worth mapping those protections to the attack steps directly.
| Apple mechanism | Addressed at step | How |
|---|---|---|
| Sandboxing | Step 1 | Jailbreak removes the sandbox entirely |
| Code Signing | Step 3 | FairPlay decryption via memory dump produces a usable Mach-O; code signing says nothing about reading |
| ASLR | Step 5 | LLDB post-rebase shows real addresses; Frida resolves symbols without caring about ASLR |
| Execute Never | — | Never directly addressed — the attack doesn't inject shellcode, it hooks existing functions |
| ATS / SSL Pinning | Step 7 | SSL Kill Switch or Frida hooks bypass certificate validation |
Execute Never is the one protection that holds throughout this attack chain — but the attack chain doesn't need to bypass it. Frida injects JavaScript that runs in the Frida runtime, not as shellcode in your app's memory pages. It hooks functions by modifying the function pointer indirection mechanisms that already exist. The hardware restriction is never relevant.
The practical takeaway: the attack chain doesn't require bypassing every protection. It routes around the ones that matter, and ignores the ones that don't apply.
What This Means for the Defender
The goal of this walkthrough isn't to be discouraging. Most apps won't face a sophisticated attacker who goes through all seven steps. But the steps exist, the tools are free and well-documented, and "nobody would bother" is a risk tolerance decision, not a security property.
A few things worth internalising from this:
Static analysis is free and instant. strings requires no skill, no device, no jailbreak. Hardcoded secrets are found before an attacker has done anything sophisticated. This is the attack that scales: automated scanners run it against every app ever uploaded to a repository or package registry.
Runtime extraction defeats storage-time encryption. Any secret your app decrypts at runtime can be captured at the moment of decryption. Obfuscation and encryption protect the at-rest representation, not the value that enters a function argument.
Network interception is trivially easy without SSL pinning. If your app fetches secrets from a backend, and you haven't implemented pinning, the MITM attack requires no binary analysis at all.
The attack chain has seven steps. Each one filters out a different class of attacker. The strings pass runs at scale — automated scanners apply it to every app in a repo or registry, and no human expertise is needed. Frida and LLDB require time, a jailbroken device, and genuine skill. These aren't the same threat. Defence in depth works by making sure the cheap attacks find nothing, so only a motivated, targeted attacker reaches the expensive steps — and most don't. An attacker who only has strings and an unpacked .ipa should find nothing. An attacker who has Frida and a running app should find obfuscated, encrypted values. An attacker with LLDB, a jailbroken device, and a complete static analysis — that's a much higher bar than most apps will ever face.
The attack chain also shows the defensive gaps clearly: the binary is trivially readable, memory is readable at the moment of use, and network traffic is readable without pinning.