Skip to main content
Frida is a dynamic instrumentation toolkit that allows you to inject JavaScript into native iOS applications to hook functions, trace calls, modify behavior, and bypass security controls at runtime.

Overview

Frida is essential for iOS reverse engineering because it enables:
  • Runtime manipulation of iOS applications
  • Method hooking without recompilation
  • Bypassing jailbreak detection, SSL pinning, and other protections
  • Real-time monitoring and tracing
  • Rapid prototyping of security research
Frida operates at runtime, complementing static analysis tools like Ghidra and IDA Pro. Use both approaches together for comprehensive analysis.

Installing Frida

1

Install Frida CLI Tools

# Using pip
pip3 install frida-tools

# Or using Homebrew
brew install frida-tools

# Verify installation
frida --version
2

Install Frida on iOS Device

Requires a jailbroken iOS device. Frida does not work on stock iOS devices.
# Add Frida repository to Cydia/Sileo
# Open Cydia → Sources → Edit → Add
# URL: https://build.frida.re

# Search for "Frida" and install
# This installs frida-server on the device
Or manually via SSH:
# SSH to device
ssh root@<device-ip>

# Download frida-server for your iOS version
wget https://github.com/frida/frida/releases/download/16.x.x/frida-server-16.x.x-ios-arm64.deb

# Install
dpkg -i frida-server-16.x.x-ios-arm64.deb

# Start frida-server
frida-server &
3

Verify Connection

# List running processes on device
frida-ps -U

# Should show iOS processes like:
# PID  Name
# ---  ----
# 123  SpringBoard
# 456  Safari
# 789  YourApp
-U flag means “USB device”. Use -H <device-ip> for network connection.

Hooking iOS Applications

Basic Hooking

// Hook a simple method
if (ObjC.available) {
    // Get class reference
    var LoginViewController = ObjC.classes.LoginViewController;
    
    // Hook instance method
    var validateMethod = LoginViewController['- validateCredentials:password:'];
    
    Interceptor.attach(validateMethod.implementation, {
        onEnter: function(args) {
            // args[0] = self
            // args[1] = selector
            // args[2] = first argument (username)
            // args[3] = second argument (password)
            
            var username = ObjC.Object(args[2]).toString();
            var password = ObjC.Object(args[3]).toString();
            
            console.log("[+] validateCredentials called");
            console.log("    Username: " + username);
            console.log("    Password: " + password);
        },
        onLeave: function(retval) {
            console.log("    Return value: " + retval);
            
            // Modify return value to always return true
            retval.replace(ptr(1));
        }
    });
    
    console.log("[*] Hooked validateCredentials:password:");
}

Advanced Hooking Patterns

// Enumerate and hook all methods in a class
var LoginViewController = ObjC.classes.LoginViewController;

var methods = LoginViewController.$ownMethods;
console.log("[*] Methods in LoginViewController:");

methods.forEach(function(method) {
    console.log("  " + method);
    
    try {
        var impl = LoginViewController[method].implementation;
        Interceptor.attach(impl, {
            onEnter: function(args) {
                console.log("[+] Called: " + method);
            }
        });
    } catch(err) {
        console.log("  [!] Failed to hook: " + err);
    }
});

Common Use Cases

Bypassing Jailbreak Detection

1

Identify Detection Methods

