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

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

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

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

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

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

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

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.jstraced UIKit action dispatch to identify the target class and button selector.enum_q9v0.jsenumerated methods onSwizzleMeTimbers.Q9V0.trace_methods.jsvalidated the runtime call flow and confirmed_9zBreturned0x0.solve.jsforced_9zBto returnYES, 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.