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

25. Script Bridging with SBValue & Memory
Written by Derek Selander

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

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. Nobody likes stringly typed things!

So, it’s time to talk about a new 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 which 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, an error is spat out.

Build and run the application on any iOS 12 Simulator. 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 *(id *)(0x600000031f80)
DSObjectiveCObject
(lldb) x/gx 0x600000031f80
0x600000031f80: 0x0000000108b06568
(lldb) po 0x0000000108b06568
(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 ABI is still fluctuating. This means the information below could change before the Swift ABI completes. The day a new version of Xcode breaks the information in the following section, feel free to complain in ALL CAPS in the forums!

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 _StringCore {
    uintptr_t _object;    // packed bits for string type
    uintptr_t rawBits;    // raw data
  } firstName;

  struct _StringCore {
    uintptr_t _object;    // packed bits for string type
    uintptr_t rawBits;    // raw data
  } lastName;
}
// ## _StringObject bit layout
//
// x86-64 and arm64: (one 64-bit word)
// +---+---+---|---+------+------------------------------------------+
// + t | v | o | w | uuuu | payload (56 bits)                        |
// +---+---+---|---+------+------------------------------------------+
// most significant bit                          least significatn bit
//
// where t: is-a-value, i.e. a tag bit that says not to perform ARC
//       v: sub-variant bit, i.e. set for isCocoa or isSmall
//       o: is-opaque, i.e. opaque vs contiguously stored strings
//       w: width indicator bit (0: ASCII, 1: UTF-16)
//       u: unused bits
//
// payload is:
//   isNative: the native StringStorage object
//   isCocoa: the Cocoa object
//   isOpaque & !isCocoa: the _OpaqueString object
//   isUnmanaged: the pointer to code units
//   isSmall: opaque bits used for inline storage // TODO: use them!
//

<Allocator.ASwiftClass: 0x61800009d830>
(lldb) po 0x61800009d830
<Allocator.ASwiftClass: 0x61800009d830>
(lldb) po [0x61800009d830 superclass]
SwiftObject
(lldb) po *(id *)0x61800009d830
(lldb) po *(id *)(0x61800009d830 + 0x8)
0x0000000000000002
(lldb) po [0x61800009d830 retain]
(lldb) po *(id *)(0x61800009d830 + 0x8)
0x0000000200000002
(lldb) po [0x61800009d830 release]
(lldb) po *(id *)(0x61800009d830 + 0x8)
0x0000000000000002
(lldb) po *(id *)(0x61800009d830 + 0x10)
UIExtendedSRGBColorSpace 0.6 0.4 0.2 1
(lldb) x/gt '0x61800009d830 + 0x18'
0x600003c3aa18: 0b1110010100000000000000000000000000000000000000000000000000000000
typedef struct {
  char spillover[7];
  char bits; // msb (tvow) bit types, lsb (uuuu) string length
  char start[8]; // start address of String
} SmallUTF8String;
(lldb) x/s '0x61800009d830 + 0x20'
"Derek"
(lldb) x/gx '0x61800009d830 + 0x18'
0x61800009d848: 0xe500000000000000
p/d *(int *)(0x61800009d830 + 0x18 + 7) & 0xf 

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 *) $0 = 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) $3 = 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                          `2...`..
(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

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