Common jailbreak checks:
  • File existence checks (/Applications/Cydia.app)
  • Fork detection
  • Suspicious libraries loaded
  • URL scheme detection (cydia://)
2

Create Bypass Script

// bypass-jailbreak.js

// Hook file access functions
var fopen = new NativeFunction(
    Module.findExportByName(null, "fopen"),
    'pointer', ['pointer', 'pointer']
);

Interceptor.replace(fopen, new NativeCallback(function(path, mode) {
    var pathStr = Memory.readUtf8String(path);
    
    // List of jailbreak-related paths
    var jbPaths = [
        "/Applications/Cydia.app",
        "/bin/bash",
        "/usr/sbin/sshd",
        "/etc/apt",
        "/Library/MobileSubstrate"
    ];
    
    for (var i = 0; i < jbPaths.length; i++) {
        if (pathStr.indexOf(jbPaths[i]) !== -1) {
            console.log("[+] Blocked jailbreak check: " + pathStr);
            return NULL;
        }
    }
    
    return fopen(path, mode);
}, 'pointer', ['pointer', 'pointer']));

// Hook fork() to prevent detection
var fork = new NativeFunction(
    Module.findExportByName(null, "fork"),
    'int', []
);

Interceptor.replace(fork, new NativeCallback(function() {
    console.log("[+] Blocked fork() jailbreak check");
    return -1; // Simulate failure
}, 'int', []));

console.log("[*] Jailbreak detection bypass active");
3

Run the Script

frida -U -l bypass-jailbreak.js -f com.example.app

SSL Pinning Bypass

// ssl-bypass.js - Bypass most SSL pinning implementations

if (ObjC.available) {
    // Bypass NSURLSession SSL pinning
    var NSURLSession = ObjC.classes.NSURLSession;
    var delegate = NSURLSession['- URLSession:didReceiveChallenge:completionHandler:'];
    
    Interceptor.attach(delegate.implementation, {
        onEnter: function(args) {
            console.log("[+] Bypassing SSL pinning");
            
            // args[4] is the completion handler
            var completionHandler = new ObjC.Block(args[4]);
            
            // Create disposition (NSURLSessionAuthChallengeUseCredential = 0)
            var disposition = 0;
            
            // Create credential
            var NSURLCredential = ObjC.classes.NSURLCredential;
            var credential = NSURLCredential.credentialForTrust_(args[3]);
            
            // Call completion handler with "trust this certificate"
            completionHandler(disposition, credential);
        }
    });
    
    console.log("[*] SSL pinning bypass active");
}

Dumping Decrypted App Binary

// Use frida-ios-dump tool
// Install: pip install frida-ios-dump

// On your computer:
frida-ios-dump -H <device-ip> -u com.example.app

// Or with USB:
frida-ios-dump -U com.example.app

// This will:
// 1. Attach to the running app
// 2. Dump decrypted binary from memory
// 3. Package it as an IPA
// 4. Save to current directory

Runtime Manipulation

// Change authentication to always succeed
if (ObjC.available) {
    var AuthManager = ObjC.classes.AuthManager;
    var authMethod = AuthManager['- authenticateUser:password:'];
    
    Interceptor.attach(authMethod.implementation, {
        onLeave: function(retval) {
            console.log("[+] Original auth result: " + retval);
            
            // Force success (BOOL YES = 1)
            retval.replace(ptr(1));
            
            console.log("[+] Forced authentication success");
        }
    });
}

Frida Tools and Utilities

Automatically trace function calls
# Trace all methods in a class
frida-trace -U -m "*[LoginViewController *]" YourApp

# Trace specific Objective-C method
frida-trace -U -m "-[NSURLSession dataTaskWithRequest:*]" YourApp

# Trace C function
frida-trace -U -i "ptrace" YourApp

# Trace multiple patterns
frida-trace -U -m "*[*Auth* *]" -m "*[*Login* *]" YourApp
This generates handler files in __handlers__/ that you can customize.

Best Practices

Always wrap hooks in try-catch:
try {
    var LoginVC = ObjC.classes.LoginViewController;
    // ... hook code ...
    console.log("[*] Hook successful");
} catch(err) {
    console.log("[!] Hook failed: " + err.message);
}
  • Avoid hooking high-frequency functions (like drawing/rendering)
  • Use Interceptor.attach sparingly
  • Consider using Interceptor.replace for simple modifications
  • Detach hooks when no longer needed
// Enable verbose logging
console.log("[*] Script loaded");

// Check if ObjC is available
if (!ObjC.available) {
    console.log("[!] Objective-C runtime not available");
}

// Verify class exists
if (!ObjC.classes.LoginViewController) {
    console.log("[!] LoginViewController not found");
    console.log("[*] Available classes:");
    for (var className in ObjC.classes) {
        if (className.indexOf("Login") !== -1) {
            console.log("  " + className);
        }
    }
}
Organize complex scripts:
// main.js
var JailbreakBypass = require('./modules/jailbreak-bypass');
var SSLBypass = require('./modules/ssl-bypass');
var Logging = require('./modules/logging');

// Initialize modules
JailbreakBypass.init();
SSLBypass.init();
Logging.traceNetworkCalls();

console.log("[*] All modules loaded");

Troubleshooting

# Can't connect to device
# 1. Check frida-server is running on device
ssh root@<device-ip> "ps aux | grep frida"

# 2. Restart frida-server
ssh root@<device-ip> "killall frida-server; frida-server &"

# 3. Check USB connection
idevice_id -l

# 4. Verify port forwarding (if using USB)
iproxy 27042 27042
  • Verify class names with ObjC.classes
  • Check method signatures carefully
  • Swift names are mangled - use frida-trace to find correct names
  • Some apps detect Frida - use anti-detection techniques
  • Remove hooks one by one to isolate the problematic hook
  • Check thread safety - use ObjC.schedule for main thread
  • Verify argument types and counts
  • Check for null pointers before dereferencing

Ghidra

Static analysis to identify hooking targets

IDA Pro

Find addresses and understand code flow

LLDB

Traditional debugging companion

Hopper

Identify methods and classes to hook

Resources

Build docs developers (and LLMs) love