Налаштування 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
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.
Встановлення Frida
Кроки для встановлення Frida на пристрій з Jailbreak:
- Відкрийте додаток Cydia/Sileo.
- Перейдіть до Manage -> Sources -> Edit -> Add.
- Введіть "https://build.frida.re" як URL.
- Перейдіть до щойно доданого джерела Frida.
- Встановіть пакет 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:
pip install frida-tools
pip install frida
Зі встановленим Frida server та пристроєм, увімкненим і підключеним, перевірте, чи клієнт працює:
frida-ls-devices # List devices
frida-ps -Uia # Get running processes
Frida Trace
note
Якщо вам колись знадобиться навчання з reversing iOS / Frida, перегляньте https://reversing.training/
# 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>
-
Отримати всі доступні класи (фільтрувати за рядком)
// 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.")
}
- Отримати усі методи класу (фільтрувати за рядком)
// 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.")
}
- Виклик функції
// 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:
// 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:
// 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:
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.
- Install fpicker & radamsa
# Get fpicker
git clone https://github.com/ttdennis/fpicker
cd fpicker
# Get Frida core devkit and prepare fpicker
wget https://github.com/frida/frida/releases/download/16.1.4/frida-core-devkit-16.1.4-[yourOS]-[yourarchitecture].tar.xz
# e.g. https://github.com/frida/frida/releases/download/16.1.4/frida-core-devkit-16.1.4-macos-arm64.tar.xz
tar -xf ./*tar.xz
cp libfrida-core.a libfrida-core-[yourOS].a #libfrida-core-macos.a
# Install fpicker
make fpicker-[yourOS] # fpicker-macos
# This generates ./fpicker
# Install radamsa (fuzzer generator)
brew install radamsa
- Підготуйте FS:
# 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
):
// 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:
# From inside fpicker clone
## Compile from "myfuzzer.js" to "harness.js"
frida-compile examples/target-app/myfuzzer.js -o harness.js
- Викличте fuzzer
fpicker
за допомогоюradamsa
:
# 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 використовуйте цей розширений скрипт:
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:
# 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 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/`)
}
Запустіть його за допомогою:
frida -U -l simple-fuzzer.js <Program>
Найкращі практики fuzzing
- Start with small corpus: Почніть з 3-5 добре сформованих вводів
- Monitor memory: Використовуйте
Process.enumerateRanges()
для перевірки memory leaks - Save interesting crashes: Часто перевіряйте
/var/mobile/Library/Logs/CrashReporter/
- Use coverage feedback: Режим AFL++ в fpicker дає кращий coverage
- Timeout detection: Додавайте тайм-аути для виявлення зависань (не лише crashes)
- State restoration: Скидайте стан додатка між ітераціями, коли це можливо
- Multiple mutation strategies: Комбінуйте random, format string та grammar-based fuzzing
- 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 інформацію.
Якщо не знаєте, що робити:
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 перестане створювати нові звіти.
Перегляд та маніпуляція пам'яттю
Сканувати та змінювати пам'ять процесу:
// 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
Посилання
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
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.