DEV Community

Jr Carreiro
Jr Carreiro

Posted on

🧠 iOS Reverse Engineering: Defeating Anti-Debug

A Technical Walkthrough from Static Reversing to Dynamic Hooking

Welcome to this advanced walkthrough of the Captain Nohook iOS challenge, part of the Mobile Hacking Lab training platform.

Our mission?

✅ Analyze the binary.
✅ Identify anti-debug and anti-Frida mechanisms.
✅ Patch them.
✅ Instrument the app dynamically.
✅ Retrieve the hidden flag — and document every single step.


🧭 Objective

Bypass anti-debug and anti-Frida protections, inject FridaGadget, hook runtime components, and dynamically retrieve a hidden flag from the binary.


🛠 Tools & Environment

  • Frida 16.6.6
  • Radare2
  • Rabin2 (from r2 toolset)
  • insert_dylib
  • TrollStore (for installing the patched .ipa)
  • Jailbroken iPhone
  • FridaGadget.dylib

📦 Step 1: Extracting the .IPA

We began by unzipping the provided .ipa file:

unzip com.mobilehackinglab.Captain-Nohook.ipa -d captain_nohook_dev-io 
Enter fullscreen mode Exit fullscreen mode

Result:

The app is extracted to:

captain_nohook_dev-io/Payload/Captain Nohook.app/ 
Enter fullscreen mode Exit fullscreen mode

We now have access to the application bundle, which includes the binary Captain Nohook.


🔍 Step 2: Static String Inspection with Rabin2

We used the following command to find strings related to jailbreak, Frida, ptrace, and debug protection:

rabin2 -zq captain_nohook_dev-io/Payload/Captain\ Nohook.app/Captain\ Nohook | grep -iE 'frida|ptrace|sysctl|debug|jail|hook|flag' 
Enter fullscreen mode Exit fullscreen mode

Output (excerpt):

0x1001563a0 34 33 Arrr, find yerr hidden flag here! 0x10015641f 5 4 flag 0x100156450 22 21 T@"UILabel",N,W,Vflag ... 0x100157380 23 22 /usr/sbin/frida-server 0x1001573b8 12 11 FridaGadget ... 
Enter fullscreen mode Exit fullscreen mode

We found an interesting string at address 0x1001563a0 which is a candidate for the hidden flag.


🧠 Step 3: Analyzing the Binary with Radare2

We opened the binary with:

r2 -AAA captain_nohook_dev-io/Payload/Captain\ Nohook.app/Captain\ Nohook 
Enter fullscreen mode Exit fullscreen mode

To find references to the interesting string:

axt 0x1001563a0 
Enter fullscreen mode Exit fullscreen mode

Output:

sub.Captain_Nohook.ViewController.whereIsflag.allocator...ySo8UIButtonCF_100009e70 0x10000a284 [STRN:r--] add x0, x0, str.Arrr__find_yerr_hidden_flag_here_ 
Enter fullscreen mode Exit fullscreen mode

This confirms that the string is used in a function associated with a button action. We now know what to hook dynamically.


📦 Step 4: Injecting FridaGadget (Dynamic Instrumentation)

After identifying anti-Frida protections and patching static binary checks, we instrumented the app with FridaGadget, a dynamic and injectable version of the Frida runtime.

FridaGadget is used to instrument iOS apps that cannot be easily hooked with frida-server, especially in non-jailbroken environments or when runtime detection is active. It works by being embedded into the app’s binary as a .dylib, which is automatically loaded on app launch and communicates with Frida via USB. This setup lets us inject scripts and hook into runtime behavior without triggering traditional detection.


🧩 Step-by-Step: Embedding FridaGadget

1. Copy the FridaGadget.dylib

We used the FridaGadget.dylib already available in our system (through Objection or manual download).

cp ~/.objection/ios/FridaGadget.dylib Captain\ Nohook.app 
Enter fullscreen mode Exit fullscreen mode

2. Insert the Gadget into the binary

insert_dylib --weak --all-yes @executable_path/FridaGadget.dylib Captain\ Nohook.app/Captain\ Nohook Captain\ Nohook.app/Captain\ Nohook_patched mv Captain\ Nohook.app/Captain\ Nohook_patched Captain\ Nohook.app/Captain\ Nohook 
Enter fullscreen mode Exit fullscreen mode

