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

In this tutorial, 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 an article by Ryan Peterson 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.

You’ll explore UIDebuggingInformationOverlay and learn why this class fails to work in iOS 11, 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 an iOS 11 Simulator for this tutorial as Apple can impose new checks on these classes in the future where I have no intention to “up the ante” when they make this class harder to use or remove it from release UIKit builds altogether.

Between iOS 10 and 11

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 11. Executing the exact same LLDB commands in iOS 11 will produce nothing.

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

Use LLDB to attach to any iOS 11.x Simulator process, this can MobileSafari, SpringBoard, or your own work. It doesn’t matter if it’s your own app or not, as you will be exploring assembly in the UIKit 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 overridden methods by the UIDebuggingInformationOverlay class.

You can use the image lookup LLDB command:

(lldb) image lookup -rn UIDebuggingInformationOverlay

Or alternatively, you can use the methods command you create in Chapter 14 of the book, “Dynamic Frameworks”:

(lldb) methods UIDebuggingInformationOverlay

The following command would be equivalent to that:

(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: on the DerekSelander/LLDB GitHub.

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

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 enforced in iOS 11 thanks to UIDebuggingOverlayIsEnabled() 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 11 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: UIKit[0x00000000015b00d8] (UIKit.__DATA.__bss + 24824)
Summary: UIKit`UIDebuggingOverlayIsEnabled.onceToken

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