mobile security

Reverse Engineering iOS App with Frida

Reverse engineers iOS applications using Frida dynamic instrumentation to understand internal logic, extract encryption keys, bypass security controls, and discover hidden functionality without source code access. Use when performing authorized iOS penetration testing, analyzing proprietary protocols, understanding obfuscated logic, or extracting runtime secrets from iOS binaries. Activates for requests involving iOS reverse engineering, Frida iOS hooking, Objective-C/Swift method tracing, or iOS binary analysis.

fridaiosmobile-securityowasp-mobilepenetration-testingreverse-engineering
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

Use this skill when:

  • Analyzing iOS app internals during authorized security assessments without source code
  • Extracting encryption keys, API secrets, or proprietary protocol details from running iOS apps
  • Understanding obfuscated Swift/Objective-C logic through runtime method tracing
  • Bypassing complex security mechanisms (jailbreak detection, anti-tampering, anti-debugging)

Do not use this skill for unauthorized reverse engineering that violates terms of service or intellectual property law.

Prerequisites

  • Jailbroken iOS device with Frida server installed via Cydia/Sileo, or non-jailbroken device with Frida Gadget-injected IPA
  • Python 3.10+ with frida-tools (pip install frida-tools)
  • USB connection to iOS device
  • class-dump or dsdump for Objective-C header extraction
  • Hopper Disassembler or Ghidra for static binary analysis (complementary)
  • Knowledge of Objective-C runtime and Swift name mangling

Workflow

Step 1: Extract and Analyze the Binary

# On jailbroken device, find app binary
ssh root@<device_ip>
find /var/containers/Bundle/Application/ -name "TargetApp" -type f
 
# Pull decrypted binary (apps from App Store are encrypted with FairPlay)
# Use frida-ios-dump or Clutch for decryption
pip install frida-ios-dump
dump.py com.target.app
 
# Extract Objective-C class headers
class-dump -H decrypted_binary -o headers/
ls headers/  # Lists all class header files

Step 2: Enumerate Classes and Methods at Runtime

// enumerate_classes.js - List all loaded classes
Java.perform(function() {});  // N/A for iOS
 
// iOS uses ObjC runtime
if (ObjC.available) {
    var classes = ObjC.classes;
    for (var className in classes) {
        if (className.indexOf("Target") !== -1 ||
            className.indexOf("Auth") !== -1 ||
            className.indexOf("Crypto") !== -1) {
            console.log("[Class] " + className);
 
            // List methods
            var methods = classes[className].$ownMethods;
            for (var i = 0; i < methods.length; i++) {
                console.log("  [Method] " + methods[i]);
            }
        }
    }
}
frida -U -n TargetApp -l enumerate_classes.js

Step 3: Trace Method Calls with frida-trace

# Trace all methods of a class
frida-trace -U -n TargetApp -m "*[TargetAuth *]"
 
# Trace specific patterns
frida-trace -U -n TargetApp -m "*[*Crypto* *]"
frida-trace -U -n TargetApp -m "*[*KeyChain* *]"
frida-trace -U -n TargetApp -m "*[*Token* *]"
 
# Trace Swift methods (mangled names)
frida-trace -U -n TargetApp -m "*[*$s*Auth*]"

Step 4: Hook and Modify Method Behavior

// hook_auth.js - Intercept authentication logic
if (ObjC.available) {
    // Hook Objective-C method
    var AuthManager = ObjC.classes.AuthManager;
    if (AuthManager) {
        Interceptor.attach(AuthManager["- validateToken:"].implementation, {
            onEnter: function(args) {
                // args[0] = self, args[1] = selector, args[2+] = method args
                var token = new ObjC.Object(args[2]);
                console.log("[Auth] validateToken called with: " + token.toString());
            },
            onLeave: function(retval) {
                console.log("[Auth] validateToken returned: " + retval);
                // Optionally modify return value
                // retval.replace(ptr(1));  // Force return true
            }
        });
    }
 
    // Hook CommonCrypto for encryption analysis
    var CCCrypt = Module.findExportByName("libcommonCrypto.dylib", "CCCrypt");
    if (CCCrypt) {
        Interceptor.attach(CCCrypt, {
            onEnter: function(args) {
                this.operation = args[0].toInt32();  // 0=encrypt, 1=decrypt
                this.algorithm = args[1].toInt32();  // 0=AES128, 1=DES, 2=3DES
                this.keyLength = args[4].toInt32();
                this.key = Memory.readByteArray(args[3], this.keyLength);
                console.log("[CCCrypt] Op:" + (this.operation === 0 ? "Encrypt" : "Decrypt"));
                console.log("[CCCrypt] Key: " + hexify(this.key));
            },
            onLeave: function(retval) {
                console.log("[CCCrypt] Status: " + retval);
            }
        });
    }
}
 
