iOS Frida Configuration

Reading time: 29 minutes

tip

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

Support HackTricks

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:

bash
pip install frida-tools
pip install frida

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

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

Frida Trace

bash
# 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 = "" // Leave empty to list all classes, or set to "NSString" for example

if (ObjC.available) {
  var classCount = 0
  var classList = []
  
  for (var className in ObjC.classes) {
    if (ObjC.classes.hasOwnProperty(className)) {
      if (!filterClass || className.toLowerCase().includes(filterClass.toLowerCase())) {
        classList.push(className)
        classCount++
      }
    }
  }
  
  // Sort alphabetically for better readability
  classList.sort()
  
  console.log(`\n[*] Found ${classCount} classes matching '${filterClass || "all"}':\n`)
  classList.forEach(function(name) {
    console.log(name)
  })
} 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 = "NSURL" // Change to your target class
var filterMethod = "" // Leave empty to list all methods, or set to "init" for example

if (ObjC.available) {
  if (ObjC.classes.hasOwnProperty(specificClass)) {
    var methods = ObjC.classes[specificClass].$ownMethods
    var filteredMethods = []
    
    for (var i = 0; i < methods.length; i++) {
      if (!filterMethod || methods[i].toLowerCase().includes(filterMethod.toLowerCase())) {
        filteredMethods.push(methods[i])
      }
    }
    
    console.log(`\n[*] Found ${filteredMethods.length} methods in class '${specificClass}' matching '${filterMethod || "all"}':\n`)
    filteredMethods.forEach(function(method) {
      console.log(`${specificClass}: ${method}`)
    })
    
    // Also show inherited methods
    var inheritedMethods = ObjC.classes[specificClass].$methods
    console.log(`\n[*] Total methods including inherited: ${inheritedMethods.length}`)
  } else {
    console.log(`[!] Class '${specificClass}' not found.`)
    console.log("[*] Tip: Use the class enumeration script to find available classes.")
  }
} else {
  console.log("[!] Objective-C runtime is not available.")
}
  • Call a function
javascript
// Find the address of the function to call
const func_addr = Module.findExportByName("<Prog Name>", "<Func Name>")

if (!func_addr) {
  console.log("[!] Function not found. Available exports:")
  Module.enumerateExports("<Prog Name>").slice(0, 10).forEach(function(exp) {
    console.log(`  ${exp.name} at ${exp.address}`)
  })
  throw new Error("Function not found")
}

// Declare the function to call
const func = new NativeFunction(
  func_addr,
  "void",
  ["pointer", "pointer", "pointer"],
  {}
)

var arg0 = null
var attempt = 0
var maxAttempts = 100

console.log("[*] Waiting for function to be called to capture arg0...")

// In this case to call this function we need to intercept a call to it to copy arg0
Interceptor.attach(func_addr, {
  onEnter: function (args) {
    if (!arg0) {
      arg0 = new NativePointer(args[0])
      console.log(`[+] Captured arg0: ${arg0}`)
    }
  },
})

// Wait until a call to the func occurs (with timeout)
while (!arg0 && attempt < maxAttempts) {
  Thread.sleep(0.1)
  attempt++
  if (attempt % 10 == 0) {
    console.log(`[*] Still waiting... (${attempt}/${maxAttempts})`)
  }
}

if (!arg0) {
  throw new Error("Timeout: Could not capture arg0. Try triggering the function in the app.")
}

// Now call the function with custom arguments
var arg1 = Memory.allocUtf8String("custom_tag")
var arg2 = Memory.allocUtf8String("Custom message from Frida")

console.log("[+] Calling function with custom arguments...")
func(arg0, arg1, arg2)

console.log("[+] Function called successfully!")

Hook Objective-C Methods

Intercept and modify Objective-C method calls:

/tmp/hook-objc.js
// frida -U <program> -l /tmp/hook-objc.js

// Hook a specific Objective-C method
function hookMethod(className, methodName) {
  var hook = ObjC.classes[className][methodName]
  
  if (!hook) {
    console.log(`[!] Method ${className}.${methodName} not found`)
    return
  }
  
  Interceptor.attach(hook.implementation, {
    onEnter: function(args) {
      console.log(`\n[*] Called: [${className} ${methodName}]`)
      
      // args[0] is self, args[1] is _cmd (selector)
      // Actual method arguments start at args[2]
      
      // Print self
      try {
        var selfObj = new ObjC.Object(args[0])
        console.log(`    self: ${selfObj}`)
      } catch (e) {
        console.log(`    self: ${args[0]}`)
      }
      
      // Print arguments (adjust based on method signature)
      for (var i = 2; i < 6; i++) {
        if (args[i]) {
          try {
            // Try as ObjC object
            var obj = new ObjC.Object(args[i])
            console.log(`    arg[${i-2}]: ${obj} (${obj.$className})`)
          } catch (e) {
            // Try as string
            try {
              var str = args[i].readUtf8String()
              console.log(`    arg[${i-2}]: "${str}"`)
            } catch (e2) {
              // Just print pointer
              console.log(`    arg[${i-2}]: ${args[i]}`)
            }
          }
        }
      }
      
      // You can modify arguments here
      // args[2] = ObjC.classes.NSString.stringWithString_("Modified!")
    },
    onLeave: function(retval) {
      // Print return value
      try {
        var ret = new ObjC.Object(retval)
        console.log(`    => ${ret}`)
      } catch (e) {
        console.log(`    => ${retval}`)
      }
      
      // You can modify return value here
      // retval.replace(ObjC.classes.NSString.stringWithString_("Hijacked!"))
    }
  })
  
  console.log(`[+] Hooked: [${className} ${methodName}]`)
}

// Example: Hook multiple methods
if (ObjC.available) {
  console.log("[*] Objective-C runtime available")
  
  // Hook authentication methods
  hookMethod("LoginViewController", "- authenticate:")
  hookMethod("AuthManager", "- validatePassword:")
  
  // Hook data storage methods
  hookMethod("NSUserDefaults", "+ standardUserDefaults")
  hookMethod("NSUserDefaults", "- setObject:forKey:")
  hookMethod("NSUserDefaults", "- objectForKey:")
  
  // Hook crypto methods
  hookMethod("NSString", "- dataUsingEncoding:")
  
  // Hook network methods
  hookMethod("NSURLSession", "- dataTaskWithRequest:completionHandler:")
  
  console.log("[+] All hooks installed successfully")
} else {
  console.log("[!] Objective-C runtime not available")
}

Advanced Objective-C hooking with method swizzling:

/tmp/swizzle-method.js
// Replace method implementation entirely
function swizzleMethod(className, methodName, newImplementation) {
  if (!ObjC.available) {
    console.log("[!] Objective-C runtime not available")
    return
  }
  
  var targetClass = ObjC.classes[className]
  if (!targetClass) {
    console.log(`[!] Class ${className} not found`)
    return
  }
  
  var method = targetClass[methodName]
  if (!method) {
    console.log(`[!] Method ${methodName} not found in ${className}`)
    return
  }
  
  var originalImpl = method.implementation
  
  method.implementation = ObjC.implement(method, function(handle, selector) {
    // handle is 'self', selector is the method selector
    console.log(`[*] Swizzled method called: [${className} ${methodName}]`)
    
    // Call custom logic
    var result = newImplementation(handle, selector, arguments)
    
    // Optionally call original
    // var original = new NativeFunction(originalImpl, method.returnType, method.argumentTypes)
    // return original(handle, selector, ...)
    
    return result
  })
  
  console.log(`[+] Swizzled: [${className} ${methodName}]`)
}

// Example: Always return true for authentication
swizzleMethod("AuthManager", "- isAuthenticated", function(self, sel) {
  console.log("[!] Bypassing authentication check!")
  return 1 // true
})

// Example: Bypass jailbreak detection
if (ObjC.available) {
  var jailbreakMethods = [
    ["JailbreakDetector", "- isJailbroken"],
    ["SecurityChecker", "- checkJailbreak"],
    ["AntiDebug", "- isDebugged"]
  ]
  
  jailbreakMethods.forEach(function(item) {
    try {
      swizzleMethod(item[0], item[1], function() {
        console.log(`[!] Bypassing ${item[0]}.${item[1]}`)
        return 0 // false
      })
    } catch (e) {
      // Method doesn't exist, ignore
    }
  })
}

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:

javascript
console.log("[*] Starting Stalker setup...")

const TARGET_MODULE = "<Program>"
const TARGET_FUNCTION = "<function_name>"

const func_addr = Module.findExportByName(TARGET_MODULE, TARGET_FUNCTION)

if (!func_addr) {
  console.log(`[!] Function '${TARGET_FUNCTION}' not found in module '${TARGET_MODULE}'`)
  throw new Error("Target function not found")
}

console.log(`[+] Found target function at: ${func_addr}`)

const func = new NativeFunction(
  func_addr,
  "void",
  ["pointer", "pointer", "pointer"],
  {}
)

var callCount = 0
var coverageMap = {}

Interceptor.attach(func_addr, {
  onEnter: function (args) {
    callCount++
    console.log(`\n[*] Call #${callCount} - Message: ${args[2].readCString()}`)

    // Follow the current thread
    Stalker.follow(Process.getCurrentThreadId(), {
      events: {
        compile: true, // Only collect coverage for newly encountered blocks
      },
      onReceive: function (events) {
        const bbs = Stalker.parse(events, {
          stringify: false,
          annotate: false,
        })
        
        // Track unique code blocks for coverage
        var newBlocks = 0
        bbs.flat().forEach(function(addr) {
          var addrStr = addr.toString()
          if (!coverageMap[addrStr]) {
            coverageMap[addrStr] = true
            newBlocks++
          }
        })
        
        console.log(`[+] Executed ${bbs.flat().length} blocks (${newBlocks} new)`)
        console.log(`[+] Total unique blocks covered: ${Object.keys(coverageMap).length}`)
        
        // Optionally print trace (can be verbose)
        if (callCount <= 3) { // Only print first 3 traces
          console.log("\n[*] Execution trace:")
          bbs.flat().slice(0, 20).forEach(function(addr) { // Limit to first 20
            console.log(`  ${DebugSymbol.fromAddress(addr)}`)
          })
          if (bbs.flat().length > 20) {
            console.log(`  ... and ${bbs.flat().length - 20} more blocks`)
          }
        }
      },
    })
  },
  onLeave: function (retval) {
    Stalker.unfollow(Process.getCurrentThreadId())
    Stalker.flush() // Important: flush all events before unfollow
    Stalker.garbageCollect() // Clean up
  },
})

console.log("[+] Stalker attached successfully. Waiting for function calls...")

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.

