Chapters

Hide chapters

Advanced Apple Debugging & Reverse Engineering

Third Edition · iOS 12 · Swift 4.2 · Xcode 10

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section III: Low Level

Section 3: 7 chapters
Show chapters Hide chapters

Section IV: Custom LLDB Commands

Section 4: 8 chapters
Show chapters Hide chapters

1. Getting Started
Written by Derek Selander

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 — Xcode!

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 saved for later chapters.

Let’s get started.

Getting around Rootless

Before you can start working with LLDB, you need to learn about a feature introduced by Apple to thwart malware. Unfortunately, this feature will also thwart 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, 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 Rootless 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 that you remove this feature while you learn about debugging so you can inspect any application of your choosing.

If you currently have Rootless enabled, you’ll be unable to attach to the majority of Apple’s programs.

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: cannot attach to process due to System Integrity Protection

Note: There are many ways to attach to a process, as well as specific configurations when LLDB attaches successfully. To learn more about attaching to a process, check out Chapter 3, “Attaching with LLDB”.

Disabling Rootless

Note: A safer way to follow along with this book would be to create a dedicated virtual machine using VMWare or VirtualBox and disable Rootless 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. If you choose to disable Rootless on your computer without a VM, it would be ideal to re-enable Rootless once you’re done with that particular chapter. Fortunately, there’s only a handful of chapters in this book that require Rootless to be disabled!

To disable Rootless, perform the following steps:

  1. Restart your macOS machine.

  2. When the screen turns blank, hold down Command + R until the Apple boot logo appears. This will put 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 Rootless disabled.

You can verify if you’ve successfully disabled Rootless 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

LLDB should now 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 killing the Terminal window, or typing quit and confirming in the LLDB console.

Attaching LLDB to Xcode

Now that you’ve disabled Rootless, 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 you frequently use in your day-to-day development: Xcode! Make sure you have the latest version of Xcode 10 installed on your computer before continuing.

Open a new Terminal window. Next, edit the Terminal tab’s title by pressing ⌘ + Shift + I. A new popup window will appear. Edit the Tab Title to be LLDB.

Next, make sure Xcode isn’t running, or you’ll end up with multiple running instances of Xcode, which could cause confusion.

In Terminal, type the following:

lldb

This launches LLDB.

Create a new Terminal tab by pressing ⌘ + T. Edit the tab’s title again using ⌘ + Shift + I and name the tab Xcode stderr. This Terminal tab will contain all output when you print content from the debugger.

Make sure you are on the Xcode stderr Terminal tab and type the following:

tty

You should see something similar to below:

/dev/ttys027

Don’t worry if yours is different; I’d be surprised if it wasn’t. Think of this as the address to your Terminal session.

To illustrate what you’ll do with the Xcode stderr tab, create yet another tab and type the following into it:

echo "hello debugger" 1>/dev/ttys027

Be sure to replace your Terminal path with your unique one obtained from the tty command.

Now switch back to the Xcode stderr tab. The words hello debugger should have popped up. You’ll use the same trick to pipe the output of Xcode’s stderr to this tab.

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

To summarize: You should now have two Terminal tabs: a tab named “LLDB”, which contains an instance of LLDB running, and another tab named “Xcode stderr”, which contains the tty command you performed earlier.

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

(lldb) file /Applications/Xcode.app/Contents/MacOS/Xcode

This will set the executable target to Xcode.

Note: If you are using a prerelease version of Xcode, then the name and path of Xcode could be different.

You can check the path of the Xcode you are currently running by launching Xcode and typing the following in Terminal:

ps -ef `pgrep -x Xcode`

Once you have the path of Xcode, use that new path instead.

Now launch the Xcode process from LLDB, replacing /dev/ttys027 with your Xcode 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.

Xcode will launch after a moment. Switch over to Xcode and click File ▸ New ▸ Project…. Next, select iOS ▸ Application ▸ Single View Application and click Next. Name the product Hello Debugger. Make sure to select Swift as the programming language and deselect any options for Unit or UI tests. Click Next and save the project wherever you wish.

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

Navigate to Xcode and open ViewController.swift.

Note: You might notice some output on the Xcode stderr Terminal window; this is due to content logged by the authors of Xcode 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 — and understandably so. No matter one’s (seemingly religious) beliefs on Swift, for better or worse, it’s still an immature language moving at an incredible pace with breaking changes abound.

However, things are a changin’ at Apple. Apple is now more aggressively adopting Swift in their own applications such as the iOS Simulator… and even Xcode!

At last count, Xcode 10 includes almost 200 frameworks which contain Swift.

How can you verify this information yourself? This info was obtained using a combination of my helper LLDB scripts found here: https://github.com/DerekSelander/LLDB which are free to all. I would strongly recommend that you clone and install this repo, as I’ll occasionally push out new LLDB commands that make debugging much more enjoyable. 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 will dump 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 Xcode is set up and your Terminal debugging windows are correctly created and positioned, it’s time to start exploring Xcode 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, type Ctrl + C to pause the debugger. From there, type:

(lldb) b -[NSView hitTest:]
Breakpoint 1: where = AppKit`-[NSView hitTest:], address = 0x000000010338277b

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:].

Xcode is now paused thanks to the debugger. Resume the program:

(lldb) continue

Click anywhere in the Xcode window (or in some cases even moving your cursor over Xcode will do the same); Xcode 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 RDI CPU register. Print it out in LLDB:

(lldb) po $rdi

This command instructs LLDB to print out the contents of the object at the memory address referenced by what’s stored in the RDI assembly register.

Note: Wondering why the command is po? po stands for print object. There’s also p, which simply prints the contents of RDI. 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 $rdi 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 Xcode you’re using. It could give a private class specific to Xcode, 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, Xcode 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 Xcode.

Automate the hitTest:

The process of clicking on a view, stopping, po’ing the RDI 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 $rdi" -G1

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

Resume execution with the continue command:

(lldb) continue

Now, click anywhere in Xcode and check out the output in the Terminal 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 Xcode, 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.

As of Xcode 10, the class responsible for visually displaying your code in the Xcode IDE is a private Swift class belonging to the IDESourceEditor module, named IDESourceEditorView. This class acts as the visual coordinator to hand off all your code to other private classes to help compile and create your applications.

Let’s say you want to break only when you click an instance of IDESourceEditorView. You can modify the existing breakpoint to stop only on a IDESourceEditorView 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)[$rdi class]) containsString:@"IDESourceEditorView"]' -G0

This command modifies all existing breakpoints in your debugging session and creates a condition which gets evaluated everytime -[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 IDESourceEditorView. The final -G0 says to modify the breakpoint to not automatically resume execution after the action has been performed.

After modifying your breakpoint above, click on the code area in Xcode. 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:

IDESourceEditorView: Frame: (0.0, 0.0, 1109.0, 462.0), Bounds: (0.0, 0.0, 1109.0, 462.0) contentViewOffset: 0.0

This is printing out the object’s description. You’ll notice that there is no pointer reference within this, because Swift hides the pointer reference. There’s several ways to get around this if you need the pointer reference. The easiest is to use print formatting. Type the following in LLDB:

(lldb) p/x $rdi

You’ll get something similar to the following:

(unsigned long) $3 = 0x0000000110a42600

Since RDI 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 RDI 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 RDI is pointing to the correct reference, your code 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 [$rdi string]

This will dump out the contents of your source code 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 comands!

When you get bored of playing with the NSView APIs on this instance, copy the address down that RDI 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 $rdi 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 RDI 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 console’s source code 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. 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 IDESourceEditorView instance — but this time using Swift!

Now, add some text to your source code via LLDB:

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

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. The Swift ABI must first stabilize. Only then, can the Swift tooling really become rock solid.

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!

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.