iOS Frida 구성
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 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
Frida 설치
Steps to install Frida on a Jailbroken device:
- 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
앱을 패치하지 않고 탈옥되지 않은 기기에서 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가 설치되어 있고 디바이스가 실행되어 연결된 상태라면, 클라이언트가 작동하는지 확인하세요:
frida-ls-devices # List devices
frida-ps -Uia # Get running processes
Frida 추적
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.")
}
- 클래스의 모든 메서드 가져오기 (문자열로 필터링)
// 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 with 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
}
})
}
LLDB-Assisted Frida Detection Bypass & Swift Hooking
원격 디버깅 파이프라인
Penetration tests against production-like builds는 jailbreak 보호 기능을 활성화한 상태에서 Frida를 연결해야 하는 경우가 많습니다. 신뢰할 수 있는 워크플로우는 Apple의 debugserver와 LLDB를 USB 멀티플렉싱으로 연동하는 것입니다:
- Wi-Fi가 없어도 탈옥된 폰에 접근할 수 있도록 SSH를 포워딩합니다:
iproxy 2222 22 &다음에ssh root@localhost -p 2222. - 기기에서 디버거 스텁을 실행하고 대상 프로세스를 기다리게 합니다:
debugserver *:5678 --waitfor <BundleName>그런 다음 SpringBoard에서 앱을 실행합니다. - 디버깅 포트를 포워딩하고 macOS에서 LLDB로 연결합니다:
iproxy 1234 5678 &
lldb
(lldb) process connect connect://localhost:1234
- 생성자가 반환되어 LLDB가 모든 Swift/ObjC 이미지를 해석할 수 있도록
finish를 몇 번 사용한 후 심볼 패치 작업을 시작하세요.
앱이 시작 시 anti-instrumentation checks를 수행하더라도 frida-server를 병행 실행하는 것이 이제 실용적입니다.
Patching Swift jailbreak / Frida checks
Swift 앱은 종종 jailbreak 감지를 systemSanityCheck() -> Bool 같은 불리언 헬퍼로 중앙집중화합니다. LLDB가 이미 연결된 상태라면 함수 이름을 찾고 바이너리를 수정하지 않고 해당 함수가 false를 반환하도록 강제할 수 있습니다:
(lldb) image lookup -rn 'frida'
(lldb) image lookup -rn 'Check' FridaInTheMiddle.debug.dylib
(lldb) breakpoint set --name 'FridaInTheMiddle.systemSanityCheck'
(lldb) c
(lldb) finish
(lldb) register write x0 0
(lldb) c
arm64에서는 Swift 반환 값이 x0에 있으므로, finish 이후 해당 레지스터를 0으로 하면 모든 호출자가 환경이 깨끗하다고 믿게 되어 frida-server가 리스닝하는 동안 UI가 유지됩니다.
Frida용 Swift 타깃 발견
탐지 코드를 무력화하면 민감한 데이터를 처리하는 함수의 mangled name(예: “Get Flag” 버튼 뒤의 동작)을 추측하는 대신 동적으로 찾아낼 수 있습니다:
frida-trace -U <BundleName> -i "*dummy*"
Trigger the UI action and frida-trace will log the exact symbol such as $s16FridaInTheMiddle11ContentViewV13dummyFunction4flagySS_tF. That string can be fed into Module.load(<app>.debug.dylib).findExportByName() inside a Frida script for precise hooking.
Hooking Swift String arguments
순수 Swift 함수를 가로챌 때 레지스터에서 고수준 인자를 재구성하려면 Swift ABI를 이해하는 것이 필수적입니다:
- 작은 문자열 (≤15 bytes) 은 인라인으로 저장되며
x0의 하위 바이트가 길이를 담고 있습니다. 문자열 자체는x0/x1의 나머지 부분에 패킹되어 있습니다. - 큰 문자열 (>15 bytes) 은 힙 기반 객체입니다.
x1은 객체 헤더에 대한 포인터를 담고 있으며 UTF‑8 버퍼는x1 + 32에서 시작합니다.
단일 훅으로 앱의 소스를 리버스 엔지니어링하지 않고도 두 경우를 모두 추출할 수 있습니다:
const mod = Module.load('FridaInTheMiddle.debug.dylib')
const fn = mod.findExportByName('$s16FridaInTheMiddle11ContentViewV13dummyFunction4flagySS_tF')
Interceptor.attach(fn, {
onEnter() {
const inlineLen = this.context.x0.and(0xff)
if (inlineLen.toInt32() > 0 && inlineLen.toInt32() <= 15) {
console.log('flag:', this.context.x0.readUtf8String(inlineLen.toInt32()))
return
}
const heapPtr = ptr(this.context.x1).add(32)
console.log('flag:', heapPtr.readUtf8String())
}
})
이 수준에서 함수를 계측하면 어떤 비밀 String 인수들—flags, session tokens, or dynamically generated credentials—은 UI에 절대 표시되지 않더라도 덤프할 수 있습니다. 이 훅을 위의 LLDB 패치와 결합하면 jailbreak나 Frida 탐지에도 불구하고 앱을 관찰 상태로 계속 실행할 수 있습니다.
Frida Fuzzing
Frida Stalker
From the docs: Stalker는 Frida의 코드 tracing engine입니다. 스레드를 followed하고, 실행되는 모든 every function, 모든 every block, 심지어 실행되는 모든 instruction까지 capturing할 수 있습니다.
Frida Stalker를 구현한 예제는 https://github.com/poxyran/misc/blob/master/frida-stalker-example.py
다음은 function이 호출될 때마다 Frida Stalker를 attach하는 또 다른 예제입니다:
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++ 모드나 passive tracing mode 같은 다양한 fuzzing 모드를 제공합니다. 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)
}
- 컴파일 fuzzer를:
# From inside fpicker clone
## Compile from "myfuzzer.js" to "harness.js"
frida-compile examples/target-app/myfuzzer.js -o harness.js
- **
radamsa**를 사용하여 fuzzer **fpicker**를 호출:
# 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 이후에 aren’t restarting the app or restoring the state 않습니다. 그래서 Frida가 crash를 발견하면, 해당 payload 이후의 next inputs도 앱이 불안정한 상태에 있기 때문에 crash the app할 수 있습니다(해당 input shouldn’t crash해야 함에도 불구하고).
Moreover, Frida will hook into exception signals of iOS, so when Frida finds a crash, probably an iOS crash reports won’t be generated.
To prevent this, for example, we could restart the app after each Frida crash.
고급 Fuzzing with Crash Monitoring
자동 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
고급 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
- Start with small corpus: 3-5개의 잘 형성된 입력으로 시작하세요
- Monitor memory:
Process.enumerateRanges()를 사용하여 메모리 leaks를 확인하세요 - Save interesting crashes:
/var/mobile/Library/Logs/CrashReporter/를 자주 확인해 흥미로운 크래시를 저장하세요 - Use coverage feedback: fpicker의 AFL++ 모드는 더 나은 커버리지를 제공합니다
- Timeout detection: 정지(hangs)를 감지하기 위해 타임아웃을 추가하세요(단지 크래시만이 아닙니다)
- State restoration: 가능하면 반복 간에 앱 상태를 리셋하세요
- Multiple mutation strategies: random, format string, grammar-based fuzzing을 결합하세요
- Log systematically: 크래시를 유발한 입력의 자세한 로그를 보관하세요
Logs & Crashes
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
- 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 튜토리얼
참고자료
- Great Reversing Training
- Getting Started with Frida
- Bypassing iOS Frida detection with LLDB and Frida
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 지원하기
- 구독 계획 확인하기!
- **💬 디스코드 그룹 또는 텔레그램 그룹에 참여하거나 트위터 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
HackTricks