bash
# 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:
bash
# 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: Initializing fuzzer...")

    // ============================================================
    // CONFIGURATION SECTION
    // ============================================================
    // These are the values you need to customize for your target:
    
    const TARGET_MODULE = "<Program name>"      // The binary/library name (e.g., "MyApp" or "libcrypto.dylib")
                                                 // Use Process.enumerateModules() to find module names
    
    const TARGET_FUNCTION = "<func name to fuzz>" // The exported function name to fuzz (e.g., "process_input")
                                                   // Use Module.enumerateExports() to find function names
    
    const CAPTURE_TIMEOUT = 30                   // Seconds to wait for capturing function arguments
                                                 // Increase if function is rarely called
    
    // ============================================================
    // FUNCTION DISCOVERY
    // ============================================================
    // Find the address of the target function in memory
    console.log(`[*] Looking for function '${TARGET_FUNCTION}' in module '${TARGET_MODULE}'...`)
    var target_addr = Module.findExportByName(TARGET_MODULE, TARGET_FUNCTION)
    
    // Validate that the function was found
    if (!target_addr) {
      console.log(`[!] Function not found. Available exports from ${TARGET_MODULE}:`)
      Module.enumerateExports(TARGET_MODULE).slice(0, 10).forEach(function(exp) {
        console.log(`  - ${exp.name}`)
      })
      throw new Error(`Function '${TARGET_FUNCTION}' not found`)
    }
    
    console.log(`[+] Found target function at: ${target_addr}`)
    
    // ============================================================
    // FUNCTION SIGNATURE SETUP
    // ============================================================
    // Create a NativeFunction wrapper so we can call the function
    // Signature: void function_name(pointer arg0, pointer arg1, pointer arg2)
    // IMPORTANT: Adjust the return type and argument types to match your target function
    //   - First parameter: return type ("void", "int", "pointer", etc.)
    //   - Second parameter: array of argument types
    var target_func = new NativeFunction(
      target_addr,
      "void",                              // Return type - change if function returns a value
      ["pointer", "pointer", "pointer"],   // Argument types - adjust based on actual function signature
      {}
    )

    // ============================================================
    // PARENT CLASS INITIALIZATION
    // ============================================================
    // Initialize the fpicker Fuzzer base class with our target information
    super(TARGET_MODULE, target_addr, target_func)
    this.target_addr = target_addr

    // ============================================================
    // STATISTICS TRACKING
    // ============================================================
    // Keep track of fuzzing progress and results
    this.fuzzCount = 0      // Total number of fuzzing iterations executed
    this.crashCount = 0     // Number of crashes/exceptions encountered
    this.startTime = Date.now()  // Start time for calculating execution rate

    // ============================================================
    // STATIC ARGUMENTS PREPARATION
    // ============================================================
    // Some functions require specific arguments that don't change
    // Here we prepare the second argument (a tag string)
    this.tag = Memory.allocUtf8String("FUZZ_TAG")
    console.log("[+] Allocated tag argument")

    // ============================================================
    // DYNAMIC ARGUMENT CAPTURE
    // ============================================================
    // Many functions require a context pointer or handle as first argument
    // We can't create this ourselves, so we intercept a real call to capture it
    
    var captured_ptr = null   // Will hold the captured pointer
    var attempts = 0          // Counter for timeout mechanism
    var maxAttempts = CAPTURE_TIMEOUT * 10 // Total attempts (checking every 100ms)
    
    console.log(`[*] Waiting up to ${CAPTURE_TIMEOUT}s to capture first argument...`)
    console.log("[*] Please trigger the target function in the app!")
    console.log("[*] (Interact with the app to make it call the function)")
    
    // Attach an interceptor to capture arguments when function is called
    var interceptor = Interceptor.attach(this.target_addr, {
      onEnter: function (args) {
        // Only capture once (first call)
        if (!captured_ptr) {
          captured_ptr = new NativePointer(args[0])
          console.log(`[+] Captured first argument: ${captured_ptr}`)
          
          // Try to read and display other arguments for debugging
          // This helps verify we're hooking the right function
          try {
            if (args[1]) console.log(`[*] Arg 1: ${args[1].readCString()}`)
            if (args[2]) console.log(`[*] Arg 2: ${args[2].readCString()}`)
          } catch (e) {
            console.log("[*] Could not read string arguments (might not be strings)")
          }
        }
      },
    })

    // ============================================================
    // WAIT FOR CAPTURE WITH TIMEOUT
    // ============================================================
    // Poll until we capture the argument or timeout
    while (!captured_ptr && attempts < maxAttempts) {
      Thread.sleep(0.1)  // Sleep 100ms between checks
      attempts++
      
      // Print progress every 5 seconds so user knows we're still waiting
      if (attempts % 50 == 0) {
        console.log(`[*] Still waiting... (${attempts / 10}s / ${CAPTURE_TIMEOUT}s)`)
      }
    }
    
    // ============================================================
    // CLEANUP AND VALIDATION
    // ============================================================
    // Detach the interceptor - we don't need it anymore
    interceptor.detach()

    // Check if we successfully captured the argument
    if (!captured_ptr) {
      throw new Error(`Timeout: Could not capture first argument after ${CAPTURE_TIMEOUT}s. Ensure the function is being called.`)
    }

    // Store the captured pointer for use in fuzz() method
    this.captured_ptr = captured_ptr
    console.log("[+] Fuzzer initialization complete!")
    console.log("[+] Ready to fuzz...")
  }

  // This function is called by fpicker for each fuzzing iteration
  // @param payload: NativePointer - Pointer to the fuzzing input data in memory
  // @param len: Number - Length of the input data in bytes
  fuzz(payload, len) {
    this.fuzzCount++
    
    try {
      // ============================================================
      // STEP 1: Convert the raw payload to a usable format
      // ============================================================
      // The payload comes as a pointer to memory. We need to:
      // 1. Read the raw bytes from that memory location
      // 2. Allocate new memory for a null-terminated C string
      // 3. Copy the data and add null terminator
      
      var payload_mem = Memory.alloc(len + 1)  // Allocate len + 1 for null terminator
      Memory.copy(payload_mem, payload, len)   // Copy the payload bytes
      payload_mem.add(len).writeU8(0)          // Write null terminator at the end
      
      // ============================================================
      // STEP 2: Progress monitoring and statistics
      // ============================================================
      // Log progress every 100 iterations to avoid spamming console
      if (this.fuzzCount % 100 == 0) {
        var elapsed = ((Date.now() - this.startTime) / 1000).toFixed(2)
        var rate = (this.fuzzCount / elapsed).toFixed(2)
        console.log(`[*] Fuzzing iteration ${this.fuzzCount} (${rate} exec/s, ${this.crashCount} crashes)`)
      }
      
      // ============================================================
      // STEP 3: Debug logging for initial iterations
      // ============================================================
      // For the first 3 payloads, show what we're testing
      // This helps verify the fuzzer is working correctly
      if (this.fuzzCount <= 3) {
        try {
          var preview = payload.readCString(Math.min(len, 50))
          console.log(`[*] Payload preview (${len} bytes): ${preview}${len > 50 ? '...' : ''}`)
        } catch (e) {
          // If readCString fails, it's likely binary data
          console.log(`[*] Binary payload (${len} bytes)`)
        }
      }

      // ============================================================
      // STEP 4: Execute the target function with the fuzzed input
      // ============================================================
      // Call the target function with:
      // - captured_ptr: The first argument we captured during initialization
      // - tag: A static tag/label for the log entry
      // - payload_mem: Our fuzzed input as a null-terminated string
      this.target_function(this.captured_ptr, this.tag, payload_mem)
      
    } catch (e) {
      // ============================================================
      // STEP 5: Exception handling
      // ============================================================
      // If the target function crashes or throws an exception:
      // 1. Increment crash counter
      // 2. Log the details for later analysis
      // 3. Re-throw so fpicker can record it
      this.crashCount++
      console.log(`[!] Exception in iteration ${this.fuzzCount}: ${e.message}`)
      console.log(`[!] Stack: ${e.stack}`)
      
      // Re-throw to let fpicker handle crash detection and logging
      throw e
    }
  }

  // Optional: Cleanup method called when fuzzing ends
  cleanup() {
    var elapsed = ((Date.now() - this.startTime) / 1000).toFixed(2)
    console.log(`\n[*] Fuzzing session complete:`)
    console.log(`    - Total iterations: ${this.fuzzCount}`)
    console.log(`    - Total crashes: ${this.crashCount}`)
    console.log(`    - Duration: ${elapsed}s`)
    console.log(`    - Average rate: ${(this.fuzzCount / elapsed).toFixed(2)} exec/s`)
  }
}

