27.
SB Examples, Resymbolicating a Stripped ObjC Binary
Written by Derek Selander
This will be a novel example of what you can do with some knowledge of the Objective-C runtime mixed in with knowledge of the lldb Python module.
When LLDB comes up against a stripped executable (an executable devoid of DWARF debugging information), LLDB won’t have the symbol information to give you the stack trace.
Instead, LLDB will generate a synthetic name for a method it recognizes as a method, but doesn’t know what to call it.
Here’s an example of a synthetic method created by LLDB on a fun-to-explore process…
___lldb_unnamed_symbol906$$SpringBoard
One strategy to reverse engineer the name of this method is to create a breakpoint on it and explore the registers right at the start of the method.
Using your assembly knowledge of the Objective-C runtime, you know the RSI register (x64) or the X1 register (ARM64) will contain the Objective-C Selector that holds the name of method. In addition, you also have the RDI (x64) or X0 (ARM64) register which holds the reference to the instance (or class).
However, as soon as you leave the function prologue, you have no guarantee that either of these registers will contain the values of interest, as they will likely be overwritten. What if a stripped method of interest calls another function? The registers you care about are now lost, as they’re set for the parameters for this new function. You need a way to resymbolicate a stack trace without having to rely upon these registers.
In this chapter, you’ll build an LLDB script that will resymbolicate stripped Objective-C functions in a stack trace.
When you called bt for this process, LLDB didn’t have the function names for the highlighted methods. You will build a new command named sbt that will look for stripped functions and try to resymbolicate them using the Objective-C runtime. By the end of the chapter, your sbt command will produce this:
Those once stripped-out Objective-C function calls are now resymbolicated. As with any of these scripts, you can run this new sbt script on any Objective-C executable provided LLDB can attach to it.
So how are you doing this, exactly?
Let’s first discuss how one can go about resymbolicating Objective-C code in a stripped binary with the Objective-C runtime.
The Objective-C runtime can list all classes from a particular image (an image being the main executable, a dynamic library, an NSBundle, etc.) provided you have the full path to the image. This can be accomplished through the objc_copyClassNamesForImage API.
From there, you can get a list of all classes returned by objc_copyClassNamesForImage where you can dump all class and instance methods for a particular class using the class_copyMethodList API.
Therefore, you can grab all the method addresses and compare them to the addresses of the stack trace. If the stack trace’s function can’t generate a default function name (such as if the SBSymbol is synthetically generated by LLDB), then you can assume LLDB has no debug info for this address.
Using the lldb Python module, you can get the starting address for a particular function — even when a function’s execution is partially complete. This is accomplished using SBValue’s reference to an SBAddress. From there, you can compare the addresses of all the Objective-C methods you’ve obtained to the starting address of the synthetic SBSymbol. If two addresses match, then you can swap out the stripped (synthetic) method name and replace it with the function name that was obtained with the Objective-C runtime.
Don’t worry: You’ll explore this systematically using LLDB’s script command before you go building this Python script.
50 Shades of Ray
Included in the starter directory is an application called 50 Shades of Ray. A well-chosen name (in my humble opinion) for a project that showcases the many faces of Ray Wenderlich. There’s gentle Ray, there’s superhero Ray, there’s confused Ray, there’s even goat BFF Ray!
When tapping the UIButton at the bottom, a randomly generated picture of Ray pops up in a UIView of random size.
Wow, that will make billions on the App Store!
Open the 50 Shades of Ray project and build and run the app. In the Xcode project, there are two schemes. Make sure you select the 50 Shades of Ray scheme and not the Stripped scheme. You’ll use that scheme later.
Once you’ve gotten your enjoyment out of generating random pictures of Ray, click on the ObjC UIBarButtonItem in the upper right hand corner.
This UIBarButtonItem is tied to an IBAction that will print out all the methods implemented by the main executable and displays them to stderr in your console. In fact, you can see the name of the method that triggered the console output within the console output!
Scan the console for the method -[ViewController dumpObjCMethodsTapped:]. It’s this method which dumped all the Objective-C methods in the main executable.
Preceding the function is a number (in my case, 4449531728), which holds the starting address for this Objective-C method.
Don’t believe me? Pause execution and type the following into LLDB:
(lldb) image lookup -a 4449531728
You address will be different. This is hunting down the location of the address 4483016672 in memory and seeing where it relates in reference to your project.
Address: 50 Shades of Ray[0x00000001000017e0] (50 Shades of Ray.__TEXT.__text + 624)
Summary: 50 Shades of Ray`-[ViewController dumpObjCMethodsTapped:] at ViewController.m:36
Groovy. This is telling us the location in memory 4449531728 is what was loaded from -[ViewController dumpObjCMethodsTapped:]. Let’s look at the code in this method.
Head on in to ViewController.m and hunt for the dumpObjCMethodsTapped:
The exact details don’t need to be covered too closely, but it’s worth pointing out the following:
- All the Objective-C classes implemented in the main executable are enumerated through
objc_copyClassNamesForImage. - For each class, there’s logic to grab all the class and instance methods.
- In order to grab the class methods for a particular Objective-C Class, you must get the meta class. No, that term was not made up by some hipster developer in tight jeans, plaid shirt & beard. The meta class is the class responsible for the static methods of a particular class. For example, all methods that begin with + are implemented by the meta Class and not the Class.
- All the methods are aggregated into a
NSMutableDictionary, where the key for each of these methods is the location in memory where the function resides.
Using script to guide your way
Time to use the script LLDB command to explore the lldb module APIs and build a quick POC to see how you’re going to tackle finding the starting address of a function in memory.
In the LLDB console, set a breakpoint on NSLog:
(lldb) b NSLog
You’ll get multiple SBBreakpointLocation hits. That’s fine. Now continue running the application.
Tap on the ObjC UIBarButtonItem in the upper right corner of the Simulator.
Execution will stop right before content is spat out to stderr.
Using the global variable lldb.frame, dig into what APIs are available to you to grab the starting address of the NSLog function.
Start with the global variable and build from there.
(lldb) script print lldb.frame
You’ll get the __str__() representation of the SBFrame. Nothing new.
frame #0: 0x000000010b472390 Foundation`NSLog
If you decided to use gdocumentation to search documentation for SBFrame (from Chapter 23, “Script Bridging Classes and Hierarchy,” you’ll see SBFrame has a few potential candidates for getting the start address of a function.
pc looks interesting to grab the RIP regster (x64) or the PC (ARM64), but that will only work at the start of a function. You need to grab the starting address from any offset inside the SBFrame.
Unfortunately, there are no APIs you can use in the SBFrame to get the starting address from any instruction offset within the function. You’ll need to turn your attention to other classes referenced by the SBFrame to get what you need.
Grab the SBSymbol reference for the SBFrame:
(lldb) script print lldb.frame.symbol
The SBSymbol is responsible for the implementation offset address of NSLog. That is, the SBSymbol will tell you where this function is implemented in a module; it doesn’t hold the actual address of where the NSLog was loaded into memory.
However, you can use the SBAddress property along with the GetLoadAddress API of SBAddress to find where the start location of NSLog is in your current process.
(lldb) script print lldb.frame.symbol.addr.GetLoadAddress(lldb.target)
You’ll get a number in decimal. I got 4484178832. Convert it to hex using LLDB and compare the output to the start address of NSLog:
(lldb) p/x 4484178832
I got 0x000000010b472390 as my hexadecimal representation.
Compare your output with the starting address of NSLog to see if they match.
Woot! A match! That’s your path to resymbolication redemption.
lldb.value with NSDictionary
Since you’re already here, you can explore one more thing. How are you going to parse this NSDictionary with all these addresses?
You’ll copy the code, almost verbatim, that generates all the methods and apply it to an EvaluateExpression API to get an SBValue.
You should still be paused at the beginning of NSLog. Jump to the calling frame, -[ViewController dumpObjCMethodsTapped:].
(lldb) f 1
This will get to the previous frame, dumpObjCMethodsTapped:. You now have access to all variables within this method, including the retdict that’s responsible for dumping out all the methods implemented within the main executable.
Grab the SBValue interpretation of the retdict reference.
(lldb) script print lldb.frame.FindVariable('retdict')
This will print the SBValue for retdict:
(__NSDictionaryM *) retdict = 0x000060800024ce10 10 key/value pairs
Since this an NSDictionary, you actually want to dereference this value so you can enumerate it.
(lldb) script print lldb.frame.FindVariable('retdict').deref
You’ll get some more relevant output (which is truncated):
(__NSDictionaryM) *retdict = {
[0] = {
key = 0x000060800002bb80 @"4411948768"
value = 0x000060800024c660 @"-[AppDelegate window]"
}
[1] = {
key = 0x000060800002c1e0 @"4411948592"
value = 0x000060800024dd10 @"-[ViewController toolBar]"
}
[2] = {
key = 0x000060800002bc00 @"4411948800"
value = 0x000060800024c7e0 @"-[AppDelegate setWindow:]"
}
[3] = {
key = 0x000060800002bba0 @"4411948864"
value = 0x000060800004afe0 @"-[AppDelegate .cxx_destruct]"
}
It’s this you want to start with, since this prints out all the values for the keys.
Make a lldb.value out of this SBValue and assign it to a variable a.
(lldb) script a = lldb.value(lldb.frame.FindVariable('retdict').deref)
This is one of those times where I would prefer to work with an lldb.value over an SBValue. From here, you can easily explore the values within this NSDictionary.
Print the first value within this lldb.value NSDictionary.
(lldb) script print a[0]
From there, you can have either the key or value that you can print out.
Print out the key first:
(lldb) script print a[0].key
You’ll get something similar to the following:
(__NSCFString *) key = 0x000060800002bb80 @"4411948768"
Print the value:
(lldb) script print a[0].value
This will print something similar to the following:
(__NSCFString *) value = 0x000060800024c660 @"-[AppDelegate window]"
If you only want the return value without the referencing address, you’ll need to cast this lldb.value back into a SBValue then grab the description.
(lldb) script print a[0].value.sbvalue.description
This will get you the desired -[AppDelegate window] for output. Note you may have a different method.
If you wanted to dump all keys in this lldb.value a instance, you can use Python List comprehensions to dump all the keys out.
(lldb) script print '\n'.join([x.key.sbvalue.description for x in a])
You’ll get output similar to the following:
4411948768
4411948592
4411948800
4411948864
4411948656
4411948720
4411949072
4411946944
4411946352
4411946976
Same approach for values:
(lldb) script print '\n'.join([x.value.sbvalue.description for x in a])
You now know how to parse this NSDictionary if, hypothetically, it were to be placed in some JIT code…
The plan is to copy the code from the dumpObjCMethodsTapped: into the Python script, and have it execute as JIT code. From there, you’ll use the same procedure to parse it out from the NSDictionary.
Sounds good? Get your gameplan ready and head on in to the next section!
The “stripped” 50 Shades of Ray
Yeah, that title got your attention, didn’t it?
Within the Xcode schemes of the 50 Shades of Ray executable, there is a scheme named Stripped 50 Shades of Ray.
Stop the execution of the current process (⌘ + .) and select the Stripped 50 Shades of Ray Xcode scheme.
This scheme will build a debug executable, but remove the debugging information that you have become accustomed to in your day-to-day development cycles.
Build and run the executable. Included within this project is a shared symbolic breakpoint. Enable this breakpoint.
There’s no need to modify this symbolic breakpoint, but it’s worth noting what this breakpoint will do.
This breakpoint will stop on -[UIView initWithFrame:] and has a condition to only stop if the UIView is of type RayView, a subclass of UIView. This RayView is responsible for displaying the lovely images of Ray Wenderlich within the application.
Tap the Generate a Ray! button. Execution will stop on -[UIView initWithFrame:] method.
Take a look at the stack trace.
There’s something interesting about stack frame 1 & 3: There’s no debug information in there. LLDB has defaulted to generating a synthetic function name for those methods.
Confirm this in LLDB.
In LLDB, make sure you are in the starting frame (initWithFrame:):
(lldb) f 0
Use script to see if it’s synthetic or not:
(lldb) script lldb.frame.symbol.synthetic
You’ll get False. Makes sense, because you know this is initWithFrame:. Jump to one of the synthetic frames:
(lldb) f 1
Execute the previous script logic:
(lldb) script lldb.frame.symbol.synthetic
You’ll get True this time.
This is enough research to get you going with the Python script.
Building sbt.py
Included within the starter folder is a Python script named sbt.py.
Stick this script into your ~/lldb directory. Provided you’ve installed the lldbinit.py script, this will load all the Python files into the LLDB directory.
If you didn’t follow along in Chapter 26, “SB Examples, Improved Lookup”, you can manually install the sbt.py by modifying your ~/.lldbinit file.
Once you’ve placed the sbt.py file into the ~/lldb directory, reload your commands in ~/.lldbinit using the reload_script you created in Chapter 23, “Script Bridging Classes and Hierarchy”.
Check and see if LLDB correctly recognizes the sbt command:
(lldb) help sbt
You’ll get some help text if LLDB recognizes the command. This will be the starting point for the sbt command.
Open this file up and jump down to generateExecutableMethodsScript. There’s something interesting here that’s worth pointing out.
Do you remember in the previous chapter, how I mentioned lldb.value is slooooooooooooooooow? If you’re exploring a huge executable with lots of methods, the amount of time it takes for Python to go through every value in an NSDictionary takes forever.
Instead, you don’t need to grab every reference to every single function in your NSDictionary. You only need to grab the locations of the start of each function in the stack trace.
def generateExecutableMethodsScript(frame_addresses):
frame_addr_str = 'NSArray *ar = @['
for f in frame_addresses:
frame_addr_str += '@"' + str(f) + '",'
frame_addr_str = frame_addr_str[:-1]
frame_addr_str += '];'
# #############################
# Truncated content...
# #############################
command_script += frame_addr_str
command_script += r'''
NSMutableDictionary *stackDict = [NSMutableDictionary dictionary];
[retdict keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL *stop) {
if ([ar containsObject:key]) {
[stackDict setObject:obj forKey:key];
return YES;
}
return NO;
}];
stackDict;
'''
return command_script
This is a pretty sweet optimization, because instead of evaluating potentially thousands (if not tens of thousands) of Objective-C methods, you’ll only need to evaluate less than 20 keys or so in an NSDictionary, or whatever amount of synthetic functions are in the stack frame.
With the symbolic breakpoint still active and program stopped, give the script a run.
Just a normal stack frame will be printed out that doesn’t have logic to resymbolicate the symbols.
It’s time to make a few modifications to fix that.
Implementing the code
The JIT code is already set up. All you need to do is just call it, then compare the return NSDictionary against any synthetic SBValues.
Inside processStackTraceStringFromAddresses, search for the following comments:
# New content start 1
# New content end 1
Stick your new code here to call the JIT code to generate a list of potential methods in a NSDictionary:
# New content start 1
methods = target.EvaluateExpression(script, generateOptions())
methodsVal = lldb.value(methods.deref)
# New content end 1
You’ve called the code that returns the NSDictionary representation and assigned it to the SBValue instance variable methods.
You can cast the SBValue into a lldb.value (technically it’s just a value, but you might get confused if I don’t have the module in there) and assign it to the variable methodsVal.
Now for the final part of Python code. All you need to do is determine if a SBFrame’s SBSymbol is synthetic or not and perform the appropriate logic.
Search the following commented out code further down in processStackTraceStringFromAddresses:
# New content start 2
name = symbol.name
# New content end 2
Change this to look like the following:
# New content start 2
if symbol.synthetic: # 1
children = methodsVal.sbvalue.GetNumChildren() # 2
name = symbol.name + r' ... unresolved womp womp' # 3
loadAddr = symbol.addr.GetLoadAddress(target) # 4
for i in range(children):
key = long(methodsVal[i].key.sbvalue.description) # 5
if key == loadAddr:
name = methodsVal[i].value.sbvalue.description # 6
break
else:
name = symbol.name # 7
# New content end 2
offset_str = ''
Breaking this down, you have the following:
- You’re enumerating the frames, which occur outside the scope of this code block. For each symbol, a check is performed to see if the symbol is
syntheticor not. If it is, the memory address will be compared to theNSDictionaryof addresses that were gathered. - This will grab the number of children in the
lldb.valuethat will be enumerated to see if there’s a match from the Objective-C list of classes. - Either way, a valid reference to the
namevariable needs to be produced for the display of the stack trace. You’re opting to say you know this is a synthetic function, but fail to resolve it if your upcoming logic fails to produce a result. - This gets the address in memory to the synthetic function in question.
- The
keyvalue given by thelldb.valueis internally made up from aNSNumber, so you need to grab thedescriptionof this method and cast it into a number. Confusingly, it’s assigned to a Python variable namedkeyas well. - If the
keyvariable is equal to theloadAddr, then you have a match. Assign thenamevariable to thedescriptionof the variable in theNSDictionary.
That should be it. Save your work and reload your LLDB contents using reload_script and give it a go.
Provided you are still in the Stripped 50 Shades of Ray scheme and are paused in the symbolic breakpoint that stops only in UIView’s initWithFrame: (with the special condition), run the sbt command in the debugger to see if the originally unavailable frames 1 & 3 can be read.
(lldb) sbt bt
frame #0: 0x1053fe694 UIKit`-[UIView initWithFrame:]
frame #1: 0x103cf53ac ShadesOfRay`-[RayView initWithFrame:] + 924
frame #2: 0x1053fdda2 UIKit`-[UIView init] + 62
frame #3: 0x103cf45bf ShadesOfRay`-[ViewController generateRayViewTapped:] + 79
Beautiful.
Where to go from here?
Congratulations! You’ve used the Objective-C runtime to successful resymbolicate a stripped binary! It’s crazy what you can do with the proper application of Objective-C.
There are still a few holes in this script. This script doesn’t play nice with Objective-C blocks. However, a careful study of how blocks are implemented as well as exploring the lldb Python module might reveal a way to indicate Objective-C block functions that have been stripped away.
In addition, this script will not work with an iOS executable in release mode. LLDB will not find the functions for a synthetic SBSymbol to reference the start address. This means that you would have to manually search upwards in the ARM64 assembly until you stumbled across an assembly instruction that looked like the start of a function (can you guess which instruction(s) to look for?).
If those script extensions don’t interest you, try your luck with figuring out how to resymbolicate a Swift executable. The challenge definitely goes up by an order of magnitude, but it’s still within the realm of possibility to do with LLDB. Have fun!