Skip to main content
LLDB (Low-Level Debugger) is Apple’s debugger, built on the LLVM compiler infrastructure. It’s the standard debugger for iOS and macOS development, providing powerful capabilities for runtime inspection and manipulation.

Overview

LLDB is essential for iOS reverse engineering when you need:
  • Step-by-step execution analysis
  • Real-time memory inspection
  • Conditional breakpoints
  • Register and stack examination
  • Integration with Xcode and command-line workflows
  • Lower-level control than Frida for specific debugging tasks
LLDB provides more granular control than Frida but requires more setup. Use LLDB for precise debugging and Frida for rapid hooking/instrumentation.

Remote Debugging iOS Apps

Setting Up the Environment

1

Prepare Jailbroken Device

LLDB remote debugging requires a jailbroken iOS device.
# SSH into your device
ssh root@<device-ip>

# Install debugserver (usually comes with developer disk image)
# Check if debugserver exists
ls /Developer/usr/bin/debugserver

# If not present, copy from Xcode:
# /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/
# DeviceSupport/<iOS-version>/DeveloperDiskImage.dmg
2

Sign debugserver with Task-for-PID Entitlement

The stock debugserver lacks necessary entitlements. Create and sign:
<!-- 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 debugserver
codesign -s - --entitlements entitlements.xml -f /path/to/debugserver

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

Start debugserver on Device

# Find the app's process ID
ps aux | grep -i YourApp

# Start debugserver attached to PID
debugserver *:6666 --attach=<PID>

# Or attach by name
debugserver *:6666 --attach=YourApp
*:6666 means listen on all interfaces on port 6666. You can use any port number.
4

Connect from Host Machine

# On your Mac
lldb

# Connect to device
(lldb) process connect connect://<device-ip>:6666

# You should see:
# Process <PID> stopped

Useful Commands

# Continue execution
(lldb) continue
(lldb) c

# Step over (next line)
(lldb) next
(lldb) n

# Step into (follow function calls)
(lldb) step
(lldb) s

# Step out (finish current function)
(lldb) finish

# Step instruction (assembly level)
(lldb) nexti
(lldb) ni

# Step into instruction
(lldb) stepi
(lldb) si

Breakpoints and Watchpoints

# Break at address
(lldb) br s -a 0x100004a20
(lldb) breakpoint set --address 0x100004a20

# Break at address with condition
(lldb) br s -a 0x100004a20 -c 'x0 == 0'

# Break at offset from module
(lldb) br s -a YourApp[0x4a20]

# List breakpoints
(lldb) br list

# Delete breakpoint
(lldb) br delete <breakpoint-id>

# Disable/enable breakpoint
(lldb) br disable <breakpoint-id>
(lldb) br enable <breakpoint-id>

# Delete all breakpoints
(lldb) br delete

Memory and Register Inspection

# Show all registers
(lldb) register read

# Show specific register
(lldb) register read x0
(lldb) p/x $x0

# Modify register
(lldb) register write x0 0x1

# Show floating point registers
(lldb) register read --all

# ARM64 common registers:
# x0-x30: General purpose
# x29: Frame pointer (FP)
# x30: Link register (LR)
# sp: Stack pointer
# pc: Program counter

# Function arguments are in:
# x0: self (Objective-C)
# x1: _cmd (selector)
# x2-x7: Arguments 1-6

Examining Objective-C Objects

# Print object description
(lldb) po $x0
(lldb) expression -O -- $x0

# Print all properties of object
(lldb) po [$x0 _ivarDescription]

# Print methods
(lldb) po [$x0 _methodDescription]

# Print view hierarchy
(lldb) po [[UIWindow keyWindow] recursiveDescription]

# Print view controllers
(lldb) po [[UIWindow keyWindow] rootViewController]

Practical Debugging Workflows

Finding and Breaking on Functions

1

Find Function Address

# Search for function by name
(lldb) image lookup -rn "validateCredentials"

# Output shows address:
# 1 match found in /var/containers/Bundle/Application/.../YourApp:
#   Address: YourApp[0x0000000100004a20]
#   Summary: YourApp`-[LoginViewController validateCredentials:password:]
2

Set Breakpoint

# Break at the function
(lldb) br s -a 0x100004a20

# Or by name
(lldb) br s -n "-[LoginViewController validateCredentials:password:]"
3

Trigger and Inspect

# Continue execution
(lldb) c

# When breakpoint hits, inspect arguments
(lldb) po $x0  # self
(lldb) po $x1  # _cmd (selector)
(lldb) po $x2  # first argument (username)
(lldb) po $x3  # second argument (password)

# Show registers
(lldb) register read

# Show backtrace
(lldb) bt

Bypassing Jailbreak Detection

# Find the jailbreak check function
(lldb) image lookup -rn "isJailbroken"