console.log("[*] Creating fuzzer instance...")
const f = new WGLogFuzzer()
rpc.exports.fuzzer = f

// Export cleanup method if available
if (f.cleanup) {
  rpc.exports.cleanup = f.cleanup.bind(f)
}
  • Compile the fuzzer:
bash
# 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:
bash
# Basic fuzzing with radamsa mutation
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"

# With AFL++ mode for better coverage
fpicker -v --fuzzer-mode afl -e attach -p <Program to fuzz> -D usb \
  -o examples/wg-log/out/ -i examples/wg-log/in/ -f harness.js

# You can find code coverage and crashes in examples/wg-log/out/
# Check crashes: ls -la examples/wg-log/out/crashes/
# Check coverage: ls -la examples/wg-log/out/coverage/

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.

Advanced Fuzzing with Crash Monitoring

For more robust fuzzing with automatic crash detection and app restart, use this enhanced script:

examples/wg-log/advanced-fuzzer.js
import { Fuzzer } from "../../harness/fuzzer.js"

class AdvancedFuzzer extends Fuzzer {
  constructor() {
    console.log("[*] Advanced Fuzzer: Initializing with crash monitoring...")
    
    // ============================================================
    // CONFIGURATION
    // ============================================================
    const TARGET_MODULE = "<Program name>"   // Module containing the target function
    const TARGET_FUNCTION = "<func name>"    // Function to fuzz
    
    // ============================================================
    // FIND AND SETUP TARGET FUNCTION
    // ============================================================
    var target_addr = Module.findExportByName(TARGET_MODULE, TARGET_FUNCTION)
    if (!target_addr) {
      throw new Error(`Function '${TARGET_FUNCTION}' not found`)
    }
    
    var target_func = new NativeFunction(target_addr, "void", ["pointer", "pointer", "pointer"], {})
    super(TARGET_MODULE, target_addr, target_func)
    
    // ============================================================
    // ADVANCED CRASH DETECTION SETUP
    // ============================================================
    // Install comprehensive crash monitoring before starting fuzzing
    this.setupCrashMonitoring()
    
    // Hook dangerous functions that often indicate crashes
    this.setupSignalHandlers()
    
    // ============================================================
    // CAPTURE RUNTIME ARGUMENTS
    // ============================================================
    // Capture the context pointer needed to call the function
    this.captured_ptr = this.captureArgument(target_addr, 0)
    this.tag = Memory.allocUtf8String("FUZZ")
    
    console.log("[+] Advanced fuzzer ready with crash monitoring enabled")
  }
  
