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

17. Exploring & Method Swizzling Objective-C Frameworks
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.

In the previous two chapters, you’ve explored dynamic loading as well as how to use the dlopen and dlsym functions. So long as you knew the name of the function, it didn’t matter if the compiler tried to hide a function from you.

You’ll cap off this round of dynamic framework exploration by digging into Objective-C frameworks using the Objective-C runtime to hook and execute methods of interest.

For this chapter, you’ll go after a series of private UIKIt classes that help aid in visual debugging. The chief of these private classes, UIDebuggingInformationOverlay was introduced in iOS 9.0 and has received widespread attention in May 2017, thanks to @ryanipete’s article http://ryanipete.com/blog/ios/swift/objective-c/uidebugginginformationoverlay/ highlighting these classes and usage.

Unfortunately, as of iOS 11, Apple caught wind of developers accessing this class (likely through the popularity of the above article) and has added several checks to ensure that only internal apps that link to UIKIt have access to these private debugging classes. As of iOS 12, Apple has not added additional checks, but the logic has migrated from UIKIt to UIKItCore (likely) due to the upcoming macOS/iOS integration announced at WWDC 2018. In iOS 12, UIKIt doesn’t contain any code, but simply just links to several frameworks.

You’ll explore UIDebuggingInformationOverlay and learn why this class fails to work in iOS 11 and above, as well as explore avenues to get around these checks imposed by Apple by writing to specific areas in memory first through LLDB. Then, you’ll learn alternative tactics you can use to enable UIDebuggingInformationOverlay through Objective-C’s method swizzling.

I specifically require you to use iOS 12 for this chapter as Apple can impose new checks on these classes in the future that this chapter doesn’t cover.

Between iOS 10 and 12

In iOS 9 & 10, setting up and displaying the overlay was rather trivial. In both these iOS versions, the following LLDB commands were all that was needed:

(lldb) po [UIDebuggingInformationOverlay prepareDebuggingOverlay]
(lldb) po [[UIDebuggingInformationOverlay overlay] toggleVisibility]

This would produce the following overlay:

If you have an iOS 10 Simulator on your computer, I’d recommend you attach to any iOS process and try the above LLDB commands out so you know what is expected.

Unfortunately, some things changed in iOS 12. Executing the exact same LLDB commands in iOS 12 will produce nothing.

To understand what’s happening, you need to explore the overriden methods UIDebuggingInformationOverlay contains and wade into the assembly.

Use LLDB to attach to any iOS 12 Simulator process, this can be MobileSafari, SpringBoard, any of the apps you’ve explored in the previous chapters, or your own work. It doesn’t matter if it’s your own app or not, as you will be exploring assembly in the UIKitCore module.

For this example, I’ll launch the Photos application in the Simulator. Head on over to Terminal, then type the following:

(lldb) lldb -n MobileSlideShow

Once you’ve attached to any iOS Simulator process, use LLDB to search for any overriden methods by the UIDebuggingInformationOverlay class.

You can use the image lookup LLDB command:

(lldb) image lookup -rn UIDebuggingInformationOverlay

Or alternatively, use the methods command you created in Chapter 15, “Dynamic Frameworks”:

(lldb) methods UIDebuggingInformationOverlay

If you decided to skip that chapter, the following command would be equivalent:

(lldb) exp -lobjc -O -- [UIDebuggingInformationOverlay _shortMethodDescription]

Take note of the overridden init instance method found in the output of either command.

You’ll need to explore what this init is doing. You can follow along with LLDB’s disassemble command, but for visual clarity, I’ll use my own custom LLDB disassembler, dd, which outputs in color and is available here: https://github.com/DerekSelander/LLDB/blob/master/lldb_commands/disassemble.py.

Here’s the init method’s assembly in iOS 10. If you want to follow along in black & white in LLDB, type:

(lldb) disassemble -n "-[UIDebuggingInformationOverlay init]"

Again, this is showing the assembly of this method in iOS 10.

Colors (and dd’s comments marked in green) make reading x64 assembly soooooooooooo much easier.

In pseudo-Objective-C code, this translates to the following:

@implementation UIDebuggingInformationOverlay

- (instancetype)init {
  if (self = [super init]) {
    [self _setWindowControlsStatusBarOrientation:NO];
  }
  return self;
}

@end

Nice and simple for iOS 10. Let’s look at the same method for iOS 12:

This roughly translates to the following:


@implementation UIDebuggingInformationOverlay

- (instancetype)init {
  static BOOL overlayEnabled = NO;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    overlayEnabled = UIDebuggingOverlayIsEnabled();
  });
  if (!overlayEnabled) { 
    return nil;
  }

  if (self = [super init]) {
    [self _setWindowControlsStatusBarOrientation:NO];
  }
  return self;
}

@end

