iOS Frida Configuration

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks

Deepen your expertise in Mobile Security with 8kSec Academy. Master iOS and Android security through our self-paced courses and get certified:

Installing Frida

Steps to install Frida on a Jailbroken device:

  1. Open Cydia/Sileo app.

  2. Navigate to Manage -> Sources -> Edit -> Add.

  3. Enter "https://build.frida.re" as the URL.

  4. Go to the newly added Frida source.

  5. Install the Frida package.

If you are using Corellium you will need to download the Frida release from https://github.com/frida/frida/releases (frida-gadget-[yourversion]-ios-universal.dylib.gz) and unpack and copy to the dylib location Frida asks for, e.g.: /Users/[youruser]/.cache/frida/gadget-ios.dylib

After installed, you can use in your PC the command frida-ls-devices and check that the device appears (your PC needs to be able to access it). Execute also frida-ps -Uia to check the running processes of the phone.

Frida without Jailbroken device & without patching the app

Check this blog post about how to use Frida in non-jailbroken devices without patching the app: https://mrbypass.medium.com/unlocking-potential-exploring-frida-objection-on-non-jailbroken-devices-without-application-ed0367a84f07

Frida Client Installation

Install frida tools:

pip install frida-tools
pip install frida

With the Frida server installed and the device running and connected, check if the client is working:

frida-ls-devices  # List devices
frida-ps -Uia     # Get running processes

Frida Trace

# Functions
## Trace all functions with the word "log" in their name
frida-trace -U <program> -i "*log*"
frida-trace -U <program> -i "*log*" | swift demangle # Demangle names

# Objective-C
## Trace all methods of all classes
frida-trace -U <program> -m "*[* *]"

## Trace all methods with the word "authentication" from classes that start with "NE"
frida-trace -U <program> -m "*[NE* *authentication*]"

# Plug-In
## To hook a plugin that is momentarely executed prepare Frida indicating the ID of the Plugin binary
frida-trace -U -W <if-plugin-bin> -m '*[* *]'

Get all classes and methods

  • Auto complete: Just execute frida -U <program>

  • Get all available classes (filter by string)

/tmp/script.js
// frida -U <program> -l /tmp/script.js

var filterClass = "filterstring";

if (ObjC.available) {
    for (var className in ObjC.classes) {
        if (ObjC.classes.hasOwnProperty(className)) {
            if (!filterClass || className.includes(filterClass)) {
                console.log(className);
            }
        }
    }
} else {
    console.log("Objective-C runtime is not available.");
}
  • Get all methods of a class (filter by string)

/tmp/script.js
// frida -U <program> -l /tmp/script.js

var specificClass = "YourClassName";
var filterMethod = "filtermethod";

if (ObjC.available) {
    if (ObjC.classes.hasOwnProperty(specificClass)) {
        var methods = ObjC.classes[specificClass].$ownMethods;
        for (var i = 0; i < methods.length; i++) {
            if (!filterMethod || methods[i].includes(filterClass)) {
                console.log(specificClass + ': ' + methods[i]);
            }
        }
    } else {
        console.log("Class not found.");
    }
} else {
    console.log("Objective-C runtime is not available.");
}
  • Call a function

// Find the address of the function to call
const func_addr = Module.findExportByName("<Prog Name>", "<Func Name>");
// Declare the function to call
const func = new NativeFunction(
   func_addr,
   "void", ["pointer", "pointer", "pointer"], {
});

var arg0 = null;

// In this case to call this function we need to intercept a call to it to copy arg0
Interceptor.attach(wg_log_addr, {
   onEnter: function(args) {
       arg0 = new NativePointer(args[0]);
   }
});

// Wait untill a call to the func occurs 
while (! arg0) {
   Thread.sleep(1);
   console.log("waiting for ptr");
}


var arg1 = Memory.allocUtf8String('arg1');
var txt = Memory.allocUtf8String('Some text for arg2');
wg_log(arg0, arg1, txt);

console.log("loaded");

Frida Fuzzing

Frida Stalker

From the docs: Stalker is Frida’s code tracing engine. It allows threads to be followed, capturing every function, every block, even every instruction which is executed.

You have an example implementing Frida Stalker in https://github.com/poxyran/misc/blob/master/frida-stalker-example.py

This is another example to attach Frida Stalker every time a function is called:

console.log("loading");
const wg_log_addr = Module.findExportByName("<Program>", "<function_name>");
const wg_log = new NativeFunction(
    wg_log_addr,
    "void", ["pointer", "pointer", "pointer"], {
});

Interceptor.attach(wg_log_addr, {
    onEnter: function(args) {
        console.log(`logging the following message: ${args[2].readCString()}`);
        
        Stalker.follow({ 
            events: {
                // only collect coverage for newly encountered blocks
                compile: true,
            },
            onReceive: function (events) {
                const bbs = Stalker.parse(events, {
                    stringify: false,
                    annotate: false
                });
                console.log("Stalker trace of write_msg_to_log: \n" + bbs.flat().map(DebugSymbol.fromAddress).join('\n'));
            }
        });
    },
    onLeave: function(retval) {
        Stalker.unfollow();
        Stalker.flush();  // this is important to get all events  
    }
});

This is interesting from debugging purposes but for fuzzing, to be constantly .follow() and .unfollow() is very inefficient.

