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

26. SB Examples, Resymbolicating a Stripped ObjC Binary
Written by Walter Tyree

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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 a proper stack trace.

Instead, LLDB generates a synthetic name for any 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). Remember that you can use arg1 and arg2 as aliases for these registers, so you don’t have to worry about chip architecture.

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 for the same stack trace:

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 the 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!

(lldb) image lookup -a 4449531728
Address: 50 Shades of Ray[0x00000001000017e0] (50 Shades of Ray.__TEXT.__text + 624)
Summary: 50 Shades of Ray`-[ViewController dumpObjCMethodsTapped:] at ViewController.m:36

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.

(lldb) b NSLog
(lldb) script print (lldb.frame)
frame #0: 0x000000010b472390 Foundation`NSLog
(lldb) script print (lldb.frame.symbol)
(lldb) script print (lldb.frame.symbol.addr.GetLoadAddress(lldb.target))
(lldb) p/x 4484178832

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?

(lldb) f 1
(lldb) script print (lldb.frame.FindVariable('retdict'))
(__NSDictionaryM *) retdict = 0x000060800024ce10 10 key/value pairs
(lldb) script print (lldb.frame.FindVariable('retdict').deref)
(__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]"
  }
(lldb) script a = lldb.value(lldb.frame.FindVariable('retdict').deref)
(lldb) script (print a[0])
(lldb) script print (a[0].key)
(__NSCFString *) key = 0x000060800002bb80 @"4411948768"
(lldb) script print (a[0].value)
(__NSCFString *) value = 0x000060800024c660 @"-[AppDelegate window]"

(lldb) script print (a[0].value.sbvalue.description)
(lldb) script print ('\n'.join([x.key.sbvalue.description for x in a]))
4411948768
4411948592
4411948800
4411948864
4411948656
4411948720
4411949072
4411946944
4411946352
4411946976
(lldb) script print ('\n'.join([x.value.sbvalue.description for x in a]))

The “Stripped” 50 Shades of Ray

Yeah, that title got your attention, didn’t it?

(lldb) script lldb.frame.symbol.synthetic
(lldb) f 1
(lldb) script lldb.frame.symbol.synthetic

Building sbt.py

Included within the starter folder is a Python script named sbt.py.

(lldb) help sbt

Implementing the Code

The JIT code is already set up. All you need to do is call it, then compare the return NSDictionary against any synthetic SBValues.

    # New content start 1
    # New content end 1
    # New content start 1
    methods = target.EvaluateExpression(script,
                                        generateOptions())
    methodsVal = lldb.value(methods.deref)
    # New content end 1
    # New content start 2
    name = symbol.name
    # New content end 2
  # 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 = ''
(lldb) sbt
frame #0 : 0x11466a130 UIKitCore`-[UIView initWithFrame:]
frame #1 : 0x1042b1d78 ShadesOfRay`-[RayView initWithFrame:] + 556
frame #2 : 0x1042b12f8 ShadesOfRay`-[ViewController generateRayViewTapped:] + 72
frame #3 : 0x1141fb6c8 UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 96

Key Points

  • Using the EvaluateExpression command with SBTarget allows you to execute Swift or Objective-C from your Python script in the context of the running application.
  • The Objective-C runtime has access to all of the methods in an application whether the symbols are stripped or not.
  • When planning an LLDB script, use the interactive console to experiment with ideas.
  • Console logs are stored in DerivedData, if you want to keep them for reference, you need to move them somewhere safe.

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.

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.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now