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

24. Script Bridging With SBValue & Memory
Written by Walter Tyree

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

So far, when evaluating JIT code (i.e. Objective-C, Swift, C, etc. code that’s executed through your Python script), you’ve used a small set of APIs to evaluate the code.

For example, you’ve used SBDebugger and SBCommandReturnObject’s HandleCommand method to evaluate code. SBDebugger’s HandleCommand goes straight to stderr, while you have a little more control over where the SBCommandReturnObject result ends up. Once evaluated, you had to manually parse the return output for anything of interest. This manual searching of the output from the JIT code is a bit unsightly and hinders you making anything usefully complex. Nobody likes stringly typed things!

So, it’s time to learn about another class in the lldb Python module, SBValue, and how it can simplify the parsing of JIT code output. Open up the Xcode project named Allocator in the starter folder for this chapter. This is a simple application that dynamically generates classes based upon input from a text field.

This is accomplished by taking the string from the text field and using it as an input to the NSClassFromString function. If a valid class is returned, it’s initialized using the plain old init method. Otherwise, it generates an error.

Build and run the application. You’ll make zero modifications to this project, yet you’ll explore object layouts in memory through SBValue, as well as manually with pointers through LLDB.

A Detour Down Memory Layout Lane

To truly appreciate the power of the SBValue class, you’re going to explore the memory layout of three unique objects within the Allocator application. You’ll start with an Objective-C class, then explore a Swift class with no superclass, then finally explore a Swift class that inherits from NSObject.

All three of these classes have three properties with the following order:

  • A UIColor called eyeColor.
  • A language specific string (String/NSString) called firstName.
  • A language specific string (String/NSString) called lastName.

Each instance of these classes is initialized with the same values. They are:

  • eyeColor will be UIColor.brown or [UIColor brownColor] depending on language.
  • firstName will be "Derek" or @"Derek" depending on language.
  • lastName will be "Selander" or @"Selander" depending on language.

Objective-C Memory Layout

You’ll explore the Objective-C class first, as it’s the foundation for how these objects are laid out in memory. Jump over to the DSObjectiveCObject.h and take a look at it. Here it is for your reference:

@interface DSObjectiveCObject : NSObject

@property (nonatomic, strong) UIColor *eyeColor;
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;

@end
@implementation DSObjectiveCObject

- (instancetype)init
{
  self = [super init];
  if (self) {
    self.eyeColor = [UIColor brownColor];
    self.firstName = @"Derek";
    self.lastName = @"Selander";
  }
  return self;
}
@end
struct DSObjectiveCObject {
  Class isa;
  UIColor *eyeColor;
  NSString *firstName
  NSString *lastName
}

(lldb) po 0x600000031f80
<DSObjectiveCObject: 0x600000031f80>
(lldb) po object_getClass(0x600000031f80)
DSObjectiveCObject
(lldb) x/gt 0x600000031f80
0x600000031f80: 0b0000000100000000000000000000000100000100101110111100111101010001
(lldb) po *(id *)(0x600000031f80 + 0x8)
UIExtendedSRGBColorSpace 0.6 0.4 0.2 1
(lldb) po sizeof(Class)
(lldb) po *(id *)(0x600000031f80 + 0x10)
(lldb) po *(id *)(0x600000031f80 + 0x18)

Swift Memory Layout With no Superclass

Note: It’s worth mentioning right up front: the Swift is still evolving. Though Swift achieved ABI stability in version 5 and module stability in version 5.1, things are still changing and evolving as Swift begins to support other CPUs and operating systems. So, if you find that something has changed, be sure to check the forums as we will all be adapting.

class ASwiftClass {
  let eyeColor = UIColor.brown
  let firstName = "Derek"
  let lastName = "Selander"

  required init() { }
}
struct ASwiftClass {
  Class isa;

  // Simplified, see "InlineRefCounts"
  // in https://github.com/apple/swift
  uintptr_t refCounts;

  UIColor *eyeColor;

  // Simplified, see "_StringGuts"
  // in https://github.com/apple/swift
  struct _StringObject {
    uintptr_t _countAndFlagBits; // packed bits for string type
    uintptr_t _object;           // raw data
  } firstName;

