Chapters

Hide chapters

Advanced Apple Debugging & Reverse Engineering

Fourth Edition · iOS 16, macOS 13.3 · Swift 5.8, Python 3 · Xcode 14

Section I: Beginning LLDB Commands

Section 1: 10 chapters
Show chapters Hide chapters

Section IV: Custom LLDB Commands

Section 4: 8 chapters
Show chapters Hide chapters

1. Getting Started
Written by Walter Tyree

In this chapter, you’re going to get acquainted with LLDB and investigate the process of introspecting and debugging a program. You’ll start off by introspecting a program you didn’t even write!

You’ll take a whirlwind tour of a debugging session using LLDB and discover the amazing changes you can make to a program you’ve absolutely zero source code for. This first chapter heavily favors doing over learning, so a lot of the concepts and deep dives into certain LLDB functionality will be re-introduced, with explanation, in later chapters.

Let’s get started.

Getting around System Integrity Protection (SIP)

Before you can start working with LLDB, you need to learn about a feature introduced by Apple to thwart malware. Unfortunately, this feature also thwarts your attempts to introspect and debug using LLDB and other tools like DTrace. Never fear though, because Apple included a way to turn this feature off — for those who know what they’re doing. And you’re going to become one of these people who knows what they’re doing!

The feature blocking your introspection and debugging attempts is System Integrity Protection (SIP), also known as rootless. This system restricts what programs can do — even if they have root access — to stop malware from planting itself deep inside your system.

Although SIP is a substantial leap forward in security, it introduces some annoyances as it makes programs harder to debug. Specifically, it prevents other processes from attaching a debugger to programs Apple signs.

Since this book involves debugging not only your own applications, but any application you’re curious about, it’s important you remove this feature while you learn about debugging so you can inspect any application of your choosing.

If you currently have SIP enabled, you’ll be unable to attach to the majority of the programs on your computer.

For example, try attaching LLDB to the Finder application.

Open up a Terminal window and look for the Finder process, like so:

lldb -n Finder

You’ll notice the following error:

error: attach failed: attach failed (Not allowed to attach to process.  Look in the console messages (Console.app), near the debugserver entries, when the attach failed.  The subsystem that denied the attach permission will likely have logged an informative message about why it was denied.)

Note: There are many ways to attach to a process, as well as specific configurations when LLDB attaches successfully. You’ll learn all about attaching to a process, in Chapter 3, “Attaching With LLDB”.

Disabling System Integrity Protection

Note: A safer way to follow along with this book would be to create a dedicated virtual machine using VMWare or VirtualBox and disable SIP on that VM following the steps detailed below. Downloading and setting up a macOS VM can take about an hour depending on your computer’s hardware (and internet speed!). Get the latest installation virtual machine instructions from Google since the macOS version and VM software will have different installation steps.

At WWDC 2022, Apple, updated their own virtualization technologies to support macOS VMs. If you are running a computer with Apple Silicon, check out Session 10002 “Create macOS or Linux virtual machines” that explains the technology and even offers a demo project that creates a VM you can use with this book. If you do use Apple’s demo code, can start your VM in Recovery Mode by setting the .startUpFromMacOSRecovery option to true and adding that to the virtualMachine.start command in the AppDelegate. Your changes to their demo code would look something like:

let options = VZMacOSVirtualMachineStartOptions()
options.startUpFromMacOSRecovery = true
virtualMachine.start(options: options, completionHandler:

If you choose to disable SIP on your computer without a VM, it would be ideal to re-enable SIP once you’re done with that particular chapter. Fortunately, there’s only a handful of chapters in this book that require SIP to be disabled!

To disable SIP, perform the following steps:

  1. Restart your macOS machine.

  2. When the screen turns blank, press and hold down Command-R until the Apple boot logo appears for x86 Macs, or hold the power button until the Apple boot logo appears for Apple Silicon Macs. This puts your computer into Recovery Mode.

  3. Now, find the Utilities menu from the top and then select Terminal.

  4. With the Terminal window open, type:

csrutil disable && reboot
  1. Provided the csrutil disable command succeeded, your computer will restart with SIP disabled.

Note: Apple provides documentation for how to get into Recovery Mode on different Mac models. If you can’t get into Recovery Mode following the instructions above, check the documentation.

You can verify if you’ve successfully disabled SIP by querying its status in Terminal once your computer starts up by typing:

csrutil status

You should see the following:

System Integrity Protection status: disabled.

Now that SIP is disabled, perform the same “Attach to Finder” LLDB command you tried earlier.

lldb -n Finder

Your computer will ask for your password and then LLDB should attach itself to the current Finder process. The output of a successful attach should look like this:

After verifying a successful attach, detach LLDB by either closing the Terminal window, or typing quit and confirming in the LLDB console.

Attaching LLDB to the Notes App

Now that you’ve disabled SIP, you can attach LLDB to any process on your macOS machine (some hurdles may apply, such as with ptrace system call, but we’ll get to that later). You’re first going to look into an application that’s guaranteed to be installed on your computer – Notes!

Open a new Terminal window. Next, edit the Terminal tab’s title by pressing Command-Shift-I to display the Inspector popup. Edit the Tab Title to be LLDB.

Next, close Notes on your Mac if it’s running. You wouldn’t want to have multiple instances of Notes running, it would get confusing.

In Terminal, type the following:

$ lldb

This launches LLDB! Create a new Terminal tab by pressing Command-T. Edit the new tab’s title using Command-Shift-I and name the tab stderr or something interesting. This tab is going to contain all of the output that Notes.app normally sends to the system logs when it’s running. It’ll also contain any output you print from the debugging session.

Make sure you’re still in the new Terminal tab and type the command to get it’s address:

$ tty

Terminal should respond with the address to the tab. It should look simiar to below:

/dev/ttys001

Don’t worry if yours is different; it’s the address of this, specific terminal instance.

To illustrate what you’ll be doing with the stderr tab, create yet another tab and type this command into it:

$ echo "hello debugger" 1>/dev/ttys001

Be sure to use the address you obtained from the tty command if it’s different from the one in the example. Now switch back to the stderr tab. The words hello debugger should have appeared. You’ll use this same trick to pipe the output of Notes’ stderr to this tab.

Finally, close the unnamed, third tab and go back to the LLDB tab.

To summarize, you should now have two Terminal tabs open:

  • a tab named “LLDB”, which contains an instance of LLDB running
  • a tab named “stderr”, which contains the tty command you just performed

From there, enter the following into the LLDB Terminal tab:

(lldb) file /System/Applications/Notes.app

This will set the executable target to Notes.

Now launch the Notes process from LLDB, replacing /dev/ttys027 with your Notes stderr tab’s tty address again:

(lldb) process launch -e /dev/ttys027 --

The launch argument e specifies the location of stderr. Common logging functionality, such as Objective-C’s NSLog or Swift’s print function, outputs to stderr — yes, not stdout! You’ll print your own logging to stderr later.

Notes will launch after a moment. Switch over to Notes and make sure that you’re looking at a new, clean note. You’re going to be making changes with the debugger and it would be a shame if you ruined that great chocolate chip cookie recipe note or some other important note.

You now have a new note. Arrange the windows so you can see both Terminal and Notes.

Note: You might notice some output on the stderr Terminal window – ok lots of output :]. This is due to content logged by the authors of Notes via NSLog or another stderr console printing function.

A “Swiftly” Changing Landscape

Apple has been cautious in its adoption of Swift in its own software. However Apple has been ramping up the usage of Swift ever since the language was born. Adoption is still cautious, and understandably so given that the language is still evolving and needs further battle testing.

The adoption of Swift in Apple’s own applications range from the iOS Simulator and even Xcode to the Notes app!

Notes has a few hundred Swift functions and references thirty or so Swift libraries.

Note: How can you verify this information yourself? This info was obtained using a combination of nm and otool and the helper LLDB scripts found in Appendix C “Helpful Python Scripts” which are free to all. Installation instructions are in the README of the repo. I’ll refer to this repo throughout the book when there’s a situation that’s significantly easier through these LLDB scripts.