  // ============================================================
  // CRASH MONITORING SETUP
  // ============================================================
  // This method installs a global exception handler that catches:
  // - Segmentation faults (invalid memory access)
  // - Arithmetic exceptions (divide by zero, etc.)
  // - Abort signals
  // - Any other exceptions that would normally crash the app
  setupCrashMonitoring() {
    Process.setExceptionHandler(function(details) {
      console.log("\n[!!!] CRASH DETECTED [!!!]")
      console.log(`[!] Type: ${details.type}`)           // Exception type (e.g., "access-violation")
      console.log(`[!] Address: ${details.address}`)     // Address where crash occurred
      
      // If it's a memory-related crash, show the operation and address
      console.log(`[!] Memory operation: ${details.memory ? details.memory.operation : 'N/A'}`)
      
      // ============================================================
      // DUMP CPU REGISTERS
      // ============================================================
      // Show CPU register state at crash time (useful for exploitation analysis)
      if (details.context) {
        console.log("[!] Registers:")
        Object.keys(details.context).slice(0, 8).forEach(function(reg) {
          console.log(`    ${reg}: ${details.context[reg]}`)
        })
      }
      
      // ============================================================
      // DUMP CALL STACK (BACKTRACE)
      // ============================================================
      // Show the call stack leading to the crash
      // This helps identify which code path triggered the issue
      console.log("[!] Backtrace:")
      Thread.backtrace(details.context, Backtracer.ACCURATE)
        .map(DebugSymbol.fromAddress)
        .slice(0, 10)
        .forEach(function(symbol, idx) {
          console.log(`    ${idx}: ${symbol}`)
        })
      
      // Return false to let iOS handle the crash (generates crash report)
      // Return true to suppress the crash and continue (dangerous - app in undefined state)
      return false
    })
  }
  
  // ============================================================
  // DANGEROUS FUNCTION MONITORING
  // ============================================================
  // Hook common functions that indicate problems:
  // - abort(): Explicit crash
  // - __stack_chk_fail(): Stack buffer overflow detected
  // - __assert_rtn(): Failed assertion
  // - malloc/free: Memory allocation (can detect double-free, use-after-free)
  // - memcpy/strcpy: Memory operations (can detect buffer overflows)
  setupSignalHandlers() {
    var crashFuncs = [
      "abort",              // Explicit abort() call
      "__stack_chk_fail",   // Stack canary check failed (buffer overflow)
      "__assert_rtn",       // Assertion failure
      "malloc",             // Memory allocation
      "free",               // Memory deallocation
      "memcpy",             // Memory copy
      "strcpy"              // String copy
    ]
    
    crashFuncs.forEach(function(funcName) {
      try {
        // Find the function in any loaded module (null = search all)
        var addr = Module.findExportByName(null, funcName)
        if (addr) {
          Interceptor.attach(addr, {
            onEnter: function(args) {
              // Only log critical functions to avoid spam
              if (funcName === "abort" || funcName === "__stack_chk_fail" || funcName === "__assert_rtn") {
                console.log(`[!] ${funcName} called - potential crash imminent!`)
                console.log("[!] Backtrace:")
                // Show where this function was called from
                Thread.backtrace(this.context, Backtracer.ACCURATE)
                  .map(DebugSymbol.fromAddress)
                  .slice(0, 5)
                  .forEach(function(s) { console.log(`    ${s}`) })
              }
            }
          })
        }
      } catch (e) {
        // Function not available on this platform, skip it
      }
    })
  }
  
  // ============================================================
  // ARGUMENT CAPTURE HELPER
  // ============================================================
  // Generic method to capture any argument from a function call
  // @param addr: Address of the function to monitor
  // @param argIndex: Which argument to capture (0 = first, 1 = second, etc.)
  // @param timeout: How long to wait (seconds) before giving up
  captureArgument(addr, argIndex, timeout = 30) {
    var captured = null
    var attempts = 0
    var maxAttempts = timeout * 10  // Check every 100ms
    
    console.log(`[*] Capturing argument ${argIndex}...`)
    console.log(`[*] Trigger the function in the app to capture its arguments`)
    
    // Hook the function temporarily
    var hook = Interceptor.attach(addr, {
      onEnter: function(args) {
        if (!captured && args[argIndex]) {
          captured = new NativePointer(args[argIndex])
          console.log(`[+] Captured arg[${argIndex}]: ${captured}`)
        }
      }
    })
    
    // Wait for a call to occur
    while (!captured && attempts < maxAttempts) {
      Thread.sleep(0.1)
      attempts++
    }
    
    // Clean up the hook
    hook.detach()
    
    if (!captured) {
      throw new Error(`Failed to capture argument ${argIndex} after ${timeout}s`)
    }
    
    return captured
  }
  
  // ============================================================
  // FUZZ EXECUTION METHOD
  // ============================================================
  // Called by fpicker for each fuzzing iteration
  // @param payload: Pointer to the mutated input data
  // @param len: Length of the input in bytes
  fuzz(payload, len) {
    try {
      // ============================================================
      // STEP 1: Input validation
      // ============================================================
      // Reject unreasonably large inputs to prevent memory exhaustion
      if (len > 1024 * 1024) { // 1MB limit
        console.log(`[!] Payload too large: ${len} bytes, skipping`)
        return
      }
      
      // ============================================================
      // STEP 2: Prepare the fuzzed input
      // ============================================================
      // Allocate new memory and copy the payload
      // Add null terminator for C string compatibility
      var fuzz_data = Memory.alloc(len + 1)    // Allocate space + 1 byte for null
      Memory.copy(fuzz_data, payload, len)     // Copy the payload
      fuzz_data.add(len).writeU8(0)            // Add null terminator
      
      // ============================================================
      // STEP 3: Execute with timeout detection
      // ============================================================
      // Some inputs might cause infinite loops (hangs)
      // Use a timer to detect when execution takes too long
      var executed = false
      var timer = setTimeout(function() {
        if (!executed) {
          console.log("[!] Execution timeout - possible hang")
          // Note: This doesn't stop execution, just logs it
          // Consider using Stalker or watchdog thread for true timeout
        }
      }, 5000) // 5 second timeout
      
      // Call the target function
      this.target_function(this.captured_ptr, this.tag, fuzz_data)
      
      // Mark as completed and cancel timeout
      executed = true
      clearTimeout(timer)
      
    } catch (e) {
      // Exception occurred - likely a crash
      console.log(`[!] Fuzz iteration exception: ${e.message}`)
      throw e  // Re-throw for fpicker to handle
    }
  }
}