fpicker is a Frida-based fuzzing suite that offers a variety of fuzzing modes for in-process fuzzing, such as an AFL++ mode or a passive tracing mode. It should run on all platforms that are supported by Frida.

# Get fpicker
git clone https://github.com/ttdennis/fpicker
cd fpicker

# Get Frida core devkit and prepare fpicker
wget https://github.com/frida/frida/releases/download/16.1.4/frida-core-devkit-16.1.4-[yourOS]-[yourarchitecture].tar.xz
# e.g. https://github.com/frida/frida/releases/download/16.1.4/frida-core-devkit-16.1.4-macos-arm64.tar.xz 
tar -xf ./*tar.xz
cp libfrida-core.a libfrida-core-[yourOS].a #libfrida-core-macos.a

# Install fpicker
make fpicker-[yourOS] # fpicker-macos
# This generates ./fpicker

# Install radamsa (fuzzer generator)
brew install radamsa
  • Prepare the FS:

# From inside fpicker clone
mkdir -p examples/wg-log # Where the fuzzing script will be
mkdir -p examples/wg-log/out # For code coverage and crashes
mkdir -p examples/wg-log/in # For starting inputs

# Create at least 1 input for the fuzzer
echo Hello World > examples/wg-log/in/0
  • Fuzzer script (examples/wg-log/myfuzzer.js):

examples/wg-log/myfuzzer.js
// Import the fuzzer base class
import { Fuzzer } from "../../harness/fuzzer.js";

class WGLogFuzzer extends Fuzzer {

    constructor() {
        console.log("WGLogFuzzer constructor called")

        // Get and declare the function we are going to fuzz
        var wg_log_addr = Module.findExportByName("<Program name>", "<func name to fuzz>");
        var wg_log_func = new NativeFunction(
            wg_log_addr,
            "void", ["pointer", "pointer", "pointer"], {
        });

        // Initialize the object
        super("<Program nane>", wg_log_addr, wg_log_func);
        this.wg_log_addr = wg_log_addr; // We cannot use "this" before calling "super"
        
        console.log("WGLogFuzzer in the middle");
        
        // Prepare the second argument to pass to the fuzz function
        this.tag = Memory.allocUtf8String("arg2");
        
        // Get the first argument we need to pass from a call to the functino we want to fuzz
        var wg_log_global_ptr = null;
        console.log(this.wg_log_addr);
        Interceptor.attach(this.wg_log_addr, {
            onEnter: function(args) {
                console.log("Entering in the function to get the first argument");
                wg_log_global_ptr = new NativePointer(args[0]);
            }
         });

        while (! wg_log_global_ptr) {
            Thread.sleep(1)
        }
        this.wg_log_global_ptr = wg_log_global_ptr;
        console.log("WGLogFuzzer prepare ended")
    }


    // This function is called by the fuzzer with the first argument being a pointer into memory
    // where the payload is stored and the second the length of the input.
    fuzz(payload, len) {
        // Get a pointer to payload being a valid C string (with a null byte at the end)
        var payload_cstring = payload.readCString(len);
        this.payload = Memory.allocUtf8String(payload_cstring);

        // Debug and fuzz
        this.debug_log(this.payload, len);
        // Pass the 2 first arguments we know the function needs and finally the payload to fuzz
        this.target_function(this.wg_log_global_ptr, this.tag, this.payload);
    }
}

const f = new WGLogFuzzer();
rpc.exports.fuzzer = f;
  • Compile the fuzzer:

# From inside fpicker clone
## Compile from "myfuzzer.js" to "harness.js"
frida-compile examples/wg-log/myfuzzer.js -o harness.js
  • Call fuzzer fpicker using radamsa:

# Indicate fpicker to fuzz a program with the harness.js script and which folders to use
fpicker -v --fuzzer-mode active -e attach -p <Program to fuzz> -D usb -o examples/wg-log/out/ -i examples/wg-log/in/ -f harness.js --standalone-mutator cmd --mutator-command "radamsa"
# You can find code coverage and crashes in examples/wg-log/out/

In this case we aren't restarting the app or restoring the state after each payload. So, if Frida finds a crash the next inputs after that payload might also crash the app (because the app is in a unstable state) even if the input shouldn't crash the app.

Moreover, Frida will hook into exception signals of iOS, so when Frida finds a crash, probably an iOS crash reports won't be generated.

To prevent this, for example, we could restart the app after each Frida crash.

Logs & Crashes

You can check the macOS console or the log cli to check macOS logs. You can check also the logs from iOS using idevicesyslog. Some logs will omit information adding <private>. To show all the info you need to install some profile from https://developer.apple.com/bug-reporting/profiles-and-logs/ to enable that private info.

If you don't know what to do:

vim /Library/Preferences/Logging/com.apple.system.logging.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>Enable-Private-Data</key>
        <true/>
</dict>
</plist>

killall -9 logd

You can check the crashes in:

  • iOS

    • Settings → Privacy → Analytics & Improvements → Analytics Data

    • /private/var/mobile/Library/Logs/CrashReporter/

  • macOS:

    • /Library/Logs/DiagnosticReports/

    • ~/Library/Logs/DiagnosticReports

iOS only stores 25 crashes of the same app, so you need to clean that or iOS will stop creating crashes.

Frida Android Tutorials

Frida Tutorial

References

Deepen your expertise in Mobile Security with 8kSec Academy. Master iOS and Android security through our self-paced courses and get certified:

Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks

Last updated