Skip to main content

Overview

Dynamic analysis involves examining an iOS application while it’s running. This technique reveals runtime behavior, exposes hidden functionality, and enables real-time manipulation.
Dynamic analysis requires a jailbroken device or simulator. All techniques assume you have root access.

Runtime Analysis

Setting Up Your Environment

1

Jailbreak your device

Use checkra1n, unc0ver, or another jailbreak tool appropriate for your iOS version.
2

Install essential tools

Via Cydia or Sileo:
  • Frida (runtime instrumentation)
  • Cycript (Objective-C/JavaScript runtime)
  • OpenSSH (remote access)
  • AppSync Unified (install unsigned apps)
3

Setup SSH access

# From your computer
ssh root@<device-ip>
# Default password: alpine (change immediately!)
passwd

Frida Fundamentals

Frida is the most powerful tool for iOS dynamic analysis.
# On your computer
pip install frida-tools

# On jailbroken device (via SSH)
wget https://build.frida.re/frida/ios/frida_16.1.4_iphoneos-arm.deb
dpkg -i frida_16.1.4_iphoneos-arm.deb

Real-World Hooking Examples

// Hook common jailbreak detection methods
var fileManager = ObjC.classes.NSFileManager;

Interceptor.attach(
    fileManager['- fileExistsAtPath:'].implementation,
    {
        onEnter: function(args) {
            var path = ObjC.Object(args[2]).toString();
            
            // Jailbreak-related paths
            var jbPaths = [
                '/Applications/Cydia.app',
                '/bin/bash',
                '/usr/sbin/sshd',
                '/etc/apt',
                '/private/var/lib/apt'
            ];
            
            if (jbPaths.includes(path)) {
                console.log('[!] Blocking jailbreak check: ' + path);
                this.blocked = true;
            }
        },
        onLeave: function(retval) {
            if (this.blocked) {
                retval.replace(0); // Return NO
                this.blocked = false;
            }
        }
    }
);

// Hook fork() system call
Interceptor.attach(Module.findExportByName(null, 'fork'), {
    onLeave: function(retval) {
        console.log('[!] fork() called, returning 0');
        retval.replace(0);
    }
});
// Trace all methods in a specific class
function traceClass(className) {
    var targetClass = ObjC.classes[className];
    var methods = targetClass.$ownMethods;
    
    methods.forEach(function(method) {
        try {
            Interceptor.attach(
                targetClass[method].implementation,
                {
                    onEnter: function(args) {
                        console.log('[+] ' + className + ' ' + method);
                        
                        // Log arguments (skip self and selector)
                        for (var i = 2; i < args.length; i++) {
                            try {
                                var arg = ObjC.Object(args[i]);
                                console.log('    arg[' + i + ']: ' + arg.toString());
                            } catch(e) {
                                console.log('    arg[' + i + ']: ' + args[i]);
                            }
                        }
                    }
                }
            );
        } catch(e) {
            console.log('[-] Failed to hook ' + method);
        }
    });
}

// Use it
traceClass('SubscriptionManager');
// Hook CommonCrypto CCCrypt function
var CCCrypt = new NativeFunction(
    Module.findExportByName('libcommonCrypto.dylib', 'CCCrypt'),
    'int',
    ['int', 'int', 'int', 'pointer', 'int', 'pointer', 'pointer', 'int', 'pointer', 'int', 'pointer']
);

Interceptor.replace(CCCrypt, new NativeCallback(function(
    op, alg, options, key, keyLength, iv, dataIn, dataInLength,
    dataOut, dataOutAvailable, dataOutMoved
) {
    console.log('[+] CCCrypt called');
    console.log('    Operation: ' + (op === 0 ? 'Encrypt' : 'Decrypt'));
    console.log('    Algorithm: ' + alg);
    
    // Read key
    if (key && keyLength > 0) {
        var keyData = Memory.readByteArray(key, keyLength);
        console.log('    Key: ' + hexdump(keyData));
    }
    
    // Read IV
    if (iv) {
        var ivData = Memory.readByteArray(iv, 16);
        console.log('    IV: ' + hexdump(ivData));
    }
    
    // Call original
    return CCCrypt(op, alg, options, key, keyLength, iv,
                  dataIn, dataInLength, dataOut,
                  dataOutAvailable, dataOutMoved);
}, 'int', ['int', 'int', 'int', 'pointer', 'int', 'pointer',
           'pointer', 'int', 'pointer', 'int', 'pointer']));

Memory Inspection

Cycript for Runtime Exploration

Cycript combines Objective-C and JavaScript for interactive runtime manipulation.
# Attach to running app
cycript -p "Target App"

# Or by PID
cycript -p 1234

Memory Dumping with Frida

// Dump a memory region
function dumpMemory(address, size, filename) {
    var addr = ptr(address);
    var data = Memory.readByteArray(addr, size);
    
    var file = new File(filename, 'wb');
    file.write(data);
    file.close();
    
    console.log('[+] Dumped ' + size + ' bytes to ' + filename);
}

