iOS Frida Configuration - HackTricks

# iOS Frida Configuration

> [!TIP] Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
> Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

> Support HackTricks
> - Check the subscription plans!
> - Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
> - Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.

## 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)

    // 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)

    // 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
      },
    })

> [!CAUTION] This is interesting from debugging purposes but for fuzzing, to be constantly `.follow()` and `.unfollow()` is very inefficient.

## Fpicker

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.

- Install fpicker & radamsa

    # 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`):

    // 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/

> [!CAUTION] 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`

> [!WARNING] 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

- https://www.briskinfosec.com/blogs/blogsdetail/Getting-Started-with-Frida

> [!TIP] Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
> Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

> Support HackTricks
> - Check the subscription plans!
> - Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
> - Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.