Swizzling in iOS 11 With UIDebuggingInformationOverlay

Learn how to swizzle “hidden” low-level features like UIDebuggingInformationOverlay into your own iOS 11 apps! By Derek Selander.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

So, Recapping…

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

You found the memory address of UIDebuggingOverlayIsEnabled.onceToken:

(lldb) image lookup -vs UIDebuggingOverlayIsEnabled.onceToken

And then set it to -1 via LLDB’s memory write or just casting the address to a long pointer and setting the value to -1 like so:

(lldb) po *(long *)0x000000010e1fb0d0 = -1

You also performed the same action for UIDebuggingOverlayIsEnabled.__overlayIsEnabled.

You then created a breakpoint on _UIGetDebuggingOverlayEnabled(), executed the +[UIDebuggingInformationOverlay prepareDebuggingOverlay] command and changed the return value that _UIGetDebuggingOverlayEnabled() produced so the rest of the method could continue to execute.

This was one of the many ways to bypass Apple’s new iOS 11 checks to prevent you from using these classes.

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.

You’ll be greeted with the fully functional UIDebuggingInformationOverlay!

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.

But before diving into how, let’s talk about the what.

Method swizzling is the process of dynamically changing what an Objective-C method does at runtime. Compiled code in the __TEXT section of a binary can’t be modified (well, it can with the proper entitlements that Apple will not give you, but we won’t get into that). However, when executing Objective-C code, objc_msgSend comes into play. In case you forgot, objc_msgSend will take an instance (or class), a Selector and a variable number of arguments and jump to the location of the function.

Method swizzling has many uses, but oftentimes people use this tactic to modify a parameter or return value. Alternatively, they can snoop and see when a function is executing code without searching for references in assembly. In fact, Apple even (precariously) uses method swizzling in it’s own codebase like KVO!

Since the internet is full of great references on method swizzling, I won’t start at square one (but if you want to, I’d say NSHipster’s swizzling article has the clearest and cleanest discussion of it). Instead, we’ll start with the basic example, then quickly ramp up to something I haven’t seen anyone do with method swizzling: use it to jump into an offset of a method to avoid any unwanted checks!

Finally — Onto A Sample Project

Included in this tutorial is a sample project named Overlay, which you can download here. It’s quite minimal; it only has a UIButton smack in the middle that executes the expected logic to display the UIDebuggingInformationOverlay.

You’ll build an Objective-C NSObject category to perform the Objective-C swizzling on the code of interest as soon as the module loads, using the Objective-C-only load class method.

Build and run the project. Tap on the lovely UIButton. You’ll only get some angry output from stderr saying:

UIDebuggingInformationOverlay 'overlay' method returned nil

As you already know, this is because of the short-circuited overriden init method for UIDebuggingInformationOverlay.

Let’s knock out this easy swizzle first; open NSObject+UIDebuggingInformationOverlayInjector.m. Jump to Section 1, marked by a pragma. In this section, add the following Objective-C class:

//****************************************************/
#pragma mark - Section 1 - FakeWindowClass
//****************************************************/

@interface FakeWindowClass : UIWindow
@end

@implementation FakeWindowClass

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

@end

For this part, you declared an Objective-C class named FakeWindowClass, which is a subclass of a UIWindow. Unfortunately, this code will not compile since _setWindowControlsStatusBarOrientation: is a private method.

Jump up to section 0 and forward declare this private method.

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

@interface NSObject()
- (void)_setWindowControlsStatusBarOrientation:(BOOL)orientation;
@end

This will quiet the compiler and let the code build. The UIDebuggingInformationOverlay‘s init method has checks to return nil. Since the init method was rather simple, you just completely sidestepped this logic and reimplemented it yourself and removed all the “bad stuff”!

Now, replace the code for UIDebuggingInformationOverlay‘s init with FakeWindowClass‘s initSwizzled method. Jump down to section 2 in NSObject‘s load method and replace the load method with the following:

+ (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];
  });
}

Rerun and build the Overlay app with this new code. Tap on the UIButton to see what happens now that you’ve replaced the init to produce a valid instance.

