Post

iOS Jailbreak Detection Bypass Using LLDB

iOS Jailbreak Detection Bypass Using LLDB

First, I’d like to point out that this lab can be solved using easier approaches such as Frida scripts or patching the Mach‑O binary and reinstalling the modified application. However, I challenged myself to solve it in a different way—specifically using low‑level debugging with LLDB on a real device.

This approach helps deepen understanding of ARM64 architecture, return values, and runtime behavior, so let’s get started.

Prerequisite Tools

On your machine:

  • A disassembler such as Ghidra, Hopper, or IDA
  • LLDB

On your iPhone:

  • debugserver (LLDB)
  • SSH for remote connection

Application Behavior

After downloading the lab files and installing the IPA on the device, let’s observe the application behavior:

As we can see, the app displays a simple screen indicating that the device is jailbroken. When pressing the OK button, the application exits.


Static Analysis (Ghidra)

Now we will analyze the Mach-O binary using Ghidra. Since this is a jailbreak detection challenge, it makes sense that the responsible function contains the word “jailbreak”. Using Ghidra, we search for relevant symbols:

We find this function named isJailbroken: _$s9No_Escape12isJailbrokenSbyF So this is the function we will focus on.

Understanding the Logic

By analyzing the function, we see that it calls four different jailbreak checks. Conceptually, the function can be simplified as follows:

1
2
3
4
5
6
bool isJailbroken() {
    return checkForJailbreakFiles()
        || checkForWritableSystemDirectories()
        || canOpenCydia()
        || checkSandboxViolation();
}

This is a Boolean OR chain—if any check returns true, the device is considered jailbroken. Our goal is to force this function to return false at runtime using a debugger.


Dynamic Analysis with LLDB

1. Start debugserver

Start debugserver on the iPhone using: debugserver <machineIP>:4444 --waitfor="No Escape"

Launch the app on the device. It will appear stuck on a black screen, which is expected. A connection will be received by debugserver.

1
2
3
iPhone:/ root# debugserver 192.168.1.6:4444                                     
debugserver-@(#)PROGRAM:LLDB  PROJECT:lldb-16.0.0 for arm64.
Listening to port 4444 for a connection from 192.168.1.6...

2. Connect LLDB

On the machine, attach the process to LLDB using: process connect connect://<iPhoneIP>:4444


Handling ASLR

Because iOS uses Address Space Layout Randomization (ASLR), we must calculate the runtime address of the ret instruction.

Calculate the Runtime Address

  1. List the image base address: image list -o -f "No Escape"
    1
    2
    3
    4
    
    (lldb) process connect connect://192.168.1.11:4444
    Process 3156 stopped and restarted: thread 1 received signal: SIGCHLD
    (lldb) image list -o -f "No Escape"
    [  0] 0x0000000002674000 /../No Escape.app/No Escape(0x0000000102674000)
    

    So, we can see the image base address is 0x102674000.

  2. From Ghidra, the offset of the ret instruction is 0xa114 as shown below:
    1
    2
    3
    4
    5
    6
    
                               LAB_10000a104
     10000a104 e8 03 40 b9 ldr w8,[sp]=>local_20 
     10000a108 00 01 00 12 and w0,w8,#0x1 
     10000a10c fd 7b 41 a9 ldp x29=>local_10,x30,[sp, #0x10] 
     10000a110 ff 83 00 91 add sp,sp,#0x20 
     10000a114 c0 03 5f d6 ret // Note Here
    

    The runtime address becomes: 0x102674000 + 0xa114 = 0x10267e114


Setting the Breakpoint

First, we confirm the instruction address using dis -s 0x10267e114.

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) dis -s 0x10267e114 
No Escape$s9No_Escape12isJailbrokenSbyF: 
0x10267e114 <+172>: ret // Note Here 

No Escape$s9No_Escape22checkForJailbreakFiles33_03F80A81LLSbyF: 
0x10267e118 <+0>: sub sp, sp, #0xf0 
0x10267e11c <+4>: stp x20, x19, [sp, #0xd0] 
0x10267e120 <+8>: stp x29, x30, [sp, #0xe0] 
0x10267e124 <+12>: add x29, sp, #0xe0 
0x10267e128 <+16>: stur xzr, [x29, #-0x18] 
0x10267e12c <+20>: sub x8, x29, #0x28 
0x10267e130 <+24>: stur x8, [x29, #-0x60]

It works fine; then set a breakpoint and continue execution:

1
2
(lldb) breakpoint set -a 0x10267e114
(lldb) c 

After a short wait, the breakpoint is triggered:

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) breakpoint set -a 0x10267e114
Breakpoint 1: where = No Escape`$s9No_Escape12isJailbrokenSbyF + 172, address = 0x000000010267e114
(lldb) c 
Process 3156 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x000000010267e114 No Escape`$s9No_Escape12isJailbrokenSbyF + 172
No Escape`$s9No_Escape12isJailbrokenSbyF:
->  0x10267e114 <+172>: ret  // Note Here

No Escape`$s9No_Escape22checkForJailbreakFiles33_BCE8F13474E5A52C60853EA803F80A81LLSbyF:
    0x10267e118 <+0>:   sub    sp, sp, #0xf0
    0x10267e11c <+4>:   stp    x20, x19, [sp, #0xd0]
    0x10267e120 <+8>:   stp    x29, x30, [sp, #0xe0]

Modifying the Return Value

Reading the registers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(lldb) register read 
General Purpose Registers:
        x0 = 0x0000000000000001
        x1 = 0x0000000000000000
        x2 = 0x0000000000000000
        x3 = 0x000000028135e140
                ........
       x23 = 0x0000000000000001
       x24 = 0x0000000000000000
       x25 = 0x00000001ed176000  (void *)0x00000001f210ca18: UIAnimatablePropertyBase + 160
       x26 = 0x00000001ed18c000  UIKitCore`UIPeripheralHost._interfaceAutorotationDisabled + 5052
       x27 = 0x000000002b870064
       x28 = 0x0000000000000010
        fp = 0x000000016d78ad10
        lr = 0x000000010267a970  No Escape`$s9No_Escape11AppDelegateC11application_29didFinishLaunchingWithOptionsSbSo13UIApplicationC_SDySo0k6LaunchJ3KeyaypGSgtF + 48
        sp = 0x000000016d78acd0
        pc = 0x000000010267e114  No Escape`$s9No_Escape12isJailbrokenSbyF + 172
      cpsr = 0x60000000

Note: On ARM64, the return value of a function is stored in the x0 register.

You’ll notice that x0 = 1. A value of 1 means the function is returning true (jailbroken). To bypass the detection, overwrite the return value to 0 (false).

1
2
3
(lldb) register write x0 0 
(lldb) c 
Process 3156 resuming

Result

After continuing execution, the jailbreak detection is successfully bypassed at runtime, and the application proceeds normally, revealing the flag:

Final Notes

  • This method does not modify the binary.
  • It demonstrates how understanding ARM64 calling conventions enables powerful runtime analysis.
  • The same approach applies to many jailbreak detection implementations.

THANKS FOR READING ❤️

This post is licensed under CC BY 4.0 by the author.