mobile security

Performing iOS App Security Assessment

Performs comprehensive iOS application security assessments using Frida for dynamic instrumentation, Objection for runtime exploration, SSL pinning bypass for traffic interception, keychain extraction for credential analysis, and IPA static analysis for binary-level review. Use when conducting authorized iOS penetration tests, evaluating mobile app security posture against OWASP MASTG, or assessing iOS app data protection and transport security controls. Activates for requests involving iOS app pentesting, Frida-based iOS instrumentation, mobile app SSL pinning bypass, or IPA reverse engineering.

fridaiosipa-analysiskeychainmobile-securityobjectionowasp-mastgssl-pinning
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Disclaimer

This skill is intended for authorized security testing, penetration testing engagements, CTF competitions, and educational purposes only. Unauthorized access to applications or devices is illegal. Always obtain written authorization before performing any security assessment. Misuse of these techniques may violate computer fraud and abuse laws in your jurisdiction.

When to Use

Use this skill when:

  • Conducting authorized penetration tests of iOS applications against OWASP MASVS/MASTG criteria
  • Performing dynamic analysis of iOS apps using Frida instrumentation and Objection runtime exploration
  • Bypassing SSL/TLS certificate pinning to intercept and analyze app network traffic through a proxy
  • Extracting and auditing iOS Keychain contents for insecure credential storage practices
  • Performing static analysis of IPA packages to identify hardcoded secrets, entitlements, and binary protections
  • Assessing jailbreak detection and anti-tampering controls in iOS applications

Do not use against applications without explicit written authorization. Do not use on production devices containing real user data unless the engagement scope permits it.

Prerequisites

  • Python 3.10+ with pip
  • Frida toolkit: pip install frida-tools frida
  • Objection: pip install objection
  • Target iOS device (jailbroken with frida-server, or non-jailbroken with patched IPA)
  • macOS with Xcode command-line tools (recommended for code signing and ideviceinstaller)
  • Burp Suite or mitmproxy for traffic interception after SSL pinning bypass
  • For jailbroken devices: SSH access and frida-server running on the device
  • For non-jailbroken devices: Apple Developer certificate for IPA re-signing

Workflow

Step 1: IPA Static Analysis

Extract and analyze the IPA binary before runtime testing:

# Unzip IPA for static analysis
unzip target.ipa -d target_app/
 