UIDebuggingInformationOverlay now pops up without any content. Almost there!

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.

In order to get around this, you’ll you’ll “simulate” a call instruction by pushing a return address onto the stack, but instead of call‘ing, you’ll jmp into an offset past the _UIGetDebuggingOverlayEnabled check. That way, you can perform the function proglogue in your stack frame and directly skip the dreaded check in the beginning of prepareDebuggingOverlay.

In NSObject+UIDebuggingInformationOverlayInjector.m, Navigate down to Section 3 – prepareDebuggingOverlay, and add the following snippet of code:

+ (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
}

Let’s break this crazy witchcraft down:

  1. I want to get the starting address of the original prepareDebuggingOverlay. However, I know this will be swizzled code, so when this code executes, prepareDebuggingOverlaySwizzled will actually point to the real, prepareDebuggingOverlay starting address.
  2. I take the starting address of the original prepareDebuggingOverlay (given to me through the imp variable) and I offset the value in memory past the _UIGetDebuggingOverlayEnabled() check. I used LLDB to figure the exact offset by dumping the assembly and calculating the offset (disassemble -n "+[UIDebuggingInformationOverlay prepareDebuggingOverlay]"). This is insanely brittle as any new code or compiler changes from clang will likely break this. I strongly recommend you calculate this yourself in case this changes past iOS 11.1.1.
  3. Since you are faking a function call, you need an address to return to after this soon-to-be-executed function offset finishes. This is accomplished by getting the address of a declared label. Labels are a not often used feature by normal developers which allow you to jmp to different areas of a function. The use of labels in modern programming is considered bad practice as if/for/while loops can accomplish the same thing… but not for this crazy hack.
  4. This is the declaration of the label RETURNADDRESS. No, you do need that semicolon after the label as the C syntax for a label to have a statement immediately following it.

Time to cap this bad boy off with some sweet inline assembly! Right above the label RETURNADDRESS declaration, add the following inline assembly:

+ (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
}
  1. Don’t be scared, you’re about to write x86_64 assembly in AT&T format (Apple’s assembler is not a fan of Intel). That __volatile__ is there to hint to the compiler to not try and optimize this away.
  2. You can think of this sort of like C’s printf where the %0 will be replaced by the value supplied by the returnAddr. In x86, the return address is pushed onto the stack right before entering a function. As you know, returnAddr points to an executable address following this assembly. This is how we are faking an actual function call!
  3. The following assembly is copy pasted from the function prologue in the +[UIDebuggingInformationOverlay prepareDebuggingOverlay]. This lets us perform the setup of the function, but allows us to skip the dreaded check.
  4. Finally we are jumping to offset 27 of the prepareDebuggingOverlay after we have set up all the data and stack information we need to not crash. The jmp *%1 will get resolved to jmp‘ing to the value stored at methodOffset. Finally, what are those “r” strings? I won’t get too into the details of inline assembly as I think your head might explode with an information overload (think Scanners), but just know that this is telling the assembler that your assembly can use any register for reading these values.

Jump back up to section 2 where the swizzling is performed in the +load method and add the following line of code to the end of the method:

[self swizzleOriginalSelector:@selector(prepareDebuggingOverlay) 
          withSizzledSelector:@selector(prepareDebuggingOverlaySwizzled) 
                     forClass:cls
                isClassMethod:YES];

Build and run. Tap on the UIButton to execute the required code to setup the UIDebuggingInformationOverlay class, then perform the two-finger tap on the status bar.

Omagerd, can you believe that worked?

I am definitely a fan of the hidden status bar dual tap thing, but let’s say you wanted to bring this up solely from code. Here’s what you can do:

Open ViewController.swift. At the top of the file add:

import UIKit.UIGestureRecognizerSubclass

This will let you set the state of a UIGestureRecognizer (default headers allow only read-only access to the state variable).

Once that’s done, augment the code in overlayButtonTapped(_ sender: Any) to be the following:

@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)
}

Final build and run. Tap on the button and see what happens.

Boom.