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)