const fuzzer = new AdvancedFuzzer()
rpc.exports.fuzzer = fuzzer

To use the advanced fuzzer:

bash
# Compile the advanced fuzzer
frida-compile examples/wg-log/advanced-fuzzer.js -o harness-advanced.js

# Run with automatic restart on crash using a wrapper script
cat > fuzz-with-restart.sh << 'EOF'
#!/bin/bash

APP_NAME="<Program to fuzz>"
OUTPUT_DIR="examples/wg-log/out"
INPUT_DIR="examples/wg-log/in"
HARNESS="harness-advanced.js"

while true; do
    echo "[*] Starting fuzzing session at $(date)"
    
    # Run fpicker (will exit on crash)
    fpicker -v --fuzzer-mode active -e attach -p "$APP_NAME" -D usb \
        -o "$OUTPUT_DIR" -i "$INPUT_DIR" -f "$HARNESS" \
        --standalone-mutator cmd --mutator-command "radamsa"
    
    EXIT_CODE=$?
    echo "[!] Fuzzer exited with code $EXIT_CODE"
    
    if [ $EXIT_CODE -ne 0 ]; then
        echo "[*] Crash detected, saving crash info..."
        echo "Crash at $(date)" >> "$OUTPUT_DIR/crash_log.txt"
        
        # Kill the app if still running
        killall "$APP_NAME" 2>/dev/null
        
        # Wait for app to fully stop
        sleep 2
        
        # Restart the app
        echo "[*] Restarting app..."
        frida -U -f "$APP_NAME" --no-pause &
        sleep 3
    else
        echo "[*] Fuzzing session completed normally"
        break
    fi
done
EOF

chmod +x fuzz-with-restart.sh
./fuzz-with-restart.sh

Simple Standalone Fuzzer (Without fpicker)

For quick fuzzing tests without fpicker setup, use this standalone script:

simple-fuzzer.js
// ============================================================
// SIMPLE STANDALONE FUZZER
// ============================================================
// This fuzzer works without fpicker - just load it with Frida
// Usage: frida -U -l simple-fuzzer.js <Program>
//
// This is great for:
// - Quick fuzzing tests
// - When you can't set up fpicker
// - Testing if a function is fuzzable
// - Learning how fuzzing works

console.log("[*] Simple Fuzzer starting...")

// ============================================================
// CONFIGURATION
// ============================================================
const TARGET_MODULE = "<Program>"          // Your app's main binary name
const TARGET_FUNCTION = "<function_name>"  // The function to fuzz
const ITERATIONS = 1000                    // How many times to fuzz
const MAX_PAYLOAD_SIZE = 1024              // Maximum size for random payloads

// Helper to build ArrayBuffer from byte array
function bytesToBuffer(bytes) {
  var buffer = new ArrayBuffer(bytes.length)
  var view = new Uint8Array(buffer)
  for (var i = 0; i < bytes.length; i++) {
    view[i] = bytes[i]
  }
  return buffer
}

// Helper to convert ASCII string into byte array (lossy for non-ASCII)
function stringToBytes(str) {
  var bytes = []
  for (var i = 0; i < str.length; i++) {
    bytes.push(str.charCodeAt(i) & 0xff)
  }
  return bytes
}

// ============================================================
// MUTATION STRATEGIES
// ============================================================
// This function implements various fuzzing mutation strategies
// Each strategy targets different types of vulnerabilities
// Returns an object describing the mutation so we can handle
// both text and binary payloads safely
function mutatePayload(seed) {
  var mutations = [
    // Strategy 1: Buffer overflow - very long strings
    function() {
      return { type: "string", value: "A".repeat(Math.floor(Math.random() * 10000)), description: "Long 'A' string" }
    },

    // Strategy 2: Format string bugs
    function() {
      return { type: "string", value: "%s%s%s%s%s%s%s%s%s%s%n%n%n%n", description: "Format string" }
    },

    // Strategy 3: Null bytes and boundary characters
    function() {
      return {
        type: "binary",
        value: bytesToBuffer([0, 0, 0].concat(stringToBytes(seed), [0xff, 0xff, 0xff])),
        description: "Boundary chars"
      }
    },

    // Strategy 4: SQL injection patterns
    function() {
      return { type: "string", value: "' OR '1'='1", description: "SQL injection" }
    },

    // Strategy 5: XSS/script injection patterns
    function() {
      return { type: "string", value: "<script>alert(1)</script>", description: "XSS payload" }
    },

    // Strategy 6: Path traversal
    function() {
      return { type: "string", value: "../../../etc/passwd", description: "Path traversal" }
    },

    // Strategy 7: Invalid Unicode sequences
    function() {
      // Build deliberately malformed UTF sequence (includes null)
      return {
        type: "binary",
        value: bytesToBuffer([0x00, 0xef, 0xff, 0xed, 0xa0, 0x80]),
        description: "Invalid Unicode"
      }
    },

    // Strategy 8: Extremely long repeated input
    function() {
      return { type: "string", value: seed.repeat(100), description: "Repeated seed" }
    },

    // Strategy 9: Null byte injection
    function() {
      return {
        type: "binary",
        value: bytesToBuffer(stringToBytes(seed).concat([0, 0, 0, 0])),
        description: "Null byte injection"
      }
    },

    // Strategy 10: Completely random bytes (binary payload)
    function() {
      var len = Math.floor(Math.random() * MAX_PAYLOAD_SIZE)
      var bytes = []
      for (var i = 0; i < len; i++) {
        bytes.push(Math.floor(Math.random() * 256))
      }
      return { type: "binary", value: bytesToBuffer(bytes), description: `Random ${len}-byte buffer` }
    }
  ]

  // Randomly select one mutation strategy
  return mutations[Math.floor(Math.random() * mutations.length)]()
}

