daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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