function hexify(buffer) {
    var bytes = new Uint8Array(buffer);
    var hex = [];
    for (var i = 0; i < bytes.length; i++) {
        hex.push(("0" + bytes[i].toString(16)).slice(-2));
    }
    return hex.join("");
}

Step 5: Analyze Swift Code

// swift_analysis.js - Hook Swift methods
// Swift methods use name mangling: $s<module><class><method>
// Use frida-trace to discover actual mangled names first
 
if (ObjC.available) {
    // Swift classes that inherit from NSObject are accessible via ObjC runtime
    var swiftClasses = Object.keys(ObjC.classes).filter(function(name) {
        return name.indexOf("_TtC") === 0 || name.indexOf("TargetApp.") !== -1;
    });
 
    swiftClasses.forEach(function(className) {
        console.log("[Swift] " + className);
        var methods = ObjC.classes[className].$ownMethods;
        methods.forEach(function(method) {
            console.log("  " + method);
        });
    });
}
 
// For pure Swift (non-ObjC-bridged), use Module.enumerateExports
Module.enumerateExports("TargetApp", {
    onMatch: function(exp) {
        if (exp.name.indexOf("Auth") !== -1 || exp.name.indexOf("Crypto") !== -1) {
            console.log("[Export] " + exp.name + " @ " + exp.address);
        }
    },
    onComplete: function() {}
});

Step 6: Extract Secrets and Proprietary Data

// extract_secrets.js
if (ObjC.available) {
    // Hook NSUserDefaults
    var NSUserDefaults = ObjC.classes.NSUserDefaults;
    Interceptor.attach(NSUserDefaults["- objectForKey:"].implementation, {
        onEnter: function(args) {
            this.key = new ObjC.Object(args[2]).toString();
        },
        onLeave: function(retval) {
            if (retval.isNull()) return;
            var value = new ObjC.Object(retval);
            console.log("[NSUserDefaults] " + this.key + " = " + value.toString());
        }
    });
 
    // Hook Keychain access
    var SecItemCopyMatching = Module.findExportByName("Security", "SecItemCopyMatching");
    Interceptor.attach(SecItemCopyMatching, {
        onEnter: function(args) {
            var query = new ObjC.Object(args[0]);
            console.log("[Keychain] Query: " + query.toString());
        },
        onLeave: function(retval) {
            console.log("[Keychain] Result: " + retval);
        }
    });
}

Key Concepts

Term Definition
Objective-C Runtime Dynamic runtime enabling method dispatch, class introspection, and method swizzling at runtime
Swift Name Mangling Compiler-applied encoding of Swift function signatures into linker-compatible symbol names
FairPlay DRM Apple's encryption applied to App Store binaries; must be decrypted before static analysis
class-dump Tool extracting Objective-C class declarations from Mach-O binaries for header-level analysis
CommonCrypto Apple's C-level cryptographic library; primary target for encryption key extraction via Frida hooks

Tools & Systems

  • Frida: Dynamic instrumentation framework for iOS runtime hooking and method interception
  • frida-trace: Automated tracing utility that generates handler stubs for matched methods
  • frida-ios-dump: Tool for decrypting FairPlay-protected iOS apps via memory dumping
  • class-dump / dsdump: Objective-C header extraction from Mach-O binaries
  • Ghidra: NSA's reverse engineering framework for static ARM64 binary analysis of iOS apps

Common Pitfalls

  • FairPlay encryption: Apps downloaded from the App Store are encrypted. You must decrypt before static analysis. Use frida-ios-dump on a jailbroken device.
  • Swift-only classes: Pure Swift classes without @objc annotation are not visible through ObjC.classes. Use Module.enumerateExports() instead.
  • Stripped binaries: Release builds strip debug symbols. Combine frida-trace with class-dump output for effective analysis.
  • Anti-Frida measures: Sophisticated apps check for Frida artifacts (frida-server process, Frida agent strings in memory, injected libraries in dyld). Use stealthy Frida builds or Frida Gadget injection.
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md1.7 KB

API Reference: iOS App Reverse Engineering with Frida

Frida CLI Tools