// ============================================================
// FIND TARGET FUNCTION
// ============================================================
const target_addr = Module.findExportByName(TARGET_MODULE, TARGET_FUNCTION)
if (!target_addr) {
  console.log("[!] Target function not found!")
  console.log("[*] Available functions (first 20):")
  Module.enumerateExports(TARGET_MODULE).slice(0, 20).forEach(function(exp) {
    console.log(`    - ${exp.name}`)
  })
  throw new Error("Function not found")
}

console.log(`[+] Found target at ${target_addr}`)

// ============================================================
// CREATE FUNCTION WRAPPER
// ============================================================
// Wrap the native function so we can call it from JavaScript
// Adjust signature if your function has different parameters
const target_func = new NativeFunction(
  target_addr,
  "void",                              // Return type
  ["pointer", "pointer", "pointer"],   // Argument types
  {}
)

// ============================================================
// CAPTURE REQUIRED ARGUMENTS
// ============================================================
// Many functions need a context pointer or handle
// We capture it from a real call instead of guessing
var captured_arg = null
console.log("[*] Waiting to capture arguments...")
console.log("[*] Please trigger the function in the app!")

var hook = Interceptor.attach(target_addr, {
  onEnter: function(args) {
    if (!captured_arg) {
      captured_arg = new NativePointer(args[0])
      console.log(`[+] Captured arg: ${captured_arg}`)
    }
  }
})

// Wait for the function to be called
while (!captured_arg) {
  Thread.sleep(0.1)
}
hook.detach()

// ============================================================
// START FUZZING LOOP
// ============================================================
console.log(`[*] Starting ${ITERATIONS} fuzzing iterations...`)
var tag = Memory.allocUtf8String("FUZZ")  // Static second argument
var crashes = 0
var startTime = Date.now()

for (var i = 0; i < ITERATIONS; i++) {
  var mutation = null
  var payload_ptr = null
  var payload_length = 0
  var payload_preview = ""

  try {
    // ========================================================
    // GENERATE MUTATED INPUT
    // ========================================================
    mutation = mutatePayload("Hello World")

    if (mutation.type === "string") {
      payload_length = mutation.value.length
      payload_ptr = Memory.allocUtf8String(mutation.value)
      payload_preview = mutation.value
    } else {
      payload_length = mutation.value.byteLength
      var mem = Memory.alloc(payload_length + 1)
      Memory.writeByteArray(mem, mutation.value)
      mem.add(payload_length).writeU8(0)
      payload_ptr = mem
      payload_preview = hexdump(mem, { offset: 0, length: Math.min(payload_length, 32) })
    }

    // ========================================================
    // EXECUTE TARGET FUNCTION
    // ========================================================
    target_func(captured_arg, tag, payload_ptr)

    // ========================================================
    // PROGRESS REPORTING
    // ========================================================
    if ((i + 1) % 100 == 0) {
      var elapsed = (Date.now() - startTime) / 1000
      var rate = (i + 1) / elapsed
      console.log(`[*] Progress: ${i + 1}/${ITERATIONS} (${rate.toFixed(2)} exec/s) | Last mutation: ${mutation.description}`)
    }

  } catch (e) {
    // ========================================================
    // CRASH DETECTED
    // ========================================================
    crashes++
    console.log(`\n[!] CRASH at iteration ${i}`)
    console.log(`[!] Mutation: ${mutation ? mutation.description : 'Unknown'}`)
    console.log(`[!] Exception: ${e.message}`)
    console.log(`[!] Payload length: ${payload_length} bytes`)
    try {
      console.log(`    Preview (truncated):\n${payload_preview}`)
    } catch (err) {
      console.log(`    (Could not display payload preview)`)
    }

    // Note: After a crash, app state might be corrupted
    // Ideally should restart app here, but that's complex in simple fuzzer
  }
}

// ============================================================
// FINAL STATISTICS
// ============================================================
var elapsed = (Date.now() - startTime) / 1000
console.log(`\n[+] Fuzzing complete!`)
console.log(`    Iterations: ${ITERATIONS}`)
console.log(`    Crashes: ${crashes}`)
console.log(`    Crash rate: ${((crashes / ITERATIONS) * 100).toFixed(2)}%`)
console.log(`    Duration: ${elapsed.toFixed(2)}s`)
console.log(`    Rate: ${(ITERATIONS / elapsed).toFixed(2)} exec/s`)

if (crashes > 0) {
  console.log(`\n[!] Found ${crashes} crashes!`)
  console.log(`[*] Check iOS crash logs at:`)
  console.log(`    /private/var/mobile/Library/Logs/CrashReporter/`)
}

Run it with:

bash
frida -U -l simple-fuzzer.js <Program>

Fuzzing Best Practices

  1. Start with small corpus: Begin with 3-5 well-formed inputs
  2. Monitor memory: Use Process.enumerateRanges() to check for memory leaks
  3. Save interesting crashes: Check /var/mobile/Library/Logs/CrashReporter/ frequently
  4. Use coverage feedback: AFL++ mode in fpicker provides better coverage
  5. Timeout detection: Add timeouts to detect hangs (not just crashes)
  6. State restoration: Reset app state between iterations when possible
  7. Multiple mutation strategies: Combine random, format string, and grammar-based fuzzing
  8. Log systematically: Keep detailed logs of crash-inducing inputs

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:

sh
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.

Memory Inspection and Manipulation

