← Back to blog

Bypassing iOS Frida Detection with LLDB and Frida

6 min read

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 with brew install libimobiledevice
  • frida-tools – install with pipx install frida-tools
  • LLDB – 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.12 on iPhone X with paler1an
  • iOS 18.7.2 on iPad 7 with paler1an

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.

  1. Install palera1n following the documentation
  2. Run the palera1n -l command using a USB-A cable
  3. Follow the instructions in the console
  4. If there is a "timeout" message, unplug the phone and plug it again

You should see something like:

paler1an-exploit paler1an-exploit

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.

Links: