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
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:
-
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 theimp
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 thereturnAddr
. 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. Thejmp *%1
will get resolved tojmp
‘ing to the value stored atmethodOffset
. 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.