# Set breakpoint
(lldb) br s -n "-[JailbreakDetector isJailbroken]"

# When hit, force return false
(lldb) thread return NO

# Or modify return register before return
(lldb) br s -a 0x<return-address>
(lldb) br command add <breakpoint-id>
> register write x0 0
> c
> DONE

Dumping Decrypted Binary

1

Attach to Running App

# On device
debugserver *:6666 --attach=YourApp

# On Mac
lldb
(lldb) process connect connect://<device-ip>:6666
2

Find Image Base

(lldb) image list YourApp
# Output:
# [  0] <UUID> 0x0000000100000000 /var/containers/.../YourApp.app/YourApp

# Note the load address: 0x0000000100000000
3

Dump Memory

# Get binary size
(lldb) image dump sections YourApp

# Dump to file (adjust size as needed)
(lldb) memory read --outfile /tmp/decrypted.bin --binary \
       0x0000000100000000 0x0000000100000000+0x1000000
4

Fix Mach-O Header

# Transfer to Mac
scp root@<device-ip>:/tmp/decrypted.bin ~/

# Use tools like MachOView or jtool to fix headers
# Or use automated tools like Clutch or frida-ios-dump

Analyzing Crypto Operations

# Break on CommonCrypto functions
(lldb) br s -n CCCrypt
(lldb) br s -n CC_SHA256
(lldb) br s -n CCKeyDerivationPBKDF

# When hit, examine parameters
(lldb) br command add 1  # Assuming CCCrypt is breakpoint 1
> # x0 = operation (encrypt/decrypt)
> # x1 = algorithm
> # x2 = options
> # x3 = key
> # x4 = key length
> # x5 = IV
> po "Operation: ", (int)$x0
> po "Algorithm: ", (int)$x1
> memory read -c 32 $x3  # Dump key
> memory read -c 16 $x5  # Dump IV
> c
> DONE

Advanced LLDB Features

Python Scripting

# Create lldb_commands.py
import lldb

def dump_keychain(debugger, command, result, internal_dict):
    """Dump keychain items"""
    target = debugger.GetSelectedTarget()
    process = target.GetProcess()
    thread = process.GetSelectedThread()
    frame = thread.GetSelectedFrame()
    
    # Call SecItemCopyMatching
    expr = '''
        NSDictionary *query = @{
            (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
            (__bridge id)kSecReturnAttributes: @YES,
            (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitAll
        };
        CFTypeRef result = NULL;
        OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
        result;
    '''
    
    value = frame.EvaluateExpression(expr)
    print("Keychain items:")
    print(value.GetObjectDescription())

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand('command script add -f lldb_commands.dump_keychain dump_keychain')

Symbolic Breakpoints

# Break on any method containing "login"
(lldb) br s -r ".*login.*" -s YourApp

# Break on all methods in a file
(lldb) br s -f LoginViewController.m

# Break on Objective-C exception throw
(lldb) br s -n objc_exception_throw

# Break on malloc/free for tracking memory
(lldb) br s -n malloc
(lldb) br command add <breakpoint-id>
> bt 5
> register read x0
> c
> DONE

Best Practices

# Save breakpoints
(lldb) br write -f ~/breakpoints.txt

# Load breakpoints
(lldb) br read -f ~/breakpoints.txt

# Save commands to file
(lldb) settings set target.save-jit-to-disk true
# Create ~/.lldbinit
command script import ~/lldb_commands.py

# Auto-connect shortcut
command alias connect process connect connect://localhost:6666

# Custom shortcuts
command alias bpt breakpoint set -a
command alias rd register read
  1. Use Ghidra/IDA to find interesting functions
  2. Note addresses and function names
  3. Set breakpoints in LLDB at those addresses
  4. Examine runtime behavior
  5. Return to static analysis with new insights
  • Use hardware breakpoints when possible (limited to 4 on ARM64)
  • Disable breakpoints when not needed
  • Use conditional breakpoints to reduce noise
  • Detach when done to avoid impact on app performance

Troubleshooting

Common issues and solutions:
# Check debugserver permissions
ls -la /usr/bin/debugserver

# Re-sign with proper entitlements
# See "Sign debugserver" step above

# Check if app has anti-debugging
# Look for ptrace(PT_DENY_ATTACH)
# Patch it out or use anti-anti-debugging tweaks
# Add symbol path
(lldb) settings set target.debug-file-search-paths /path/to/symbols

# Force symbol loading
(lldb) target symbols add /path/to/YourApp.app/YourApp
# Use USB instead of WiFi
iproxy 6666 6666

# Increase timeout
(lldb) settings set plugin.process.gdb-remote.packet-timeout 60

Frida

Higher-level dynamic instrumentation

Ghidra

Find functions to debug

IDA Pro

Identify debugging targets

Hopper

Static analysis companion

Resources

Build docs developers (and LLMs) love