Skip to main content

Overview

Debugging is essential for understanding iOS app behavior at the instruction level. With a debugger, you can pause execution, inspect registers, modify memory, and control program flow.
Debugging provides the most granular control over app execution, enabling precise analysis and manipulation.

Attaching Debuggers

Remote Debugging with debugserver

Apple’s debugserver enables remote debugging on iOS devices.
1

Extract debugserver from device

# SSH into jailbroken device
ssh root@<device-ip>

# Copy debugserver
scp /Developer/usr/bin/debugserver computer@<ip>:~/
2

Sign debugserver with entitlements

Create entitlements.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.springboard.debugapplications</key>
    <true/>
    <key>get-task-allow</key>
    <true/>
    <key>task_for_pid-allow</key>
    <true/>
    <key>run-unsigned-code</key>
    <true/>
</dict>
</plist>
Sign it:
codesign -s - --entitlements entitlements.xml -f debugserver
3

Deploy and run debugserver

# Copy back to device
scp debugserver root@<device-ip>:/usr/bin/

# On device, attach to running app
debugserver *:6666 -a "Target App"

# Or launch app under debugger
debugserver *:6666 /var/containers/Bundle/Application/.../MyApp.app/MyApp
4

Connect with LLDB

# On your computer
lldb
(lldb) process connect connect://<device-ip>:6666

Alternative: Using Xcode

Easiest approach for testing:
  1. Install app on simulator
  2. Xcode > Debug > Attach to Process
  3. Select your app from the list
Simulator is x86_64/arm64 macOS, not actual iOS. Behavior may differ.

Setting Breakpoints

Basic Breakpoints

# Break on Objective-C method
(lldb) b "-[LoginViewController validatePassword:]"

# Break on C function
(lldb) b strcmp

# Break on Swift method (use mangled name)
(lldb) b _TFC6MyApp18ViewControllerC11viewDidLoadfT_T_

Advanced Breakpoint Techniques

Execute commands automatically when breakpoint hits:
(lldb) b validatePassword:
(lldb) breakpoint command add 1
> po $arg1
> po $arg2  
> po (NSString *)$arg3
> continue
> DONE
This logs arguments and continues execution automatically.
Use continue as the last command for non-invasive logging.
Break when memory location is accessed:
# Watch a variable
(lldb) watchpoint set variable self->_password

# Watch a memory address
(lldb) watchpoint set expression -- 0x00000001000a4000

# Watch with size
(lldb) watchpoint set expression -w write -s 4 -- 0x00000001000a4000

# List watchpoints
(lldb) watchpoint list

# Delete watchpoint
(lldb) watchpoint delete 1
Hardware watchpoints are limited (usually 4 on ARM). Use sparingly.
Break on all methods matching a pattern:
# Break on all methods containing "password"
(lldb) rb password

# Break on all methods in a class
(lldb) rb LoginViewController

# Break on category methods
(lldb) rb "\[NSString\(Crypto\) *\]"

Practical Breakpoint Scenarios

Break on certificate validation:
# Common SSL validation methods
(lldb) b SecTrustEvaluate
(lldb) b SecTrustEvaluateWithError
(lldb) b "-[NSURLSession didReceiveChallenge:completionHandler:]"

# When hit, force success:
(lldb) expr (OSStatus)$rax = 0  # SecTrustEvaluate returns success
(lldb) c

Memory Manipulation

Reading Memory

# Print Objective-C object
(lldb) po $arg1  # self
(lldb) po $arg2  # selector
(lldb) po $arg3  # first argument

# Print variable
(lldb) p self->_username
(lldb) po [self username]

# Print register
(lldb) register read
(lldb) p/x $rax

Writing Memory

# Change Objective-C property
(lldb) po self.isPremium = YES
(lldb) expr self->_isPremium = 1

# Change local variable
(lldb) expr password = @"bypass123"

# Modify return value
(lldb) finish  # Execute until return
(lldb) expr (BOOL)$rax = 1

Advanced Memory Techniques

# Find byte pattern
(lldb) memory find -s "password" 0x100000000 0x200000000

# Find hex pattern
(lldb) memory find -e 0x1234567890abcdef 0x100000000 0x200000000

# Search all readable memory
(lldb) script
>>> import lldb
>>> for region in lldb.process.GetMemoryRegions():
...     if region.IsReadable():
...         # Search this region
...         pass
# List all objects of a class
(lldb) script
>>> def find_instances(classname):
...     return lldb.debugger.HandleCommand(
...         'expression -lobjc -O -- [[[NSMutableArray alloc] init] '
...         'init]; @import Foundation; '
...         '[NSObject performSelector:NSSelectorFromString(@"_copyDescription")]'
...     )

