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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Swizzling in iOS 11 With UIDebuggingInformationOverlay
35 mins
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) {
UIGestureRecognizerStatePossible,
UIGestureRecognizerStateBegan,
UIGestureRecognizerStateChanged,
UIGestureRecognizerStateEnded,
UIGestureRecognizerStateCancelled,
UIGestureRecognizerStateFailed,
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?