This is an abridged chapter from our book Advanced Apple Debugging & Reverse Engineering, which has been completely updated for Xcode 9.1 and iOS 11. Enjoy!
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 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.
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: https://github.com/DerekSelander/lldb.
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:
-
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.
-
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.
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?
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 http://nshipster.com/method-swizzling/ 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:
-
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.
-
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.
-
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.
-
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
}
-
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.
-
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!
-
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.
-
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.
Where to Go From Here?
You can download the final project from this tutorial here.
Crazy tutorial, 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 11 while getting around Apple’s newly introduced checks to prevent you from using this class.
Then you used Objective-C’s method swizzling to perform the same actions as well as hook into only a portion of the original method, bypassing several short-circuit checks.
This is why reverse engineering Objective-C is so much fun, because you can hook into methods that are quietly called in private code you don’t have the source for and make changes or monitor what it’s doing.
Still have energy after that brutal chapter? This swizzled code will not work on an ARM64 device. You’ll need to look at the assembly and perform an alternative action for that architecture likely through a preprocessor macro.
If you enjoyed what you learned in the tutorial, why not check out the complete Advanced Apple Debugging & Reverse Engineering book, available in our store?
Here’s a taste of what’s in the book:
One thing you can be sure of: after reading this book, you’ll have the tools and knowledge to answer even the most obscure question about your code — or even someone else’s.
Questions? Comments? Come join the forum discussion below!
The post Swizzling in iOS 11 with UIDebuggingInformationOverlay appeared first on Ray Wenderlich.