The scary command to obtain this information is the following. You’ll need to install the repo mentioned in the note above if you wish to execute this command.

(lldb) sys echo "$(dclass -t swift)" | grep -v _ | grep "\." | cut -d. -f1 | uniq | wc -l

Breaking this command down, the dclass -t swift command is a custom LLDB command that dumps all classes known to the process that are Swift classes. The sys command will allow you to execute commands like you were in Terminal, but anything in the $() will get evaluated first via LLDB. From there, it’s a matter of manipulating the output of all the Swift classes given by the dclass command.

Swift class naming will typically have the form ModuleName.ClassName where the module is the framework that the class is implemented in. The rest of the command does the following:

  • grep -v _: Exclude any Swift names that include an underscore, which is a typical trait of the class names in the Swift standard library.
  • grep "\.": Filter by Swift classes that contain a period in the class name.
  • cut -d. -f1: Isolate the module name before the period.
  • uniq: Then grab all unique values of the modules.
  • wc -l: and get the count of it.

These custom LLDB commands (dclass, sys) were built using Python along with LLDB’s Python module (confusingly also called lldb). You’ll get very accustomed to working with this Python module in Section IV of this book as you learn to build custom, advanced LLDB scripts.

Finding a Class With a Click

Now that Notes is running and your Terminal debugging windows are correctly created and positioned, it’s time to start exploring using the help of the debugger.

While debugging, knowledge of the Cocoa SDK can be extremely helpful. For example, -[NSView hitTest:] is a useful Objective-C method that returns the class responsible for the handled click or gesture for an event in the run loop. This method will first be triggered on the containing NSView and recursively drill into the furthest subview that handles this touch.

You can use this knowledge of the Cocoa SDK to help determine the class of the view you’ve clicked on.

In your LLDB tab, press Control-C to pause the debugger. From there, type:

(lldb) b -[NSView hitTest:]

LLDB will respond with information about the new breakpoint, its name and where it’s been set. This is your first breakpoint of many to come. You’ll learn the details of how to create, modify, and delete breakpoints in Chapter 4, “Stopping in Code”, but for now simply know you’ve created a breakpoint on -[NSView hitTest:].

Notes is now paused thanks to the debugger. Resume the program by typing the continue command in LLDB:

(lldb) continue

Click anywhere in the Notes window, or in some cases even moving your cursor over Notes will do the same; Notes will instantly pause and LLDB will indicate a breakpoint has been hit.

The hitTest: breakpoint has fired. You can inspect which view was hit by inspecting the CPU register. Print it out in LLDB:

(lldb) po $arg1

This command instructs LLDB to print out the contents of the object at the memory address referenced by what’s stored in the arg1 assembly register. This is a virtual register that LLDB uses because it supports multiple CPU architectures. It’s equal to $x0 on Apple Silicon and $rdi on x86_64 machines.

Note: Wondering why the command is po? po stands for print object. There’s also p, which simply prints the contents of arg1. po is usually more useful as it gives the NSObject’s (or Swift’s SwiftObject’s) description or debugDescription methods, if available.

Assembly is an important skill to learn if you want to take your debugging to the next level. It will give you insight into Apple’s code — even when you don’t have any source code to read from. It will give you a greater appreciation of how the Swift compiler team danced in and out of Objective-C with Swift, and it will give you a greater appreciation of how everything works on your Apple devices.

You’ll learn more about registers and assembly in Chapter 11, “Assembly Register Calling Convention”.

For now, simply know the $arg register in the above LLDB command contains the instance of the subclass NSView the hitTest: method was called upon.

Note: The output will produce different results depending on where you clicked and what version of Notes you’re using. It could give a private class specific to Notes, or it could give you a public class belonging to Cocoa.

In LLDB, type the following to resume the program:

(lldb) continue

Instead of continuing, Notes will likely hit another breakpoint for hitTest: and pause execution. This is due to the fact that the hitTest: method is recursively calling this method for all subviews contained within the parent view that was clicked. You can inspect the contents of this breakpoint, but this will soon become tedious since there are so many views that make up the Notes UI.