# Use Heap tool (on macOS)
$ heap MyApp.app | grep LoginViewController
$ heap MyApp.app -addresses all | grep 0x600
# Change instruction (ARM64)
# ret instruction is 0xD65F03C0
(lldb) memory write 0x00000001000a4000 0xC0 0x03 0x5F 0xD6

# NOP instruction is 0xD503201F
(lldb) memory write 0x00000001000a4008 0x1F 0x20 0x03 0xD5

# Useful for patching checks
# Find: bl _jailbreak_check
# Replace with: nop (0xD503201F)

Anti-Debugging Bypass

Common Anti-Debugging Techniques

Apps use ptrace(PT_DENY_ATTACH) to prevent debuggers:
// What apps do
ptrace(PT_DENY_ATTACH, 0, 0, 0);
Bypass methods:
1

Hook ptrace with Frida

Interceptor.attach(Module.findExportByName(null, 'ptrace'), {
    onEnter: function(args) {
        if (args[0].toInt32() === 31) {  // PT_DENY_ATTACH
            console.log('[!] Blocking PT_DENY_ATTACH');
            args[0] = ptr(0);
        }
    }
});
2

Binary patching

# Find ptrace call in Hopper
# Replace entire function with:
# mov w0, #0
# ret
3

Runtime hooking with LLDB

(lldb) b ptrace
(lldb) breakpoint command add 1
> expr (int)$arg1 = 0
> continue
> DONE

Advanced Anti-Debugging

Exception Port Monitoring

// Apps monitor exception ports
task_get_exception_ports(mach_task_self(), ...);
Hook to hide debugger:
Interceptor.attach(
    Module.findExportByName(null, 'task_get_exception_ports'),
    {
        onLeave: function(retval) {
            // Clear exception port masks
            // that indicate debugger presence
        }
    }
);

Signal Handling

// Apps handle SIGTRAP
signal(SIGTRAP, handler);
Bypass:
# Ignore SIGTRAP signals
(lldb) process handle SIGTRAP --stop false --pass false --notify true

Inline Checks

; Inline assembly checks
mov x0, #31  ; PT_DENY_ATTACH
svc #0x80    ; syscall
Patch directly:
(lldb) memory write 0x1000a4000 0x1F 0x20 0x03 0xD5  # NOP

Integrity Checks

Apps checksum their code:
if (calculate_checksum() != expected) exit(1);
Hook checksum function:
(lldb) b calculate_checksum
(lldb) breakpoint command add 1
> thread return 0x12345678
> continue
> DONE

Debugging Workflows

1

Initial Setup

  • Attach debugger to running app
  • Set breakpoints on entry points
  • Configure signal handling
  • Load symbols if available
2

Locate Target Code

  • Use static analysis to find addresses
  • Break on API calls of interest
  • Use symbolic breakpoints for classes
3

Analyze Execution

  • Step through code
  • Inspect registers and stack
  • Log function arguments
  • Trace execution paths
4

Test Modifications

  • Change variables in real-time
  • Patch instructions temporarily
  • Call functions manually
  • Verify behavior changes
5

Document Findings

  • Note important addresses
  • Save successful patches
  • Record function signatures
  • Map out control flow
Debugging session successful when:
  • You understand the execution flow
  • Can modify behavior predictably
  • Bypassed all anti-debugging measures
  • Documented all findings for later use

LLDB Python Scripting

Extend LLDB with Python for automation:
# auto_bypass.py - Automatically bypass common protections
import lldb

def bypass_jailbreak(debugger, command, result, internal_dict):
    """Bypass jailbreak detection"""
    target = debugger.GetSelectedTarget()
    process = target.GetProcess()
    
    # Hook fileExistsAtPath:
    debugger.HandleCommand(
        'breakpoint set -n "-[NSFileManager fileExistsAtPath:]" '
        '-C "expr (BOOL)$rax = 0" -C "continue"'
    )
    
    # Hook fork
    debugger.HandleCommand(
        'breakpoint set -n fork '
        '-C "finish" -C "expr (int)$rax = 0" -C "continue"'
    )
    
    print("[+] Jailbreak bypass activated")

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand(
        'command script add -f auto_bypass.bypass_jailbreak bypass'
    )
    print('[+] Loaded auto_bypass commands')
Load in LLDB:
(lldb) command script import ~/auto_bypass.py
(lldb) bypass

Resources

LLDB Documentation

Official LLDB reference

iOS Debugging Magic

Advanced LLDB scripts for iOS

Frida Scripts

Community debugging scripts

ARM64 ISA

ARM instruction reference

Build docs developers (and LLMs) love