frida-configuration-in-ios.md (59937B)
1 --- 2 title: "iOS Frida Configuration" 3 section: "Mobile" 4 sectionSlug: "mobile-pentesting" 5 sourcePath: "src/mobile-pentesting/ios-pentesting/frida-configuration-in-ios.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/mobile-pentesting/ios-pentesting/frida-configuration-in-ios.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # iOS Frida Configuration 14 15 ## Installing Frida 16 17 **Steps to install Frida on a Jailbroken device:**<sup>[[2]](#references)</sup> 18 19 1. Open Cydia/Sileo app. 20 2. Navigate to Manage -> Sources -> Edit -> Add. 21 3. Enter "https://build.frida.re" as the URL. 22 4. Go to the newly added Frida source. 23 5. Install the Frida package. 24 25 If you are using **Corellium** you will need to download the Frida release from [https://github.com/frida/frida/releases](https://github.com/frida/frida/releases) (`frida-gadget-[yourversion]-ios-universal.dylib.gz`) and unpack and copy to the dylib location Frida asks for, e.g.: `/Users/[youruser]/.cache/frida/gadget-ios.dylib` 26 27 After installed, you can use in your PC the command **`frida-ls-devices`** and check that the device appears (your PC needs to be able to access it).\ 28 Execute also **`frida-ps -Uia`** to check the running processes of the phone. 29 30 ## Frida without Jailbroken device & without patching the app 31 32 Check this blog post about how to use Frida in non-jailbroken devices without patching the app: [https://mrbypass.medium.com/unlocking-potential-exploring-frida-objection-on-non-jailbroken-devices-without-application-ed0367a84f07](https://mrbypass.medium.com/unlocking-potential-exploring-frida-objection-on-non-jailbroken-devices-without-application-ed0367a84f07)<sup>[[4]](#references)</sup> 33 34 ## Frida Client Installation 35 36 Install **frida tools**: 37 38 ```bash 39 pip install frida-tools 40 pip install frida 41 ``` 42 43 With the Frida server installed and the device running and connected, **check** if the client is **working**: 44 45 ```bash 46 frida-ls-devices # List devices 47 frida-ps -Uia # Get running processes 48 ``` 49 50 ## Frida Trace 51 52 > [!NOTE] 53 > If at some point you need a training on reversing iOS / Frida check [https://reversing.training/](https://reversing.training/)<sup>[[1]](#references)</sup> 54 55 ```bash 56 # Functions 57 ## Trace all functions with the word "log" in their name 58 frida-trace -U <program> -i "*log*" 59 frida-trace -U <program> -i "*log*" | swift demangle # Demangle names 60 61 # Objective-C 62 ## Trace all methods of all classes 63 frida-trace -U <program> -m "*[* *]" 64 65 ## Trace all methods with the word "authentication" from classes that start with "NE" 66 frida-trace -U <program> -m "*[NE* *authentication*]" 67 68 # Plug-In 69 ## To hook a short-lived plugin, start Frida with the plugin binary's identifier 70 frida-trace -U -W <if-plugin-bin> -m '*[* *]' 71 ``` 72 73 ### Get all classes and methods 74 75 - Auto complete: Just execute `frida -U <program>` 76 77 - Get **all** available **classes** (filter by string) 78 79 ```javascript 80 // frida -U <program> -l /tmp/script.js 81 82 var filterClass = "" // Leave empty to list all classes, or set to "NSString" for example 83 84 if (ObjC.available) { 85 var classCount = 0 86 var classList = [] 87 88 for (var className in ObjC.classes) { 89 if (ObjC.classes.hasOwnProperty(className)) { 90 if (!filterClass || className.toLowerCase().includes(filterClass.toLowerCase())) { 91 classList.push(className) 92 classCount++ 93 } 94 } 95 } 96 97 // Sort alphabetically for better readability 98 classList.sort() 99 100 console.log(`\n[*] Found ${classCount} classes matching '${filterClass || "all"}':\n`) 101 classList.forEach(function(name) { 102 console.log(name) 103 }) 104 } else { 105 console.log("[!] Objective-C runtime is not available.") 106 } 107 ``` 108 109 - Get **all** **methods** of a **class** (filter by string) 110 111 ```javascript 112 // frida -U <program> -l /tmp/script.js 113 114 var specificClass = "NSURL" // Change to your target class 115 var filterMethod = "" // Leave empty to list all methods, or set to "init" for example 116 117 if (ObjC.available) { 118 if (ObjC.classes.hasOwnProperty(specificClass)) { 119 var methods = ObjC.classes[specificClass].$ownMethods 120 var filteredMethods = [] 121 122 for (var i = 0; i < methods.length; i++) { 123 if (!filterMethod || methods[i].toLowerCase().includes(filterMethod.toLowerCase())) { 124 filteredMethods.push(methods[i]) 125 } 126 } 127 128 console.log(`\n[*] Found ${filteredMethods.length} methods in class '${specificClass}' matching '${filterMethod || "all"}':\n`) 129 filteredMethods.forEach(function(method) { 130 console.log(`${specificClass}: ${method}`) 131 }) 132 133 // Also show inherited methods 134 var inheritedMethods = ObjC.classes[specificClass].$methods 135 console.log(`\n[*] Total methods including inherited: ${inheritedMethods.length}`) 136 } else { 137 console.log(`[!] Class '${specificClass}' not found.`) 138 console.log("[*] Tip: Use the class enumeration script to find available classes.") 139 } 140 } else { 141 console.log("[!] Objective-C runtime is not available.") 142 } 143 ``` 144 145 - **Call a function** 146 147 ```javascript 148 // Find the address of the function to call 149 const func_addr = Module.findExportByName("<Prog Name>", "<Func Name>") 150 151 if (!func_addr) { 152 console.log("[!] Function not found. Available exports:") 153 Module.enumerateExports("<Prog Name>").slice(0, 10).forEach(function(exp) { 154 console.log(` ${exp.name} at ${exp.address}`) 155 }) 156 throw new Error("Function not found") 157 } 158 159 // Declare the function to call 160 const func = new NativeFunction( 161 func_addr, 162 "void", 163 ["pointer", "pointer", "pointer"], 164 {} 165 ) 166 167 var arg0 = null 168 var attempt = 0 169 var maxAttempts = 100 170 171 console.log("[*] Waiting for function to be called to capture arg0...") 172 173 // In this case to call this function we need to intercept a call to it to copy arg0 174 Interceptor.attach(func_addr, { 175 onEnter: function (args) { 176 if (!arg0) { 177 arg0 = new NativePointer(args[0]) 178 console.log(`[+] Captured arg0: ${arg0}`) 179 } 180 }, 181 }) 182 183 // Wait until a call to the func occurs (with timeout) 184 while (!arg0 && attempt < maxAttempts) { 185 Thread.sleep(0.1) 186 attempt++ 187 if (attempt % 10 == 0) { 188 console.log(`[*] Still waiting... (${attempt}/${maxAttempts})`) 189 } 190 } 191 192 if (!arg0) { 193 throw new Error("Timeout: Could not capture arg0. Try triggering the function in the app.") 194 } 195 196 // Now call the function with custom arguments 197 var arg1 = Memory.allocUtf8String("custom_tag") 198 var arg2 = Memory.allocUtf8String("Custom message from Frida") 199 200 console.log("[+] Calling function with custom arguments...") 201 func(arg0, arg1, arg2) 202 203 console.log("[+] Function called successfully!") 204 ``` 205 206 ### Hook Objective-C Methods 207 208 Intercept and modify Objective-C method calls: 209 210 ```javascript 211 // frida -U <program> -l /tmp/hook-objc.js 212 213 // Hook a specific Objective-C method 214 function hookMethod(className, methodName) { 215 var hook = ObjC.classes[className][methodName] 216 217 if (!hook) { 218 console.log(`[!] Method ${className}.${methodName} not found`) 219 return 220 } 221 222 Interceptor.attach(hook.implementation, { 223 onEnter: function(args) { 224 console.log(`\n[*] Called: [${className} ${methodName}]`) 225 226 // args[0] is self, args[1] is _cmd (selector) 227 // Actual method arguments start at args[2] 228 229 // Print self 230 try { 231 var selfObj = new ObjC.Object(args[0]) 232 console.log(` self: ${selfObj}`) 233 } catch (e) { 234 console.log(` self: ${args[0]}`) 235 } 236 237 // Print arguments (adjust based on method signature) 238 for (var i = 2; i < 6; i++) { 239 if (args[i]) { 240 try { 241 // Try as ObjC object 242 var obj = new ObjC.Object(args[i]) 243 console.log(` arg[${i-2}]: ${obj} (${obj.$className})`) 244 } catch (e) { 245 // Try as string 246 try { 247 var str = args[i].readUtf8String() 248 console.log(` arg[${i-2}]: "${str}"`) 249 } catch (e2) { 250 // Just print pointer 251 console.log(` arg[${i-2}]: ${args[i]}`) 252 } 253 } 254 } 255 } 256 257 // You can modify arguments here 258 // args[2] = ObjC.classes.NSString.stringWithString_("Modified!") 259 }, 260 onLeave: function(retval) { 261 // Print return value 262 try { 263 var ret = new ObjC.Object(retval) 264 console.log(` => ${ret}`) 265 } catch (e) { 266 console.log(` => ${retval}`) 267 } 268 269 // You can modify return value here 270 // retval.replace(ObjC.classes.NSString.stringWithString_("Hijacked!")) 271 } 272 }) 273 274 console.log(`[+] Hooked: [${className} ${methodName}]`) 275 } 276 277 // Example: Hook multiple methods 278 if (ObjC.available) { 279 console.log("[*] Objective-C runtime available") 280 281 // Hook authentication methods 282 hookMethod("LoginViewController", "- authenticate:") 283 hookMethod("AuthManager", "- validatePassword:") 284 285 // Hook data storage methods 286 hookMethod("NSUserDefaults", "+ standardUserDefaults") 287 hookMethod("NSUserDefaults", "- setObject:forKey:") 288 hookMethod("NSUserDefaults", "- objectForKey:") 289 290 // Hook crypto methods 291 hookMethod("NSString", "- dataUsingEncoding:") 292 293 // Hook network methods 294 hookMethod("NSURLSession", "- dataTaskWithRequest:completionHandler:") 295 296 console.log("[+] All hooks installed successfully") 297 } else { 298 console.log("[!] Objective-C runtime not available") 299 } 300 ``` 301 302 Advanced Objective-C hooking with method swizzling: 303 304 ```javascript 305 // Replace method implementation entirely 306 function swizzleMethod(className, methodName, newImplementation) { 307 if (!ObjC.available) { 308 console.log("[!] Objective-C runtime not available") 309 return 310 } 311 312 var targetClass = ObjC.classes[className] 313 if (!targetClass) { 314 console.log(`[!] Class ${className} not found`) 315 return 316 } 317 318 var method = targetClass[methodName] 319 if (!method) { 320 console.log(`[!] Method ${methodName} not found in ${className}`) 321 return 322 } 323 324 var originalImpl = method.implementation 325 326 method.implementation = ObjC.implement(method, function(handle, selector) { 327 // handle is 'self', selector is the method selector 328 console.log(`[*] Swizzled method called: [${className} ${methodName}]`) 329 330 // Call custom logic 331 var result = newImplementation(handle, selector, arguments) 332 333 // Optionally call original 334 // var original = new NativeFunction(originalImpl, method.returnType, method.argumentTypes) 335 // return original(handle, selector, ...) 336 337 return result 338 }) 339 340 console.log(`[+] Swizzled: [${className} ${methodName}]`) 341 } 342 343 // Example: Always return true for authentication 344 swizzleMethod("AuthManager", "- isAuthenticated", function(self, sel) { 345 console.log("[!] Bypassing authentication check!") 346 return 1 // true 347 }) 348 349 // Example: Bypass jailbreak detection 350 if (ObjC.available) { 351 var jailbreakMethods = [ 352 ["JailbreakDetector", "- isJailbroken"], 353 ["SecurityChecker", "- checkJailbreak"], 354 ["AntiDebug", "- isDebugged"] 355 ] 356 357 jailbreakMethods.forEach(function(item) { 358 try { 359 swizzleMethod(item[0], item[1], function() { 360 console.log(`[!] Bypassing ${item[0]}.${item[1]}`) 361 return 0 // false 362 }) 363 } catch (e) { 364 // Method doesn't exist, ignore 365 } 366 }) 367 } 368 ``` 369 370 ## LLDB-Assisted Frida Detection Bypass & Swift Hooking 371 372 ### Remote debugging pipeline 373 374 Penetration tests against production-like builds often require keeping jailbreak protections enabled while still attaching Frida. A reliable workflow is to pair Apple’s `debugserver` with LLDB over USB multiplexing:<sup>[[3]](#references)</sup> 375 376 1. Forward SSH so the jailbroken phone is reachable even without Wi-Fi: `iproxy 2222 22 &` followed by `ssh root@localhost -p 2222`. 377 2. On the device, spawn the debugger stub and make it wait for the target process: `debugserver *:5678 --waitfor <BundleName>` and then launch the app from the SpringBoard. 378 3. Forward the debugging port and attach LLDB from macOS: 379 380 ```bash 381 iproxy 1234 5678 & 382 lldb 383 (lldb) process connect connect://localhost:1234 384 ``` 385 386 4. Use `finish` a few times so constructors return and LLDB can resolve every Swift/ObjC image before you start patching symbols. 387 388 Keeping `frida-server` running in parallel now becomes viable even if the app performs anti-instrumentation checks during startup. 389 390 ### Patching Swift jailbreak / Frida checks 391 392 Swift apps frequently centralize jailbreak detection into a boolean helper such as `systemSanityCheck() -> Bool`. With LLDB already attached you can resolve the function name and force it to return `false` without touching the binary: 393 394 ```bash 395 (lldb) image lookup -rn 'frida' 396 (lldb) image lookup -rn 'Check' FridaInTheMiddle.debug.dylib 397 (lldb) breakpoint set --name 'FridaInTheMiddle.systemSanityCheck' 398 (lldb) c 399 (lldb) finish 400 (lldb) register write x0 0 401 (lldb) c 402 ``` 403 404 On arm64 the Swift return value lives in `x0`, so zeroing that register after `finish` makes every caller believe the environment is clean, which keeps the UI alive while `frida-server` remains listening. 405 406 ### Discovering Swift targets for Frida 407 408 Once the detection code is neutralized you can dynamically discover the mangled name of the function that handles sensitive data (e.g. the action behind a “Get Flag” button) instead of guessing: 409 410 ```bash 411 frida-trace -U <BundleName> -i "*dummy*" 412 ``` 413 414 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. 415 416 ### Hooking Swift `String` arguments 417 418 Understanding the Swift ABI is essential to rebuild high-level arguments from registers when you intercept pure Swift functions: 419 420 - **Small strings (≤15 bytes)** are stored inline and the low byte of `x0` carries the length. The characters themselves are packed in the remainder of `x0`/`x1`. 421 - **Large strings (>15 bytes)** are heap-backed objects. `x1` holds the pointer to the object header and the UTF‑8 buffer starts at `x1 + 32`. 422 423 A single hook can extract both cases without reverse engineering the app’s source: 424 425 ```javascript 426 const mod = Module.load('FridaInTheMiddle.debug.dylib') 427 const fn = mod.findExportByName('$s16FridaInTheMiddle11ContentViewV13dummyFunction4flagySS_tF') 428 Interceptor.attach(fn, { 429 onEnter() { 430 const inlineLen = this.context.x0.and(0xff) 431 if (inlineLen.toInt32() > 0 && inlineLen.toInt32() <= 15) { 432 console.log('flag:', this.context.x0.readUtf8String(inlineLen.toInt32())) 433 return 434 } 435 const heapPtr = ptr(this.context.x1).add(32) 436 console.log('flag:', heapPtr.readUtf8String()) 437 } 438 }) 439 ``` 440 441 Instrumenting the function at this level means any secret `String` arguments—flags, session tokens, or dynamically generated credentials—can be dumped even when the UI never displays them. Combine this hook with the LLDB patch above to keep the app running under observation despite jailbreak or Frida detections. 442 443 ## Frida Fuzzing 444 445 ### Frida Stalker 446 447 [From the docs](https://frida.re/docs/stalker/): Stalker is Frida’s code **tracing engine**. It allows threads to be **followed**, **capturing** every function, **every block**, even every instruction which is executed.<sup>[[5]](#references)</sup> 448 449 You have an example implementing Frida Stalker in [https://github.com/poxyran/misc/blob/master/frida-stalker-example.py](https://github.com/poxyran/misc/blob/master/frida-stalker-example.py) 450 451 This is another example to attach Frida Stalker every time a function is called: 452 453 ```javascript 454 console.log("[*] Starting Stalker setup...") 455 456 const TARGET_MODULE = "<Program>" 457 const TARGET_FUNCTION = "<function_name>" 458 459 const func_addr = Module.findExportByName(TARGET_MODULE, TARGET_FUNCTION) 460 461 if (!func_addr) { 462 console.log(`[!] Function '${TARGET_FUNCTION}' not found in module '${TARGET_MODULE}'`) 463 throw new Error("Target function not found") 464 } 465 466 console.log(`[+] Found target function at: ${func_addr}`) 467 468 const func = new NativeFunction( 469 func_addr, 470 "void", 471 ["pointer", "pointer", "pointer"], 472 {} 473 ) 474 475 var callCount = 0 476 var coverageMap = {} 477 478 Interceptor.attach(func_addr, { 479 onEnter: function (args) { 480 callCount++ 481 console.log(`\n[*] Call #${callCount} - Message: ${args[2].readCString()}`) 482 483 // Follow the current thread 484 Stalker.follow(Process.getCurrentThreadId(), { 485 events: { 486 compile: true, // Only collect coverage for newly encountered blocks 487 }, 488 onReceive: function (events) { 489 const bbs = Stalker.parse(events, { 490 stringify: false, 491 annotate: false, 492 }) 493 494 // Track unique code blocks for coverage 495 var newBlocks = 0 496 bbs.flat().forEach(function(addr) { 497 var addrStr = addr.toString() 498 if (!coverageMap[addrStr]) { 499 coverageMap[addrStr] = true 500 newBlocks++ 501 } 502 }) 503 504 console.log(`[+] Executed ${bbs.flat().length} blocks (${newBlocks} new)`) 505 console.log(`[+] Total unique blocks covered: ${Object.keys(coverageMap).length}`) 506 507 // Optionally print trace (can be verbose) 508 if (callCount <= 3) { // Only print first 3 traces 509 console.log("\n[*] Execution trace:") 510 bbs.flat().slice(0, 20).forEach(function(addr) { // Limit to first 20 511 console.log(` ${DebugSymbol.fromAddress(addr)}`) 512 }) 513 if (bbs.flat().length > 20) { 514 console.log(` ... and ${bbs.flat().length - 20} more blocks`) 515 } 516 } 517 }, 518 }) 519 }, 520 onLeave: function (retval) { 521 Stalker.unfollow(Process.getCurrentThreadId()) 522 Stalker.flush() // Important: flush all events before unfollow 523 Stalker.garbageCollect() // Clean up 524 }, 525 }) 526 527 console.log("[+] Stalker attached successfully. Waiting for function calls...") 528 ``` 529 530 > [!CAUTION] 531 > This is interesting from debugging purposes but for fuzzing, to be constantly **`.follow()`** and **`.unfollow()`** is very inefficient. 532 533 ## [Fpicker](https://github.com/ttdennis/fpicker) 534 535 [**fpicker**](https://github.com/ttdennis/fpicker) is a **Frida-based fuzzing suite** that offers a variety of fuzzing modes for in-process fuzzing, such as an AFL++ mode or a passive tracing mode. It should run on all platforms that are supported by Frida. 536 537 - [**Install fpicker**](https://github.com/ttdennis/fpicker#requirements-and-installation) **& radamsa** 538 539 ```bash 540 # Get fpicker 541 git clone https://github.com/ttdennis/fpicker 542 cd fpicker 543 544 # Get Frida core devkit and prepare fpicker 545 wget https://github.com/frida/frida/releases/download/16.1.4/frida-core-devkit-16.1.4-[yourOS]-[yourarchitecture].tar.xz 546 # e.g. https://github.com/frida/frida/releases/download/16.1.4/frida-core-devkit-16.1.4-macos-arm64.tar.xz 547 tar -xf ./*tar.xz 548 cp libfrida-core.a libfrida-core-[yourOS].a #libfrida-core-macos.a 549 550 # Install fpicker 551 make fpicker-[yourOS] # fpicker-macos 552 # This generates ./fpicker 553 554 # Install radamsa (fuzzer generator) 555 brew install radamsa 556 ``` 557 558 - **Prepare the FS:** 559 560 ```bash 561 # From inside fpicker clone 562 mkdir -p examples/target-app # Where the fuzzing script will be 563 mkdir -p examples/target-app/out # For code coverage and crashes 564 mkdir -p examples/target-app/in # For starting inputs 565 566 # Create at least 1 input for the fuzzer 567 echo Hello World > examples/target-app/in/0 568 ``` 569 570 - **Fuzzer script** (`examples/target-app/myfuzzer.js`): 571 572 ```javascript 573 // Import the fuzzer base class 574 import { Fuzzer } from "../../harness/fuzzer.js" 575 576 class TargetAppFuzzer extends Fuzzer { 577 constructor() { 578 console.log("[*] TargetAppFuzzer: Initializing fuzzer...") 579 580 // ============================================================ 581 // CONFIGURATION SECTION 582 // ============================================================ 583 // These are the values you need to customize for your target: 584 585 const TARGET_MODULE = "<Program name>" // The binary/library name (e.g., "MyApp" or "libcrypto.dylib") 586 // Use Process.enumerateModules() to find module names 587 588 const TARGET_FUNCTION = "<func name to fuzz>" // The exported function name to fuzz (e.g., "process_input") 589 // Use Module.enumerateExports() to find function names 590 591 const CAPTURE_TIMEOUT = 30 // Seconds to wait for capturing function arguments 592 // Increase if function is rarely called 593 594 // ============================================================ 595 // FUNCTION DISCOVERY 596 // ============================================================ 597 // Find the address of the target function in memory 598 console.log(`[*] Looking for function '${TARGET_FUNCTION}' in module '${TARGET_MODULE}'...`) 599 var target_addr = Module.findExportByName(TARGET_MODULE, TARGET_FUNCTION) 600 601 // Validate that the function was found 602 if (!target_addr) { 603 console.log(`[!] Function not found. Available exports from ${TARGET_MODULE}:`) 604 Module.enumerateExports(TARGET_MODULE).slice(0, 10).forEach(function(exp) { 605 console.log(` - ${exp.name}`) 606 }) 607 throw new Error(`Function '${TARGET_FUNCTION}' not found`) 608 } 609 610 console.log(`[+] Found target function at: ${target_addr}`) 611 612 // ============================================================ 613 // FUNCTION SIGNATURE SETUP 614 // ============================================================ 615 // Create a NativeFunction wrapper so we can call the function 616 // Signature: void function_name(pointer arg0, pointer arg1, pointer arg2) 617 // IMPORTANT: Adjust the return type and argument types to match your target function 618 // - First parameter: return type ("void", "int", "pointer", etc.) 619 // - Second parameter: array of argument types 620 var target_func = new NativeFunction( 621 target_addr, 622 "void", // Return type - change if function returns a value 623 ["pointer", "pointer", "pointer"], // Argument types - adjust based on actual function signature 624 {} 625 ) 626 627 // ============================================================ 628 // PARENT CLASS INITIALIZATION 629 // ============================================================ 630 // Initialize the fpicker Fuzzer base class with our target information 631 super(TARGET_MODULE, target_addr, target_func) 632 this.target_addr = target_addr 633 634 // ============================================================ 635 // STATISTICS TRACKING 636 // ============================================================ 637 // Keep track of fuzzing progress and results 638 this.fuzzCount = 0 // Total number of fuzzing iterations executed 639 this.crashCount = 0 // Number of crashes/exceptions encountered 640 this.startTime = Date.now() // Start time for calculating execution rate 641 642 // ============================================================ 643 // STATIC ARGUMENTS PREPARATION 644 // ============================================================ 645 // Some functions require specific arguments that don't change 646 // Here we prepare the second argument (a tag string) 647 this.tag = Memory.allocUtf8String("FUZZ_TAG") 648 console.log("[+] Allocated tag argument") 649 650 // ============================================================ 651 // DYNAMIC ARGUMENT CAPTURE 652 // ============================================================ 653 // Many functions require a context pointer or handle as first argument 654 // We can't create this ourselves, so we intercept a real call to capture it 655 656 var captured_ptr = null // Will hold the captured pointer 657 var attempts = 0 // Counter for timeout mechanism 658 var maxAttempts = CAPTURE_TIMEOUT * 10 // Total attempts (checking every 100ms) 659 660 console.log(`[*] Waiting up to ${CAPTURE_TIMEOUT}s to capture first argument...`) 661 console.log("[*] Please trigger the target function in the app!") 662 console.log("[*] (Interact with the app to make it call the function)") 663 664 // Attach an interceptor to capture arguments when function is called 665 var interceptor = Interceptor.attach(this.target_addr, { 666 onEnter: function (args) { 667 // Only capture once (first call) 668 if (!captured_ptr) { 669 captured_ptr = new NativePointer(args[0]) 670 console.log(`[+] Captured first argument: ${captured_ptr}`) 671 672 // Try to read and display other arguments for debugging 673 // This helps verify we're hooking the right function 674 try { 675 if (args[1]) console.log(`[*] Arg 1: ${args[1].readCString()}`) 676 if (args[2]) console.log(`[*] Arg 2: ${args[2].readCString()}`) 677 } catch (e) { 678 console.log("[*] Could not read string arguments (might not be strings)") 679 } 680 } 681 }, 682 }) 683 684 // ============================================================ 685 // WAIT FOR CAPTURE WITH TIMEOUT 686 // ============================================================ 687 // Poll until we capture the argument or timeout 688 while (!captured_ptr && attempts < maxAttempts) { 689 Thread.sleep(0.1) // Sleep 100ms between checks 690 attempts++ 691 692 // Print progress every 5 seconds so user knows we're still waiting 693 if (attempts % 50 == 0) { 694 console.log(`[*] Still waiting... (${attempts / 10}s / ${CAPTURE_TIMEOUT}s)`) 695 } 696 } 697 698 // ============================================================ 699 // CLEANUP AND VALIDATION 700 // ============================================================ 701 // Detach the interceptor - we don't need it anymore 702 interceptor.detach() 703 704 // Check if we successfully captured the argument 705 if (!captured_ptr) { 706 throw new Error(`Timeout: Could not capture first argument after ${CAPTURE_TIMEOUT}s. Ensure the function is being called.`) 707 } 708 709 // Store the captured pointer for use in fuzz() method 710 this.captured_ptr = captured_ptr 711 console.log("[+] Fuzzer initialization complete!") 712 console.log("[+] Ready to fuzz...") 713 } 714 715 // This function is called by fpicker for each fuzzing iteration 716 // @param payload: NativePointer - Pointer to the fuzzing input data in memory 717 // @param len: Number - Length of the input data in bytes 718 fuzz(payload, len) { 719 this.fuzzCount++ 720 721 try { 722 // ============================================================ 723 // STEP 1: Convert the raw payload to a usable format 724 // ============================================================ 725 // The payload comes as a pointer to memory. We need to: 726 // 1. Read the raw bytes from that memory location 727 // 2. Allocate new memory for a null-terminated C string 728 // 3. Copy the data and add null terminator 729 730 var payload_mem = Memory.alloc(len + 1) // Allocate len + 1 for null terminator 731 Memory.copy(payload_mem, payload, len) // Copy the payload bytes 732 payload_mem.add(len).writeU8(0) // Write null terminator at the end 733 734 // ============================================================ 735 // STEP 2: Progress monitoring and statistics 736 // ============================================================ 737 // Log progress every 100 iterations to avoid spamming console 738 if (this.fuzzCount % 100 == 0) { 739 var elapsed = ((Date.now() - this.startTime) / 1000).toFixed(2) 740 var rate = (this.fuzzCount / elapsed).toFixed(2) 741 console.log(`[*] Fuzzing iteration ${this.fuzzCount} (${rate} exec/s, ${this.crashCount} crashes)`) 742 } 743 744 // ============================================================ 745 // STEP 3: Debug logging for initial iterations 746 // ============================================================ 747 // For the first 3 payloads, show what we're testing 748 // This helps verify the fuzzer is working correctly 749 if (this.fuzzCount <= 3) { 750 try { 751 var preview = payload.readCString(Math.min(len, 50)) 752 console.log(`[*] Payload preview (${len} bytes): ${preview}${len > 50 ? '...' : ''}`) 753 } catch (e) { 754 // If readCString fails, it's likely binary data 755 console.log(`[*] Binary payload (${len} bytes)`) 756 } 757 } 758 759 // ============================================================ 760 // STEP 4: Execute the target function with the fuzzed input 761 // ============================================================ 762 // Call the target function with: 763 // - captured_ptr: The first argument we captured during initialization 764 // - tag: A static tag/label for the log entry 765 // - payload_mem: Our fuzzed input as a null-terminated string 766 this.target_function(this.captured_ptr, this.tag, payload_mem) 767 768 } catch (e) { 769 // ============================================================ 770 // STEP 5: Exception handling 771 // ============================================================ 772 // If the target function crashes or throws an exception: 773 // 1. Increment crash counter 774 // 2. Log the details for later analysis 775 // 3. Re-throw so fpicker can record it 776 this.crashCount++ 777 console.log(`[!] Exception in iteration ${this.fuzzCount}: ${e.message}`) 778 console.log(`[!] Stack: ${e.stack}`) 779 780 // Re-throw to let fpicker handle crash detection and logging 781 throw e 782 } 783 } 784 785 // Optional: Cleanup method called when fuzzing ends 786 cleanup() { 787 var elapsed = ((Date.now() - this.startTime) / 1000).toFixed(2) 788 console.log(`\n[*] Fuzzing session complete:`) 789 console.log(` - Total iterations: ${this.fuzzCount}`) 790 console.log(` - Total crashes: ${this.crashCount}`) 791 console.log(` - Duration: ${elapsed}s`) 792 console.log(` - Average rate: ${(this.fuzzCount / elapsed).toFixed(2)} exec/s`) 793 } 794 } 795 796 console.log("[*] Creating fuzzer instance...") 797 const f = new TargetAppFuzzer() 798 rpc.exports.fuzzer = f 799 800 // Export cleanup method if available 801 if (f.cleanup) { 802 rpc.exports.cleanup = f.cleanup.bind(f) 803 } 804 ``` 805 806 - **Compile** the fuzzer: 807 808 ```bash 809 # From inside fpicker clone 810 ## Compile from "myfuzzer.js" to "harness.js" 811 frida-compile examples/target-app/myfuzzer.js -o harness.js 812 ``` 813 814 - Call fuzzer **`fpicker`** using **`radamsa`**: 815 816 ```bash 817 # Basic fuzzing with radamsa mutation 818 fpicker -v --fuzzer-mode active -e attach -p <Program to fuzz> -D usb \ 819 -o examples/target-app/out/ -i examples/target-app/in/ -f harness.js \ 820 --standalone-mutator cmd --mutator-command "radamsa" 821 822 # With AFL++ mode for better coverage 823 fpicker -v --fuzzer-mode afl -e attach -p <Program to fuzz> -D usb \ 824 -o examples/target-app/out/ -i examples/target-app/in/ -f harness.js 825 826 # You can find code coverage and crashes in examples/target-app/out/ 827 # Check crashes: ls -la examples/target-app/out/crashes/ 828 # Check coverage: ls -la examples/target-app/out/coverage/ 829 ``` 830 831 > [!CAUTION] 832 > In this case, the harness **does not restart the app or restore its state** after each payload. If one input crashes or corrupts the app state, later inputs may also crash even when they would not fail from a clean start. 833 > 834 > Frida also hooks iOS exception signals, so a crash observed by Frida may not produce a normal iOS crash report. 835 > 836 > To prevent this, for example, we could restart the app after each Frida crash. 837 838 #### Advanced Fuzzing with Crash Monitoring 839 840 For more robust fuzzing with automatic crash detection and app restart, use this enhanced script: 841 842 ```javascript 843 import { Fuzzer } from "../../harness/fuzzer.js" 844 845 class AdvancedFuzzer extends Fuzzer { 846 constructor() { 847 console.log("[*] Advanced Fuzzer: Initializing with crash monitoring...") 848 849 // ============================================================ 850 // CONFIGURATION 851 // ============================================================ 852 const TARGET_MODULE = "<Program name>" // Module containing the target function 853 const TARGET_FUNCTION = "<func name>" // Function to fuzz 854 855 // ============================================================ 856 // FIND AND SETUP TARGET FUNCTION 857 // ============================================================ 858 var target_addr = Module.findExportByName(TARGET_MODULE, TARGET_FUNCTION) 859 if (!target_addr) { 860 throw new Error(`Function '${TARGET_FUNCTION}' not found`) 861 } 862 863 var target_func = new NativeFunction(target_addr, "void", ["pointer", "pointer", "pointer"], {}) 864 super(TARGET_MODULE, target_addr, target_func) 865 866 // ============================================================ 867 // ADVANCED CRASH DETECTION SETUP 868 // ============================================================ 869 // Install comprehensive crash monitoring before starting fuzzing 870 this.setupCrashMonitoring() 871 872 // Hook dangerous functions that often indicate crashes 873 this.setupSignalHandlers() 874 875 // ============================================================ 876 // CAPTURE RUNTIME ARGUMENTS 877 // ============================================================ 878 // Capture the context pointer needed to call the function 879 this.captured_ptr = this.captureArgument(target_addr, 0) 880 this.tag = Memory.allocUtf8String("FUZZ") 881 882 console.log("[+] Advanced fuzzer ready with crash monitoring enabled") 883 } 884 885 // ============================================================ 886 // CRASH MONITORING SETUP 887 // ============================================================ 888 // This method installs a global exception handler that catches: 889 // - Segmentation faults (invalid memory access) 890 // - Arithmetic exceptions (divide by zero, etc.) 891 // - Abort signals 892 // - Any other exceptions that would normally crash the app 893 setupCrashMonitoring() { 894 Process.setExceptionHandler(function(details) { 895 console.log("\n[!!!] CRASH DETECTED [!!!]") 896 console.log(`[!] Type: ${details.type}`) // Exception type (e.g., "access-violation") 897 console.log(`[!] Address: ${details.address}`) // Address where crash occurred 898 899 // If it's a memory-related crash, show the operation and address 900 console.log(`[!] Memory operation: ${details.memory ? details.memory.operation : 'N/A'}`) 901 902 // ============================================================ 903 // DUMP CPU REGISTERS 904 // ============================================================ 905 // Show CPU register state at crash time (useful for exploitation analysis) 906 if (details.context) { 907 console.log("[!] Registers:") 908 Object.keys(details.context).slice(0, 8).forEach(function(reg) { 909 console.log(` ${reg}: ${details.context[reg]}`) 910 }) 911 } 912 913 // ============================================================ 914 // DUMP CALL STACK (BACKTRACE) 915 // ============================================================ 916 // Show the call stack leading to the crash 917 // This helps identify which code path triggered the issue 918 console.log("[!] Backtrace:") 919 Thread.backtrace(details.context, Backtracer.ACCURATE) 920 .map(DebugSymbol.fromAddress) 921 .slice(0, 10) 922 .forEach(function(symbol, idx) { 923 console.log(` ${idx}: ${symbol}`) 924 }) 925 926 // Return false to let iOS handle the crash (generates crash report) 927 // Return true to suppress the crash and continue (dangerous - app in undefined state) 928 return false 929 }) 930 } 931 932 // ============================================================ 933 // DANGEROUS FUNCTION MONITORING 934 // ============================================================ 935 // Hook common functions that indicate problems: 936 // - abort(): Explicit crash 937 // - __stack_chk_fail(): Stack buffer overflow detected 938 // - __assert_rtn(): Failed assertion 939 // - malloc/free: Memory allocation (can detect double-free, use-after-free) 940 // - memcpy/strcpy: Memory operations (can detect buffer overflows) 941 setupSignalHandlers() { 942 var crashFuncs = [ 943 "abort", // Explicit abort() call 944 "__stack_chk_fail", // Stack canary check failed (buffer overflow) 945 "__assert_rtn", // Assertion failure 946 "malloc", // Memory allocation 947 "free", // Memory deallocation 948 "memcpy", // Memory copy 949 "strcpy" // String copy 950 ] 951 952 crashFuncs.forEach(function(funcName) { 953 try { 954 // Find the function in any loaded module (null = search all) 955 var addr = Module.findExportByName(null, funcName) 956 if (addr) { 957 Interceptor.attach(addr, { 958 onEnter: function(args) { 959 // Only log critical functions to avoid spam 960 if (funcName === "abort" || funcName === "__stack_chk_fail" || funcName === "__assert_rtn") { 961 console.log(`[!] ${funcName} called - potential crash imminent!`) 962 console.log("[!] Backtrace:") 963 // Show where this function was called from 964 Thread.backtrace(this.context, Backtracer.ACCURATE) 965 .map(DebugSymbol.fromAddress) 966 .slice(0, 5) 967 .forEach(function(s) { console.log(` ${s}`) }) 968 } 969 } 970 }) 971 } 972 } catch (e) { 973 // Function not available on this platform, skip it 974 } 975 }) 976 } 977 978 // ============================================================ 979 // ARGUMENT CAPTURE HELPER 980 // ============================================================ 981 // Generic method to capture any argument from a function call 982 // @param addr: Address of the function to monitor 983 // @param argIndex: Which argument to capture (0 = first, 1 = second, etc.) 984 // @param timeout: How long to wait (seconds) before giving up 985 captureArgument(addr, argIndex, timeout = 30) { 986 var captured = null 987 var attempts = 0 988 var maxAttempts = timeout * 10 // Check every 100ms 989 990 console.log(`[*] Capturing argument ${argIndex}...`) 991 console.log(`[*] Trigger the function in the app to capture its arguments`) 992 993 // Hook the function temporarily 994 var hook = Interceptor.attach(addr, { 995 onEnter: function(args) { 996 if (!captured && args[argIndex]) { 997 captured = new NativePointer(args[argIndex]) 998 console.log(`[+] Captured arg[${argIndex}]: ${captured}`) 999 } 1000 } 1001 }) 1002 1003 // Wait for a call to occur 1004 while (!captured && attempts < maxAttempts) { 1005 Thread.sleep(0.1) 1006 attempts++ 1007 } 1008 1009 // Clean up the hook 1010 hook.detach() 1011 1012 if (!captured) { 1013 throw new Error(`Failed to capture argument ${argIndex} after ${timeout}s`) 1014 } 1015 1016 return captured 1017 } 1018 1019 // ============================================================ 1020 // FUZZ EXECUTION METHOD 1021 // ============================================================ 1022 // Called by fpicker for each fuzzing iteration 1023 // @param payload: Pointer to the mutated input data 1024 // @param len: Length of the input in bytes 1025 fuzz(payload, len) { 1026 try { 1027 // ============================================================ 1028 // STEP 1: Input validation 1029 // ============================================================ 1030 // Reject unreasonably large inputs to prevent memory exhaustion 1031 if (len > 1024 * 1024) { // 1MB limit 1032 console.log(`[!] Payload too large: ${len} bytes, skipping`) 1033 return 1034 } 1035 1036 // ============================================================ 1037 // STEP 2: Prepare the fuzzed input 1038 // ============================================================ 1039 // Allocate new memory and copy the payload 1040 // Add null terminator for C string compatibility 1041 var fuzz_data = Memory.alloc(len + 1) // Allocate space + 1 byte for null 1042 Memory.copy(fuzz_data, payload, len) // Copy the payload 1043 fuzz_data.add(len).writeU8(0) // Add null terminator 1044 1045 // ============================================================ 1046 // STEP 3: Execute with timeout detection 1047 // ============================================================ 1048 // Some inputs might cause infinite loops (hangs) 1049 // Use a timer to detect when execution takes too long 1050 var executed = false 1051 var timer = setTimeout(function() { 1052 if (!executed) { 1053 console.log("[!] Execution timeout - possible hang") 1054 // Note: This doesn't stop execution, just logs it 1055 // Consider using Stalker or watchdog thread for true timeout 1056 } 1057 }, 5000) // 5 second timeout 1058 1059 // Call the target function 1060 this.target_function(this.captured_ptr, this.tag, fuzz_data) 1061 1062 // Mark as completed and cancel timeout 1063 executed = true 1064 clearTimeout(timer) 1065 1066 } catch (e) { 1067 // Exception occurred - likely a crash 1068 console.log(`[!] Fuzz iteration exception: ${e.message}`) 1069 throw e // Re-throw for fpicker to handle 1070 } 1071 } 1072 } 1073 1074 const fuzzer = new AdvancedFuzzer() 1075 rpc.exports.fuzzer = fuzzer 1076 ``` 1077 1078 To use the advanced fuzzer: 1079 1080 ```bash 1081 # Compile the advanced fuzzer 1082 frida-compile examples/target-app/advanced-fuzzer.js -o harness-advanced.js 1083 1084 # Run with automatic restart on crash using a wrapper script 1085 cat > fuzz-with-restart.sh << 'EOF' 1086 #!/bin/bash 1087 1088 APP_NAME="<Program to fuzz>" 1089 OUTPUT_DIR="examples/target-app/out" 1090 INPUT_DIR="examples/target-app/in" 1091 HARNESS="harness-advanced.js" 1092 1093 while true; do 1094 echo "[*] Starting fuzzing session at $(date)" 1095 1096 # Run fpicker (will exit on crash) 1097 fpicker -v --fuzzer-mode active -e attach -p "$APP_NAME" -D usb \ 1098 -o "$OUTPUT_DIR" -i "$INPUT_DIR" -f "$HARNESS" \ 1099 --standalone-mutator cmd --mutator-command "radamsa" 1100 1101 EXIT_CODE=$? 1102 echo "[!] Fuzzer exited with code $EXIT_CODE" 1103 1104 if [ $EXIT_CODE -ne 0 ]; then 1105 echo "[*] Crash detected, saving crash info..." 1106 echo "Crash at $(date)" >> "$OUTPUT_DIR/crash_log.txt" 1107 1108 # Kill the app if still running 1109 killall "$APP_NAME" 2>/dev/null 1110 1111 # Wait for app to fully stop 1112 sleep 2 1113 1114 # Restart the app 1115 echo "[*] Restarting app..." 1116 frida -U -f "$APP_NAME" --no-pause & 1117 sleep 3 1118 else 1119 echo "[*] Fuzzing session completed normally" 1120 break 1121 fi 1122 done 1123 EOF 1124 1125 chmod +x fuzz-with-restart.sh 1126 ./fuzz-with-restart.sh 1127 ``` 1128 1129 #### Simple Standalone Fuzzer (Without fpicker) 1130 1131 For quick fuzzing tests without fpicker setup, use this standalone script: 1132 1133 ```javascript 1134 // ============================================================ 1135 // SIMPLE STANDALONE FUZZER 1136 // ============================================================ 1137 // This fuzzer works without fpicker - just load it with Frida 1138 // Usage: frida -U -l simple-fuzzer.js <Program> 1139 // 1140 // This is great for: 1141 // - Quick fuzzing tests 1142 // - When you can't set up fpicker 1143 // - Testing if a function is fuzzable 1144 // - Learning how fuzzing works 1145 1146 console.log("[*] Simple Fuzzer starting...") 1147 1148 // ============================================================ 1149 // CONFIGURATION 1150 // ============================================================ 1151 const TARGET_MODULE = "<Program>" // Your app's main binary name 1152 const TARGET_FUNCTION = "<function_name>" // The function to fuzz 1153 const ITERATIONS = 1000 // How many times to fuzz 1154 const MAX_PAYLOAD_SIZE = 1024 // Maximum size for random payloads 1155 1156 // Helper to build ArrayBuffer from byte array 1157 function bytesToBuffer(bytes) { 1158 var buffer = new ArrayBuffer(bytes.length) 1159 var view = new Uint8Array(buffer) 1160 for (var i = 0; i < bytes.length; i++) { 1161 view[i] = bytes[i] 1162 } 1163 return buffer 1164 } 1165 1166 // Helper to convert ASCII string into byte array (lossy for non-ASCII) 1167 function stringToBytes(str) { 1168 var bytes = [] 1169 for (var i = 0; i < str.length; i++) { 1170 bytes.push(str.charCodeAt(i) & 0xff) 1171 } 1172 return bytes 1173 } 1174 1175 // ============================================================ 1176 // MUTATION STRATEGIES 1177 // ============================================================ 1178 // This function implements various fuzzing mutation strategies 1179 // Each strategy targets different types of vulnerabilities 1180 // Returns an object describing the mutation so we can handle 1181 // both text and binary payloads safely 1182 function mutatePayload(seed) { 1183 var mutations = [ 1184 // Strategy 1: Buffer overflow - very long strings 1185 function() { 1186 return { type: "string", value: "A".repeat(Math.floor(Math.random() * 10000)), description: "Long 'A' string" } 1187 }, 1188 1189 // Strategy 2: Format string bugs 1190 function() { 1191 return { type: "string", value: "%s%s%s%s%s%s%s%s%s%s%n%n%n%n", description: "Format string" } 1192 }, 1193 1194 // Strategy 3: Null bytes and boundary characters 1195 function() { 1196 return { 1197 type: "binary", 1198 value: bytesToBuffer([0, 0, 0].concat(stringToBytes(seed), [0xff, 0xff, 0xff])), 1199 description: "Boundary chars" 1200 } 1201 }, 1202 1203 // Strategy 4: SQL injection patterns 1204 function() { 1205 return { type: "string", value: "' OR '1'='1", description: "SQL injection" } 1206 }, 1207 1208 // Strategy 5: XSS/script injection patterns 1209 function() { 1210 return { type: "string", value: "<script>alert(1)</script>", description: "XSS payload" } 1211 }, 1212 1213 // Strategy 6: Path traversal 1214 function() { 1215 return { type: "string", value: "../../../etc/passwd", description: "Path traversal" } 1216 }, 1217 1218 // Strategy 7: Invalid Unicode sequences 1219 function() { 1220 // Build deliberately malformed UTF sequence (includes null) 1221 return { 1222 type: "binary", 1223 value: bytesToBuffer([0x00, 0xef, 0xff, 0xed, 0xa0, 0x80]), 1224 description: "Invalid Unicode" 1225 } 1226 }, 1227 1228 // Strategy 8: Extremely long repeated input 1229 function() { 1230 return { type: "string", value: seed.repeat(100), description: "Repeated seed" } 1231 }, 1232 1233 // Strategy 9: Null byte injection 1234 function() { 1235 return { 1236 type: "binary", 1237 value: bytesToBuffer(stringToBytes(seed).concat([0, 0, 0, 0])), 1238 description: "Null byte injection" 1239 } 1240 }, 1241 1242 // Strategy 10: Completely random bytes (binary payload) 1243 function() { 1244 var len = Math.floor(Math.random() * MAX_PAYLOAD_SIZE) 1245 var bytes = [] 1246 for (var i = 0; i < len; i++) { 1247 bytes.push(Math.floor(Math.random() * 256)) 1248 } 1249 return { type: "binary", value: bytesToBuffer(bytes), description: `Random ${len}-byte buffer` } 1250 } 1251 ] 1252 1253 // Randomly select one mutation strategy 1254 return mutations[Math.floor(Math.random() * mutations.length)]() 1255 } 1256 1257 // ============================================================ 1258 // FIND TARGET FUNCTION 1259 // ============================================================ 1260 const target_addr = Module.findExportByName(TARGET_MODULE, TARGET_FUNCTION) 1261 if (!target_addr) { 1262 console.log("[!] Target function not found!") 1263 console.log("[*] Available functions (first 20):") 1264 Module.enumerateExports(TARGET_MODULE).slice(0, 20).forEach(function(exp) { 1265 console.log(` - ${exp.name}`) 1266 }) 1267 throw new Error("Function not found") 1268 } 1269 1270 console.log(`[+] Found target at ${target_addr}`) 1271 1272 // ============================================================ 1273 // CREATE FUNCTION WRAPPER 1274 // ============================================================ 1275 // Wrap the native function so we can call it from JavaScript 1276 // Adjust signature if your function has different parameters 1277 const target_func = new NativeFunction( 1278 target_addr, 1279 "void", // Return type 1280 ["pointer", "pointer", "pointer"], // Argument types 1281 {} 1282 ) 1283 1284 // ============================================================ 1285 // CAPTURE REQUIRED ARGUMENTS 1286 // ============================================================ 1287 // Many functions need a context pointer or handle 1288 // We capture it from a real call instead of guessing 1289 var captured_arg = null 1290 console.log("[*] Waiting to capture arguments...") 1291 console.log("[*] Please trigger the function in the app!") 1292 1293 var hook = Interceptor.attach(target_addr, { 1294 onEnter: function(args) { 1295 if (!captured_arg) { 1296 captured_arg = new NativePointer(args[0]) 1297 console.log(`[+] Captured arg: ${captured_arg}`) 1298 } 1299 } 1300 }) 1301 1302 // Wait for the function to be called 1303 while (!captured_arg) { 1304 Thread.sleep(0.1) 1305 } 1306 hook.detach() 1307 1308 // ============================================================ 1309 // START FUZZING LOOP 1310 // ============================================================ 1311 console.log(`[*] Starting ${ITERATIONS} fuzzing iterations...`) 1312 var tag = Memory.allocUtf8String("FUZZ") // Static second argument 1313 var crashes = 0 1314 var startTime = Date.now() 1315 1316 for (var i = 0; i < ITERATIONS; i++) { 1317 var mutation = null 1318 var payload_ptr = null 1319 var payload_length = 0 1320 var payload_preview = "" 1321 1322 try { 1323 // ======================================================== 1324 // GENERATE MUTATED INPUT 1325 // ======================================================== 1326 mutation = mutatePayload("Hello World") 1327 1328 if (mutation.type === "string") { 1329 payload_length = mutation.value.length 1330 payload_ptr = Memory.allocUtf8String(mutation.value) 1331 payload_preview = mutation.value 1332 } else { 1333 payload_length = mutation.value.byteLength 1334 var mem = Memory.alloc(payload_length + 1) 1335 Memory.writeByteArray(mem, mutation.value) 1336 mem.add(payload_length).writeU8(0) 1337 payload_ptr = mem 1338 payload_preview = hexdump(mem, { offset: 0, length: Math.min(payload_length, 32) }) 1339 } 1340 1341 // ======================================================== 1342 // EXECUTE TARGET FUNCTION 1343 // ======================================================== 1344 target_func(captured_arg, tag, payload_ptr) 1345 1346 // ======================================================== 1347 // PROGRESS REPORTING 1348 // ======================================================== 1349 if ((i + 1) % 100 == 0) { 1350 var elapsed = (Date.now() - startTime) / 1000 1351 var rate = (i + 1) / elapsed 1352 console.log(`[*] Progress: ${i + 1}/${ITERATIONS} (${rate.toFixed(2)} exec/s) | Last mutation: ${mutation.description}`) 1353 } 1354 1355 } catch (e) { 1356 // ======================================================== 1357 // CRASH DETECTED 1358 // ======================================================== 1359 crashes++ 1360 console.log(`\n[!] CRASH at iteration ${i}`) 1361 console.log(`[!] Mutation: ${mutation ? mutation.description : 'Unknown'}`) 1362 console.log(`[!] Exception: ${e.message}`) 1363 console.log(`[!] Payload length: ${payload_length} bytes`) 1364 try { 1365 console.log(` Preview (truncated):\n${payload_preview}`) 1366 } catch (err) { 1367 console.log(` (Could not display payload preview)`) 1368 } 1369 1370 // Note: After a crash, app state might be corrupted 1371 // Ideally should restart app here, but that's complex in simple fuzzer 1372 } 1373 } 1374 1375 // ============================================================ 1376 // FINAL STATISTICS 1377 // ============================================================ 1378 var elapsed = (Date.now() - startTime) / 1000 1379 console.log(`\n[+] Fuzzing complete!`) 1380 console.log(` Iterations: ${ITERATIONS}`) 1381 console.log(` Crashes: ${crashes}`) 1382 console.log(` Crash rate: ${((crashes / ITERATIONS) * 100).toFixed(2)}%`) 1383 console.log(` Duration: ${elapsed.toFixed(2)}s`) 1384 console.log(` Rate: ${(ITERATIONS / elapsed).toFixed(2)} exec/s`) 1385 1386 if (crashes > 0) { 1387 console.log(`\n[!] Found ${crashes} crashes!`) 1388 console.log(`[*] Check iOS crash logs at:`) 1389 console.log(` /private/var/mobile/Library/Logs/CrashReporter/`) 1390 } 1391 ``` 1392 1393 Run it with: 1394 ```bash 1395 frida -U -l simple-fuzzer.js <Program> 1396 ``` 1397 1398 #### Fuzzing Best Practices 1399 1400 1. **Start with small corpus**: Begin with 3-5 well-formed inputs 1401 2. **Monitor memory**: Use `Process.enumerateRanges()` to check for memory leaks 1402 3. **Save interesting crashes**: Check `/var/mobile/Library/Logs/CrashReporter/` frequently 1403 4. **Use coverage feedback**: AFL++ mode in fpicker provides better coverage 1404 5. **Timeout detection**: Add timeouts to detect hangs (not just crashes) 1405 6. **State restoration**: Reset app state between iterations when possible 1406 7. **Multiple mutation strategies**: Combine random, format string, and grammar-based fuzzing 1407 8. **Log systematically**: Keep detailed logs of crash-inducing inputs 1408 1409 ### Logs & Crashes 1410 1411 You can check the **macOS console** or the **`log`** cli to check macOS logs.\ 1412 You can check also the logs from iOS using **`idevicesyslog`**.\ 1413 Some logs will omit information adding **`<private>`**. To show all the info you need to install some profile from [https://developer.apple.com/bug-reporting/profiles-and-logs/](https://developer.apple.com/bug-reporting/profiles-and-logs/) to enable that private info. 1414 1415 If you don't know what to do: 1416 1417 ```bash 1418 vim /Library/Preferences/Logging/com.apple.system.logging.plist 1419 <?xml version="1.0" encoding="UTF-8"?> 1420 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 1421 <plist version="1.0"> 1422 <dict> 1423 <key>Enable-Private-Data</key> 1424 <true/> 1425 </dict> 1426 </plist> 1427 1428 killall -9 logd 1429 ``` 1430 1431 You can check the crashes in: 1432 1433 - **iOS** 1434 - Settings → Privacy → Analytics & Improvements → Analytics Data 1435 - `/private/var/mobile/Library/Logs/CrashReporter/` 1436 - **macOS**: 1437 - `/Library/Logs/DiagnosticReports/` 1438 - `~/Library/Logs/DiagnosticReports` 1439 1440 > [!WARNING] 1441 > iOS only stores 25 crashes of the same app, so you need to clean that or iOS will stop creating crashes. 1442 1443 ### Memory Inspection and Manipulation 1444 1445 Scan and modify process memory: 1446 1447 ```javascript 1448 // frida -U <program> -l /tmp/memory-scan.js 1449 1450 console.log("[*] Memory scanning and manipulation tools loaded") 1451 1452 // Search for string in memory 1453 function findString(searchString) { 1454 console.log(`[*] Searching for: "${searchString}"`) 1455 var results = [] 1456 1457 Process.enumerateRanges('r--').forEach(function(range) { 1458 try { 1459 Memory.scan(range.base, range.size, searchString, { 1460 onMatch: function(address, size) { 1461 results.push(address) 1462 console.log(`[+] Found at: ${address}`) 1463 1464 // Read context around the match 1465 try { 1466 var context = address.readUtf8String(50) 1467 console.log(` Context: "${context}"`) 1468 } catch (e) {} 1469 }, 1470 onComplete: function() {} 1471 }) 1472 } catch (e) { 1473 // Range not readable 1474 } 1475 }) 1476 1477 console.log(`[*] Found ${results.length} occurrences`) 1478 return results 1479 } 1480 1481 // Search for byte pattern 1482 function findBytes(pattern) { 1483 console.log(`[*] Searching for byte pattern: ${pattern}`) 1484 var results = [] 1485 1486 Process.enumerateRanges('r--').forEach(function(range) { 1487 try { 1488 Memory.scan(range.base, range.size, pattern, { 1489 onMatch: function(address, size) { 1490 results.push(address) 1491 console.log(`[+] Found at: ${address}`) 1492 1493 // Dump bytes 1494 var bytes = address.readByteArray(16) 1495 console.log(` Bytes: ${hexdump(bytes, { length: 16 })}`) 1496 }, 1497 onComplete: function() {} 1498 }) 1499 } catch (e) {} 1500 }) 1501 1502 return results 1503 } 1504 1505 // Dump memory region 1506 function dumpMemory(address, size) { 1507 try { 1508 var addr = ptr(address) 1509 var data = addr.readByteArray(size) 1510 console.log(hexdump(data, { offset: 0, length: size, header: true, ansi: true })) 1511 return data 1512 } catch (e) { 1513 console.log(`[!] Failed to read memory: ${e.message}`) 1514 return null 1515 } 1516 } 1517 1518 // Write to memory 1519 function patchMemory(address, bytes) { 1520 try { 1521 var addr = ptr(address) 1522 1523 // Save original bytes 1524 var original = addr.readByteArray(bytes.length) 1525 console.log("[*] Original bytes:") 1526 console.log(hexdump(original)) 1527 1528 // Write new bytes 1529 addr.writeByteArray(bytes) 1530 console.log("[+] Memory patched successfully") 1531 console.log("[*] New bytes:") 1532 console.log(hexdump(addr.readByteArray(bytes.length))) 1533 1534 return true 1535 } catch (e) { 1536 console.log(`[!] Failed to patch memory: ${e.message}`) 1537 return false 1538 } 1539 } 1540 1541 // Watch memory region for changes 1542 function watchMemory(address, size) { 1543 var addr = ptr(address) 1544 var original = addr.readByteArray(size) 1545 1546 console.log(`[*] Watching ${size} bytes at ${address}`) 1547 1548 setInterval(function() { 1549 var current = addr.readByteArray(size) 1550 if (JSON.stringify(original) !== JSON.stringify(current)) { 1551 console.log(`[!] Memory changed at ${address}`) 1552 console.log("[*] Old:") 1553 console.log(hexdump(original, { length: Math.min(size, 64) })) 1554 console.log("[*] New:") 1555 console.log(hexdump(current, { length: Math.min(size, 64) })) 1556 original = current 1557 } 1558 }, 1000) 1559 } 1560 1561 // Enumerate loaded modules and their ranges 1562 function enumerateModules() { 1563 console.log("\n[*] Loaded modules:") 1564 Process.enumerateModules().forEach(function(module) { 1565 console.log(`\n ${module.name}`) 1566 console.log(` Base: ${module.base}`) 1567 console.log(` Size: ${module.size}`) 1568 console.log(` Path: ${module.path}`) 1569 }) 1570 } 1571 1572 // Find pointers to a specific address 1573 function findPointers(targetAddress) { 1574 var target = ptr(targetAddress) 1575 var results = [] 1576 1577 console.log(`[*] Searching for pointers to ${target}`) 1578 1579 Process.enumerateRanges('r--').forEach(function(range) { 1580 try { 1581 Memory.scan(range.base, range.size, target.toString().slice(2), { 1582 onMatch: function(address, size) { 1583 results.push(address) 1584 console.log(`[+] Pointer found at: ${address}`) 1585 }, 1586 onComplete: function() {} 1587 }) 1588 } catch (e) {} 1589 }) 1590 1591 return results 1592 } 1593 1594 // Protection utilities 1595 function getProtection(address) { 1596 var addr = ptr(address) 1597 var ranges = Process.enumerateRanges('---') 1598 1599 for (var i = 0; i < ranges.length; i++) { 1600 var range = ranges[i] 1601 if (addr.compare(range.base) >= 0 && 1602 addr.compare(range.base.add(range.size)) < 0) { 1603 return range.protection 1604 } 1605 } 1606 1607 return "unknown" 1608 } 1609 1610 function changeProtection(address, size, protection) { 1611 try { 1612 Memory.protect(ptr(address), size, protection) 1613 console.log(`[+] Changed protection at ${address} to ${protection}`) 1614 return true 1615 } catch (e) { 1616 console.log(`[!] Failed to change protection: ${e.message}`) 1617 return false 1618 } 1619 } 1620 1621 // Export functions for interactive use 1622 rpc.exports = { 1623 findString: findString, 1624 findBytes: findBytes, 1625 dumpMemory: dumpMemory, 1626 patchMemory: patchMemory, 1627 watchMemory: watchMemory, 1628 enumerateModules: enumerateModules, 1629 findPointers: findPointers, 1630 getProtection: getProtection, 1631 changeProtection: changeProtection 1632 } 1633 1634 console.log("\n[+] Available functions:") 1635 console.log(" - findString(str)") 1636 console.log(" - findBytes(pattern)") 1637 console.log(" - dumpMemory(address, size)") 1638 console.log(" - patchMemory(address, [bytes])") 1639 console.log(" - watchMemory(address, size)") 1640 console.log(" - enumerateModules()") 1641 console.log(" - findPointers(address)") 1642 console.log(" - getProtection(address)") 1643 console.log(" - changeProtection(address, size, 'rwx')") 1644 1645 // Example usage: 1646 // findString("password") 1647 // dumpMemory("0x100000000", 256) 1648 // patchMemory("0x100000000", [0x90, 0x90, 0x90]) 1649 ``` 1650 1651 ## Frida Android Tutorials 1652 1653 1654 [Frida Tutorial](/hacktricks/mobile-pentesting/android-app-pentesting/frida-tutorial/overview) 1655 1656 ## References 1657 1658 - [1] [Great Reversing Training](https://reversing.training/) 1659 - [2] [Getting Started with Frida](https://www.briskinfosec.com/blogs/blogsdetail/Getting-Started-with-Frida) 1660 - [3] [Bypassing iOS Frida detection with LLDB and Frida](https://tonygo.tech/blog/2025/8ksec-ios-ctf-writeup) 1661 - [4] [Unlocking Potential: Exploring Frida & Objection on Non-Jailbroken Devices Without Application](https://mrbypass.medium.com/unlocking-potential-exploring-frida-objection-on-non-jailbroken-devices-without-application-ed0367a84f07) 1662 - [5] [Stalker - Frida docs](https://frida.re/docs/stalker/)