Note: Apple uses a pattern they call the “responder chain”. Since the UI is made up of views within views within views, each one of them has the opportunity to respond to a click and then pass the click along to the next one until it gets to the root window. This is another reason it’s important to remember to call super in your view code.

Automate the hitTest:

The process of clicking on a view, stopping, po’ing the arg1 register then continuing can get tiring quickly. What if you created a breakpoint to automate all of this?

There’s several ways to achieve this, but perhaps the cleanest way is to declare a new breakpoint with all the traits you want. Wouldn’t that be neat?! :]

Remove the previous breakpoint with the following command:

(lldb) breakpoint delete

LLDB will ask if you sure you want to delete all breakpoints, either press enter or press ‘Y’ then enter to confirm.

Now, create a new breakpoint with the following:

(lldb) breakpoint set -n  "-[NSView hitTest:]" -C "po $arg1" -G1

The gist of this command says to create a breakpoint on -[NSView hitTest:], have it execute the “po $arg1” command, then automatically continue after executing the command. You’ll learn more about these options in a later chapter.

Resume execution with the c or continue command:

(lldb) continue

Now, click anywhere in Note and check out the output in the LLDB console. You’ll see many many NSViews being called to see if they should take the mouse click!

Filter Breakpoints for Important Content

Since there are so many NSViews that make up Notes, you need a way to filter out some of the noise and only stop on the NSView relevant to what you’re looking for. This is an example of debugging a frequently-called method, where you want to find a unique case that helps pinpoint what you’re really looking for.

Since Notes is all about saving text it has a lot of views that handle text. Some are public, and some are private like ICMacTextView.

Let’s say you want to break only when you click an instance of ICMacTextView. You can modify the existing breakpoint to stop only on a ICMacTextView click by using breakpoint conditions.

Provided you still have your -[NSView hitTest:] breakpoint set, and it’s the only active breakpoint in your LLDB session, you can modify that breakpoint with the following LLDB command:

(lldb) breakpoint modify -c '(BOOL)[NSStringFromClass((id)[$arg1 class]) containsString:@"ICMacTextView"]' -G0

This command modifies all existing breakpoints in your debugging session and creates a condition which gets evaluated every time -[NSView hitTest:] fires. If the condition evaluates to true, then execution will pause in the debugger. This condition checks that the instance of the NSView is of type ICMacTextView. The final -G0 says to modify the breakpoint to not automatically resume execution after the action has been performed.

After modifying your breakpoint above, use the c or continue command in LLDB to resume execution of Notes. Now click on the main note area in Notes, where you’d write text in your note.po LLDB should stop on hitTest:. Print out the instance of the class this method was called on:

(lldb) po $rdi

Your output should look something similar to the following:

<ICMacTextView: 0x1010cf200>
  Frame = {{0.00, 0.00}, {505.00, 604.00}}, Bounds = {{0.00, 0.00} {505.00, 604.00}}
  Horizontally resizable: NO, Vertically resizable: YES
  MinSize = {505.00, 604.00}, MaxSize = {731.00, 10000000.00}

This is printing out the object’s description. You’ll notice that there is a pointer reference within this. That’s the location in memory where this ICMacTextView lives. Type the following in LLDB:

(lldb) p/x $arg1

You’ll get something similar to the following:

(unsigned long) $3 = 0x0000000110a42600

Since arg1 points to a valid Objective-C NSObject subclass (written in Swift), you can also get the same info just by po’ing this address instead of the register.

Type the following into LLDB while making sure to replace the address with your own:

(lldb) po 0x0000000110a42600

You’ll get the same output as earlier.

You might be skeptical that this reference pointed at by the arg1 register is actually pointing to the NSView that displays your code. You can easily verify if that’s true or not by typing the following in LLDB:

(lldb) po [$rdi setHidden:!(BOOL)[$rdi isHidden]]; [CATransaction flush]

Note: Kind of a long command to type out, right? In Chapter 10: “Regex Commands”, you’ll learn how to build convenient shortcuts so you don’t have to type out these long LLDB commands. If you chose to install the LLDB repo mentioned earlier, a convenience command for this above action the tv command, or “toggle view”.