// Usage
dumpMemory('0x1000a4000', 0x10000, '/tmp/dump.bin');

Monitoring API Calls

NSURLSession Interception

// Intercept all network requests
var NSURLSession = ObjC.classes.NSURLSession;
var NSURLRequest = ObjC.classes.NSURLRequest;

Interceptor.attach(
    ObjC.classes.NSURLSessionTask['- resume'].implementation,
    {
        onEnter: function(args) {
            var task = ObjC.Object(args[0]);
            var request = task.currentRequest();
            
            console.log('\n[+] Network Request');
            console.log('    URL: ' + request.URL().absoluteString());
            console.log('    Method: ' + request.HTTPMethod());
            
            // Log headers
            var headers = request.allHTTPHeaderFields();
            if (headers) {
                console.log('    Headers: ' + headers.toString());
            }
            
            // Log body
            var body = request.HTTPBody();
            if (body) {
                var data = ObjC.Object(body);
                var str = ObjC.classes.NSString.alloc()
                    .initWithData_encoding_(data, 4); // UTF-8
                console.log('    Body: ' + str.toString());
            }
        }
    }
);

AFNetworking Hooks

Many apps use AFNetworking for networking:
// Hook AFHTTPSessionManager
if (ObjC.classes.AFHTTPSessionManager) {
    var methods = [
        '- GET:parameters:success:failure:',
        '- POST:parameters:success:failure:',
        '- PUT:parameters:success:failure:',
        '- DELETE:parameters:success:failure:'
    ];
    
    methods.forEach(function(method) {
        var parts = method.split(' ');
        var httpMethod = parts[1].split(':')[0];
        
        Interceptor.attach(
            ObjC.classes.AFHTTPSessionManager[method].implementation,
            {
                onEnter: function(args) {
                    var url = ObjC.Object(args[2]).toString();
                    var params = ObjC.Object(args[3]);
                    
                    console.log('[+] AFNetworking ' + httpMethod);
                    console.log('    URL: ' + url);
                    console.log('    Params: ' + params.toString());
                }
            }
        );
    });
}

Tracing Execution Flow

Method Tracing with Frida-Trace

1

Trace specific methods

# Trace all methods in a class
frida-trace -U "Target App" -m "-[LoginViewController *]"

# Trace multiple classes
frida-trace -U "Target App" \
  -m "-[Login* *]" \
  -m "-[Auth* *]"
2

Trace Objective-C runtime

# Trace objc_msgSend
frida-trace -U "Target App" -i "objc_msgSend"

# Trace specific modules
frida-trace -U "Target App" -I "libSystem*"
3

Custom trace handlers

Frida-trace generates handlers in __handlers__/ directory. Edit them:
// __handlers__/LoginViewController/validatePassword_.js
{
  onEnter(log, args, state) {
    log('-[LoginViewController validatePassword:' + 
        ObjC.Object(args[2]) + ']');
    state.password = ObjC.Object(args[2]).toString();
  },
  
  onLeave(log, retval, state) {
    log('  => ' + retval + ' for password: ' + state.password);
  }
}

Call Stack Tracing

// Get call stack when method is called
Interceptor.attach(
    ObjC.classes.ViewController['- viewDidLoad'].implementation,
    {
        onEnter: function(args) {
            console.log('[+] viewDidLoad called');
            console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)
                .map(DebugSymbol.fromAddress)
                .join('\n'));
        }
    }
);

Advanced Techniques

Runtime Class Manipulation

// Add a new method at runtime
var LoginVC = ObjC.classes.LoginViewController;

LoginVC['- debugBypass'] = function() {
    console.log('[+] Debug bypass called');
    return true;
};

// Method swizzling
var original = LoginVC['- validatePassword:'];
LoginVC['- validatePassword:'] = function(pwd) {
    console.log('[+] Swizzled method');
    return true; // Always succeed
};

Stalker for Instruction Tracing

// Trace every instruction in a function
var addr = Module.findExportByName(null, 'strcmp');

Stalker.follow({
    events: {
        call: true,
        ret: false,
        exec: false
    },
    onReceive: function(events) {
        console.log(Stalker.parse(events));
    }
});
Stalker tracing generates massive amounts of data. Use sparingly and with filters.

Best Practices

  • Hook selectively - only target methods of interest
  • Use early returns when conditions aren’t met
  • Disable verbose logging in production scripts
  • Consider memory impact of large data captures
  • Always wrap hooks in try-catch blocks
  • Check if classes/methods exist before hooking
  • Handle both ARM64 and ARMv7 if needed
  • Test on multiple iOS versions
  • Never leave Frida server running in production
  • Change default SSH password immediately
  • Use secure channels for data exfiltration
  • Clear logs and temporary files

Resources

Frida Docs

Official Frida documentation

Frida CodeShare

Community Frida scripts

Cycript Manual

Cycript syntax and usage

iOS Runtime Headers

Private framework headers

Build docs developers (and LLMs) love