Context & Setup
After long weeks of macOS reverse engineering, I decided to take a break. These 8ksec challenges came just in time.
It was a good opportunity to test the reverse engineering techniques I learned on an iOS application.
First, let's talk about the device I'll use for this challenge.
Host machine setup
Before going further, be sure that you have installed on your host computer:
libimobiledevice– install withbrew install libimobiledevicefrida-tools– install withpipx install frida-toolsLLDB– install by downloading Xcode
Jailbreak
I was really confused about jailbreak at first. I thought it was available for any iPhone... which is not the case.
The thing I wish I knew before starting: you have to find a match between an iOS version and a phone that supports the jailbreak. The website ios.cfw.guide is an excellent companion for your jailbreak journey.
The trickiest part is that even if you find a lot of matches browsing the website, when you try to download the corresponding firmware you will notice that Apple revoked the signature of some of them. As you could guess, these non-revoked signatures are often firmware versions vulnerable to exploits used by jailbreaks.
Then it is a cat and mouse game: when Apple releases a new version with a patch for a vulnerability used by the jailbreak, the community has to figure out a new way to exploit the phone.
I was still able to find the following matches:
- iOS
16.7.12on iPhone X withpaler1an - iOS
18.7.2on iPad 7 withpaler1an
You can buy these devices as refurbished for cheap.
So, for my setup I used an iPhone X with iOS 16.7.12.
If you don't have a jailbroken iPhone as 8kSec recommends, you can follow this guide.
Paler1an
Running the jailbreak is actually pretty easy.
- Install
palera1nfollowing the documentation - Run the
palera1n -lcommand using a USB-A cable - Follow the instructions in the console
- If there is a "timeout" message, unplug the phone and plug it again
You should see something like:
Challenge Overview
We have to intercept the dummyFunction(flag:) call and retrieve the flag passed as a Swift string argument.
Phase 0: Load the application, connect the device
We start this challenge with an .ipa, in this part of the post we will install the application on the iPhone.
Load the application
This is detailed in the 8kSec challenges and it's the only guided part they provide. I'll let you follow the instructions there.
Connect to the device using SSH
Now that the application is installed, we can start exploring.
Tools to install on the jailbroken device
Via a package manager like Sileo or Zebra:
- OpenSSH – enables SSH access to the device
- Frida – the Frida server (
frida-server) - debugserver – for remote debugging with LLDB (sometimes bundled in developer tools packages)
Note: I had to install Termnux3 on the phone and reset the root password to enable SSH access.
SSH into your iPhone
First, forward the iPhone's SSH port to your host using iproxy:
iproxy 2222 22 &
Then connect:
ssh root@localhost -p 2222
Default password is usually alpine – change it with passwd if you haven't.
Start debugserver and attach with LLDB
On the iPhone (via SSH), start debugserver attached to the target app:
debugserver *:5678 --waitfor FridaInTheMiddle
You can now, tap on the application on the phone.
On your host, forward the debug port and connect LLDB:
iproxy 1234 5678 & lldb (lldb) process connect connect://localhost:1234
You're now attached to the running process and ready to debug.
Phase 1: Reversing Frida Detection Logic
Once Frida was installed on the jailbroken device, an alert appeared and the app exited.
Tap the app icon to launch it, then attach LLDB using the debugserver setup from earlier.
After attaching LLDB to the application, the modules were not immediately loaded. I had to use the finish command several times to reach the point where modules (like dylib) were loaded.
I started investigating with LLDB, using a naive symbol lookup:
(lldb) image lookup -rn 'frida'
I got several matches in the debug dylib:
FridaInTheMiddle.debug.dylib`property wrapper backing initializer of FridaInTheMiddle.ContentView.fridaDetected : Swift.Bool
...
There's a boolean property called fridaDetected in a SwiftUI state. This likely controls the alert behavior, but we want to identify the function that performs the detection.
Let’s search again:
(lldb) image lookup -rn 'check' FridaInTheMiddle.debug.dylib
Nothing. Let's try uppercase:
(lldb) image lookup -rn 'Check' FridaInTheMiddle.debug.dylib
Success:
FridaInTheMiddle.debug.dylib`FridaInTheMiddle.systemSanityCheck() -> Swift.Bool
This looks promising. Let's add a breakpoint:
(lldb) breakpoint set --name 'FridaInTheMiddle.systemSanityCheck' (lldb) continue
Once the breakpoint was hit, I finished the function:
(lldb) finish (lldb) register write x0 0 (lldb) c
We successfully bypassed the Frida detection logic.
Phase 2: Tracing the Function
We now need the mangled name of dummyFunction(flag:) for use with Frida's JavaScript API.
To trigger the function, tap the "Intercept First Argument Using Frida" button in the app.
I used frida-trace to trace matching functions:
frida-trace -U FridaInTheMiddle -i "*dummy*"
Eventually:
$s16FridaInTheMiddle11ContentViewV13dummyFunction4flagySS_tF()
That's our target.
Phase 3: Hooking the Swift Function
I wrote the following Frida hook script and ran it with:
frida -U FridaInTheMiddle -l script.js
// Load the debug dylib module
const mod = Module.load('FridaInTheMiddle.debug.dylib');
// Find the mangled function name in the module exports
const fn = mod.findExportByName("$s16FridaInTheMiddle11ContentViewV13dummyFunction4flagySS_tF");
// Attach interceptor to hook the function
Interceptor.attach(fn, {
onEnter(args) {
// In ARM64, Swift strings are passed in x0 and x1 registers
console.log('x0', this.context.x0); // String metadata/length
console.log('x1', this.context.x1); // String pointer (for large strings)
}
});
To trigger the function, we tap the button again.
Output:
x0 0xd00000000000001f
x1 0x80000001042996c0
Phase 4: Understanding Swift String ABI (ARM64)
After researching the Swift String ABI for ARM64, I learned:
- Small strings (≤ 15 bytes) are packed in x0 and x1.
- Large strings are heap-allocated. x1 holds a pointer to their contents (offset by +32).
0x1f in x0 = 31, suggesting a large string.
Read the String
Interceptor.attach(fn, {
onEnter(args) {
// Get x1 register which holds the pointer to the string object
const x1 = this.context.x1;
// For heap-allocated strings, the actual string data is at offset +32
const ptr_x1 = ptr(x1).add(32);
// Read and display the UTF-8 string
console.log(ptr_x1.readUtf8String());
}
});
Output:
CTF{you_evaded_frida_detection}
Boom.
Conclusion
We successfully:
- Identified and bypassed dynamic Frida detection using LLDB.
- Traced Swift symbols with Frida.
- Hooked and intercepted Swift function arguments.
- Decoded the Swift string layout at runtime.