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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Bypassing Checks by Changing Memory

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

Let’s first see what value this has:

(lldb) x/gx 0x000000010e1fb0d8

This will dump out 8 bytes in hex located at 0x000000010e1fb0d8 (your address will be different). If you’ve executed the po [UIDebuggingInformationOverlay new] command earlier, you’ll see -1; if you haven’t, you’ll see 0.

Let’s change this. In LLDB type:

(lldb) mem write 0x000000010e1fb0d8 0xffffffffffffffff -s 8

The -s option specifies the amount of bytes to write to. If typing out 16 f’s is unappealing to you, there’s always alternatives to complete the same task. For example, the following would be equivalent:

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

You can of course verify your work be just examining the memory again.

(lldb) x/gx 0x000000010e1fb0d8

The output should be 0xffffffffffffffff now.

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.

Re-run the disassemble command you typed earlier:

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

At the very bottom of output are these two lines:

0x10d800256 <+24>: cmp    byte ptr [rip + 0x9fae73], 0x0 
    ; mainHandler.onceToken + 7
0x10d80025d <+31>: je     0x10d8002a8               ; <+106>

This mainHandler.onceToken is again, the wrong symbol; you care about the symbol immediately following it in memory. I want you to perform the same actions you did on UIDebuggingOverlayIsEnabled.__overlayIsEnabled, but instead apply it to the memory address pointed to by the mainHandler.onceToken symbol. Once you perform the RIP arithmetic, referencing mainHandler.onceToken, you’ll realize the correct symbol, UIDebuggingOverlayIsEnabled.__overlayIsEnabled, is the symbol you are after.

You first need to the find the location of mainHandler.onceToken in memory. You can either perform the RIP arithmetic from the above assembly or use image lookup -vs mainHandler.onceToken to find the end location. Once you found the memory address, write a -1 value into this memory address.

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.

In LLDB type:

(lldb) po [UIDebuggingInformationOverlay new]

Provided you correctly augmented the memory, you’ll be greeted with some more cheery output:

<UIDebuggingInformationOverlay: 0x7fb622107860; frame = (0 0; 768 1024); hidden = YES; gestureRecognizers = <NSArray: 0x60400005aac0>; layer = <UIWindowLayer: 0x6040000298a0>>

And while you’re at it, make sure the class method overlay returns a valid instance:

(lldb) po [UIDebuggingInformationOverlay overlay]

If you got nil for either of the above LLDB commands, make sure you have augmented the correct addresses in memory. If you’re absolutely sure you have augmented the correct addresses and you still get a nil return value, make sure you’re running either the iOS 11.0-11.1 Simulator as Apple could have added additional checks to prevent this from working in a version since this tutorial was written!

If all goes well, and you have a valid instance, let’s put this thing on the screen!

In LLDB, type:

(lldb) po [[UIDebuggingInformationOverlay overlay] toggleVisibility]

Then resume the process:

(lldb) continue

Alright… we got something on the screen, but it’s blank!?

Sidestepping Checks in prepareDebuggingOverlay

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

Dumping the assembly for this method, we can see one concerning check immediately:

Offsets 14, 19, and 21. Call a function named _UIGetDebuggingOverlayEnabled test if AL (RAX‘s single byte cousin) is 0. If yes, jump to the end of this function. The logic in this function is gated by the return value of _UIGetDebuggingOverlayEnabled.

Since we are still using LLDB to build a POC, let’s set a breakpoint on this function, step out of _UIGetDebuggingOverlayEnabled, then augment the value stored in the AL register before the check in offset 19 occurs.

Create a breakpoint on _UIGetDebuggingOverlayEnabled:

(lldb) b _UIGetDebuggingOverlayEnabled

LLDB will indicate that it’s successfully created a breakpoint on the _UIGetDebuggingOverlayEnabled method.

Now, let’s execute the [UIDebuggingInformationOverlay prepareDebuggingOverlay] method, but have LLDB honor breakpoints. Type the following:

(lldb) exp -i0 -O -- [UIDebuggingInformationOverlay prepareDebuggingOverlay]

This uses the -i option that determines if LLDB should ignore breakpoints. You’re specifying 0 to say that LLDB shouldn’t ignore any breakpoints.

Provided all went well, execution will start in the prepareDebuggingOverlay method and call out to the _UIGetDebuggingOverlayEnabled where execution will stop.

Let’s just tell LLDB to resume execution until it steps out of this _UIGetDebuggingOverlayEnabled function:

(lldb) finish

Control flow will finish up in _UIGetDebuggingOverlayEnabled and we’ll be back in the prepareDebuggingOverlay method, right before the test of the AL register on offset 19:

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

Through LLDB, print out the value in the AL register:

(lldb) p/x $al

Unless you work at a specific fruit company inside a fancy new “spaceship” campus, you’ll likely get 0x00.

Change this around to 0xff:

(lldb) po $al = 0xff

Let’s verify this worked by single instruction stepping:

(lldb) si

This will get you onto the following line:

 je     0x11191a430               ; <+286>

If AL was 0x0 at the time of the test assembly instruction, this will move you to offset 286. If AL wasn’t 0x0 at the time of the test instruction, you’ll keep on executing without the conditional jmp instruction.

Make sure this succeeded by performing one more instruction step.

(lldb) si

If you’re on offset 286, this has failed and you’ll need to repeat the process. However, if you find the instruction pointer has not conditionally jumped, then this has worked!

There’s nothing more you need to do now, so resume execution in LLDB:

(lldb) continue

So, what did the logic do exactly in +[UIDebuggingInformationOverlay prepareDebuggingOverlay]?

To help ease the visual burden, here is a rough translation of what the +[UIDebuggingInformationOverlay prepareDebuggingOverlay] method is doing:

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

This is interesting: There is logic to handle a two finger tap on UIApp’s statusBarWindow. Once that happens, a method called _handleActivationGesture: will be executed on a UIDebuggingInformationOverlayInvokeGestureHandler singleton, mainHandler.

That makes you wonder what’s the logic in -[UIDebuggingInformationOverlayInvokeGestureHandler _handleActivationGesture:] is for?

A quick assembly dump using dd brings up an interesting area:

The UITapGestureRecognizer instance passed in by the RDI register, is getting the state compared to the value 0x3 (see offset 30). If it is 3, then control continues, while if it’s not 3, control jumps towards the end of the function.

A quick lookup in the header file for UIGestureRecognizer, tells us the state has the following enum values:

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded 

Counting from 0, we can see control will only execute the bulk of the code if the UITapGestureRecognizer‘s state is equal to UIGestureRecognizerStateEnded.

So what does this mean exactly? Not only did UIKit developers put restrictions on accessing the UIDebuggingInformationOverlay class (which you’ve already modified in memory), they’ve also added a “secret” UITapGestureRecognizer to the status bar window that executes the setup logic only when you complete a two finger tap on it.

How cool is that?