App Store binaries are encrypted at rest and decrypted by the kernel at load time, so every static tool fails on a downloaded IPA until you dump the decrypted image from memory. That single step gates everything else, and it is the reason most iOS analysis stalls before it starts.
When to Use
- You have an IPA or
.appbundle and need to read the binary class-dumpreturns nothing or garbageotoolshowscryptid 1- You need to enumerate Objective-C classes, Swift types, or exported symbols
- Analyzing embedded frameworks, app extensions, or dynamic libraries
When NOT to Use
- Android targets — use
testing-mobile-applicationsor the relevant
reversing-* skill
- The wider iOS assessment (storage, keychain, IPC) — use
- TLS interception — use
bypassing-mobile-pinning - Jailbreak detection blocking your tooling — use
bypassing-root-jailbreak-detection first
- Cross-platform frameworks — Flutter, Unity, and Xamarin have their own
skills; use them for the managed layer and this one for the native shell
Check Encryption First
otool -l TargetApp | grep -A5 LC_ENCRYPTION_INFO
# cryptid 1 → FairPlay-encrypted, decrypt before anything else
# cryptid 0 → already decrypted (dev build, or you already dumped it)Everything downstream is meaningless on an encrypted binary. class-dump returning nothing is almost always this, not obfuscation.
# Decrypt on a jailbroken device by dumping the loaded image
frida-ios-dump -l # list installed apps
frida-ios-dump com.target.app # produces a decrypted IPA
bagbak com.target.app # alternative, handles extensions/frameworks well
# Rootless jailbreaks and TrollStore installs work with the same tools
# Older devices: Clutch, flexdecryptDump frameworks and extensions too, not just the main binary. PlugIns/ (share sheets, widgets, keyboards) and Frameworks/ carry their own encrypted Mach-Os, and app extensions frequently hold the interesting entitlements and a weaker security posture than the main app.
Mach-O Structure and Protections
file TargetApp # thin or fat/universal
lipo -info TargetApp # architectures present
lipo -thin arm64 TargetApp -o app64 # extract one before analysis
otool -hv app64 # header flags: PIE
otool -l app64 | rg 'LC_LOAD_DYLIB|LC_RPATH|LC_CODE_SIGNATURE|LC_ENCRYPTION'
otool -Iv app64 | head # indirect symbols
nm -m app64 | rg -v ' U ' | head # defined symbols
# Protections at a glance
otool -hv app64 | rg PIE # ASLR
otool -Iv app64 | rg stack_chk # stack canaries
otool -Iv app64 | rg objc_release # ARC in useExtract the entitlements — they define what the app is allowed to do and often reveal the interesting attack surface:
codesign -d --entitlements :- TargetApp.app
# Look for: keychain-access-groups, App Groups, associated-domains,
# get-task-allow (debuggable!), custom URL scheme claimsget-task-allow set to true on a production build means the app is debuggable and is a finding on its own.
Objective-C
Objective-C keeps its full class metadata in the binary, so recovery is excellent — method names, selectors, class hierarchy, ivars.
class-dump -H app64 -o ./headers # classic
class-dump-dyld # for dyld shared cache resident classes
dsdump --objc --color app64 # modern alternative, handles Swift too
# Then read the type map
rg -l 'Manager|Service|API|Auth|Crypto|Keychain|Payment' ./headers | head -20In a disassembler, Objective-C calls go through objc_msgSend, so the callee is a selector string in a register rather than a direct branch. Both IDA and Ghidra have plugins that resolve this; without them the call graph is largely useless.
Swift
Swift is harder. Names are mangled, and method dispatch is often static or through witness tables rather than objc_msgSend.
# Demangle what you find
nm app64 | swift demangle
xcrun swift-demangle '$s10TargetApp11AuthManagerC5loginyyF'
# Swift type metadata lives in dedicated sections
otool -l app64 | rg '__swift5_types|__swift5_proto|__swift5_reflstr'
dsdump --swift app64Practical approach for Swift-heavy apps: @objc and @objcMembers members still appear in the Objective-C metadata, so class-dump gives you a partial map. For the rest, work from string references and the reflection sections rather than trying to recover a full class list. Runtime enumeration with Frida is usually faster than static recovery:
frida -U -f com.target.app -l enumerate-swift-classes.jsRuntime Analysis
# Enumerate everything the runtime knows
objection -g com.target.app explore
# ios hooking list classes
# ios hooking search methods <keyword>
# ios hooking watch method "-[AuthManager login:]" --dump-args --dump-return
# Direct Frida for anything objection does not cover
frida -U -f com.target.app -l hooks.jsRuntime beats static on iOS more often than on other platforms: the Objective-C runtime is introspectable, so listing classes and watching methods gets you to the logic faster than reading a disassembly of objc_msgSend dispatch.
What to Look For
- Entitlements over-provisioned — App Groups shared with less-trusted
extensions, keychain groups shared too widely, get-task-allow in production
- Hardcoded secrets in
__cstringand in the plist files inside the bundle - Weak keychain accessibility — items stored with
kSecAttrAccessibleAlways rather than a ThisDeviceOnly class
- Custom crypto in the binary rather than CryptoKit/CommonCrypto
- Debug and logging code left in release builds
- Insecure
WKWebViewconfiguration —allowFileAccessFromFileURLs,
loading remote content into a JS-bridged view
- Third-party SDKs with their own network stacks and their own pinning
behaviour; enumerate Frameworks/ and check each
Rationalizations to Reject
- "class-dump returned nothing, the app is obfuscated." Check
cryptid
first. It is almost always encryption.
- "I dumped the main binary, that's the app." Extensions and frameworks are
separate Mach-Os with separate entitlements, and are frequently weaker.
- "It's Swift, so nothing is recoverable." Reflection sections,
@objc
members, and string references recover a great deal. Enumerate at runtime.
- "Static analysis is enough." On iOS the runtime is introspectable; skipping
it costs more than it saves.
- "The binary has PIE and canaries, so it's hardened." Those are compiler
defaults, not evidence of a security review.
- "I can't jailbreak, so I can't analyze." You can still read the bundle,
plists, entitlements, and assets, and repackage with a Frida gadget for sideloading. Say what the constraint cost you in the report.
References
testing-mobile-applications— storage, keychain, IPC, and the wider iOS testbypassing-root-jailbreak-detection— when detection blocks your toolingbypassing-mobile-pinning— TLS interception on iOS stacksanalyzing-binaries— deeper native RE of the same Mach-O- frida-ios-dump, bagbak, class-dump, dsdump, objection, Hopper/IDA/Ghidra