Command Description
frida-ps -Ua List running apps on USB device
frida -U -n AppName -e "script" Attach to app and run script
frida -U -f com.app.bundle -l script.js Spawn app with script
frida-trace -U -n AppName -m "*[ClassName *]" Trace ObjC methods
frida-discover -U -n AppName Discover available functions

Frida JavaScript API

API Description
ObjC.classes.ClassName Access Objective-C class
ObjC.classes.Cls.$ownMethods List class methods
Interceptor.attach(target, callbacks) Hook native function
Interceptor.replace(target, replacement) Replace function implementation
Module.findExportByName(null, "func") Find exported C function
ObjC.Object(ptr) Wrap pointer as ObjC object
Memory.readUtf8String(ptr) Read string from memory

Common iOS Security Hooks

Target Purpose
SSLSetPeerDomainName Bypass SSL pinning
NSFileManager fileExistsAtPath: Jailbreak detection
CCCrypt Intercept encryption calls
NSURLSession Monitor network requests
SecItemCopyMatching Keychain access

Python Libraries

Library Version Purpose
subprocess stdlib Execute frida CLI tools
frida >=16.0 Frida Python bindings
json stdlib Report generation

References

standards.md1.4 KB

Standards Reference: iOS Reverse Engineering with Frida

OWASP Mobile Top 10 2024 Mapping

OWASP ID Risk RE Assessment
M1 Improper Credential Usage Extract hardcoded keys via runtime hooking
M7 Insufficient Binary Protections Assess anti-RE measures (obfuscation, anti-debug, anti-Frida)
M10 Insufficient Cryptography Hook CommonCrypto to extract keys and observe algorithms

OWASP MASVS v2.0 - MASVS-RESILIENCE Controls

Control Description Frida Test Method
MASVS-RESILIENCE-1 App detects and responds to reverse engineering Test with Frida attachment, observe detection
MASVS-RESILIENCE-2 App detects tampering Modify binary, observe integrity checks
MASVS-RESILIENCE-3 App uses obfuscation Assess class/method name readability
MASVS-RESILIENCE-4 App detects debuggers Attach debugger, check ptrace/sysctl hooks

CWE Mappings

CWE ID Title RE Discovery Method
CWE-798 Use of Hard-coded Credentials Hook string initialization, NSUserDefaults access
CWE-321 Use of Hard-coded Cryptographic Key Hook CCCrypt, SecKeyCreateWithData
CWE-327 Broken Crypto Algorithm Observe algorithm parameter in CCCrypt calls
CWE-693 Protection Mechanism Failure Bypass jailbreak detection, Frida detection
workflows.md1.7 KB

Workflows: iOS Reverse Engineering with Frida

Workflow 1: Full iOS RE Pipeline

[Obtain IPA/binary] --> [Decrypt FairPlay] --> [Static analysis] --> [Dynamic analysis]
                              |                      |                      |
                       [frida-ios-dump]        [class-dump]          [frida-trace]
                       [Clutch]                [Ghidra]              [Custom hooks]
                                               [Hopper]             [Method interception]
                                                                           |
                                                                    [Extract secrets]
                                                                    [Map logic flow]
                                                                    [Document findings]

Workflow 2: Crypto Key Extraction

[Hook CommonCrypto] --> [Capture CCCrypt calls] --> [Extract key material]
                              |
                       [Log algorithm, mode, IV]
                       [Log input/output data]
                              |
                       [Reconstruct protocol]
                       [Document encryption scheme]

Decision Matrix: iOS RE Approach

Binary Type Static Tool Dynamic Tool Notes
Objective-C class-dump + Ghidra Frida ObjC.classes Full runtime visibility
Swift (NSObject-based) dsdump + Ghidra Frida ObjC.classes Partial visibility
Pure Swift Ghidra + Swift demangling Frida Module.enumerateExports Limited runtime access
C/C++ native Ghidra Frida Interceptor.attach Address-based hooking

Scripts 2

agent.py5.9 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for iOS app reverse engineering with Frida.

Uses frida-tools to attach to iOS processes, hook Objective-C
methods, bypass SSL pinning, dump keychain entries, and trace
API calls for security assessment.
"""

import subprocess
import json
import sys
from datetime import datetime
from pathlib import Path


class FridaIOSAgent:
    """Reverse engineers iOS applications using Frida."""

    def __init__(self, target_app, device_id=None, output_dir="./frida_ios"):
        self.target_app = target_app
        self.device_id = device_id
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.findings = []

    def _frida_cmd(self, script_code, timeout=60):
        cmd = ["frida", "-U"]
        if self.device_id:
            cmd.extend(["-D", self.device_id])
        cmd.extend(["-n", self.target_app, "-q", "-e", script_code])
        try:
            result = subprocess.run(cmd, capture_output=True, text=True,
                                    timeout=timeout)
            return {"stdout": result.stdout, "stderr": result.stderr,
                    "returncode": result.returncode}
        except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
            return {"error": str(exc)}

    def list_running_apps(self):
        """List running applications on the connected iOS device."""
        cmd = ["frida-ps", "-Ua"]
        if self.device_id:
            cmd.extend(["-D", self.device_id])
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
            return {"apps": result.stdout}
        except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
            return {"error": str(exc)}

    def bypass_ssl_pinning(self):
        """Inject Frida script to bypass SSL certificate pinning."""
        script = """
var m = ObjC.classes.NSURLSessionConfiguration;
Interceptor.attach(m['- setTLSMinimumSupportedProtocol:'].implementation, {
    onEnter: function(args) { console.log('[*] TLS config intercepted'); }
});
try {
    var SSLSetPeerDomainName = Module.findExportByName(null, 'SSLSetPeerDomainName');
    if (SSLSetPeerDomainName) {
        Interceptor.attach(SSLSetPeerDomainName, {
            onEnter: function(args) { },
            onLeave: function(retval) { retval.replace(0); }
        });
        console.log('[+] SSL pinning bypassed');
    }
} catch(e) { console.log('[-] ' + e); }
"""
        result = self._frida_cmd(script)
        if "SSL pinning bypassed" in result.get("stdout", ""):
            self.findings.append({"type": "SSL Pinning Bypass",
                                  "severity": "Medium",
                                  "details": "SSL pinning can be bypassed with Frida"})
        return result

    def dump_keychain(self):
        """Dump iOS Keychain entries accessible by the app."""
        script = """
var kSecClass = ObjC.classes.NSString.stringWithString_('kSecClass');
var query = ObjC.classes.NSMutableDictionary.dictionary();
query.setObject_forKey_(ObjC.classes.NSString.stringWithString_('kSecClassGenericPassword'), kSecClass);
query.setObject_forKey_(ObjC.classes.NSNumber.numberWithBool_(true), ObjC.classes.NSString.stringWithString_('kSecReturnAttributes'));
query.setObject_forKey_(ObjC.classes.NSString.stringWithString_('kSecMatchLimitAll'), ObjC.classes.NSString.stringWithString_('kSecMatchLimit'));
var result = new ObjC.Object(ptr(0));
var status = ObjC.classes.NSDictionary.alloc();
console.log('[*] Keychain query executed');
"""
        result = self._frida_cmd(script)
        return result

    def trace_objc_methods(self, class_name):
        """Trace all method calls on a specific Objective-C class."""
        script = f"""
var target = ObjC.classes.{class_name};
if (target) {{
    var methods = target.$ownMethods;
    console.log('[*] Tracing ' + methods.length + ' methods on {class_name}');
    methods.forEach(function(method) {{
        try {{
            Interceptor.attach(target[method].implementation, {{
                onEnter: function(args) {{
                    console.log('[CALL] {class_name} ' + method);
                }}
            }});
        }} catch(e) {{}}
    }});
}} else {{
    console.log('[-] Class {class_name} not found');
}}
"""
        return self._frida_cmd(script, timeout=30)

    def check_jailbreak_detection(self):
        """Test if app has jailbreak detection and attempt bypass."""
        script = """
var paths = ['/Applications/Cydia.app', '/usr/sbin/sshd',
             '/bin/bash', '/usr/bin/ssh', '/etc/apt'];
var NSFileManager = ObjC.classes.NSFileManager;
Interceptor.attach(NSFileManager['- fileExistsAtPath:'].implementation, {
    onEnter: function(args) {
        var path = ObjC.Object(args[2]).toString();
        for (var i = 0; i < paths.length; i++) {
            if (path.indexOf(paths[i]) !== -1) {
                console.log('[*] Jailbreak check: ' + path);
            }
        }
    },
    onLeave: function(retval) {}
});
console.log('[+] Jailbreak detection hooks installed');
"""
        result = self._frida_cmd(script, timeout=15)
        if "Jailbreak check" in result.get("stdout", ""):
            self.findings.append({"type": "Jailbreak Detection Present",
                                  "severity": "Info"})
        return result

    def generate_report(self):
        report = {
            "target_app": self.target_app,
            "report_date": datetime.utcnow().isoformat(),
            "findings": self.findings,
        }
        report_path = self.output_dir / "frida_ios_report.json"
        with open(report_path, "w") as f:
            json.dump(report, f, indent=2)
        print(json.dumps(report, indent=2))
        return report


def main():
    app = sys.argv[1] if len(sys.argv) > 1 else "TargetApp"
    agent = FridaIOSAgent(app)
    agent.list_running_apps()
    agent.bypass_ssl_pinning()
    agent.check_jailbreak_detection()
    agent.generate_report()


if __name__ == "__main__":
    main()
process.py4.0 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
iOS Reverse Engineering Automation with Frida

Automates class enumeration, method tracing, and secret extraction from iOS apps.

Usage:
    python process.py --app TargetApp [--output report.json]
"""