There are checks added in iOS 11 and above thanks to UIDebuggingOverlayIs Enabled() to return nil if this code is not an internal Apple device. You can verify these disappointing precautions yourself by typing the following in LLDB on a iOS 12 Simulator:

(lldb) po [UIDebuggingInformationOverlay new]

This is a shorthand way of alloc/init’ing an UIDebuggingInformationOverlay. You’ll get nil.

With LLDB, disassemble the first 10 lines of assembly for -[UIDebuggingInformationOverlay init]:

(lldb) disassemble -n "-[UIDebuggingInformationOverlay init]" -c10

Your assembly won’t be color coded, but this is a small enough chunk to understand what’s going on.

Your output will look similar to:

UIKit`-[UIDebuggingInformationOverlay init]:
  0x10d80023e <+0>:  push   rbp
  0x10d80023f <+1>:  mov    rbp, rsp
  0x10d800242 <+4>:  push   r14
  0x10d800244 <+6>:  push   rbx
  0x10d800245 <+7>:  sub    rsp, 0x10
  0x10d800249 <+11>: mov    rbx, rdi
  0x10d80024c <+14>: cmp    qword ptr [rip + 0x9fae84], -0x1 
    ; UIDebuggingOverlayIsEnabled.__overlayIsEnabled + 7

  0x10d800254 <+22>: jne    0x10d8002c0               ; <+130>
  0x10d800256 <+24>: cmp    byte ptr [rip + 0x9fae73], 0x0 
    ; mainHandler.onceToken + 7

  0x10d80025d <+31>: je     0x10d8002a8               ; <+106>

Pay close attention to offset 14 and 22:

  0x10d80024c <+14>: cmp    qword ptr [rip + 0x9fae84], -0x1 
    ; UIDebuggingOverlayIsEnabled.__overlayIsEnabled + 7

  0x10d800254 <+22>: jne    0x10d8002c0               ; <+130>

Thankfully, Apple includes the DWARF debugging information with their frameworks, so we can see what symbols they are using to access certain memory addresses.

Take note of the UIDebuggingOverlayIsEnabled.__overlayIsEnabled + 7 comment in the disassembly. I actually find it rather annoying that LLDB does this and would consider this a bug. Instead of correctly referencing a symbol in memory, LLDB will reference the previous value in its comments and add a + 7. The value at UIDebuggingOverlayIsEnabled.__overlayIsEnabled + 7 is what we want, but the comment is not helpful, because it has the name of the wrong symbol in its disassembly. This is why I often choose to use my dd command over LLDB’s, since I check for this off-by one error and replace it with my own comment.

But regardless of the incorrect name LLDB is choosing in its comments, this address is being compared to -1 (aka 0xffffffffffffffff in a 64-bit process) and jumps to a specific address if this address doesn’t contain -1. Oh… and now that we’re on the subject, dispatch_once_t variables start out as 0 (as they are likely static) and get set to -1 once a dispatch_once block completes (hint, hint).

Yes, this first check in memory is seeing if code should be executed in a dispatch_once block. You want the dispatch_once logic to be skipped, so you’ll set this value in memory to -1.

From the assembly above, you have two options to obtain the memory address of interest:

  1. You can combine the RIP instruction pointer with the offset to get the load address. In my assembly, I can see this address is located at [rip + 0x9fae84]. Remember, the RIP register will resolve to the next row of assembly since the program counter increments, then executes an instruction.

This means that [rip + 0x9fae84] will resolve to [0x10d800254 + 0x9fae84] in my case. This will then resolve to 0x000000010e1fb0d8, the memory address guarding the overlay from being initialized.

  1. You can use LLDB’s image lookup command with the verbose and symbol option to find the load address for UIDebuggingOverlayIsEnabled.__overlayIsEnabled.
(lldb) image lookup -vs UIDebuggingOverlayIsEnabled.__overlayIsEnabled

From the output, look for the range field for the end address. Again, this is due to LLDB not giving you the correct symbol. For my process, I got range = [0x000000010e1fb0d0-0x000000010e1fb0d8).

This means the byte of interest for me is located at: 0x000000010e1fb0d8. If I wanted to know the symbol this address is actually referring to, I can type:

(lldb) image lookup -a 0x000000010e1fb0d8

Which will then output:

Address: UIKitCore[0x00000000015b00d8] (UIKitCore.__DATA.__bss + 24824)
Summary: UIKitCore`UIDebuggingOverlayIsEnabled.onceToken

This UIDebuggingOverlayIsEnabled.onceToken is the correct name of the symbol you want to go after.

Bypassing checks by changing memory

We now know the exact bytes where this Boolean check occurs.

(lldb) x/gx 0x000000010e1fb0d8
(lldb) mem write 0x000000010e1fb0d8 0xffffffffffffffff -s 8
(lldb) po *(long *)0x000000010e1fb0d0 = -1
(lldb) x/gx 0x000000010e1fb0d8

Your turn

I just showed you how to knock out the initial check for UIDebuggingOverlayIsEnabled.onceToken to make the dispatch_once block think it has already run, but there’s one more check that will hinder your process.

(lldb) disassemble -n "-[UIDebuggingInformationOverlay init]" -c10
0x10d800256 <+24>: cmp    byte ptr [rip + 0x9fae73], 0x0 
    ; mainHandler.onceToken + 7
0x10d80025d <+31>: je     0x10d8002a8               ; <+106>

Verifying your work

Now that you’ve successfully written a -1 value to mainHandler.onceToken, it’s time to check your work to see if any changes you’ve made have bypassed the initialization checks.

(lldb) po [UIDebuggingInformationOverlay new]
<UIDebuggingInformationOverlay: 0x7fb622107860; frame = (0 0; 768 1024); hidden = YES; gestureRecognizers = <NSArray: 0x60400005aac0>; layer = <UIWindowLayer: 0x6040000298a0>>
(lldb) po [UIDebuggingInformationOverlay overlay]
(lldb) po [[UIDebuggingInformationOverlay overlay] toggleVisibility]
(lldb) continue

Sidestepping checks in prepareDebuggingOverlay

The UIDebuggingInformationOverlay is blank because we didn’t call the class method, +[UIDebuggingInformationOverlay prepareDebuggingOverlay]

