Home iOS & Swift Books Advanced Apple Debugging & Reverse Engineering

DTrace vs. objc_msgSend Written by Derek Selander

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

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

You’ve seen how powerful DTrace is against Objective-C and Swift code which you have the source for, or code that resides in a Framework like UIKit. You’ve used DTrace to trace this code and make interesting tweaks all while performing zero modifications to already compiled source code.

Unfortunately, when DTrace is put up against a stripped executable, it is unable to create any probes to dynamically inspect those functions.

However, when exploring Apple code, you still have one very powerful ally on your side: objc_msgSend. In this chapter you’ll use DTrace to hook objc_msgSend’s entry probe and pull out the class name along with the Objective-C selector for that class.

By the end of this chapter, you’ll have LLDB generating a DTrace script which only generates tracing info for code implemented within the main executable that calls objc_msgSend.

Building your proof-of-concept

Included in the starter folder is an app called VCTransitions, which is a very basic Objective-C/Swift application that showcases a normal UINavigationController push transition, as well as a custom push transition.

Open up this Xcode project, build and run on the iPhone XS Simulator and take a quick look around.

It’s important to note, there are two schemes inside this application: VCTransitions and Stripped VCTransitions. Make sure to select the VCTransitions scheme when running. We’ll talk more about the Stripped VCTransitions scheme in a second.

Note: Normally I don’t care about the exact version of the software you’re running, so long as it’s iOS 12. This time, however, I insist you run iOS 12.1.x (or earlier) since you’ll be viewing assembly that could change in a future release. You’ll be exploring some assembly in this chapter, and I can’t guarantee it’s unchanged in a new iOS version that I’ve not viewed (at the time of writing).

There are buttons to perform the two navigation pushes, and there’s also a button named Execute Methods that will loop through all known Objective-C methods which are implemented/overriden by a given Class. If the method takes no parameters, it executes it.

For example, the first view controller displayed is ObjCViewController. If you tap Execute Methods, it will call anEmptyMethod as well as all the getters for the overridden properties, since all of those methods don’t require parameters.

Now, onto the fun stuff.

Jump over to OjbCViewController.m and take a look at the IBAction methods implemented by this class. Make a DTrace one-liner in Terminal to ensure that you can see these methods getting hit.

Make sure the Simulator is alive and running the VCTransitions project.

In Terminal:

sudo dtrace -n 'objc$target:ObjCViewController::entry' -p `pgrep VCTransitions`

Press Enter to start this bad boy up. Enter your password when DTrace asks you then head back over to the Simulator and start tapping on buttons.

You’ll see the Terminal DTrace window fill up with the IBAction methods implemented by ObjCViewController.

Now, tap one of the push buttons so you’re on the SwiftViewController view controller.

Although this is a subclass of UIViewController, tapping on the IBActions will not produce any results for the objcPID probe. Even though there are dynamic methods implemented or overridden by SwiftViewController, and being executed through objc_msgSend, the actual code is Swift code (even those @objc bridging methods).

Pop quiz: If SwiftViewController contains the following code:

class SwiftViewController: UIViewController, UIViewControllerTransitioningDelegate {
  @objc var coolViewDTraceTest: UIView? = nil
  @objc var coolBooleanDTraceTest: Bool = false

  // ...

Will an Objective-C DTrace probe pick up coolBooleanDTraceTest or coolViewDTraceTest?

To answer this, first see if these Swift properties are even exposed as Objetive-C probes. They should be, right? They have the @objc attributes.

Type the following in Terminal:

sudo dtrace -ln 'objc$target::*cool*Test*:entry' -p `pgrep VCTransitions`

Dang, only the properties for the Objective-C ObjCViewController are displayed and not SwiftViewControllers! This is because of Swift proposition 160 https://github.com/apple/swift-evolution/blob/master/proposals/0160-objc-inference.md, which includes a proposition that NSObject’s no longer infer @objc. In addition, Swift will not create an Objective-C symbol even for dynamic code.

This means you’ll have to use the non-Objective-C provider to query Swift DTrace probes.

You can confirm this by augmenting your DTrace script to dump any methods that include the word cool followed sometime later by the word Test, like so:

sudo dtrace -n 'pid$target::*cool*Test*:entry' -p `pgrep VCTransitions` 

This is another reason to go after objc_msgSend instead of the objc$target probe, because calls to objc_msgSend will catch dynamically executed Swift code, where objc$target will miss them.

Repeating your steps on a stripped build

Included within the project is a scheme called Stripped VCTransitions.

(lldb) lookup SwiftViewController
(lldb) lookup ObjCViewController
(lldb) lookup VCTransitions


sudo dtrace -ln 'objc$target:ObjCViewController::' -p `pgrep VCTransitions`
   ID   PROVIDER            MODULE                          FUNCTION NAME
dtrace: failed to match objc57009:ObjCViewController:: No probe matches description

How to get around no probes in a stripped binary

So how can you architect a DTrace action and/or probe to get around this hurdle of not being able to inspect a stripped binary?

objc_msgSend(instance_or_class, SEL,  ...);
UIViewController *vc = [UIViewController new];
[vc setTitle:@"yay, DTrace"];
vc = objc_msgSend(UIViewControllerClassRef, "new");
objc_msgSend(vc, "setTitle:", @"yay, DTrace");

Researching method calls using… DTrace!

Let’s see if there are any documented ways to go after this thing. In the objc/runtime.h header, you have the following declaration:

struct objc_class {

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;

/* Use `Class` instead of `struct objc_class *` */
po *(char *)(X + 0x10)
 * Returns the name of a class.
 * @param cls A class object.
 * @return The name of the class, or the empty string if \e cls is \c Nil.
OBJC_EXPORT const char *class_getName(Class cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
(lldb) p/x [UIView class]
(Class) $0 = 0x0000000109d4ce60 UIView
(lldb) po class_getName(0x0000000109d4ce60)
(lldb) po (char *)class_getName(0x0000000109d4ce60)
sudo dtrace -n 'pid$target:::entry' -p `pgrep VCTransitions`
(lldb) po (char *)class_getName(0x0000000109d4ce60)
:~ sudo dtrace -n 'pid$target:::entry' -p `pgrep VCTransitions`
dtrace: description 'pid$target:::entry' matched 901911 probes
CPU     ID                    FUNCTION:NAME
  6 1405417              class_getName:entry 
  6 1405416 objc_class::demangledName(bool):entry 
  6 566986        _NSPrintForDebugger:entry 
  6 1405847               objc_msgSend:entry 
(lldb) b objc_class::demangledName(bool)
(lldb) exp -i0 -O -- class_getName([UIView class])

Scary assembly, part I

As always, this stuff looks scary at first. But when you systematically go through it, it’s not that bad. You’ll actually break the assembly function into chunks to explore. The first chunk will be between offset 0-55.

(lldb) po $rdi
(lldb) po $rsi

*(uint64_t *)(X + 0x20)
*(uint64_t *)(X + 0x20) & 0x7ffffffffff8
*(uint64_t *)((*(uint64_t *)(X + 0x20) & 0x7ffffffffff8) + 0x38)
(char *)*(uint64_t *)((*(uint64_t *)(X + 0x20) & 0x7ffffffffff8) + 0x38)
(char *)*(uint64_t *)((*(uint64_t *)((*(uint64_t *)Instance_of_X) + 0x20) & 0x7ffffffffff8) + 0x38)
(lldb) p/x [UIView class]
(Class) $1 = 0x000000010c09ce60 UIView
(lldb) x/gx '0x000000010c09ce60 + 0x20'
0x10c09ce80: 0x0000608000064b80
(lldb) p/x 0x7ffffffffff8 & 0x0000608000064b80
(lldb) x/gx '0x0000608000064b80 + 0x38'
0x608000064bb8: 0x000000010bce319f
(lldb) po (char *)0x000000010bce319f

command regex getcls 's/(.+)/expression -lobjc -O -- (char *)*(uint64_t *)((*(uint64_t *)((*(uint64_t *)%1) + 0x20) & 0x7ffffffffff8) + 0x38)/'
(lldb) getcls [UIView new]
(lldb) getcls [UIAlertController new]
(lldb) po [UIAlertController class]
(lldb) getcls [UIAlertController new]

Scary assembly, part II

It’s time to revisit the second part of interest in the objc_class::demangledName(bool) C++ function. This assembly chunk will focus on what the logic does if the initial location for that char* is not in the initial location of interest — that is, if the class isn’t loaded yet.

Converting research into code

You’ve done the necessary research to figure out how to traverse memory to get the character array representation of a class. Time to implement this thing.

cat ./msgsendsnoop.d
#!/usr/sbin/dtrace -s
#pragma D option quiet  

    printf("Starting... Hit Ctrl-C to end.\n");

  this->selector = copyinstr(arg1);
  printf("0x%016p, +|-[%s %s]\n", arg0, "__TODO__",
0x00000000deadbeef, +|-[__TODO__ initWithFrame:]
sudo ./msgsendsnoop.d -p `pgrep VCTransitions`

  /* 1 */
  this->selector = copyinstr(arg1); 
  /* 2 */
  size = sizeof(uintptr_t);  
  /* 3 */
  this->isa = *((uintptr_t *)copyin(arg0, size));

  /* 4 */
  this->rax = *((uintptr_t *)copyin((this->isa + 0x20), size)); 
  this->rax =  (this->rax & 0x7ffffffffff8); 

  /* 5 */
  this->rbx = *((uintptr_t *)copyin((this->rax + 0x38), size)); 
  this->rax = *((uintptr_t *)copyin((this->rax + 0x8),  size));  

  /* 6 */
  this->rax = *((uintptr_t *)copyin((this->rax + 0x18), size));  
  /* 7 */
  this->classname = copyinstr(this->rbx != 0 ? 
                               this->rbx  : this->rax);   
  printf("0x%016p +|-[%s %s]\n", arg0, this->classname, 
sudo ./msgsendsnoop.d -p `pgrep VCTransitions`
sudo ./msgsendsnoop.d -p `pgrep VCTransitions` | grep invalid
pid$target::objc_msgSend:entry / arg0 > 0x100000000 /
(lldb) image dump sections VCTransitions

Removing noise

To be honest, I couldn’t care less about tracing memory-management code the compiler has generated. This means anything with retain or release needs to get outta here.

  this->selector = copyinstr(arg1);

/* old code below */
pid$target::objc_msgSend:entry / arg0 > 0x100000000 /
pid$target::objc_msgSend:entry / arg0 > 0x100000000 / && 
                    this->selector != "retain" && 
                    this->selector != "release" /                              
  this->selector = copyinstr(arg1); 

pid$target::objc_msgSend:entry / arg0 > 0x100000000 && 
                    this->selector != "retain" && 
                    this->selector != "release" /                              
  size = sizeof(uintptr_t);  
  this->isa = *((uintptr_t *)copyin(arg0, size));

  this->rax = *((uintptr_t *)copyin((this->isa + 0x20), size)); 
  this->rax =  (this->rax & 0x7ffffffffff8); 
  this->rbx = *((uintptr_t *)copyin((this->rax + 0x38), size)); 
  this->rax = *((uintptr_t *)copyin((this->rax + 0x8),  size));  
  this->rax = *((uintptr_t *)copyin((this->rax + 0x18), size));  
  this->classname = copyinstr(this->rbx != 0 ? 
                               this->rbx  : this->rax);   
  printf("0x%016p +|-[%s %s]\n", arg0, this->classname, 
sudo ./msgsendsnoop.d -p `pgrep VCTransitions`

Limiting scope with LLDB

Included within the starter folder is a LLDB Python script that creates a DTrace script and runs it with the exact logic you’ve just implemented.

command script import ~/lldb/snoopie.py
(lldb) p/x (void *)NSClassFromString(@"ObjCViewController")
(void *) $0 = 0x000000010db34080
(lldb) image lookup -a 0x000000010db34080
Address: VCTransitions[0x0000000100012080] (VCTransitions.__DATA.__objc_data + 40)
Summary: (void *)0x000000010db34058
(lldb) script path = lldb.target.executable.fullpath
(lldb) script path
(lldb) script print lldb.target.module[path]
(lldb) script print lldb.target.module[path].section[0]
[0x0000000000000000-0x0000000100000000) VCTransitions.__PAGEZERO
(lldb) script print lldb.target.module[path].section['__PAGEZERO']
(lldb) script print lldb.target.module[path].section['__DATA']
(lldb) script section = lldb.target.module[path].section['__DATA']
(lldb) script section.GetLoadAddress(lldb.target)
(lldb) script section.size

Fixing up the snoopie script

As indicated, this snoopie.py script works as-is, so you’re just going to add some small logic to the predicate to filter only instances.

target = debugger.GetSelectedTarget()
path = target.executable.fullpath
section = target.module[path].section['__DATA']
start_address = section.GetLoadAddress(target)
end_address = start_address + section.size

dataSectionFilter = '''{} <= *((uintptr_t *)copyin(arg0, 
    sizeof(uintptr_t))) && 
   *((uintptr_t *)copyin(arg0, sizeof(uintptr_t))) <= {}
'''.format(start_address, end_address)
(lldb) snoopie

Where to go from here?

You’ve got some homework to do on your end. This script will not play nicely with Objective-C categories. For example, there could be a class that’s implemented within a different module, which has an Objective-C category implemented within the main executable. You’ll need to figure out some creative way to check if the Objective-C selector in objc_msgSend was implemented within the main executable or not.

this->isMeta = ... // logic here
this->isMetaChar = this->isMeta ? '+' : '-'

printf("0x%016p %c[%s %s]\n", arg0, this->isMetaChar, 

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

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 raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.