iOS Frida の設定
Reading time: 33 minutes
tip
AWSハッキングを学び、実践する:
HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:
HackTricks Training GCP Red Team Expert (GRTE)
Azureハッキングを学び、実践する:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricksをサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
Frida のインストール
Jailbroken device に Frida をインストールする手順:
- Cydia/Sileo アプリを開く。
- Manage -> Sources -> Edit -> Add に移動。
- URL に "https://build.frida.re" を入力。
- 追加された Frida ソースに移動。
- Frida パッケージをインストールする。
Corellium を使用している場合は、Frida のリリースを https://github.com/frida/frida/releases(frida-gadget-[yourversion]-ios-universal.dylib.gz)からダウンロードし、展開して Frida が要求する dylib の場所にコピーする必要があります。例: /Users/[youruser]/.cache/frida/gadget-ios.dylib
インストール後、PC でコマンド frida-ls-devices を実行してデバイスが表示されるか確認してください(PC がデバイスへアクセス可能である必要があります)。また frida-ps -Uia を実行して電話の実行中プロセスを確認します。
Frida without Jailbroken device & without patching the app
non-jailbroken devices でアプリを patch せずに Frida を使う方法については、次のブログ記事を参照してください: 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 がインストールされ、デバイスが起動して接続されている状態で、確認してclient が動作しているかどうか:
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 '*[* *]'
すべての classes と methods を取得
-
オートコンプリート: 次のコマンドを実行するだけ:
frida -U <program> -
すべての 利用可能な classes を取得する(文字列でフィルタ)
// 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.")
}
- class の methods を すべて 取得する (文字列でフィルタ)
// 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!")
Objective-C メソッドをフックする
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")
}
method swizzling を用いた高度な Objective-C hooking:
// 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 の code tracing engine です。スレッドを追跡し、実行されるすべての関数、すべてのブロック、さらには各命令までキャプチャできます。
Frida Stalker を実装した例は https://github.com/poxyran/misc/blob/master/frida-stalker-example.py にあります。
これは関数が呼ばれるたびに Frida Stalker をアタッチする別の例です:
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 で、in-process fuzzing 向けに AFL++ mode や passive tracing mode など様々な fuzzing modes を提供します。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
- ファイルシステムを準備する:
# 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 script (
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)
}
- 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
この場合、各payloadの後にアプリを再起動したり状態を復元したりしていません。したがって、Fridaがcrashを検出すると、そのpayload後の次のinputもアプリをcrashさせる可能性があります(アプリが不安定な状態にあるため)、たとえそのinputは本来アプリをcrashさせるべきではない場合でも。
さらに、FridaはiOSの例外シグナルにフックするため、Fridaがcrashを検出した場合、iOSのcrash reports が生成されない可能性が高いです。
これを防ぐために、例えば各Frida crashの後にアプリを再起動することが考えられます。
高度なFuzzingとCrash監視
自動的な crash 検出と app の再起動による、より堅牢な fuzzing のために、次の拡張スクリプトを使用してください:
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
advanced 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 なし)
fpicker のセットアップなしで手早く fuzzing テストを行うには、このスタンドアロンスクリプトを使用してください:
// ============================================================
// 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 Best Practices
- 小さなコーパスから始める: まず3〜5個の適切に形成された入力から始める
- メモリを監視する:
Process.enumerateRanges()を使ってメモリ leaks をチェックする - 興味深いクラッシュを保存する:
/var/mobile/Library/Logs/CrashReporter/を頻繁に確認する - カバレッジフィードバックを利用する: fpicker の AFL++ モードはより良いカバレッジを提供する
- タイムアウト検出: ハングを検出するためにタイムアウトを追加する(クラッシュだけでなく)
- 状態の復元: 可能な場合はイテレーション間でアプリの状態をリセットする
- 複数のミューテーション戦略: ランダム、format string、grammar-based fuzzing を組み合わせる
- 体系的にログを取る: クラッシュを引き起こす入力の詳細なログを保持する
ログ & クラッシュ
macOSログを確認するには、macOSのコンソール または log CLI を確認できる。
iOSのログは idevicesyslog を使って確認できる。
一部のログは情報を省略して <private> を表示する。すべての情報を表示するには、プライベート情報を有効にするために https://developer.apple.com/bug-reporting/profiles-and-logs/ からプロファイルをインストールする必要がある。
何をすればいいかわからない場合:
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
- 設定 → プライバシー → 解析と改善 → 解析データ
/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ハッキングを学び、実践する:
HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:
HackTricks Training GCP Red Team Expert (GRTE)
Azureハッキングを学び、実践する:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricksをサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
HackTricks