import argparse
import json
import subprocess
import sys
from datetime import datetime


ENUMERATE_SCRIPT = """
if (ObjC.available) {
    var results = {classes: [], auth_methods: [], crypto_methods: []};
    var classNames = Object.keys(ObjC.classes);
    classNames.forEach(function(name) {
        if (name.indexOf("Auth") !== -1 || name.indexOf("Crypto") !== -1 ||
            name.indexOf("Token") !== -1 || name.indexOf("Key") !== -1 ||
            name.indexOf("Secret") !== -1 || name.indexOf("Login") !== -1) {
            var methods = ObjC.classes[name].$ownMethods;
            results.classes.push({name: name, method_count: methods.length});
            methods.forEach(function(m) {
                if (m.toLowerCase().indexOf("auth") !== -1 || m.toLowerCase().indexOf("login") !== -1) {
                    results.auth_methods.push(name + " " + m);
                }
                if (m.toLowerCase().indexOf("encrypt") !== -1 || m.toLowerCase().indexOf("decrypt") !== -1 ||
                    m.toLowerCase().indexOf("key") !== -1 || m.toLowerCase().indexOf("cipher") !== -1) {
                    results.crypto_methods.push(name + " " + m);
                }
            });
        }
    });
    send(JSON.stringify(results));
} else {
    send(JSON.stringify({error: "ObjC runtime not available"}));
}
"""


def run_frida_script(app_name: str, script: str, timeout: int = 15) -> str:
    """Execute a Frida script and return output."""
    cmd = ["frida", "-U", "-n", app_name, "-e", script, "--no-pause"]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
        return result.stdout
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return ""


def check_binary_protections(app_name: str) -> dict:
    """Check for binary protections."""
    checks = {
        "pie": False,
        "stack_canary": False,
        "arc": False,
        "encrypted": False,
    }

    # Use otool-equivalent checks via Frida
    script = """
    var modules = Process.enumerateModules();
    var main = modules[0];
    send(JSON.stringify({
        name: main.name,
        base: main.base.toString(),
        size: main.size,
        path: main.path
    }));
    """
    output = run_frida_script(app_name, script)
    return checks


def main():
    parser = argparse.ArgumentParser(description="iOS RE Automation with Frida")
    parser.add_argument("--app", required=True, help="Target app process name")
    parser.add_argument("--output", default="ios_re_report.json", help="Output report")
    args = parser.parse_args()

    print(f"[+] Enumerating classes and methods for {args.app}...")
    enum_output = run_frida_script(args.app, ENUMERATE_SCRIPT)

    # Parse results
    try:
        lines = [l for l in enum_output.split("\n") if l.startswith('{"') or l.startswith("[")]
        results = json.loads(lines[0]) if lines else {}
    except (json.JSONDecodeError, IndexError):
        results = {"classes": [], "auth_methods": [], "crypto_methods": []}

    report = {
        "assessment": {
            "target": args.app,
            "type": "iOS Reverse Engineering",
            "date": datetime.now().isoformat(),
        },
        "enumeration": {
            "security_classes": results.get("classes", []),
            "auth_methods": results.get("auth_methods", []),
            "crypto_methods": results.get("crypto_methods", []),
        },
        "binary_protections": check_binary_protections(args.app),
    }

    with open(args.output, "w") as f:
        json.dump(report, f, indent=2)
    print(f"[+] Report saved: {args.output}")
    print(f"[*] Found {len(results.get('classes', []))} security-related classes")
    print(f"[*] Found {len(results.get('auth_methods', []))} auth methods")
    print(f"[*] Found {len(results.get('crypto_methods', []))} crypto methods")


if __name__ == "__main__":
    main()

Assets 1

template.mdtext/markdown · 1.1 KB
Keep exploring