(lldb) b _UIGetDebuggingOverlayEnabled
(lldb) exp -i0 -O -- [UIDebuggingInformationOverlay prepareDebuggingOverlay]
(lldb) finish
UIKit`+[UIDebuggingInformationOverlay prepareDebuggingOverlay]:
    0x11191a312 <+0>:   push   rbp
    0x11191a313 <+1>:   mov    rbp, rsp
    0x11191a316 <+4>:   push   r15
    0x11191a318 <+6>:   push   r14
    0x11191a31a <+8>:   push   r13
    0x11191a31c <+10>:  push   r12
    0x11191a31e <+12>:  push   rbx
    0x11191a31f <+13>:  push   rax
    0x11191a320 <+14>:  call   0x11191b2bf               
          ; _UIGetDebuggingOverlayEnabled

->  0x11191a325 <+19>:  test   al, al
    0x11191a327 <+21>:  je     0x11191a430               ; <+286>
    0x11191a32d <+27>:  lea    rax, [rip + 0x9fc19c]     ; UIApp
(lldb) p/x $al
(lldb) po $al = 0xff
(lldb) si
 je     0x11191a430               ; <+286>
(lldb) si
(lldb) continue
+ (void)prepareDebuggingOverlay {
  if (_UIGetDebuggingOverlayEnabled()) {
    id handler = [UIDebuggingInformationOverlayInvokeGestureHandler mainHandler];
    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:handler action:@selector(_handleActivationGesture:)];
    [tapGesture setNumberOfTouchesRequired:2];
    [tapGesture setNumberOfTapsRequired:1];
    [tapGesture setDelegate:handler];
    
    UIView *statusBarWindow = [UIApp statusBarWindow];
    [statusBarWindow addGestureRecognizer:tapGesture];
  }
}

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    UIGestureRecognizerStatePossible,
    UIGestureRecognizerStateBegan,    
    UIGestureRecognizerStateChanged,   
    UIGestureRecognizerStateEnded,      
    UIGestureRecognizerStateCancelled,  
    UIGestureRecognizerStateFailed,     
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded 
};

So, recapping…

Before we try this thing out, let’s quickly recap what you did just in case you need to restart fresh.

(lldb) image lookup -vs UIDebuggingOverlayIsEnabled.onceToken
(lldb) po *(long *)0x000000010e1fb0d0 = -1

Trying this out

Since you’re using the Simulator, this means you need to hold down Option on the keyboard to simulate two touches. Once you get the two touches parallel, hold down the Shift key to drag the tap circles around the screen. Position the tap circles on the status bar of your application, and then click.

Introducing method swizzling

Reflecting, how long did that take? In addition, we have to manually set this through LLDB everytime UIKIt gets loaded into a process. Finding and setting these values in memory can definitely be done through a custom LLDB script, but there’s an elegant alternative using Objective-C’s method swizzling.

Finally, onto a sample project

Included in this chapter is an sample project called Overlay and it’s quite minimal. It only has a UIButton smack in the middle that executes the expected logic to display the UIDebuggingInformationOverlay.

UIDebuggingInformationOverlay 'overlay' method returned nil
//****************************************************/
#pragma mark - Section 1 - FakeWindowClass
//****************************************************/

@interface FakeWindowClass : UIWindow
@end

@implementation FakeWindowClass

- (instancetype)initSwizzled
{
  if (self= [super init]) {
    [self _setWindowControlsStatusBarOrientation:NO];
  }
  return self;
}

@end
//****************************************************/
#pragma mark - Section 0 - Private Declarations
//****************************************************/

@interface NSObject()
- (void)_setWindowControlsStatusBarOrientation:(BOOL)orientation;
@end
+ (void)load
{
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    Class cls = NSClassFromString(@"UIDebuggingInformationOverlay");
    NSAssert(cls, @"DBG Class is nil?");
    
    // Swizzle code here

    [FakeWindowClass swizzleOriginalSelector:@selector(init)
                         withSizzledSelector:@selector(initSwizzled)
                                    forClass:cls
                               isClassMethod:NO];
  });
}

The final push

You’re about to build the final snippet of code for the soon-to-be-replacement method of prepareDebuggingOverlay. prepareDebuggingOverlay had an initial check at the beginning of the method to see if _UIGetDebuggingOverlayEnabled() returned 0x0 or 0x1. If this method returned 0x0, then control jumped to the end of the function.

+ (void)prepareDebuggingOverlaySwizzled {
  Class cls = NSClassFromString(@"UIDebuggingInformationOverlay");
  SEL sel = @selector(prepareDebuggingOverlaySwizzled);
  Method m = class_getClassMethod(cls, sel); 
  IMP imp =  method_getImplementation(m); // 1

  void (*methodOffset) = (void *)((imp + (long)27)); // 2
  void *returnAddr = &&RETURNADDRESS; // 3
  
  // You'll add some assembly here in a sec
  RETURNADDRESS: ;  // 4
}
+ (void)prepareDebuggingOverlaySwizzled {
  Class cls = NSClassFromString(@"UIDebuggingInformationOverlay");
  SEL sel = @selector(prepareDebuggingOverlaySwizzled);
  Method m = class_getClassMethod(cls, sel); 

  IMP imp =  method_getImplementation(m); 
  void (*methodOffset) = (void *)((imp + (long)27)); 
  void *returnAddr = &&RETURNADDRESS; 
  
  __asm__ __volatile__(     // 1
      "pushq  %0\n\t"       // 2
      "pushq  %%rbp\n\t"    // 3
      "movq   %%rsp, %%rbp\n\t"
      "pushq  %%r15\n\t"
      "pushq  %%r14\n\t"
      "pushq  %%r13\n\t"
      "pushq  %%r12\n\t"
      "pushq  %%rbx\n\t"
      "pushq  %%rax\n\t"
      "jmp  *%1\n\t"        // 4
      :
      : "r" (returnAddr), "r" (methodOffset)); // 5
  
  RETURNADDRESS: ;  // 5
}
[self swizzleOriginalSelector:@selector(prepareDebuggingOverlay) 
          withSizzledSelector:@selector(prepareDebuggingOverlaySwizzled) 
                     forClass:cls
                isClassMethod:YES];

import UIKit.UIGestureRecognizerSubclass
@IBAction func overlayButtonTapped(_ sender: Any) {
  guard 
    let cls = NSClassFromString("UIDebuggingInformationOverlay") as? UIWindow.Type else {
      print("UIDebuggingInformationOverlay class doesn't exist!")
      return
  }    
  cls.perform(NSSelectorFromString("prepareDebuggingOverlay"))

  let tapGesture = UITapGestureRecognizer()
  tapGesture.state = .ended

  let handlerCls = NSClassFromString("UIDebuggingInformationOverlayInvokeGestureHandler") as! NSObject.Type
  let handler = handlerCls
    .perform(NSSelectorFromString("mainHandler"))
    .takeUnretainedValue()
  let _ = handler
    .perform(NSSelectorFromString("_handleActivationGesture:"),
             with: tapGesture)
}

Other implementations

This definitely wasn’t the only way to attack the problem. I chose the assembly option since it’s fun. But there are cleaner approaches.

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"

  id handler = [NSClassFromString(@"UIDebuggingInformationOverlayInvokeGestureHandler") performSelector:@selector(mainHandler)];

  UITapGestureRecognizer *tapGesture =  [[UITapGestureRecognizer alloc] initWithTarget:handler action:@selector(_handleActivationGesture:)];
  [tapGesture setDelegate:handler];
  [tapGesture setNumberOfTapsRequired:1];
  [tapGesture setNumberOfTouchesRequired:2];

  [[[UIApplication sharedApplication]
        performSelector:@selector(statusBarWindow)]
                   addGestureRecognizer:tapGesture];

#pragma clang diagnostic pop // end of ignore undeclared selector

Where to go from here?

Crazy chapter, eh? In this chapter, you spelunked into memory and changed dispatch_once_t tokens as well as Booleans in memory to build a POC UIDebuggingInformationOverlay that’s compatible with iOS 12 while getting around Apple’s newly introduced checks to prevent you from using this class.

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