secskills
secskills / core / analyzing-ios-binaries

analyzing-ios-binaries

core verified 2026-07-27

Analyze iOS applications at the binary level — decrypting FairPlay-protected IPAs with frida-ios-dump or bagbak, inspecting Mach-O load commands, recovering Objective-C headers with class-dump, and reading Swift metadata. Use when working with an IPA or .app bundle, when a downloaded App Store binary shows cryptid=1, when class-dump returns nothing, or when analyzing iOS frameworks and app extensions.

$ /plugin install secskills-core

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

When NOT to Use

reversing-* skill

testing-mobile-applications

bypassing-root-jailbreak-detection first

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

Dump 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 use

Extract 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 claims

get-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 -20

In 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 app64

Practical 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.js

Runtime 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.js

Runtime 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

extensions, keychain groups shared too widely, get-task-allow in production

kSecAttrAccessibleAlways rather than a ThisDeviceOnly class

loading remote content into a JS-bridged view

behaviour; enumerate Frameworks/ and check each

Rationalizations to Reject

first. It is almost always encryption.

separate Mach-Os with separate entitlements, and are frequently weaker.

members, and string references recover a great deal. Enumerate at runtime.

it costs more than it saves.

defaults, not evidence of a security review.

plists, entitlements, and assets, and repackage with a Frida gadget for sideloading. Say what the constraint cost you in the report.

References