24. 11. 2024.

Bypassing boolean-based iOS jailbreak detection with LLDB

The relevance of jailbreak detection in modern iOS security remains a subject of ongoing discussion, especially since iOS is becoming increasingly challenging to jailbreak, particularly with the latest versions. This raises the question of whether implementing jailbreak detection mechanisms is still relevant. Nonetheless, industry best practices and standards continue to emphasize a defense-in-depth approach, incorporating jailbreak detection as both a preventive security measure and a valuable telemetry source.

Jailbreak detection telemetry provides critical insights into potential security compromises and helps organizations maintain a comprehensive understanding of their security posture in real-time. While open-source solutions offer robust integration options for these features and are always recommended, it's important to acknowledge that determined attackers with sufficient resources can bypass even sophisticated detection mechanisms.

This article is aimed at iOS security researchers, penetration testers, and developers interested in understanding the internals of jailbreak detection mechanisms. While numerous guides exist on bypassing iOS jailbreak detection, particularly using Frida scripts, we'll explore an alternative approach using LLDB to bypass simple boolean-based jailbreak detection mechanisms.

Throughout this article, we will:

  • Explore essential LLDB debugging concepts and ARM architecture basics
  • Set up a remote debugging environment for iOS devices
  • Analyze jailbreak detection mechanisms using LLDB
  • Develop both manual and automated bypasses using LLDB's features
  • Learn how to leverage LLDB's Python API for automation

While demonstrating a jailbreak detection bypass, our primary focus is to deliver practical insights into LLDB debugging. We'll explore breakpoint commands and callbacks while providing a targeted overview of both LLDB and ARM architecture fundamentals necessary for understanding the techniques presented.

The LLDB Debugger

LLDB is a powerful, modern debugging platform built on the LLVM Project's foundation. By leveraging established components like the Clang expression parser and LLVM disassembler, LLDB delivers exceptional debugging performance and reliability.

As the default debugging tool in Xcode for macOS development, LLDB provides comprehensive support for C, Objective-C, and C++ across both desktop applications and iOS environments, including physical devices and simulators.

What sets LLDB apart is its flexible scripting capabilities. Developers can automate debugging sessions through Python scripts in two ways: by running non-interactive debug sessions from a Unix Python environment, or by executing Python scripts within LLDB itself. This scripting functionality enables advanced tasks such as program data inspection, container traversal, and sophisticated breakpoint management with conditional execution control.

— From the LLDB official website

Common LLDB Commands

Process Control
  • Running and Stopping
    run (r)              # Start program execution
    continue (c)         # Continue program execution
    step (s)             # Step into
    next (n)             # Step over
    finish              # Step out of the current function
    exit/quit (q)       # Exit LLDB
    
Breakpoints
  • Setting Breakpoints

    breakpoint set -n <name>           # Break on function name
    breakpoint set -f <file> -l <line> # Break on file and line
    breakpoint set -a <address>        # Break on memory address
    br s -n <name>                     # Short form
    
  • Breakpoint Management

    breakpoint list                    # List breakpoints
    breakpoint enable <id>             # Enable breakpoint
    breakpoint disable <id>            # Disable breakpoint
    breakpoint delete <id>             # Delete breakpoint
    breakpoint command add <id>        # Add commands to breakpoint
    
Register and Memory Operations
  • Register Commands

    register read                      # Read all registers
    register read <reg>                # Read specific register
    register write <reg> <value>       # Write to register
    
  • Memory Commands

    memory read <addr>                 # Read memory
    x/<size><format> <addr>           # Examine memory
    x/s <addr>                        # Read as string
    x/i <addr>                        # Read as instructions
    
Threading and Stack
  • Thread Commands

    thread list                        # List all threads
    thread select <id>                 # Switch to thread
    thread backtrace (bt)             # Show thread backtrace
    
  • Frame Commands

    frame select <id>                  # Select stack frame
    frame variable                     # Show frame variables
    frame info                        # Show frame information
    
iOS Specific
  • Platform and Process
    platform select remote-ios         # Select iOS platform
    process connect connect://<host>:<port>  # Connect to debug server
    
Data Inspection
  • Variable Inspection
    p <variable>                       # Print variable
    po <object>                        # Print object description
    expression <expr>                  # Evaluate expression
    
Script Integration
  • Python Integration
    script print(lldb.debugger)        # Access LLDB from Python
    command script import <file>       # Import Python script
    command script add <name> <file>   # Add script command
    
Useful Tips
  1. Command Aliases

    • Most commands have short aliases (r, c, n, s, etc.)
    • You can create custom aliases
  2. Regular Expression Breakpoints

    breakpoint set -r <regex>         # Break on pattern match
    

ARM Architecture