📝 Why? This embeds the FridaGadget using a weak LC_LOAD_DYLIB command so it won’t crash if not present. It allows Frida to auto-attach once the app is started.

3. Repack the IPA

cd .. zip -r CaptainNohook-Frida.ipa Payload 
Enter fullscreen mode Exit fullscreen mode

📲 4. Install the IPA on the iPhone via TrollStore

This step assumes a TrollStore-enabled device, allowing unsigned IPA installation.

scp CaptainNohook-Frida.ipa mobile@<iphone-ip>:/path/to/TrollStore/appgroup/ 
Enter fullscreen mode Exit fullscreen mode

Once installed, the app can be launched, and Frida will auto-attach.


🧵 Frida Script: monitor_flag.js

We developed the following script to watch for UILabels and capture the real flag.

// monitor_flag.js // This script is used to monitor a UILabel inside the Captain Nohook app and print whenever its text is updated. // It relies on dynamic instrumentation using FridaGadget, allowing us to observe runtime behavior. console.log("🛡️ Monitoring UILabels and checking for flag updates..."); // The Objective-C class we want to analyze is the ViewController defined in the Captain Nohook app. const vcClass = "Captain_Nohook.ViewController"; // Schedule the script to run on the main thread, where UI updates occur. ObjC.schedule(ObjC.mainQueue, function () { // Find all live instances of the ViewController class in memory. const instances = ObjC.chooseSync(ObjC.classes[vcClass]); if (instances.length === 0) { // If no instances were found, display an error and stop. console.error("❌ No ViewController instance found."); return; } // Use the first found instance of the ViewController. const viewController = instances[0]; console.log("✅ ViewController found:", viewController); // Use Key-Value Coding (KVC) to access the UILabel that holds the flag text. // The property name "flag" was discovered through static analysis of the binary. // Specifically, we used the command: // `axt 0x1001563a0` // which showed that the string "Arrr, find yerr hidden flag here!" was referenced inside the function: // `sub.Captain_Nohook.ViewController.whereIsflag.allocator...ySo8UIButtonCF` // From there, we reverse engineered the relationship between the ViewController and the flag UILabel. const flagLabel = viewController.valueForKey_("flag"); // Print the current UILabel state console.log("🏴‍☠️ UILabel da flag:"); console.log(" ▪️ Text:", flagLabel.text().toString()); console.log(" ▪️ Hidden?:", flagLabel.isHidden()); // Intercept all calls to UILabel.setText to observe when any UILabel text is changed at runtime. Interceptor.attach(ObjC.classes.UILabel["- setText:"].implementation, { onEnter: function (args) { // `args[2]` contains the new NSString being set on the UILabel. const newText = new ObjC.Object(args[2]).toString(); // Log the modification console.log("📢 UILabel modified:"); console.log(" ▪️ Class: UILabel"); console.log(" ▪️ New text:", newText); } }); }); 
Enter fullscreen mode Exit fullscreen mode

🧪 Real-Time Output After Button Press

Once the user interacts with the app, we observed the following real output:

📢 UILabel modified: ▪️ Class: UILabel ▪️ New text: Noncompliant device detected! 📢 UILabel modified: ▪️ Class: UILabel ▪️ New text: Yerr hook won't work! 📢 UILabel modified: ▪️ Class: UILabel ▪️ New text: MHL{H00k_1n_Y0ur_D3bUgg3r} 
Enter fullscreen mode Exit fullscreen mode

The final line contains the hidden flag, dynamically generated and not statically embedded.


✅ Final Thoughts

This challenge tested binary analysis, anti-debug bypass, and live instrumentation — all essential skills for mobile offensive security professionals.

FridaGadget made it possible to bypass runtime checks and extract the hidden flag, even in a protected app.


📫 About the Author

[Júnior Carreiro]
🔐 Mobile AppSec | iOS Security | Reverse Engineering
📍 Let's connect: [GitHub] · [Linkedin]
🏷️ Tags: #ios #reverseengineering #frida #infosec #mobile

Top comments (0)