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

Result:

The app is extracted to:

captain_nohook_dev-io/Payload/Captain Nohook.app/

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'

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
...

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

To find references to the interesting string:

axt 0x1001563a0

Output:

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

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

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

📝 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

📲 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@:/path/to/TrollStore/appgroup/

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);
        }
    });
});

🧪 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}

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