  struct _StringObject {
    uintptr_t _object;    // packed bits for string type
    uintptr_t rawBits;    // raw data
  } lastName;
}
┌───────────────┬─────────────────────────────┐
│ _countAndFlags                │ _object                                     │
├─┬─┬─┬─┬─┬─┬─┬─┼─┬─┬──┬──┬──┬──┬──┬──────────┤
│ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ 12 │ 13 │ 14 │     15     │
├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼──┼──┼──┼──┼──┼──────────┤
│ a │ b │ c │ d │ e │ f │ g │ h │ i │ j │ k  │ l  │ m  │ n  │ o  │ 1x10 count │
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴──┴──┴──┴──┴──┴──────────┘
┌──────┬──────┬──────┬──────┬──────┬──────────┬────────┐
│ b63  │ b62  │ b61  │ b60  │ b59  │  b58:48  │  b47:0 │
├──────┼──────┼──────┼──────┼──────┼──────────┼────────┤
│ ASCII│ NFC  │native│ tail │ UTF8 │ reserved │  count │
└──────┴──────┴──────┴──────┴──────┴──────────┴────────┘
else if let clsSwift = cls as? ASwiftClass.Type {
  let object = clsSwift.init()

exp -l objc -O -- [0x6000002ec400 retain]
exp -l objc -O -- [0x6000002ec400 release]

Swift Memory Layout With NSObject Superclass

Final one. You know the drill, so we’ll speed this one up a bit and skip the actual debugging session.

class ASwiftNSObjectClass: NSObject {
  let eyeColor = UIColor.brown
  let firstName = "Derek"
  let lastName = "Selander"

  required override init() { }
}
struct ASwiftNSObjectClass {
  Class isa;
  UIColor *eyeColor;

  struct _StringCore {
    uintptr_t _object;
    uintptr_t rawBits;
  } firstName;

  struct _StringCore {
    uintptr_t _object;
    uintptr_t rawBits;
  } lastName;
}

SBValue

Yay! Time to talk about this awesome class.

(lldb) po [DSObjectiveCObject new]
<DSObjectiveCObject: 0x61800002eec0>
(lldb) script lldb.frame.EvaluateExpression('[DSObjectiveCObject new]')
<lldb.SBValue; proxy of <Swig Object of type 'lldb::SBValue *' at 0x10ac78b10> >
(lldb) script print (lldb.target.EvaluateExpression('[DSObjectiveCObject new]'))
(DSObjectiveCObject *) $2 = 0x0000618000034280
(lldb) script a = lldb.target.EvaluateExpression('[DSObjectiveCObject new]')
(lldb) script print (a)
(DSObjectiveCObject *) $3 = 0x0000608000033260
(lldb) script print (a.description)
<DSObjectiveCObject: 0x608000033260>
(lldb) script print (a.value)
0x0000608000033260
(lldb) po 0x0000608000033260
<DSObjectiveCObject: 0x608000033260>
(lldb) script print (a.signed)
106102872289888
(lldb) p/x 106102872289888
(long) $5 = 0x0000608000033260

Exploring Properties Through SBValue Offsets

What about those properties stuffed inside that DSObjectiveCObject instance? Let’s explore those!

(lldb) script print (a.GetNumChildren())
(lldb) script print (a.GetChildAtIndex(0))
(NSObject) NSObject = {
  isa = DSObjectiveCObject
}
(lldb) script print (a.GetChildAtIndex(1))
(UICachedDeviceRGBColor *) _eyeColor = 0x0000608000070e00
(lldb) script print (a.GetChildAtIndex(2))
(__NSCFConstantString *) _firstName = 0x000000010db83368 @"Derek"
(lldb) script print (a.GetChildAtIndex(3))
(__NSCFConstantString *) _lastName = 0x000000010db83388 @"Selander"
(lldb) script print (a.GetChildAtIndex(2).description)
Derek
(lldb) script (a.size)
8
(lldb) script a.deref.size
(lldb) script print (a.type.name)
DSObjectiveCObject *
(lldb) script print (a.deref.type.name)
DSObjectiveCObject

Viewing Raw Data Through SBValue

You can even dump the raw data out with the data property in SBValue! This is represented by a class named SBData, which is yet another class you can check out on your own.

(lldb) script print (a.data)
60 32 03 00 80 60 00 00
(lldb) script print (a.value)

(lldb) script print (a.deref.data)
f0 54 b8 0d 01 00 00 00 00 0e 07 00 80 60 00 00  .T...........`..
68 33 b8 0d 01 00 00 00 88 33 b8 0d 01 00 00 00  h3.......3......

SBExpressionOptions

As mentioned when discussing the EvaluateExpression API, there’s an optional second parameter that will take an instance of type SBExpressionOptions. You can use this command to pass in specific options for the JIT execution.

(lldb) script options = lldb.SBExpressionOptions()
(lldb) script options.SetLanguage(lldb.eLanguageTypeSwift)
(lldb) script options.SetCoerceResultToId()
expression -lswift -O -- your_expression_here
(lldb) e -lswift -O -- ASwiftClass()
error: <EXPR>:3:1: error: use of unresolved identifier 'ASwiftClass'
ASwiftClass()
^~~~~~~~~~~
(lldb) e -lswift -- import Allocator
(lldb) e -lswift -O -- ASwiftClass()
(lldb) script b = lldb.target.EvaluateExpression('ASwiftClass()', options)

Referencing Variables by Name With SBValue

Referencing child SBValues via GetChildAtIndex from SBValue is a rather ho-hum way to navigate to an object in memory. What if the author of this class added a property before eyeColor that totally screwed up your offset logic when traversing this SBValue?

(lldb) script print (b.GetValueForExpressionPath('.firstName'))

lldb.value

One final cool thing you can do is create a Python reference that contains the SBValue’s properties as the Python object’s properties (wait… what?). Think of this as an object through which you can reference variables using Python properties instead of Strings.

(lldb) script c = lldb.value(b)
(lldb) script print (c.firstName)
(lldb) script print (c.firstName.sbvalue.signed)

Key Points

  • The isa variable’s unused bits are being repurposed for other things, so use object_getClass to find out what kind of object you’re exploring.
  • Use the x command in lldb or the View Memory debug workflow to see the actual bytes for an object.
  • Class objects in Swift and Objective-C offset properties by 0x8 or 0x10 depending on the type. Pointers are always of size 0x8.
  • Depending on its properties, a Swift String may be stored in the class directly, or somewhere else in memory. This can change at runtime as the string changes.
  • Swift classes that do not inherit from NSObject keep their own retain count.
  • SBValue abstracts different data into something with a common interface.
  • Use GetNumChildren() to explore the properties of the object SBValue represents.
  • Use the .data property of SBValue to get back to the raw bytes of an object.
  • Use SBExpressionOptions to set options that impact the EvalutateExpression command.
  • The lldb.value() creates a special Python object to help you avoid having to stringly type properties.

Where to Go From Here?

Holy cow… how dense was that chapter!? Fortunately you have come full circle. You can use the options provided by your custom command to dynamically generate your JIT script code. From the return value of your JIT code, you can write scripts that have custom logic based upon the return SBValue that is parsed through the EvaluateExpression APIs.

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now