Blog section
Writeup

SwizzleMeTimbers: Unlocking an Objective-C iOS App with Runtime Method Manipulation

An Objective-C runtime writeup for SwizzleMeTimbers, covering button action tracing, target method discovery, call-flow validation, and forcing the guarded unlock path with Frida.

8kSeciOS SecurityFridaObjective-CMethod Swizzling

SwizzleMeTimbers is a pirate-themed Objective-C iOS challenge application. The app contains an Unlock Treasure button, but pressing it normally does not reveal the flag because the treasure-unlock logic is protected by a method that returns a failure value.

SwizzleMeTimbers challenge overview

The challenge hint pointed directly at Objective-C runtime manipulation:

Use method swizzling to unlock the hidden flag.

The goal was to identify the guarded Objective-C method at runtime, change its behavior dynamically, and force the app into the success path.

Objective

Use runtime manipulation to bypass the treasure-unlock logic and trigger the hidden flag path.

Final flag: CTF{{Swizzle_mbers}}

Environment

The challenge was solved on a jailbroken iPhone using Frida. The target bundle identifier was:

com.8ksec.SwizzleMeTimbers
SwizzleMeTimbers bundle identifier

Initial Application Behavior

When the application opened normally, it displayed an Unlock Treasure button.

SwizzleMeTimbers initial app screen

Pressing the button did not reveal the flag. Conceptually, the app behaved like this:

Unlock Treasure button
    -> guarded unlock logic
        -> false
            -> do not reveal flag
Failed treasure unlock

Because this was an Objective-C/UIKit app, the first useful step was to trace the runtime action dispatched when the button was pressed.

Tracing the Button Action

UIKit button taps are commonly dispatched through methods such as UIApplication -sendAction:to:from:forEvent: and UIControl -sendAction:to:forEvent:. A Frida script hooked both methods and printed the action selector, target object, sender object, and button title.

frida -U -f com.8ksec.SwizzleMeTimbers -l action.js --pause
Tracing UIKit button action with Frida

The important part of action.js was converting the selector and describing the Objective-C target object passed into UIKit's action dispatch:

const method = ObjC.classes.UIApplication['- sendAction:to:from:forEvent:'];

Interceptor.attach(method.implementation, {
  onEnter(args) {
    const action = ObjC.selectorAsString(args[2]);
    const target = new ObjC.Object(args[3]);
    const sender = new ObjC.Object(args[4]);

    console.log('[action]', action);
    console.log('[target]', target.$className, target.toString());
    console.log('[sender]', sender.$className, sender.toString());
  }
});

After resuming the app and pressing Unlock Treasure, the trace revealed two important runtime values:

Target class: SwizzleMeTimbers.Q9V0
Button action selector: t4G0

Although the names were obfuscated, the Objective-C runtime still exposed the class and selector names. At this point, the button flow was understood as:

Unlock Treasure button
    -> calls - t4G0
        -> on object SwizzleMeTimbers.Q9V0

Enumerating Methods of the Target Class

After identifying the target class, I enumerated its own methods to find suspicious candidates. The discovery script only needed to resolve the class and print $ownMethods:

const targetClass = 'SwizzleMeTimbers.Q9V0';
const cls = ObjC.classes[targetClass];

console.log('[+] Found class:', targetClass);
cls.$ownMethods.forEach(m => {
  console.log(m);
});
frida -U -f com.8ksec.SwizzleMeTimbers -l enum_q9v0.js --pause
Enumerating Q9V0 Objective-C methods

The output showed these methods:

- viewDidLoad
- _9zB
- t4G0
- initWithNibName:bundle:
- initWithCoder:
- .cxx_destruct

From the action trace, t4G0 was already known to be the button action. The remaining suspicious method was _9zB. Since the challenge description mentioned a protected method returning false, _9zB became the main guard-method candidate.

t4G0 = Unlock Treasure button action
_9zB = guard method that returns false

Tracing the Runtime Call Flow

To validate the hypothesis, I hooked both t4G0 and _9zB and traced calls and return values while pressing the button. The important trace logic was:

const targetClass = 'SwizzleMeTimbers.Q9V0';
const methods = ['- t4G0', '- _9zB'];
const cls = ObjC.classes[targetClass];

methods.forEach(function (methodName) {
  Interceptor.attach(cls[methodName].implementation, {
    onEnter(args) {
      this.methodName = methodName;
      console.log('[CALL]', targetClass, methodName);
    },
    onLeave(retval) {
      console.log('[RET]', targetClass, this.methodName, '=>', retval);
    }
  });
});
frida -U -f com.8ksec.SwizzleMeTimbers -l trace_methods.js --pause
Tracing t4G0 and _9zB runtime calls

The output confirmed the control flow:

[CALL] SwizzleMeTimbers.Q9V0 - t4G0
[CALL] SwizzleMeTimbers.Q9V0 - _9zB
[RET] SwizzleMeTimbers.Q9V0 - _9zB => 0x0
[RET] SwizzleMeTimbers.Q9V0 - t4G0 => 0x4de825600

In Objective-C, Boolean NO is represented as 0. That confirmed _9zB was the guarded method returning false.

Runtime Method Manipulation

The final step was to change _9zB so that it returned true instead of false. With Frida, this can be done by hooking the method and replacing the return value in onLeave.

const targetClass = 'SwizzleMeTimbers.Q9V0';
const targetMethod = '- _9zB';
const cls = ObjC.classes[targetClass];

Interceptor.attach(cls[targetMethod].implementation, {
  onEnter(args) {
    console.log('[+] _9zB called');
  },
  onLeave(retval) {
    console.log('[+] Original return:', retval);
    retval.replace(1);
    console.log('[+] Forced return YES');
  }
});
frida -U -f com.8ksec.SwizzleMeTimbers -l solve.js --pause
Solving SwizzleMeTimbers by forcing _9zB to YES

The solve hook printed:

[+] _9zB called
[+] Original return: 0x0
[+] Forced return YES

This means the method originally returned false, but Frida changed the return value to true at runtime. The application then followed the success path and displayed the treasure popup.

Flag

After forcing _9zB to return YES, the application displayed:

Ye got it
CTF{{Swizzle_mbers}}

Demo Video

Scripts Used

  • action.js traced UIKit action dispatch to identify the target class and button selector.
  • enum_q9v0.js enumerated methods on SwizzleMeTimbers.Q9V0.
  • trace_methods.js validated the runtime call flow and confirmed _9zB returned 0x0.
  • solve.js forced _9zB to return YES, unlocking the success path.

Final Thoughts

This challenge is a compact example of why Objective-C runtime visibility is powerful during iOS dynamic analysis. Even with obfuscated names, UIKit action tracing exposed the target class and selector, method enumeration narrowed the guard candidate, and a return-value hook was enough to unlock the hidden flag path.