Provided arg1 is pointing to the correct reference, your note editor view will disappear!

You can toggle this view on and off simply by repeatedly pressing Enter. LLDB will automatically execute the previous command.

Since this is a subclass of NSView, all the methods of NSView apply. For example, the string command can query the contents of your source code through LLDB. Type the following:

(lldb) po [$arg1 string]

This will dump out the contents of your note editor. Neat!

Always remember, any APIs that you have in your development cycle can be used in LLDB. If you were crazy enough, you could create an entire app just by executing LLDB commands!

When you get bored of playing with the NSView APIs on this instance, copy the address down that arg1 is referencing (copy it to your clipboard or add it to the stickies app). You’ll reference it again in a second.

Alternatively, did you notice that output preceding the hex value in the p/x $arg1 command? In my output, I got $3, which means that you can use $3 as a reference for that pointer value you just grabbed. This is incredibly useful when the arg1 register points to something else and you still want to reference this NSView at a later time.

Swift vs Objective-C Debugging

Wait — we’re using Objective-C on a Swift class?! You bet! You’ll discover that a Swift class is mostly all Objective-C underneath the covers (however the same can’t be said about Swift structs). You’ll confirm this by modifying the note’s contents through LLDB using Swift!

First, import the following modules in the Swift debugging context:

(lldb) ex -l swift -- import Foundation
(lldb) ex -l swift -- import AppKit

The ex command (short for expression) lets you evaluate code and is the foundation for your p/po LLDB commands. -l swift tells LLDB to interpret your commands as Swift code. You just imported the headers to call appropriate methods in both of these modules through Swift. These are big modules, so don’t be alarmed if it takes LLDB a few seconds to load each one. You’ll need these in the next two commands.

Enter the following, replacing 0x0110a42600 with the memory address of your NSView subclass you recently copied to your clipboard:

(lldb) ex -l swift -o -- unsafeBitCast(0x0110a42600, to: NSView.self)

This command prints out the ICMacTextView instance — but this time using Swift!

Now, add some text to your note via LLDB:

(lldb) ex -l swift -o -- unsafeBitCast(0x0110a42600, to: NSView.self).insertText("Yay! Swift!")

You won’t see anything right away because LLDB has suspended your app. Use the c or continue command a few times to make it appear. Depending where your cursor was in the Xcode console, you’ll see the new string “Yay! Swift!” added to your source code.

When stopping the debugger out of the blue, or on Objective-C code, LLDB will default to using the Objective-C context when debugging. This means the po you execute will expect Objective-C syntax unless you force LLDB to use a different language like you did above. It’s possible to alter this, but this book prefers to use Objective-C since the Swift REPL can be brutal for error-checking, has slow compilation times for executing commands, is generally much more buggy, and prevents you from executing methods the Swift LLDB context doesn’t know about.

All of this will eventually go away, but we must be patient.

Key Points

  • SIP is Apple’s technology to keep processes from attaching to other processes unexpectedly.
  • You can only enable/disable SIP when your Mac is in Recovery Mode.
  • LLDB can attach to any process on your Mac as long as you have permissions.
  • Appendix C “Helpful Python Scripts” contains a number of scripts you’ll use as you work through this book.
  • When an app hits a breakpoint, it is suspended. Use the c or continue command in LLDB to start it again.
  • LLDB uses arg1 as a virtual address to represent the address of the code that is impacted by a breakpoint.

Where to Go From Here?

This was a breadth-first, whirlwind introduction to using LLDB and attaching to a process where you don’t have any source code to aid you. This chapter glossed over a lot of detail, but the goal was to get you right into the debugging/reverse engineering process.

To some, this first chapter might have come off as a little scary, but we’ll slow down and describe methods in detail from here on out. There are lots of chapters remaining to get you into the details!

Keep reading to learn the essentials in the remainder of Section 1. Happy debugging!

If you disabled SIP, you can enable it by following the steps at the beginning of this chapter and issuing the csrutil enable command while in Recovery Mode. Leaving SIP disabled on your regular computer is dangerous.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.