# Check binary architectures and protections
otool -hv target_app/Payload/*.app/AppExecutable
otool -l target_app/Payload/*.app/AppExecutable | grep -A4 LC_ENCRYPTION
 
# Extract Info.plist for entitlements and URL schemes
plutil -p target_app/Payload/*.app/Info.plist
 
# Search for hardcoded secrets in binary strings
strings target_app/Payload/*.app/AppExecutable | grep -iE "api[_-]?key|secret|password|token|firebase"
 
# Check embedded provisioning profile
security cms -D -i target_app/Payload/*.app/embedded.mobileprovision
 
# Identify linked frameworks
otool -L target_app/Payload/*.app/AppExecutable

Step 2: Environment Setup and Frida Attachment

# For jailbroken device: verify Frida server is running
frida-ps -U
 
# Spawn target app with Frida
frida -U -f com.target.app --no-pause
 
# For non-jailbroken device: patch IPA with Frida Gadget
objection patchipa --source target.ipa --codesign-signature "Apple Development: tester@example.com"
 
# Install patched IPA
ideviceinstaller -i target-patched.ipa
 
# Attach Objection to running app
objection --gadget "com.target.app" explore

Step 3: SSL Pinning Bypass

Bypass certificate pinning to enable traffic interception:

# Using Objection's built-in bypass
objection --gadget "com.target.app" explore --startup-command "ios sslpinning disable"
 
# Using Frida script for more comprehensive bypass
frida -U -f com.target.app -l ssl_pinning_bypass.js --no-pause
 
# Verify bypass by configuring device proxy to Burp Suite
# Device Settings -> Wi-Fi -> HTTP Proxy -> Manual -> <burp_ip>:8080
# Install Burp CA certificate on device via http://<burp_ip>:8080/cert

The Frida SSL pinning bypass script hooks into NSURLSession, NSURLConnection, and AFNetworking/Alamofire trust evaluation delegates to override certificate validation at the TLS handshake level.

Step 4: Keychain Extraction and Credential Analysis

# Dump all accessible keychain items via Objection
ios keychain dump
 
# Dump keychain with raw data output
ios keychain dump --json
 
# Check keychain item accessibility attributes
# Items with kSecAttrAccessibleAlways or kSecAttrAccessibleAfterFirstUnlock
# are accessible without device unlock - this is a finding
 
# Search for specific credential types
ios keychain dump | grep -i "password\|token\|secret\|oauth"
 
# Inspect NSUserDefaults for sensitive data leaks
ios nsuserdefaults get
 
# Check for sensitive data in app cookies
ios cookies get

Step 5: Runtime Method Hooking and Analysis

# List all loaded classes
ios hooking list classes
 
# Search for security-relevant classes
ios hooking search classes Auth
ios hooking search classes Crypto
ios hooking search classes Biometric
ios hooking search classes Jailbreak
 
# Hook authentication methods to observe parameters and return values
ios hooking watch method "+[AuthManager validateCredentials:password:]" --dump-args --dump-return
 
# Monitor biometric authentication (LocalAuthentication framework)
ios hooking watch class LAContext
 
# Bypass jailbreak detection
ios jailbreak disable
 
# Search memory for sensitive strings
memory search "Bearer " --string
memory search "password" --string
 
# Dump loaded modules for third-party library identification
memory list modules

Step 6: Data Storage Assessment

# List files in app sandbox
env
 
# Check for SQLite databases with sensitive data
sqlite connect Documents/app.db
sqlite execute query "SELECT name FROM sqlite_master WHERE type='table'"
 
# Inspect plist files for cached credentials
ios plist cat Library/Preferences/com.target.app.plist
 
# Check for sensitive data in app caches
find Library/Caches/ -type f
 
# Monitor pasteboard for credential leakage
ios pasteboard monitor
 
# Check binary cookies
ios cookies get

Step 7: Network and Transport Security Assessment

After SSL pinning bypass, analyze intercepted traffic:

# Verify App Transport Security (ATS) configuration in Info.plist
# Check for NSAllowsArbitraryLoads = true (disables ATS)
ios plist cat Info.plist | grep -A5 NSAppTransportSecurity
 
# Hook URL session delegates to monitor all network calls
ios hooking watch class NSURLSession
ios hooking watch class NSURLSessionConfiguration
 
# Check for certificate transparency validation
ios hooking search classes CT
ios hooking search classes Certificate

Key Concepts

Term Definition
Frida Dynamic instrumentation toolkit that injects a JavaScript engine into target processes, enabling runtime hooking, tracing, and modification of iOS app behavior
Objection Runtime mobile exploration toolkit built on Frida providing pre-built commands for common security tests including keychain dump, SSL pinning bypass, and method hooking
SSL Pinning Client-side certificate validation that restricts which TLS certificates the app trusts, preventing proxy-based traffic interception; bypassed by hooking trust evaluation functions
Keychain iOS secure storage API for credentials and tokens; items have accessibility attributes that control when they can be read (e.g., only when device is unlocked)
IPA iOS App Store Package; a ZIP archive containing the app binary, frameworks, assets, and provisioning profile that can be extracted for static analysis
OWASP MASTG Mobile Application Security Testing Guide; comprehensive methodology for iOS and Android security testing organized by MASVS verification categories
Frida Gadget Shared library (.dylib) injected into IPA for non-jailbroken testing; enables Frida instrumentation without requiring a jailbroken device
Method Swizzling Objective-C runtime technique that exchanges method implementations at runtime; used by Frida to intercept and modify method behavior

Tools & Systems

  • Frida: Dynamic instrumentation framework for injecting JavaScript into native app processes at runtime
  • Objection: High-level Frida-powered mobile security toolkit with pre-built exploration commands
  • frida-tools: CLI utilities including frida-ps (process listing), frida-trace (method tracing), frida-discover (API discovery)
  • Burp Suite: HTTP/HTTPS interception proxy used to analyze app traffic after SSL pinning bypass
  • ideviceinstaller: Cross-platform CLI tool for installing and managing iOS apps over USB
  • otool / rabin2: Binary analysis tools for inspecting Mach-O headers, linked libraries, and encryption info
  • Cycript / Frida REPL: Interactive consoles for exploring Objective-C runtime and modifying objects in memory

Common Pitfalls

  • Frida detection crashes the app: Some apps implement Frida detection by scanning for frida-server process names, Frida's RPC ports, or gadget signatures. Use --startup-command to hook detection checks before they execute, or rename frida-server binary.
  • Keychain scope limitation: Objection can only access keychain items within the app's keychain access group. System-wide keychain items require jailbreak-level tools like keychain-dumper.
  • Swift name mangling: Swift method names are mangled in the Objective-C runtime. Use ios hooking list classes and grep for demangled names, or use frida-trace with wildcard patterns.
  • App Transport Security enforcement: ATS may block your proxy connections even after SSL pinning bypass. Verify the Info.plist ATS configuration allows your proxy's certificate chain.
  • Code signing invalidation: Patching an IPA with Frida Gadget invalidates the original code signature. You need a valid Apple Developer certificate to re-sign the patched IPA.
  • Non-persistent modifications: All Frida/Objection hooks are runtime-only and reset when the app restarts. Document findings and capture evidence immediately.

Output Format

## Finding: Insecure Keychain Storage with kSecAttrAccessibleAlways
 
**ID**: IOS-001
**Severity**: High (CVSS 7.5)
**OWASP MASTG**: MASTG-TEST-0055 (Testing Data Storage)
**MASVS Category**: MASVS-STORAGE
 
**Description**:
The application stores OAuth refresh tokens in the iOS Keychain with
the accessibility attribute kSecAttrAccessibleAlways, making them
readable even when the device is locked or after a reboot without
user authentication.
 
**Proof of Concept**:
1. Attach Objection to com.target.app: objection --gadget com.target.app explore
2. Execute: ios keychain dump
3. Observe refresh_token item with Accessible: kSecAttrAccessibleAlways
4. Token value is accessible without device unlock
 
**Impact**:
An attacker with physical access to a locked device or forensic
image can extract OAuth refresh tokens and gain persistent access
to the user's account without knowing device passcode.
 
**Remediation**:
Store sensitive credentials with kSecAttrAccessibleWhenUnlockedThisDeviceOnly
and enable biometric protection via kSecAccessControlBiometryCurrentSet.
Source materials

References and resources

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

References 1

api-reference.md4.2 KB

API Reference: iOS App Security Assessment Agent

Overview

Automates iOS application security testing using Frida dynamic instrumentation, Objection runtime exploration, SSL pinning bypass, keychain extraction, and IPA static analysis. Covers OWASP MASVS categories including STORAGE, NETWORK, AUTH, RESILIENCE, and PLATFORM. For authorized penetration testing only.

Dependencies

Package Version Purpose
frida >=16.0 Dynamic instrumentation framework for iOS process injection
frida-tools >=12.0 CLI utilities (frida-ps, frida-trace) for device interaction
objection >=1.11 High-level Frida-powered mobile security exploration toolkit

CLI Usage

# Static IPA analysis only
python agent.py --ipa target.ipa --output-dir ./analysis
 
# Dynamic testing with SSL pinning bypass and keychain dump
python agent.py --bundle-id com.target.app --ssl-bypass --keychain --output report.json
 
# Full assessment with jailbreak bypass
python agent.py --bundle-id com.target.app --ipa target.ipa \
  --ssl-bypass --keychain --jailbreak-bypass \
  --device usb --frida-timeout 45 --output full_report.json

Arguments

Argument Required Description
--bundle-id Conditional Target app bundle identifier for dynamic testing
--ipa Conditional Path to IPA file for static analysis
--device No Frida device type: usb, remote, local (default: usb)
--ssl-bypass No Execute SSL pinning bypass Frida script
--keychain No Dump and analyze keychain item security
--jailbreak-bypass No Execute jailbreak detection bypass script
--frida-timeout No Frida script execution timeout in seconds (default: 30)
--output No Output report file path (default: ios_assessment_report.json)
--output-dir No Directory for IPA extraction artifacts (default: .)

At least one of --bundle-id or --ipa is required.

Key Functions

analyze_ipa_static(ipa_path, output_dir)

Extracts and statically analyzes an IPA package. Checks Info.plist for ATS configuration, URL schemes, and background modes. Scans binary strings for hardcoded API keys, secrets, AWS credentials, Firebase URLs, and private keys. Inspects provisioning profile for debug entitlements.

run_frida_script(target_bundle, script_source, device_type, timeout_sec)

Executes a Frida JavaScript payload against a target iOS app. Attempts to attach to a running process first, falls back to spawning the app. Collects messages sent from the Frida script via the send() API.

run_objection_command(bundle_id, command)

Runs a single Objection command against the target app using subprocess. Returns stdout, stderr, and return code. Handles timeout and missing installation gracefully.

assess_keychain_security(bundle_id)

Dumps keychain items via Objection and analyzes accessibility attributes. Flags items using insecure attributes (kSecAttrAccessibleAlways, kSecAttrAccessibleAfterFirstUnlock) and passwords lacking biometric/passcode access control.

generate_report(findings, target_app, output_path)

Aggregates all findings into a JSON report with severity breakdown (critical/high/medium/low) and metadata including timestamp and target identifier.

Frida Script Payloads

Script Target APIs Purpose
SSL_PINNING_BYPASS_SCRIPT SecTrustEvaluate, SecTrustEvaluateWithError, AFSecurityPolicy, TSKPinningValidator Bypasses certificate pinning across system and third-party frameworks
KEYCHAIN_DUMP_SCRIPT SecItemCopyMatching Enumerates keychain item classes and counts accessible items
JAILBREAK_DETECTION_BYPASS_SCRIPT NSFileManager, UIApplication canOpenURL, fork() Hides jailbreak indicators from filesystem, URL scheme, and process checks

OWASP MASVS Coverage

MASVS Category Tests Functions
MASVS-STORAGE MASTG-TEST-0055, 0058 assess_keychain_security, analyze_ipa_static
MASVS-NETWORK MASTG-TEST-0066, 0068 SSL pinning bypass, ATS configuration check
MASVS-RESILIENCE MASTG-TEST-0079, 0083 Jailbreak bypass, debug entitlement check
MASVS-PLATFORM MASTG-TEST-0075 URL scheme analysis

Scripts 1

agent.py23.4 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
# For authorized penetration testing and lab environments only
"""iOS App Security Assessment Agent - Automates Frida-based iOS security testing including
SSL pinning bypass, keychain extraction, IPA static analysis, and runtime method hooking."""

import argparse
import json
import logging
import os
import plistlib
import re
import subprocess
import sys
import zipfile
from datetime import datetime
from pathlib import Path

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

# Frida JavaScript payloads for iOS instrumentation
SSL_PINNING_BYPASS_SCRIPT = r"""
'use strict';

// Hook NSURLSessionDelegate certificate challenge methods
if (ObjC.available) {
    try {
        // Bypass NSURLSession certificate pinning
        var NSURLSessionTask = ObjC.classes.NSURLSessionTask;
        var resolveMethod = ObjC.classes.NSURLSession['- URLSession:didReceiveChallenge:completionHandler:'];

        Interceptor.attach(
            ObjC.classes.NSURLSession['- URLSession:didReceiveChallenge:completionHandler:'].implementation, {
            onEnter: function(args) {
                send('[SSL Bypass] Intercepted NSURLSession challenge');
            }
        });
    } catch(e) {}

    // Hook SecTrustEvaluate to always return success
    try {
        var SecTrustEvaluate = Module.findExportByName('Security', 'SecTrustEvaluate');
        if (SecTrustEvaluate) {
            Interceptor.attach(SecTrustEvaluate, {
                onLeave: function(retval) {
                    retval.replace(0); // errSecSuccess
                    send('[SSL Bypass] SecTrustEvaluate -> success');
                }
            });
        }
    } catch(e) {}

    // Hook SecTrustEvaluateWithError (iOS 12+)
    try {
        var SecTrustEvaluateWithError = Module.findExportByName('Security', 'SecTrustEvaluateWithError');
        if (SecTrustEvaluateWithError) {
            Interceptor.attach(SecTrustEvaluateWithError, {
                onLeave: function(retval) {
                    retval.replace(1); // true = trusted
                    send('[SSL Bypass] SecTrustEvaluateWithError -> trusted');
                }
            });
        }
    } catch(e) {}

    // Hook SecTrustGetTrustResult
    try {
        var SecTrustGetTrustResult = Module.findExportByName('Security', 'SecTrustGetTrustResult');
        if (SecTrustGetTrustResult) {
            Interceptor.attach(SecTrustGetTrustResult, {
                onLeave: function(retval) {
                    retval.replace(0);
                    send('[SSL Bypass] SecTrustGetTrustResult -> proceed');
                }
            });
        }
    } catch(e) {}

    // Bypass AFNetworking pinning
    try {
        var AFSecurityPolicy = ObjC.classes.AFSecurityPolicy;
        if (AFSecurityPolicy) {
            Interceptor.attach(AFSecurityPolicy['- setSSLPinningMode:'].implementation, {
                onEnter: function(args) {
                    args[2] = ptr(0); // AFSSLPinningModeNone
                    send('[SSL Bypass] AFNetworking pinning disabled');
                }
            });
        }
    } catch(e) {}

    // Bypass TrustKit pinning
    try {
        var TrustKit = ObjC.classes.TSKPinningValidator;
        if (TrustKit) {
            Interceptor.attach(TrustKit['- evaluateTrust:forHostname:'].implementation, {
                onLeave: function(retval) {
                    retval.replace(0); // TSKTrustEvaluationSuccess
                    send('[SSL Bypass] TrustKit pinning bypassed');
                }
            });
        }
    } catch(e) {}

    send('[SSL Bypass] All hooks installed successfully');
} else {
    send('[SSL Bypass] Objective-C runtime not available');
}
"""

KEYCHAIN_DUMP_SCRIPT = r"""
'use strict';

if (ObjC.available) {
    var SecItemCopyMatching = Module.findExportByName('Security', 'SecItemCopyMatching');
    var results = [];

    // Enumerate keychain item classes
    var kSecClasses = [
        'genp',  // kSecClassGenericPassword
        'inet',  // kSecClassInternetPassword
        'cert',  // kSecClassCertificate
        'keys',  // kSecClassKey
        'idnt'   // kSecClassIdentity
    ];

    var classNames = {
        'genp': 'GenericPassword',
        'inet': 'InternetPassword',
        'cert': 'Certificate',
        'keys': 'CryptoKey',
        'idnt': 'Identity'
    };

    for (var i = 0; i < kSecClasses.length; i++) {
        try {
            var query = ObjC.classes.NSMutableDictionary.alloc().init();
            query.setObject_forKey_(kSecClasses[i], 'class');
            query.setObject_forKey_(ObjC.classes.__NSCFBoolean.numberWithBool_(true), 'r_Ref');
            query.setObject_forKey_('m_LimitAll', 'm_Limit');
            query.setObject_forKey_(ObjC.classes.__NSCFBoolean.numberWithBool_(true), 'r_Data');
            query.setObject_forKey_(ObjC.classes.__NSCFBoolean.numberWithBool_(true), 'r_Attributes');

            var resultPtr = Memory.alloc(Process.pointerSize);
            var status = new NativeFunction(SecItemCopyMatching, 'int', ['pointer', 'pointer']);
            var ret = status(query.handle, resultPtr);

            if (ret === 0) {
                var resultObj = new ObjC.Object(resultPtr.readPointer());
                send({
                    type: 'keychain',
                    class: classNames[kSecClasses[i]],
                    count: resultObj.count ? resultObj.count() : 1
                });
            }
        } catch(e) {
            send({type: 'keychain_error', class: classNames[kSecClasses[i]], error: e.toString()});
        }
    }
    send({type: 'keychain_complete'});
}
"""

JAILBREAK_DETECTION_BYPASS_SCRIPT = r"""
'use strict';

if (ObjC.available) {
    // Hook NSFileManager fileExistsAtPath: to hide jailbreak indicators
    var NSFileManager = ObjC.classes.NSFileManager;
    var fileExistsAtPath = NSFileManager['- fileExistsAtPath:'];

    var jailbreakPaths = [
        '/Applications/Cydia.app',
        '/Applications/Sileo.app',
        '/Library/MobileSubstrate/MobileSubstrate.dylib',
        '/bin/bash', '/usr/sbin/sshd', '/etc/apt',
        '/usr/bin/ssh', '/private/var/lib/apt/',
        '/private/var/lib/cydia', '/private/var/tmp/cydia.log',
        '/var/lib/dpkg/info', '/usr/libexec/cydia/'
    ];

    Interceptor.attach(fileExistsAtPath.implementation, {
        onEnter: function(args) {
            this.path = ObjC.Object(args[2]).toString();
        },
        onLeave: function(retval) {
            for (var i = 0; i < jailbreakPaths.length; i++) {
                if (this.path.indexOf(jailbreakPaths[i]) !== -1) {
                    retval.replace(0); // false
                    send('[JB Bypass] Hidden: ' + this.path);
                    break;
                }
            }
        }
    });

    // Hook canOpenURL to block cydia:// scheme detection
    var UIApplication = ObjC.classes.UIApplication;
    Interceptor.attach(UIApplication['- canOpenURL:'].implementation, {
        onEnter: function(args) {
            this.url = ObjC.Object(args[2]).toString();
        },
        onLeave: function(retval) {
            if (this.url.indexOf('cydia://') !== -1 || this.url.indexOf('sileo://') !== -1) {
                retval.replace(0);
                send('[JB Bypass] Blocked URL scheme: ' + this.url);
            }
        }
    });

    // Hook fork() to prevent fork-based jailbreak detection
    var fork = Module.findExportByName(null, 'fork');
    if (fork) {
        Interceptor.attach(fork, {
            onLeave: function(retval) {
                retval.replace(-1); // fork fails on non-jailbroken
                send('[JB Bypass] fork() returned -1');
            }
        });
    }

    send('[JB Bypass] All jailbreak detection bypasses installed');
}
"""


def analyze_ipa_static(ipa_path, output_dir):
    """Perform static analysis on an IPA file."""
    findings = []
    extract_dir = os.path.join(output_dir, "ipa_extracted")

    if not zipfile.is_zipfile(ipa_path):
        logger.error("Not a valid IPA/ZIP file: %s", ipa_path)
        return findings

    with zipfile.ZipFile(ipa_path, "r") as zf:
        zf.extractall(extract_dir)
    logger.info("Extracted IPA to %s", extract_dir)

    # Find the .app directory
    payload_dir = os.path.join(extract_dir, "Payload")
    if not os.path.isdir(payload_dir):
        logger.error("No Payload directory found in IPA")
        return findings

    app_dirs = [d for d in os.listdir(payload_dir) if d.endswith(".app")]
    if not app_dirs:
        logger.error("No .app bundle found in Payload")
        return findings

    app_path = os.path.join(payload_dir, app_dirs[0])
    logger.info("Analyzing app bundle: %s", app_dirs[0])

    # Parse Info.plist
    info_plist_path = os.path.join(app_path, "Info.plist")
    if os.path.exists(info_plist_path):
        with open(info_plist_path, "rb") as f:
            try:
                plist_data = plistlib.load(f)
            except Exception:
                plist_data = {}

        # Check App Transport Security configuration
        ats = plist_data.get("NSAppTransportSecurity", {})
        if ats.get("NSAllowsArbitraryLoads", False):
            findings.append({
                "id": "IOS-STATIC-001",
                "severity": "Medium",
                "title": "App Transport Security Disabled",
                "detail": "NSAllowsArbitraryLoads is set to true, allowing cleartext HTTP traffic",
                "masvs": "MASVS-NETWORK",
                "mastg": "MASTG-TEST-0066",
            })

        # Check URL schemes for deep link hijacking potential
        url_types = plist_data.get("CFBundleURLTypes", [])
        custom_schemes = []
        for url_type in url_types:
            schemes = url_type.get("CFBundleURLSchemes", [])
            custom_schemes.extend(schemes)
        if custom_schemes:
            findings.append({
                "id": "IOS-STATIC-002",
                "severity": "Informational",
                "title": "Custom URL Schemes Registered",
                "detail": f"App registers URL schemes: {', '.join(custom_schemes)}. "
                          "Test for deep link hijacking and parameter injection.",
                "masvs": "MASVS-PLATFORM",
                "mastg": "MASTG-TEST-0075",
            })

        # Check for background modes that could leak data
        bg_modes = plist_data.get("UIBackgroundModes", [])
        if bg_modes:
            findings.append({
                "id": "IOS-STATIC-003",
                "severity": "Informational",
                "title": "Background Execution Modes Enabled",
                "detail": f"Background modes: {', '.join(bg_modes)}. "
                          "Verify sensitive operations are not exposed in background.",
                "masvs": "MASVS-STORAGE",
                "mastg": "MASTG-TEST-0058",
            })

    # Scan binary strings for hardcoded secrets
    executable_name = plist_data.get("CFBundleExecutable", app_dirs[0].replace(".app", ""))
    executable_path = os.path.join(app_path, executable_name)

    if os.path.exists(executable_path):
        secret_patterns = [
            (r'(?i)api[_-]?key\s*[:=]\s*["\'][A-Za-z0-9_\-]{16,}', "Hardcoded API Key"),
            (r'(?i)secret\s*[:=]\s*["\'][A-Za-z0-9_\-]{8,}', "Hardcoded Secret"),
            (r'(?i)password\s*[:=]\s*["\'][^"\']{4,}', "Hardcoded Password"),
            (r'https?://[a-zA-Z0-9._\-]+\.firebaseio\.com', "Firebase URL"),
            (r'AIza[0-9A-Za-z_\-]{35}', "Google API Key"),
            (r'AKIA[0-9A-Z]{16}', "AWS Access Key ID"),
            (r'-----BEGIN (?:RSA )?PRIVATE KEY-----', "Embedded Private Key"),
        ]

        try:
            result = subprocess.run(
                ["strings", executable_path],
                capture_output=True, text=True, timeout=60,
            )
            binary_strings = result.stdout
        except (subprocess.SubprocessError, FileNotFoundError):
            binary_strings = ""
            try:
                with open(executable_path, "rb") as f:
                    raw = f.read()
                binary_strings = raw.decode("ascii", errors="ignore")
            except Exception:
                pass

        for pattern, label in secret_patterns:
            matches = re.findall(pattern, binary_strings)
            if matches:
                findings.append({
                    "id": "IOS-STATIC-004",
                    "severity": "High",
                    "title": f"{label} Found in Binary",
                    "detail": f"Pattern match for {label}: {len(matches)} occurrence(s) found. "
                              f"Sample: {matches[0][:60]}...",
                    "masvs": "MASVS-STORAGE",
                    "mastg": "MASTG-TEST-0058",
                })

    # Check for embedded provisioning profile
    provision_path = os.path.join(app_path, "embedded.mobileprovision")
    if os.path.exists(provision_path):
        try:
            result = subprocess.run(
                ["security", "cms", "-D", "-i", provision_path],
                capture_output=True, text=True, timeout=30,
            )
            if "get-task-allow" in result.stdout and "<true/>" in result.stdout:
                findings.append({
                    "id": "IOS-STATIC-005",
                    "severity": "Medium",
                    "title": "Debug Entitlement Enabled (get-task-allow)",
                    "detail": "The provisioning profile has get-task-allow=true, "
                              "indicating a development/debug build that allows attaching debuggers.",
                    "masvs": "MASVS-RESILIENCE",
                    "mastg": "MASTG-TEST-0083",
                })
        except (subprocess.SubprocessError, FileNotFoundError):
            pass

    # Check for unprotected frameworks
    frameworks_dir = os.path.join(app_path, "Frameworks")
    if os.path.isdir(frameworks_dir):
        frameworks = [f for f in os.listdir(frameworks_dir) if f.endswith(".framework")]
        if frameworks:
            findings.append({
                "id": "IOS-STATIC-006",
                "severity": "Informational",
                "title": "Embedded Frameworks Inventory",
                "detail": f"App embeds {len(frameworks)} frameworks: {', '.join(frameworks[:10])}. "
                          "Review for known vulnerable versions.",
                "masvs": "MASVS-RESILIENCE",
                "mastg": "MASTG-TEST-0083",
            })

    logger.info("Static analysis complete: %d findings", len(findings))
    return findings


def run_frida_script(target_bundle, script_source, device_type="usb", timeout_sec=30):
    """Execute a Frida script against a target iOS application."""
    messages = []

    try:
        import frida
    except ImportError:
        logger.error("Frida Python bindings not installed: pip install frida")
        return messages

    def on_message(message, data):
        if message["type"] == "send":
            payload = message["payload"]
            messages.append(payload)
            logger.info("Frida: %s", payload)
        elif message["type"] == "error":
            logger.error("Frida error: %s", message.get("description", ""))

    try:
        if device_type == "usb":
            device = frida.get_usb_device(timeout=10)
        elif device_type == "remote":
            device = frida.get_remote_device()
        else:
            device = frida.get_local_device()

        logger.info("Attached to device: %s", device.name)

        # Try to attach to running process first
        try:
            pid = device.get_process(target_bundle).pid
            session = device.attach(pid)
            logger.info("Attached to running process PID %d", pid)
        except frida.ProcessNotFoundError:
            # Spawn the app
            pid = device.spawn([target_bundle])
            session = device.attach(pid)
            device.resume(pid)
            logger.info("Spawned and attached to PID %d", pid)

        script = session.create_script(script_source)
        script.on("message", on_message)
        script.load()

        import time
        time.sleep(timeout_sec)

        script.unload()
        session.detach()

    except Exception as e:
        logger.error("Frida execution failed: %s", e)
        messages.append({"error": str(e)})

    return messages


def run_objection_command(bundle_id, command):
    """Execute an Objection command against a target app."""
    try:
        result = subprocess.run(
            ["objection", "--gadget", bundle_id, "run", command],
            capture_output=True, text=True, timeout=60,
        )
        return {"command": command, "stdout": result.stdout, "stderr": result.stderr,
                "returncode": result.returncode}
    except FileNotFoundError:
        logger.error("Objection not found: pip install objection")
        return {"command": command, "error": "objection not installed"}
    except subprocess.TimeoutExpired:
        logger.warning("Objection command timed out: %s", command)
        return {"command": command, "error": "timeout"}


def assess_keychain_security(bundle_id):
    """Dump and analyze keychain items for insecure storage practices."""
    findings = []
    result = run_objection_command(bundle_id, "ios keychain dump --json")

    if result.get("error"):
        logger.warning("Keychain dump failed: %s", result.get("error"))
        return findings

    try:
        items = json.loads(result["stdout"])
    except (json.JSONDecodeError, KeyError):
        logger.warning("Could not parse keychain dump output")
        return findings

    insecure_accessibility = [
        "kSecAttrAccessibleAlways",
        "kSecAttrAccessibleAlwaysThisDeviceOnly",
        "kSecAttrAccessibleAfterFirstUnlock",
    ]

    for item in items:
        accessible = item.get("accessible", "")
        if any(a in accessible for a in insecure_accessibility):
            findings.append({
                "id": "IOS-KEYCHAIN-001",
                "severity": "High",
                "title": "Insecure Keychain Accessibility Attribute",
                "detail": f"Keychain item '{item.get('service', 'unknown')}' uses "
                          f"accessibility '{accessible}'. Data may be accessible "
                          "without device unlock.",
                "masvs": "MASVS-STORAGE",
                "mastg": "MASTG-TEST-0055",
            })

        # Check for unprotected password items
        if item.get("class") == "genp" and not item.get("accessControl"):
            findings.append({
                "id": "IOS-KEYCHAIN-002",
                "severity": "Medium",
                "title": "Keychain Password Without Access Control",
                "detail": f"Generic password '{item.get('service', 'unknown')}' lacks "
                          "biometric or passcode access control constraints.",
                "masvs": "MASVS-STORAGE",
                "mastg": "MASTG-TEST-0055",
            })

    logger.info("Keychain analysis: %d findings from %d items", len(findings), len(items))
    return findings


def generate_report(findings, target_app, output_path):
    """Generate comprehensive iOS security assessment report."""
    critical = [f for f in findings if f.get("severity") == "Critical"]
    high = [f for f in findings if f.get("severity") == "High"]
    medium = [f for f in findings if f.get("severity") == "Medium"]
    low = [f for f in findings if f.get("severity") in ("Low", "Informational")]

    report = {
        "assessment": "iOS Application Security Assessment",
        "target": target_app,
        "timestamp": datetime.utcnow().isoformat(),
        "summary": {
            "total_findings": len(findings),
            "critical": len(critical),
            "high": len(high),
            "medium": len(medium),
            "low_informational": len(low),
        },
        "findings": findings,
    }

    with open(output_path, "w") as f:
        json.dump(report, f, indent=2)
    logger.info("Report saved to %s (%d findings)", output_path, len(findings))
    return report


def main():
    parser = argparse.ArgumentParser(
        description="iOS App Security Assessment Agent - Frida/Objection-based testing"
    )
    parser.add_argument("--bundle-id", help="Target app bundle identifier (e.g., com.target.app)")
    parser.add_argument("--ipa", help="Path to IPA file for static analysis")
    parser.add_argument("--device", choices=["usb", "remote", "local"], default="usb",
                        help="Frida device type (default: usb)")
    parser.add_argument("--ssl-bypass", action="store_true",
                        help="Run SSL pinning bypass script")
    parser.add_argument("--keychain", action="store_true",
                        help="Dump and analyze keychain security")
    parser.add_argument("--jailbreak-bypass", action="store_true",
                        help="Run jailbreak detection bypass")
    parser.add_argument("--frida-timeout", type=int, default=30,
                        help="Frida script execution timeout in seconds")
    parser.add_argument("--output", default="ios_assessment_report.json",
                        help="Output report file path")
    parser.add_argument("--output-dir", default=".",
                        help="Directory for extracted IPA and artifacts")
    args = parser.parse_args()

    if not args.bundle_id and not args.ipa:
        parser.error("Provide --bundle-id for dynamic testing or --ipa for static analysis")

    findings = []

    # Static analysis of IPA
    if args.ipa:
        logger.info("=== IPA Static Analysis ===")
        findings.extend(analyze_ipa_static(args.ipa, args.output_dir))

    # Dynamic analysis with Frida/Objection
    if args.bundle_id:
        if args.ssl_bypass:
            logger.info("=== SSL Pinning Bypass ===")
            msgs = run_frida_script(args.bundle_id, SSL_PINNING_BYPASS_SCRIPT,
                                    args.device, args.frida_timeout)
            if any("success" in str(m).lower() for m in msgs):
                findings.append({
                    "id": "IOS-NET-001",
                    "severity": "Informational",
                    "title": "SSL Pinning Successfully Bypassed",
                    "detail": "Certificate pinning was bypassed using Frida hooks on "
                              "SecTrustEvaluate, SecTrustEvaluateWithError, and framework-specific "
                              "trust evaluation methods. Traffic can be intercepted via proxy.",
                    "masvs": "MASVS-NETWORK",
                    "mastg": "MASTG-TEST-0068",
                })

        if args.jailbreak_bypass:
            logger.info("=== Jailbreak Detection Bypass ===")
            msgs = run_frida_script(args.bundle_id, JAILBREAK_DETECTION_BYPASS_SCRIPT,
                                    args.device, args.frida_timeout)
            if any("bypass" in str(m).lower() for m in msgs):
                findings.append({
                    "id": "IOS-RES-001",
                    "severity": "Medium",
                    "title": "Jailbreak Detection Bypassed at Runtime",
                    "detail": "Jailbreak detection checks (file existence, URL scheme, fork) "
                              "were all bypassed using Frida method hooks.",
                    "masvs": "MASVS-RESILIENCE",
                    "mastg": "MASTG-TEST-0079",
                })

        if args.keychain:
            logger.info("=== Keychain Security Analysis ===")
            findings.extend(assess_keychain_security(args.bundle_id))

    target_name = args.bundle_id or args.ipa or "unknown"
    report = generate_report(findings, target_name, args.output)

    print(json.dumps(report["summary"], indent=2))


if __name__ == "__main__":
    main()
Keep exploring