Налаштування Frida на iOS

Reading time: 29 minutes

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks

Встановлення Frida

Кроки для встановлення Frida на пристрій з Jailbreak:

  1. Відкрийте додаток Cydia/Sileo.
  2. Перейдіть до Manage -> Sources -> Edit -> Add.
  3. Введіть "https://build.frida.re" як URL.
  4. Перейдіть до щойно доданого джерела Frida.
  5. Встановіть пакет Frida.

Якщо ви використовуєте Corellium, вам потрібно завантажити реліз Frida з [https://github.com/frida/frida/releases] (frida-gadget-[yourversion]-ios-universal.dylib.gz), розпакувати його та скопіювати у місце розташування dylib, яке попросить Frida, наприклад: /Users/[youruser]/.cache/frida/gadget-ios.dylib

Після встановлення ви можете на ПК виконати команду frida-ls-devices і перевірити, що пристрій з'явився (ваш ПК повинен мати доступ до нього).
Також виконайте frida-ps -Uia щоб перевірити запущені процеси на телефоні.

Frida на пристрої без Jailbreak і без патчування додатку

Перегляньте цю публікацію в блозі про те, як використовувати Frida на пристроях без Jailbreak без патчування додатку: https://mrbypass.medium.com/unlocking-potential-exploring-frida-objection-on-non-jailbroken-devices-without-application-ed0367a84f07

Встановлення клієнта Frida

Встановіть frida tools:

bash
pip install frida-tools
pip install frida

Зі встановленим Frida server та пристроєм, увімкненим і підключеним, перевірте, чи клієнт працює:

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

Frida Trace

note

Якщо вам колись знадобиться навчання з reversing iOS / Frida, перегляньте https://reversing.training/

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 '*[* *]'

Отримати всі класи та методи

  • Автодоповнення: Просто виконайте frida -U <program>

  • Отримати всі доступні класи (фільтрувати за рядком)

/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.")
}
  • Отримати усі методи класу (фільтрувати за рядком)
/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.")
}
  • Виклик функції
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

Перехоплюйте та змінюйте виклики методів Objective-C:

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

Поглиблене Objective-C hooking з 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 — це двигун трасування коду Frida. Він дозволяє відстежувати потоки, захоплюючи кожну функцію, кожен блок, навіть кожну інструкцію, яка виконується.

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

Це цікаво для цілей налагодження, але для fuzzing постійно виконувати .follow() та .unfollow() дуже неефективно.

Fpicker

fpicker — це Frida-based fuzzing suite, який пропонує різні режими fuzzing для in-process fuzzing, наприклад режим AFL++ або пасивний режим трасування. Він має працювати на всіх платформах, які підтримує 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
  • Підготуйте FS:
bash
# From inside fpicker clone
mkdir -p examples/target-app # Where the fuzzing script will be
mkdir -p examples/target-app/out # For code coverage and crashes
mkdir -p examples/target-app/in # For starting inputs

# Create at least 1 input for the fuzzer
echo Hello World > examples/target-app/in/0
  • Fuzzer скрипт (examples/target-app/myfuzzer.js):
examples/target-app/myfuzzer.js
// Import the fuzzer base class
import { Fuzzer } from "../../harness/fuzzer.js"

class TargetAppFuzzer extends Fuzzer {
constructor() {
console.log("[*] TargetAppFuzzer: 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 TargetAppFuzzer()
rpc.exports.fuzzer = f

// Export cleanup method if available
if (f.cleanup) {
rpc.exports.cleanup = f.cleanup.bind(f)
}
  • Скомпілюйте the fuzzer:
bash
# From inside fpicker clone
## Compile from "myfuzzer.js" to "harness.js"
frida-compile examples/target-app/myfuzzer.js -o harness.js
  • Викличте fuzzer fpicker за допомогою radamsa:
bash
# Basic fuzzing with radamsa mutation
fpicker -v --fuzzer-mode active -e attach -p <Program to fuzz> -D usb \
-o examples/target-app/out/ -i examples/target-app/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/target-app/out/ -i examples/target-app/in/ -f harness.js

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

caution

У цьому випадку ми aren't restarting the app or restoring the state після кожного payload. Тож, якщо Frida finds a crash the next inputs after that payload might also crash the app (оскільки app перебуває в нестабільному стані) навіть якщо input shouldn't crash app.

Крім того, Frida буде hook into exception signals of iOS, тож коли Frida finds a crash, ймовірно, iOS crash reports won't be generated.

Щоб цього уникнути, наприклад, ми могли б перезапускати app після кожного Frida crash.

Advanced Fuzzing with Crash Monitoring

Для більш надійного fuzzing з автоматичним виявленням crash та перезапуском app використовуйте цей розширений скрипт:

examples/target-app/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

Щоб використовувати просунутий fuzzer:

bash
# Compile the advanced fuzzer
frida-compile examples/target-app/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/target-app/out"
INPUT_DIR="examples/target-app/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

Простий автономний fuzzer (без fpicker)

Для швидкого fuzzing-тестування без налаштування fpicker використовуйте цей standalone скрипт:

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

Запустіть його за допомогою:

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

Найкращі практики fuzzing

  1. Start with small corpus: Почніть з 3-5 добре сформованих вводів
  2. Monitor memory: Використовуйте Process.enumerateRanges() для перевірки memory leaks
  3. Save interesting crashes: Часто перевіряйте /var/mobile/Library/Logs/CrashReporter/
  4. Use coverage feedback: Режим AFL++ в fpicker дає кращий coverage
  5. Timeout detection: Додавайте тайм-аути для виявлення зависань (не лише crashes)
  6. State restoration: Скидайте стан додатка між ітераціями, коли це можливо
  7. Multiple mutation strategies: Комбінуйте random, format string та grammar-based fuzzing
  8. Log systematically: Ведіть детальні logs для вводів, що викликають crashes

Logs & Crashes

Ви можете перевірити macOS console або log cli, щоб переглянути macOS logs.\ Також можна дивитися логи з iOS за допомогою idevicesyslog.\ Деякі логи будуть опускати інформацію, додаючи <private>. Щоб показати всю інформацію, потрібно встановити профіль з https://developer.apple.com/bug-reporting/profiles-and-logs/, щоб увімкнути цю private інформацію.

Якщо не знаєте, що робити:

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

Ви можете переглянути звіти про падіння у:

  • iOS
  • Settings → Privacy → Analytics & Improvements → Analytics Data
  • /private/var/mobile/Library/Logs/CrashReporter/
  • macOS:
  • /Library/Logs/DiagnosticReports/
  • ~/Library/Logs/DiagnosticReports

warning

iOS зберігає лише 25 звітів про падіння одного й того ж додатка, тому потрібно їх очистити, інакше iOS перестане створювати нові звіти.

Перегляд та маніпуляція пам'яттю

Сканувати та змінювати пам'ять процесу:

/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

Frida Tutorial

Посилання

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks