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.
Extract debugserver from device
# SSH into jailbroken device
ssh root@ < device-i p >
# Copy debugserver
scp /Developer/usr/bin/debugserver computer@ < i p > :~/
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
Deploy and run debugserver
# Copy back to device
scp debugserver root@ < device-i p > :/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
Connect with LLDB
# On your computer
lldb
( lldb ) process connect connect:// < device-i p > :6666
Alternative: Using Xcode
Debug on Simulator
Debug Developer App
Debug via USB
Easiest approach for testing:
Install app on simulator
Xcode > Debug > Attach to Process
Select your app from the list
Simulator is x86_64/arm64 macOS, not actual iOS. Behavior may differ.
For apps you can rebuild:
Add your provisioning profile
Enable “Debug” configuration
Run normally from Xcode (Cmd+R)
Full debugging capabilities available
For jailbroken devices:
Install via Xcode or Configurator
Use debugserver over USB tunnel:
iproxy 6666 6666
debugserver localhost:6666 -a "App"
Setting Breakpoints
Basic Breakpoints
Function Name
Memory Address
Conditional 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
SSL Pinning
Premium Features
Login Bypass
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
Break on subscription checks: (lldb) b "-[SubscriptionManager isPremiumUser]"
# When hit, force true return:
(lldb) finish # Let method complete
(lldb) expr (BOOL)$rax = 1 # Change return value to YES
(lldb) c
(lldb) b "-[LoginViewController loginButtonTapped:]"
# Skip the entire method and proceed to next screen
(lldb) thread return
(lldb) expr [self performSegueWithIdentifier:@"LoginSuccess" sender:nil]
(lldb) c
Memory Manipulation
Reading Memory
Read Variables
Read Memory
Dump 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
Modify Variables
Patch Memory
Call Functions
# 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
# Write single byte
(lldb) memory write 0x00000001000a4000 0x90 # NOP instruction
# Write multiple bytes
(lldb) memory write 0x00000001000a4000 0x90 0x90 0x90 0x90
# Write from file
(lldb) memory write --infile /tmp/patch.bin 0x00000001000a4000
Memory patches are lost when app restarts. Use binary patching for permanent changes.
# Call Objective-C method
(lldb) expr (void)[self dismissViewControllerAnimated:YES completion:nil]
# Call with return value
(lldb) expr (NSString *)[self.username uppercaseString]
# Call C function
(lldb) expr (int)printf("Hello from LLDB\\n")
# Allocate and call
(lldb) expr id $url = [NSURL URLWithString:@"https://api.example.com"]
(lldb) expr (void)[self loadURL:$url]
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
ptrace Detection
sysctl Detection
Timing Checks
Apps use ptrace(PT_DENY_ATTACH) to prevent debuggers: // What apps do
ptrace (PT_DENY_ATTACH, 0 , 0 , 0 );
Bypass methods:
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 );
}
}
});
Binary patching
# Find ptrace call in Hopper
# Replace entire function with:
# mov w0, #0
# ret
Runtime hooking with LLDB
(lldb) b ptrace
(lldb) breakpoint command add 1
> expr (int)$arg1 = 0
> continue
> DONE
Apps check for debugger presence: // Detection code
struct kinfo_proc info;
info.kp_proc.p_flag & P_TRACED // Returns true if debugged
Bypass: // Frida script
var sysctl = Module . findExportByName ( null , 'sysctl' );
Interceptor . attach ( sysctl , {
onEnter : function ( args ) {
this . name = args [ 0 ];
this . oldp = args [ 2 ];
},
onLeave : function ( retval ) {
// Check if querying process info
var name = Memory . readU32 ( this . name );
if ( name === 1 && Memory . readU32 ( this . name . add ( 4 )) === 14 ) {
// Clear P_TRACED flag (0x00000800)
var p_flag = this . oldp . add ( 0x20 );
var flags = Memory . readU32 ( p_flag );
Memory . writeU32 ( p_flag , flags & ~ 0x800 );
console . log ( '[!] Cleared P_TRACED flag' );
}
}
});
Apps measure execution time to detect breakpoints: uint64_t start = mach_absolute_time ();
// Sensitive code
uint64_t end = mach_absolute_time ();
if (end - start > threshold) exit ( 1 ); // Debugger detected
Bypass: // Hook mach_absolute_time to return consistent values
var mach_absolute_time = Module . findExportByName ( null , 'mach_absolute_time' );
var counter = 0 ;
Interceptor . replace ( mach_absolute_time , new NativeCallback ( function () {
counter += 100 ; // Small increment
return counter ;
}, 'uint64' , []));
Or use LLDB: (lldb) b mach_absolute_time
(lldb) breakpoint command add 1
> expr (uint64_t)$rax = $rax + 100
> 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
Initial Setup
Attach debugger to running app
Set breakpoints on entry points
Configure signal handling
Load symbols if available
Locate Target Code
Use static analysis to find addresses
Break on API calls of interest
Use symbolic breakpoints for classes
Analyze Execution
Step through code
Inspect registers and stack
Log function arguments
Trace execution paths
Test Modifications
Change variables in real-time
Patch instructions temporarily
Call functions manually
Verify behavior changes
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