Modern iPhones use ARM-based processors with the ARM64 (AArch64) architecture. Starting from iPhone 5S, Apple moved to 64-bit ARM processors, with the latest devices using custom Apple Silicon designs based on the ARM architecture.

Register Overview

ARM64 provides 31 general-purpose registers (X0-X30) that are each 64 bits wide. These can also be accessed as 32-bit registers (W0-W30).

Key Registers
  • X0-X7: Parameter and return value registers
    • X0: First parameter and function return value
    • Used for passing boolean results and small return values
  • X8-X15: Temporary registers
    • Used for local variables and intermediate calculations
  • X16-X30: Special purpose and callee-saved registers
  • X29 (FP): Frame pointer
  • X30 (LR): Link register, stores return address
  • SP: Stack pointer (not directly accessible as X31)
  • PC: Program counter

Function Calls and Register Usage

Calling Convention

In ARM64, function parameters are passed through registers:

  • First eight arguments use X0-X7
  • X0 is also used for return value
  • For boolean functions:
    • X0 = 1 represents true
    • X0 = 0 represents false

The Link Register (X30/LR) serves several critical functions:

  • Stores the return address when a function is called
  • Enables function return to the correct location
  • Key register for debugging and stack trace analysis

With our understanding of LLDB's capabilities and ARM architecture fundamentals, let's proceed with setting up our debugging environment.

Setup

The setup described in this blog post includes:

  • Intel-based or Apple Silicon host device
  • iPhone 8+ running iOS 16.7.10 jailbroken using palera1n jailbreak.

On the host device, we will use iproxy to connect via SSH to a jailbroken iPhone via USB. The tool can be installed on macOS using Homebrew with:

brew install libusbmuxd

There are many tutorials available online that can guide you trough setting up the debugserver on the iOS device, however, the simplest one is to install it using Sileo from Procursus repository.

Having established our testing environment, we'll now configure the necessary connections between our host device and the target iOS device.

Configuration

On the host device it is necessary to open up four terminal windows:

Terminal #1

In the first terminal window we want to setup iproxy to forward the SSH communication via USB by typing:

iproxy 2222 22

Terminal #2

The second terminal window is used to setup iproxy to forward the debugserver for remote debugging communication via USB by typing:

iproxy 1234 5678

Terminal #3

We will use the third terminal window to connect to the iPhone via SSH and attach the debugserver to the application we want to debug using the command:

debugserver *:1234 --waitfor ApplicationName
iPhone102:~ root# debugserver localhost:5678 --waitfor ApplicationName
debugserver-@(#)PROGRAM:LLDB  PROJECT:lldb-16.0.0
 for arm64.
Waiting to attach to process ApplicationName...

We can now execute on the application on the device and the debugging session will start:

Listening to port 5678 for a connection from localhost...

The steps above have to be repeated on each application execution.

Terminal #4

Fourth terminal window will be used to connect to a remote debugging session using following command:

lldb -o "platform select remote-ios" -o "process connect connect://localhost:1234"

With our debugging environment configured, we can begin analyzing the application's jailbreak detection mechanisms.

Analysis

Let's start by connecting to a remote LLDB debugging session:

user@macOS ~ % lldb -o "platform select remote-ios" -o "process connect connect://localhost:1234"
(lldb) platform select remote-ios
  Platform: remote-ios
 Connected: no
<redacted>
 SDK Roots: [ 1] "/Users/user/Library/Developer/Xcode/iOS DeviceSupport/iPhone10,5 16.7.10 (20H350)"
(lldb) process connect connect://localhost:1234
Process 46666 stopped
* thread #1, stop reason = signal SIGSTOP
    frame #0: 0x0000000101c86190 dyld`strcmp
dyld`strcmp:
->  0x101c86190 <+0>:  tst    x0, #0xf
    0x101c86194 <+4>:  b.eq   0x101c861b4    ; <+36>
    0x101c86198 <+8>:  ldrb   w4, [x0], #0x1
    0x101c8619c <+12>: ldrb   w5, [x1], #0x1
Target 0: (ApplicationName) stopped.
(lldb) continue 
Process 46666 resuming
Process 46666 exited with status = 45 (0x0000002d) 
(lldb)  

An application terminating with exit status 45 (0x0000002d) might indicate the presence of anti-debugging mechanisms.

iOS implements a robust security model based on application sandboxing, which enforces strict access controls on filesystem operations and inter-process communications. Detection mechanisms exploit these restrictions and employ various techniques to identify system modifications.

Common Detection Methods

The following list presents some examples of detection approaches, though it's not exhaustive and implementation details may vary across different solutions:

File-Based Detection
  • Access attempts to privileged paths
  • Package management paths (e.g., /etc/apt, /etc/dpkg)
  • Alternative shell binaries (e.g., /bin/bash, /bin/sh)
  • Third-party package manager paths (e.g., Cydia, Sileo, Zebra)
Runtime Environment Checks
  • System modification indicators
  • Process forking attempts
  • Dynamic linker behavior analysis
  • Suspicious dylib presence
URL Scheme Detection
  • Package manager URL schemes (e.g., Cydia, Sileo)
  • Custom URL scheme handling checks
  • Application installation source verification
Permission and Privilege Checks
  • Write attempts outside application container
  • Privilege elevation indicators
  • Sandbox violation attempts
  • Process information queries

Dynamic Analysis

Although applications generally rely on high-level APIs for file operations, intercepting low-level syscalls like stat reveals the actual filesystem queries made during jailbreak detection. The stat syscall queries the filesystem for a file's metadata (permissions, size, timestamps) and takes a file path as its first argument.

In the following example, we use several LLDB commands to monitor the application's file checks:

  • Set a breakpoint on the stat syscall to pause execution whenever it is called:

    (lldb) b stat  # This halts the program when the `stat` syscall is called
    
  • Add specific commands to the breakpoint to trigger additional actions:

    (lldb) breakpoint command add 1  # Add commands to the breakpoint
    
  • Examine the string at the memory address held by the x0 register to inspect the path being passed:

    (lldb) x/s $x0  # Display the string at the address in `x0`, showing the file path
    
  • Resume program execution until the next breakpoint or program termination:

    (lldb) c  # Continue running the program
    

With the breakpoints set and commands added, we can now step through the debugging process to observe how these instructions interact with the program flow and inspect file path checks in real-time.

user@macOS ~ % lldb -o "platform select remote-ios" -o "process connect connect://localhost:1234"
(lldb) platform select remote-ios
  Platform: remote-ios
 Connected: no
 <redacted>
 SDK Roots: [ 1] "/Users/user/Library/Developer/Xcode/iOS DeviceSupport/iPhone10,5 16.7.10 (20H350)"
(lldb) process connect connect://localhost:1234
Process 47351 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
    frame #0: 0x00000002336c28ac libmacho.dylib`getsectiondata + 116
libmacho.dylib`getsectiondata:
->  0x2336c28ac <+116>: cmp    w8, #0x19
    0x2336c28b0 <+120>: b.ne   0x2336c2998    ; <+352>
    0x2336c28b4 <+124>: add    x24, x28, #0x8
    0x2336c28b8 <+128>: mov    x0, x24
Target 0: (ApplicationName) stopped.
(lldb) b stat
Breakpoint 1: 2 locations.
(lldb) breakpoint command add 1
Enter your debugger command(s).  Type 'DONE' to end.
> x/s $x0 
> c 
> DONE
(lldb) c
Process 47351 resuming
(lldb)  x/s $x0
0x1c023f4d5: "/"
(lldb)  c
Process 47351 resuming
Command #2 'c' continued the target.
(lldb)  x/s $x0
0xc3c008c00: "/private/var/containers/Bundle/Application/4DBE1BF1-6B85-4F9B-95E1-64983FDEB0F9/ApplicationName.app/ApplicationName"
(lldb)  c
<redacted>
Process 47351 resuming
Command #2 'c' continued the target.
(lldb)  x/s $x0
0x16f7baed6: "/private"
(lldb)  c
Process 47351 resuming
Command #2 'c' continued the target.
(lldb)  x/s $x0
0x16f7bc7c6: "/usr/sbin/frida-server"
(lldb)  c
Process 47351 resuming
Command #2 'c' continued the target.
Process 47351 exited with status = 45 (0x0000002d)

Analysis of the monitored paths revealed a check for "/usr/sbin/frida-server" immediately preceding process termination. To understand the execution flow that led to this check, we repeated the analysis with the addition of the bt (backtrace) command. This command reveals the full call stack, providing visibility into the sequence of function calls leading to our breakpoint.

user@macOS ~ % lldb -o "platform select remote-ios" -o "process connect connect://localhost:1234"
(lldb) platform select remote-ios
  Platform: remote-ios
 Connected: no
 <redacted>
 SDK Roots: [ 1] "/Users/user/Library/Developer/Xcode/iOS DeviceSupport/iPhone10,5 16.7.10 (20H350)"
(lldb) process connect connect://localhost:1234
Process 47363 stopped
* thread #1, stop reason = signal SIGSTOP
    frame #0: 0x000000010599c5b0 dyld`stat64 + 8
dyld`stat64:
->  0x10599c5b0 <+8>:  b.lo   0x10599c5cc    ; <+36>
    0x10599c5b4 <+12>: stp    x29, x30, [sp, #-0x10]!
    0x10599c5b8 <+16>: mov    x29, sp
    0x10599c5bc <+20>: bl     0x10599fea0    ; cerror_nocancel
Target 0: (ApplicationName) stopped.
(lldb) b stat
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.
(lldb) breakpoint command add 1
Enter your debugger command(s).  Type 'DONE' to end.
> x/s $x0 
> bt 
> c 
> DONE
(lldb) c
Process 47363 resuming
2 locations added to breakpoint 1
(lldb)  x/s $x0
0x1c023f4d5: "/"
(lldb)  bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2
  * frame #0: 0x00000001f4b956c4 libsystem_kernel.dylib`stat
    frame #1: 0x00000001c01d96d0 libsystem_c.dylib`realpath$DARWIN_EXTSN + 428
    frame #2: 0x000000010178d57c systemhook.dylib`___lldb_unnamed_symbol117 + 224
    frame #3: 0x00000001019b442c dyld`invocation function for block in dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const::$_0::operator()() const + 152
    frame #4: 0x00000001019e36ec dyld`invocation function for block in dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const + 164
    frame #5: 0x00000001019912d0 dyld`invocation function for block in dyld3::MachOFile::forEachSection(void (dyld3::MachOFile::SectionInfo const&, bool, bool&) block_pointer) const + 520
    frame #6: 0x0000000101990788 dyld`dyld3::MachOFile::forEachLoadCommand(Diagnostics&, void (load_command const*, bool&) block_pointer) const + 280
    frame #7: 0x000000010198fd78 dyld`dyld3::MachOFile::forEachSection(void (dyld3::MachOFile::SectionInfo const&, bool, bool&) block_pointer) const + 164
    frame #8: 0x00000001019dd160 dyld`dyld3::MachOFile::forEachInitializerPointerSection(Diagnostics&, void (unsigned int, unsigned int, bool&) block_pointer) const + 132
    frame #9: 0x000000010199ad94 dyld`dyld3::MachOAnalyzer::forEachInitializer(Diagnostics&, dyld3::MachOAnalyzer::VMAddrConverter const&, void (unsigned int) block_pointer, void const*) const + 324
    frame #10: 0x000000010199797c dyld`dyld4::Loader::findAndRunAllInitializers(dyld4::RuntimeState&) const + 392
    frame #11: 0x0000000101994444 dyld`dyld4::Loader::runInitializersBottomUp(dyld4::RuntimeState&, dyld3::Array<dyld4::Loader const*>&) const + 216
    frame #12: 0x00000001019b44b4 dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&) const::$_1::operator()() const + 108
    frame #13: 0x00000001019994d4 dyld`dyld4::Loader::runInitializersBottomUpPlusUpwardLinks(dyld4::RuntimeState&) const + 272
    frame #14: 0x00000001019ca9a0 dyld`dyld4::APIs::runAllInitializersForMain() + 272
    frame #15: 0x00000001019a21cc dyld`dyld4::prepare(dyld4::APIs&, dyld3::MachOAnalyzer const*) + 2872
    frame #16: 0x00000001019a02e0 dyld`start + 1760
(lldb)  c
Process 47958 resuming
<redacted>
Command #3 'c' continued the target.
(lldb)  x/s $x0
0x16f14aee6: "/private"
(lldb)  bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2
  * frame #0: 0x00000001f4b956c4 libsystem_kernel.dylib`stat
    frame #1: 0x00000001b3723e9c Foundation`-[NSFileManager fileExistsAtPath:isDirectory:] + 92
    frame #2: 0x00000001b3722d48 Foundation`-[NSURL(NSURL) initFileURLWithPath:] + 156
    frame #3: 0x00000001b3722ca0 Foundation`+[NSURL(NSURL) fileURLWithPath:] + 28
    frame #4: 0x00000001b3b625c4 Foundation`-[NSFileManager componentsToDisplayForPath:] + 76
    frame #5: 0x00000001b3787f9c Foundation`pathComponentFromPath + 488
    frame #6: 0x00000001b378ae18 Foundation`pathComponentFromURL + 416
    frame #7: 0x00000001b3712ec8 Foundation`-[NSError(NSErrorPrivate) _formatCocoaErrorString:parameters:applicableFormatters:count:] + 216
    frame #8: 0x00000001b37129fc Foundation`-[NSError(NSErrorPrivate) _cocoaErrorString:fromBundle:tableName:] + 540
    frame #9: 0x00000001b375ccdc Foundation`-[NSError(NSErrorPrivate) _cocoaErrorString:] + 68
    frame #10: 0x00000001b375cc78 Foundation`-[NSError _cocoaErrorStringWithKind:variant:] + 140
    frame #11: 0x00000001b375cbb4 Foundation`-[NSError _cocoaErrorStringWithKind:] + 320
    frame #12: 0x00000001b375ca40 Foundation`-[NSError _retainedUserInfoCallBackForKey:] + 144
    frame #13: 0x00000001b945df5c CoreFoundation`____CFErrorSetCallBackForDomainNoLock_block_invoke + 28
    frame #14: 0x00000001b94457dc CoreFoundation`_CFErrorCopyUserInfoKeyFromCallBack + 56
    frame #15: 0x00000001b3754ac8 Foundation`-[NSError localizedFailureReason] + 52
    frame #16: 0x00000001b3753900 Foundation`-[NSFileManager _URLForReplacingItemAtURL:error:] + 428
    frame #17: 0x00000001b37534ec Foundation`_NSCreateTemporaryFile_Protected + 164
    frame #18: 0x00000001b3761318 Foundation`_NSWriteDataToFileWithExtendedAttributes + 432
    frame #19: 0x00000001b37b911c Foundation`writeStringToURLOrPath + 184
    frame #20: 0x00000001b3a2feec Foundation`Swift.StringProtocol.write<τ_0_0 where τ_1_0: Swift.StringProtocol>(toFile: τ_1_0, atomically: Swift.Bool, encoding: Swift.String.Encoding) throws -> () + 172
    frame #21: 0x0000000102b2c9a0 IOSSecuritySuite`function signature specialization <Arg[0] = Dead> of static IOSSecuritySuite.JailbreakChecker.checkRestrictedDirectoriesWriteable() -> (passed: Swift.Bool, failMessage: Swift.String) + 832
    frame #22: 0x0000000102b2d3e0 IOSSecuritySuite`function signature specialization <Arg[0] = Dead> of static IOSSecuritySuite.JailbreakChecker.performChecks() -> IOSSecuritySuite.JailbreakChecker.JailbreakStatus + 788
    frame #23: 0x0000000102b29d4c IOSSecuritySuite`static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken() -> Swift.Bool + 16
    <redacted>
    frame #63: 0x00000001019a0344 dyld`start + 1860
(lldb)  c
Process 47958 resuming
<redacted>
Command #3 'c' continued the target.
(lldb)  x/s $x0
0x16f14c7d6: "/usr/sbin/frida-server"
(lldb)  bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2
  * frame #0: 0x00000001f4b956c4 libsystem_kernel.dylib`stat
    frame #1: 0x00000001b3723f48 Foundation`-[NSFileManager fileExistsAtPath:] + 88
    frame #2: 0x0000000102b2e668 IOSSecuritySuite`function signature specialization <Arg[0] = Dead> of static IOSSecuritySuite.ReverseEngineeringToolsChecker.performChecks() -> IOSSecuritySuite.ReverseEngineeringToolsChecker.ReverseEngineeringToolsStatus + 536
    frame #3: 0x0000000102b29f00 IOSSecuritySuite`static IOSSecuritySuite.IOSSecuritySuite.amIReverseEngineered() -> Swift.Bool + 16
    <redacted>
    frame #43: 0x00000001019a0344 dyld`start + 1860
(lldb)  c
Process 47958 resuming
Command #3 'c' continued the target.
Process 47958 exited with status = 45 (0x0000002d) 

Analysis of the call stack revealed that the application relies on the IOSSecuritySuite framework for its detection routines, specifically using two functions:

  • IOSSecuritySuite`static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken() -> Swift.Bool + 16
  • IOSSecuritySuite`static IOSSecuritySuite.IOSSecuritySuite.amIReverseEngineered() -> Swift.Bool + 16

Now that we've identified the detection routines, let's explore how to bypass them manually using LLDB's debugging capabilities.

Manual Bypass

With the insights gained from our analysis, we can implement a step-by-step approach to bypass the jailbreak detection by intercepting and altering the return value of the detection routines. The following LLDB commands demonstrate the process, with explanations of each step:

  • Set a breakpoint on the jailbreak detection function:

    (lldb) b IOSSecuritySuite`static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken()  # This halts the program when the function is called
    
  • Inspect the Link Register (LR) to identify where the function will return:

    (lldb) register read lr   # LR holds the return address; we need it for setting a follow-up breakpoint
    
  • Set a one-shot breakpoint at the return address to intercept the function return:

    (lldb) br s -a $lr        # This breakpoint triggers only once at the function's return
    
  • Read the x0 register to check the current return value (1 = jailbroken, 0 = not jailbroken):

    (lldb) register read x0   # Check the current state of the return value
    
  • Modify the return value to indicate a non-jailbroken state:

    (lldb) register write x0 0  # Force `x0` to return '0' (false), bypassing the jailbreak check
    
  • Verify the change by reading the x0 register again (optional but helpful for confirmation):

    (lldb) register read x0   # Ensure `x0` is now '0'
    
  • Resume program execution:

    (lldb) c                  # Continue running the program with the modified return value
    

This debugging workflow enables us to intercept and modify the detection routine's behavior, circumventing the jailbreak check.

user@macOS ~ % lldb -o "platform select remote-ios" -o "process connect connect://localhost:1234"
(lldb) platform select remote-ios
  Platform: remote-ios
 Connected: no
 <redacted>
 SDK Roots: [ 1] "/Users/user/Library/Developer/Xcode/iOS DeviceSupport/iPhone10,5 16.7.10 (20H350)"
(lldb) process connect connect://localhost:1234
Process 47365 stopped
* thread #1, stop reason = signal SIGSTOP
    frame #0: 0x00000001038085b0 dyld`stat64 + 8
dyld`stat64:
->  0x1038085b0 <+8>:  b.lo   0x1038085cc    ; <+36>
    0x1038085b4 <+12>: stp    x29, x30, [sp, #-0x10]!
    0x1038085b8 <+16>: mov    x29, sp
    0x1038085bc <+20>: bl     0x10380bea0    ; cerror_nocancel
Target 0: (ApplicationName) stopped.
(lldb) b IOSSecuritySuite`static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken() -> Swift.Bool
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.
(lldb) c
Process 47365 resuming
1 location added to breakpoint 1
Process 47365 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000104959d3c IOSSecuritySuite`static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken() -> Swift.Bool
IOSSecuritySuite`static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken() -> Swift.Bool:
->  0x104959d3c <+0>:  stp    x20, x19, [sp, #-0x20]!
    0x104959d40 <+4>:  stp    x29, x30, [sp, #0x10]
    0x104959d44 <+8>:  add    x29, sp, #0x10
    0x104959d48 <+12>: bl     0x10495d0cc    ; function signature specialization <Arg[0] = Dead> of static IOSSecuritySuite.JailbreakChecker.performChecks() -> IOSSecuritySuite.JailbreakChecker.JailbreakStatus
Target 0: (ApplicationName) stopped.
(lldb) register read lr
      lr = 0x0000000102b5eff8  ApplicationName`___lldb_unnamed_symbol9009 + 52
(lldb) br s -a $lr
Breakpoint 2: where = ApplicationName`___lldb_unnamed_symbol9009 + 52, address = 0x0000000102b5eff8
(lldb) c
Process 47365 resuming
Process 47365 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x0000000102b5eff8 ApplicationName`___lldb_unnamed_symbol9009 + 52
ApplicationName`___lldb_unnamed_symbol9009:
->  0x102b5eff8 <+52>: and    w8, w0, #0x1
    0x102b5effc <+56>: strb   w8, [x19, #0x30]
    0x102b5f000 <+60>: mov    x0, #0x0 ; =0 
    0x102b5f004 <+64>: bl     0x103150bb8    ; symbol stub for: type metadata accessor for IOSSecuritySuite.IOSSecuritySuite
Target 0: (ApplicationName) stopped.
(lldb) register read x0
      x0 = 0x0000000000000001
(lldb) register write x0 0
(lldb) register read x0
      x0 = 0x0000000000000000
(lldb) c
Process 47365 resuming
Process 47365 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000104959d3c IOSSecuritySuite`static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken() -> Swift.Bool
IOSSecuritySuite`static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken() -> Swift.Bool:
->  0x104959d3c <+0>:  stp    x20, x19, [sp, #-0x20]!
    0x104959d40 <+4>:  stp    x29, x30, [sp, #0x10]
    0x104959d44 <+8>:  add    x29, sp, #0x10
    0x104959d48 <+12>: bl     0x10495d0cc    ; function signature specialization <Arg[0] = Dead> of static IOSSecuritySuite.JailbreakChecker.performChecks() -> IOSSecuritySuite.JailbreakChecker.JailbreakStatus
Target 0: (ApplicationName) stopped.
(lldb) register read lr
      lr = 0x0000000102b5fba0  ApplicationName`___lldb_unnamed_symbol9013 + 44
(lldb) br s -a $lr
Breakpoint 3: where = ApplicationName`___lldb_unnamed_symbol9013 + 44, address = 0x0000000102b5fba0
(lldb) c
Process 47365 resuming
Process 47365 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
    frame #0: 0x0000000102b5fba0 ApplicationName`___lldb_unnamed_symbol9013 + 44
ApplicationName`___lldb_unnamed_symbol9013:
->  0x102b5fba0 <+44>: tst    w0, #0x1
    0x102b5fba4 <+48>: mov    x8, #-0x1a00000000000000 ; =-1873497444986126336 
    0x102b5fba8 <+52>: mov    x9, #-0x1900000000000000 ; =-1801439850948198400 
    0x102b5fbac <+56>: csel   x20, x9, x8, ne
Target 0: (ApplicationName) stopped.
(lldb) register read x0
      x0 = 0x0000000000000001
(lldb) register write x0 0
(lldb) register read x0
      x0 = 0x0000000000000000
(lldb) c
Process 47365 resuming
(lldb)  

Interestingly, modifying only the return value of amIJailbroken() function was sufficient to prevent process termination, suggesting that the amIReverseEngineered function check was conditional upon the jailbreak detection result.

While the manual process demonstrates the core concepts of bypassing detection, automation can streamline and repeat the process efficiently. The following section explores how we can harness LLDB's Python API to implement an automated bypass approach, making the workflow reusable and less error-prone.

Automated Bypass

Building upon our manual bypass approach, we can automate the entire process using LLDB's Python API. This implementation showcases LLDB's scripting capabilities while providing an efficient bypass solution.

The Python script implements the same logic we used in our manual bypass: intercepting detection routines and modifying their return values. However, it adds automation through breakpoint callbacks, making the bypass process more efficient and reusable.

The script consists of two main functions:

  • __lldb_init_module: Handles the initial setup by creating breakpoints on both detection routines and assigning them callbacks:
target = debugger.GetSelectedTarget()
# Set breakpoint on jailbreak detection
bp1 = target.BreakpointCreateByName("static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken()")
bp1.SetScriptCallbackFunction(basic_callback.__module__ + '.basic_callback')
# Set breakpoint on reverse engineering detection
bp2 = target.BreakpointCreateByName("static IOSSecuritySuite.IOSSecuritySuite.amIReverseEngineered()")
bp2.SetScriptCallbackFunction(basic_callback.__module__ + '.basic_callback')

This function initializes our debugging session by setting breakpoints on both security functions we identified during analysis. Each breakpoint is configured to trigger our callback function when hit.

  • basic_callback: Automates the bypass process by handling breakpoint events. First, it gathers information about the current execution context:
thread = frame.GetThread()
process = thread.GetProcess()
target = process.GetTarget()
debugger = target.GetDebugger()

Then, it reads the Link Register to find where the function will return:

for reg in frame.registers[0]:
    if reg.name == "lr":
        return_addr = reg.unsigned
        ret_bp = target.BreakpointCreateByAddress(return_addr)

Finally, it sets up commands to modify the return value:

ret_bp.SetOneShot(True)  # Breakpoint will be removed after being hit
commands = lldb.SBStringList()
commands.AppendString("register read x0")   # Read current value
commands.AppendString("register write x0 0") # Modify return value
commands.AppendString("register read x0")   # Verify modification
commands.AppendString("c")                  # Continue execution
ret_bp.SetCommandLineCommands(commands)

The script can be executed with a single command:

lldb -o "platform select remote-ios" -o "process connect connect://localhost:1234" -o "command script import /Users/user/bypass_JailbreakDetection.py" -o "continue"

Complete Python Script

# lldb -o "platform select remote-ios" -o "process connect connect://localhost:1234" -o "command script import /Users/user/bypass_JailbreakDetection.py" -o "continue"
#!/usr/bin/env python3
import lldb

def __lldb_init_module(debugger, dict):
    try:
        target = debugger.GetSelectedTarget()
        print("[*] Setting up breakpoints...")
        bp1 = target.BreakpointCreateByName("static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken() -> Swift.Bool", "IOSSecuritySuite")
        if bp1:
            print("[*] Created breakpoint 1")
            bp1.SetScriptCallbackFunction(basic_callback.__module__ + '.basic_callback')
            print("[*] Set callback for breakpoint 1")
        bp2 = target.BreakpointCreateByName("static IOSSecuritySuite.IOSSecuritySuite.amIReverseEngineered() -> Swift.Bool", "IOSSecuritySuite")
        if bp2:
            print("[*] Created breakpoint 2")
            bp2.SetScriptCallbackFunction(basic_callback.__module__ + '.basic_callback')
            print("[*] Set callback for breakpoint 2")
    except Exception as e:
        print(f"[-] Error in init: {str(e)}")

def basic_callback(frame, bp_loc, dict):
    try:
        thread = frame.GetThread()
        process = thread.GetProcess()
        target = process.GetTarget()
        debugger = target.GetDebugger()
        interpreter = debugger.GetCommandInterpreter()
        result = lldb.SBCommandReturnObject()
        print(f"Function: {frame.GetFunctionName()}")
        print(f"Address: 0x{frame.GetPC():x}")
        interpreter.HandleCommand("register read lr", result)
        if result.Succeeded():
            print("\nRegister state:")
            print(result.GetOutput())
        for reg in frame.registers[0]:
            if reg.name == "lr":
                return_addr = reg.unsigned
                print(f"[+] Return address from lr: 0x{return_addr:x}")
                ret_bp = target.BreakpointCreateByAddress(return_addr)
                if ret_bp:
                    ret_bp.SetOneShot(True)
                    commands = lldb.SBStringList()
                    commands.AppendString("register read x0")
                    commands.AppendString("register write x0 0")
                    commands.AppendString("register read x0")
                    commands.AppendString("c")
                    ret_bp.SetCommandLineCommands(commands)
                    print(f"[+] Set return breakpoint at 0x{return_addr:x}")
                break
        process.Continue()

    except Exception as e:
        print(f"[-] Error in callback: {str(e)}")
        if thread:
            process = thread.GetProcess()
            if process:
                process.Continue()
    return False

While numerous implementation strategies exist for LLDB automation, this script exemplifies a systematic approach that directly translates our manual debugging methodology into programmatic form while highlighting essential callback mechanisms.

Script Output

user@macOS ~ % lldb -o "platform select remote-ios" -o "process connect connect://localhost:1234" -o "command script import /Users/user/bypass_JailbreakDetection.py" -o "continue"
(lldb) process connect connect://localhost:1234
Process 47960 stopped
* thread #1, stop reason = signal SIGSTOP
    frame #0: 0x000000010110ba5c dyld`__mmap + 8
dyld`__mmap:
->  0x10110ba5c <+8>:  b.lo   0x10110ba78    ; <+36>
    0x10110ba60 <+12>: stp    x29, x30, [sp, #-0x10]!
    0x10110ba64 <+16>: mov    x29, sp
    0x10110ba68 <+20>: bl     0x101103ea0    ; cerror_nocancel
Target 0: (ApplicationName) stopped.
(lldb) command script import /Users/user/bypass_JailbreakDetection.py
[*] Setting up breakpoints...
[*] Created breakpoint 1
[*] Set callback for breakpoint 1
[*] Created breakpoint 2
[*] Set callback for breakpoint 2
(lldb) continue
1 location added to breakpoint 1
1 location added to breakpoint 2
Function: static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken() -> Swift.Bool
Address: 0x10222dd3c

Register state:
      lr = 0x000000010042eff8  ApplicationName`___lldb_unnamed_symbol9009 + 52

[+] Return address from lr: 0x10042eff8
[+] Set return breakpoint at 0x10042eff8
(lldb)  register read x0
      x0 = 0x0000000000000001
(lldb)  register write x0 0
(lldb)  register read x0
      x0 = 0x0000000000000000
(lldb)  c
Process 47960 resuming
Command #4 'c' continued the target.
Function: static IOSSecuritySuite.IOSSecuritySuite.amIReverseEngineered() -> Swift.Bool
Address: 0x10222def0

Register state:
      lr = 0x000000010042f068  ApplicationName`___lldb_unnamed_symbol9009 + 164

[+] Return address from lr: 0x10042f068
[+] Set return breakpoint at 0x10042f068
(lldb)  register read x0
      x0 = 0x0000000000000001
(lldb)  register write x0 0
(lldb)  register read x0
      x0 = 0x0000000000000000
(lldb)  c
Process 47960 resuming
Command #4 'c' continued the target.
Function: static IOSSecuritySuite.IOSSecuritySuite.amIJailbroken() -> Swift.Bool
Address: 0x10222dd3c

Register state:
      lr = 0x000000010042fba0  ApplicationName`___lldb_unnamed_symbol9013 + 44

[+] Return address from lr: 0x10042fba0
[+] Set return breakpoint at 0x10042fba0
(lldb)  register read x0
      x0 = 0x0000000000000001
(lldb)  register write x0 0
(lldb)  register read x0
      x0 = 0x0000000000000000
(lldb)  c
Process 47960 resuming
Command #4 'c' continued the target.

Conclusion

This article demonstrated how LLDB's debugging capabilities, specifically its breakpoint manipulation features, can be used to bypass iOS jailbreak detection. While the example focused on jailbreak detection, these techniques help understand LLDB's breakpoint commands and callback functionality.

The manual process covered:

  • Setting breakpoints
  • Register inspection and modification
  • Function return mechanisms

The automated solution, implemented through LLDB's Python API, showed how breakpoint callbacks can be used to:

  • Automate debugging workflows
  • Implement debugging logic
  • Handle execution scenarios

This approach provides both a practical solution for security testing and examples of LLDB's debugging capabilities. Understanding these debugging techniques, particularly breakpoint commands and callbacks, is useful for iOS application security assessment and debugging tasks.