Scan and modify process memory:

/tmp/memory-scan.js
// frida -U <program> -l /tmp/memory-scan.js

console.log("[*] Memory scanning and manipulation tools loaded")

// Search for string in memory
function findString(searchString) {
  console.log(`[*] Searching for: "${searchString}"`)
  var results = []
  
  Process.enumerateRanges('r--').forEach(function(range) {
    try {
      Memory.scan(range.base, range.size, searchString, {
        onMatch: function(address, size) {
          results.push(address)
          console.log(`[+] Found at: ${address}`)
          
          // Read context around the match
          try {
            var context = address.readUtf8String(50)
            console.log(`    Context: "${context}"`)
          } catch (e) {}
        },
        onComplete: function() {}
      })
    } catch (e) {
      // Range not readable
    }
  })
  
  console.log(`[*] Found ${results.length} occurrences`)
  return results
}

// Search for byte pattern
function findBytes(pattern) {
  console.log(`[*] Searching for byte pattern: ${pattern}`)
  var results = []
  
  Process.enumerateRanges('r--').forEach(function(range) {
    try {
      Memory.scan(range.base, range.size, pattern, {
        onMatch: function(address, size) {
          results.push(address)
          console.log(`[+] Found at: ${address}`)
          
          // Dump bytes
          var bytes = address.readByteArray(16)
          console.log(`    Bytes: ${hexdump(bytes, { length: 16 })}`)
        },
        onComplete: function() {}
      })
    } catch (e) {}
  })
  
  return results
}

// Dump memory region
function dumpMemory(address, size) {
  try {
    var addr = ptr(address)
    var data = addr.readByteArray(size)
    console.log(hexdump(data, { offset: 0, length: size, header: true, ansi: true }))
    return data
  } catch (e) {
    console.log(`[!] Failed to read memory: ${e.message}`)
    return null
  }
}

// Write to memory
function patchMemory(address, bytes) {
  try {
    var addr = ptr(address)
    
    // Save original bytes
    var original = addr.readByteArray(bytes.length)
    console.log("[*] Original bytes:")
    console.log(hexdump(original))
    
    // Write new bytes
    addr.writeByteArray(bytes)
    console.log("[+] Memory patched successfully")
    console.log("[*] New bytes:")
    console.log(hexdump(addr.readByteArray(bytes.length)))
    
    return true
  } catch (e) {
    console.log(`[!] Failed to patch memory: ${e.message}`)
    return false
  }
}

// Watch memory region for changes
function watchMemory(address, size) {
  var addr = ptr(address)
  var original = addr.readByteArray(size)
  
  console.log(`[*] Watching ${size} bytes at ${address}`)
  
  setInterval(function() {
    var current = addr.readByteArray(size)
    if (JSON.stringify(original) !== JSON.stringify(current)) {
      console.log(`[!] Memory changed at ${address}`)
      console.log("[*] Old:")
      console.log(hexdump(original, { length: Math.min(size, 64) }))
      console.log("[*] New:")
      console.log(hexdump(current, { length: Math.min(size, 64) }))
      original = current
    }
  }, 1000)
}

// Enumerate loaded modules and their ranges
function enumerateModules() {
  console.log("\n[*] Loaded modules:")
  Process.enumerateModules().forEach(function(module) {
    console.log(`\n  ${module.name}`)
    console.log(`    Base: ${module.base}`)
    console.log(`    Size: ${module.size}`)
    console.log(`    Path: ${module.path}`)
  })
}

// Find pointers to a specific address
function findPointers(targetAddress) {
  var target = ptr(targetAddress)
  var results = []
  
  console.log(`[*] Searching for pointers to ${target}`)
  
  Process.enumerateRanges('r--').forEach(function(range) {
    try {
      Memory.scan(range.base, range.size, target.toString().slice(2), {
        onMatch: function(address, size) {
          results.push(address)
          console.log(`[+] Pointer found at: ${address}`)
        },
        onComplete: function() {}
      })
    } catch (e) {}
  })
  
  return results
}

// Protection utilities
function getProtection(address) {
  var addr = ptr(address)
  var ranges = Process.enumerateRanges('---')
  
  for (var i = 0; i < ranges.length; i++) {
    var range = ranges[i]
    if (addr.compare(range.base) >= 0 && 
        addr.compare(range.base.add(range.size)) < 0) {
      return range.protection
    }
  }
  
  return "unknown"
}

function changeProtection(address, size, protection) {
  try {
    Memory.protect(ptr(address), size, protection)
    console.log(`[+] Changed protection at ${address} to ${protection}`)
    return true
  } catch (e) {
    console.log(`[!] Failed to change protection: ${e.message}`)
    return false
  }
}

// Export functions for interactive use
rpc.exports = {
  findString: findString,
  findBytes: findBytes,
  dumpMemory: dumpMemory,
  patchMemory: patchMemory,
  watchMemory: watchMemory,
  enumerateModules: enumerateModules,
  findPointers: findPointers,
  getProtection: getProtection,
  changeProtection: changeProtection
}

console.log("\n[+] Available functions:")
console.log("  - findString(str)")
console.log("  - findBytes(pattern)")
console.log("  - dumpMemory(address, size)")
console.log("  - patchMemory(address, [bytes])")
console.log("  - watchMemory(address, size)")
console.log("  - enumerateModules()")
console.log("  - findPointers(address)")
console.log("  - getProtection(address)")
console.log("  - changeProtection(address, size, 'rwx')")

// Example usage:
// findString("password")
// dumpMemory("0x100000000", 256)
// patchMemory("0x100000000", [0x90, 0x90, 0x90])

Frida Android Tutorials

Frida Tutorial

References

tip

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

Support HackTricks