1. Method Swizzling
UIKit
中有很多私有的类,用来辅助调试视觉调试。其中最有名的就是UIDebuggingInformationOverlay
。它是iOS 9
引入的,在2017年5月因一篇文章得到了广泛的传播。这篇文章介绍了这个类的闪光点以及用法。
可惜的是iOS 11
,苹果发现很多开发者都在用这个类,于是加了很多检查来确保只有自家的app可以有权限范文这些私有的调试类。
接下来我们会探索UIDebuggingInformationOverlay
这个类,了解为什么这个类在iOS 11
上无法正常运行。同时,通过LLDB
学习苹果是如何在内存中设置这些检查机制的。然后,我们将利用OC的method swizzling
技术,让UIDebuggingInformationOverlay
这个类可以被使用。
1.1 iOS 10和11到底发生了什么
在iOS 9
和iOS 10
中,开启这个类是非常容易的。只需要在LLDB
中输入下面两句代码就可以开启。
(lldb) po [UIDebuggingInformationOverlay prepareDebuggingOverlay]
(lldb) po [[UIDebuggingInformationOverlay overlay] toggleVisibility]
我们在iOS 11
模拟器上附着LLDB
到Photos
上。
~> lldb -n Watermark
//通过image查询它的方法列表
(lldb) image lookup -rn UIDebuggingInformationOverlay
//或通过直接打印它的方法列表(需要配置过~/.lldb)
(lldb) methods UIDebuggingInformationOverlay
//这个方法和上面的效果一样
(lldb) exp -l objc -O -- [UIDebuggingInformationOverlay _shortMethodDescription]
我们发现了复写的init
方法。通过汇编可以看到init
干了什么。
(lldb) disassemble -n "-[UIDebuggingInformationOverlay init]"
这里做了大致的翻译工作:
- iOS 10
@implementation UIDebuggingInformationOverlay
- (instancetype)init {
if (self = [super init]) {
[self _setWindowControlsStatusBarOrientation:NO];
}
return self;
}
@end
- iOS 11
@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
iOS 11
上通过UIDebuggingOverlayIsEnabled()
来判断是不是自家的app。所以我们在LLDB
中无法初始化这个类的实例对象。
(lldb) po [UIDebuggingInformationOverlay new]
nil
(lldb) disassemble -n "-[UIDebuggingInformationOverlay init]" -c10
UIKitCore`-[UIDebuggingInformationOverlay init]:
0x7fff483f31ba <+0>: push rbp
0x7fff483f31bb <+1>: mov rbp, rsp
0x7fff483f31be <+4>: push r14
0x7fff483f31c0 <+6>: push rbx
0x7fff483f31c1 <+7>: sub rsp, 0x10
0x7fff483f31c5 <+11>: mov rbx, rdi
0x7fff483f31c8 <+14>: cmp qword ptr [rip + 0x41538710], -0x1 ; UIDebuggingOverlayIsEnabled.__overlayIsEnabled + 7
0x7fff483f31d0 <+22>: jne 0x7fff483f323c ; <+130>
0x7fff483f31d2 <+24>: cmp byte ptr [rip + 0x415386ff], 0x0 ; mainHandler.onceToken + 7
0x7fff483f31d9 <+31>: je 0x7fff483f3224 ; <+106>
幸好,苹果的动态库包含了DWARF
调试信息,我们可以用符号来获取具体的内存地址。
- 首先来研究一下下面两行汇编代码。
0x7fff483f31c8 <+14>: cmp qword ptr [rip + 0x41538710], -0x1 ; UIDebuggingOverlayIsEnabled.__overlayIsEnabled + 7
0x7fff483f31d0 <+22>: jne 0x7fff483f323c ; <+130>
我们看到这个地址和-1
在做比较,如果不是-1
则会跳转到一个特殊地址。然后我们说到正题上,dispatch_once_t
变量开始的时候是0
,在dispatch_once
代码块执行完后会被设置为-1
。是的,第一次在内存中的检查是看代码是否应该在dispatch_once
中执行。你希望dispatch_once
逻辑被跳过,所以你需要在内存中设置这个值为-1
。
从上面的汇编代码来看,我们有两个办法来获得我们感兴趣的内存地址。
-
你可以通过
RIP
来访问这个变量。比如,我们例子中是[rip + 0x41538710]
。我们知道RIP
中存储的是下一条命令的执行地址。所以,实际[rip + 0x41538710] = [0x7fff483f31d0 + 0x41538710] = 0x00007fff8992b8e0
。 -
你可以用
image lookup
方法与verbose
和symbol
选项来找到UIDebuggingOverlayIsEnabled.__overlayIsEnabled
的加载地址。(lldb) image lookup -vs UIDebuggingOverlayIsEnabled.__overlayIsEnabled 1 symbols match 'UIDebuggingOverlayIsEnabled.__overlayIsEnabled' in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore: Address: UIKitCore[0x00000000018942c8] (UIKitCore.__DATA.__bss + 27464) Summary: UIKitCore`UIDebuggingOverlayIsEnabled.__overlayIsEnabled Module: file = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", arch = "x86_64" Symbol: id = {0x0001d2b6}, range = [0x00007fff8992b8d8-0x00007fff8992b8e0), name="UIDebuggingOverlayIsEnabled.__overlayIsEnabled"
我们关注
range = [0x00007fff8992b8d8-0x00007fff8992b8e0)
。这表明,我们感兴趣的数据就在0x00007fff8992b8e0
。
我们可以通过image lookup
来验证一下。
(lldb) image lookup -a 0x00007fff8992b8e0
Address: UIKitCore[0x00000000018942d0] (UIKitCore.__DATA.__bss + 27472)
Summary: UIKitCore`UIDebuggingOverlayIsEnabled.onceToken
UIDebuggingOverlayIsEnabled.onceToken
这个就是我们想得到的符号名了。
- 通过修改内存来绕过检测
我们先来看看0x00007fff8992b8e0
里面存的什么。如果我们之前执行过po [UIDebuggingInformationOverlay new]
,则显示-1
;否则显示0
。这里由于刚刚执行过了,所以这里打印显示-1
。
(lldb) x/gx 0x00007fff8992b8e0
0x7fff8992b8e0: 0xffffffffffffffff
如果你显示的0
,你可以通过两种方式来进行修改。
// -s 表示你想写入的字节数
(lldb) mem write 0x00007fff8992b8e0 0xffffffffffffffff -s 8
// 还有一种更友好的方式
(lldb) po *(long *)0x00007fff8992b8e0 = -1
- 试一试分析:紧接在刚刚两行汇编代码下面的两句汇编代码。
0x7fff483f31d2 <+24>: cmp byte ptr [rip + 0x415386ff], 0x0 ; mainHandler.onceToken + 7
0x7fff483f31d9 <+31>: je 0x7fff483f3224 ; <+106>
通过RIP
寄存器来访问[rip + 0x415386ff] = [0x7fff483f31d9 + 0x415386ff] = 0x00007fff8992b8d8
。虽然它显示的是mainHandler.onceToken
,但其实是错误的符号。通过打印,我们可以看到UIDebuggingOverlayIsEnabled.__overlayIsEnabled
才是我们在研究的符号名。
(lldb) image lookup -a 0x00007fff8992b8d8
Address: UIKitCore[0x00000000018942c8] (UIKitCore.__DATA.__bss + 27464)
Summary: UIKitCore`UIDebuggingOverlayIsEnabled.__overlayIsEnabled
当然我们还可以使用上面的第二种方法。
(lldb) image lookup -vs mainHandler.onceToken
1 symbols match 'mainHandler.onceToken' in /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore:
Address: UIKitCore[0x00000000018942c0] (UIKitCore.__DATA.__bss + 27456)
Summary: UIKitCore`mainHandler.onceToken
Module: file = "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore", arch = "x86_64"
Symbol: id = {0x0001d2b5}, range = [0x00007fff8992b8d0-0x00007fff8992b8d8), name="mainHandler.onceToken"
同样,我们把-1
写入到内存地址。
(lldb) x/gx 0x00007fff8992b8d8
0x7fff8992b8d8: 0x0000000000000000
// -s 表示你想写入的字节数
(lldb) mem write 0x00007fff8992b8d8 0xffffffffffffffff -s 8
// 还有一种更友好的方式
(lldb) po *(long *)0x00007fff8992b8d8 = -1
- 验证一下我们的成果。
(lldb) po [UIDebuggingInformationOverlay new]
<UIDebuggingInformationOverlay: 0x7fcbdcf10420; frame = (0 0; 414 896); hidden = YES; gestureRecognizers = <NSArray: 0x600002230420>; layer = <UIWindowLayer: 0x600002df8080>>
//我们再把视图显示出来
(lldb) po [UIDebuggingInformationOverlay overlay]
<UIDebuggingInformationOverlay: 0x7fcbdcd2d540; frame = (0 0; 414 896); hidden = YES; gestureRecognizers = <NSArray: 0x600002220510>; layer = <UIWindowLayer: 0x600002df8520>>
(lldb) po [[UIDebuggingInformationOverlay overlay] toggleVisibility]
0x00000000fe8550b7
可以看到这个视图显示在了程序中,但是它是空白的?
UIDebuggingInformationOverlay
如果有兴趣可以看一下整体的
LLDB
代码。
~> lldb -n Watermark
(lldb) process attach --name "Watermark"
Process 8075 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff523b625a libsystem_kernel.dylib`mach_msg_trap + 10
libsystem_kernel.dylib`mach_msg_trap:
-> 0x7fff523b625a <+10>: ret
0x7fff523b625b <+11>: nop
libsystem_kernel.dylib`mach_msg_overwrite_trap:
0x7fff523b625c <+0>: mov r10, rcx
0x7fff523b625f <+3>: mov eax, 0x1000020
Target 0: (Watermark) stopped.
Executable module set to "/Users/ycpeng/Library/Developer/CoreSimulator/Devices/85225EEE-8D5B-4091-A742-5BEBAE1C4906/data/Containers/Bundle/Application/6DDF5638-4457-43BB-8A6E-27C170F2A2F0/Watermark.app/Watermark".
Architecture set to: x86_64h-apple-ios-.
(lldb) disassemble -n "-[UIDebuggingInformationOverlay init]" -c10
UIKitCore`-[UIDebuggingInformationOverlay init]:
0x7fff483f31ba <+0>: push rbp
0x7fff483f31bb <+1>: mov rbp, rsp
0x7fff483f31be <+4>: push r14
0x7fff483f31c0 <+6>: push rbx
0x7fff483f31c1 <+7>: sub rsp, 0x10
0x7fff483f31c5 <+11>: mov rbx, rdi
0x7fff483f31c8 <+14>: cmp qword ptr [rip + 0x41538710], -0x1 ; UIDebuggingOverlayIsEnabled.__overlayIsEnabled + 7
0x7fff483f31d0 <+22>: jne 0x7fff483f323c ; <+130>
0x7fff483f31d2 <+24>: cmp byte ptr [rip + 0x415386ff], 0x0 ; mainHandler.onceToken + 7
0x7fff483f31d9 <+31>: je 0x7fff483f3224 ; <+106>
(lldb) p/x 0x7fff483f31d0 + 0x41538710
(long) $0 = 0x00007fff8992b8e0
(lldb) po *(long *)0x00007fff8992b8e0 = -1
-1
(lldb) p/x 0x7fff483f31d9 + 0x415386ff
(long) $2 = 0x00007fff8992b8d8
(lldb) po *(long *)0x00007fff8992b8d8 = -1
-1
(lldb) x/gx 0x00007fff8992b8e0
0x7fff8992b8e0: 0xffffffffffffffff
(lldb) x/gx 0x00007fff8992b8d8
0x7fff8992b8d8: 0xffffffffffffffff
(lldb) po [UIDebuggingInformationOverlay overlay]
<UIDebuggingInformationOverlay: 0x7fe154c41680; frame = (0 0; 414 896); hidden = YES; gestureRecognizers = <NSArray: 0x600003343240>; layer = <UIWindowLayer: 0x600003d5f640>>
(lldb) po [[UIDebuggingInformationOverlay overlay] toggleVisibility]
0x000000008e064138
(lldb) continue
Process 8075 resuming
1.2 在prepareDebuggingOverlay方法中回避检测
UIDebuggingInformationOverlay
是空白的,是因为我们没有调用类方法+ [UIDebuggingInformationOverlay prepareDebuggingOverlay]
。
(lldb) disassemble -n "+[UIDebuggingInformationOverlay prepareDebuggingOverlay]" -c10
UIKitCore`+[UIDebuggingInformationOverlay prepareDebuggingOverlay]:
0x7fff483f32d2 <+0>: push rbp
0x7fff483f32d3 <+1>: mov rbp, rsp
0x7fff483f32d6 <+4>: push r14
0x7fff483f32d8 <+6>: push rbx
0x7fff483f32d9 <+7>: call 0x7fff483f4018 ; _UIGetDebuggingOverlayEnabled
0x7fff483f32de <+12>: test al, al
0x7fff483f32e0 <+14>: je 0x7fff483f333d ; <+107>
0x7fff483f32e2 <+16>: mov rdi, qword ptr [rip + 0x41483f2f] ; (void *)0x00007fff87d0e358: NSNotificationCenter
0x7fff483f32e9 <+23>: mov rsi, qword ptr [rip + 0x41431750] ; "defaultCenter"
0x7fff483f32f0 <+30>: mov r14, qword ptr [rip + 0x3e388029] ; (void *)0x00007fff513f7780: objc_msgSend
我们看到注意到偏移量7
、12
、14
执行了一个叫_UIGetDebuggingOverlayEnabled
的方法测试,是否AL
寄存器(RAX
寄存器的1字节版本)是否为0
。如果结果为真,则跳转到函数的最后。这个逻辑的关键就是_UIGetDebuggingOverlayEnabled
的返回值。
我们还是用LLDB
,在_UIGetDebuggingOverlayEnabled
处设置一个断点,在偏移量12
的检测之前增加AL
寄存器中的值。
(lldb) b _UIGetDebuggingOverlayEnabled
Breakpoint 1: where = UIKitCore`_UIGetDebuggingOverlayEnabled, address = 0x00007fff483f4018
(lldb) exp -i 0 -O -- [UIDebuggingInformationOverlay prepareDebuggingOverlay]
Process 8075 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00007fff483f4018 UIKitCore`_UIGetDebuggingOverlayEnabled
UIKitCore`_UIGetDebuggingOverlayEnabled:
-> 0x7fff483f4018 <+0>: push rbp
0x7fff483f4019 <+1>: mov rbp, rsp
0x7fff483f401c <+4>: push r15
0x7fff483f401e <+6>: push r14
Target 0: (Watermark) stopped.
error: Execution was interrupted, reason: breakpoint 1.1.
-i
参数决定了LLDB
是否需要忽略断点。0
表示LLDB
不应该忽略任何断点。
我们可以看到,方法断在了_UIGetDebuggingOverlayEnabled
刚开始执行的地方。然后我们step out
到_UIGetDebuggingOverlayEnabled
执行完毕的地方。打印AL
寄存器的值,并把原始的0x0
改成0xff
。
(lldb) finish
Process 8075 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step out
frame #0: 0x00007fff483f32de UIKitCore`+[UIDebuggingInformationOverlay prepareDebuggingOverlay] + 12
UIKitCore`+[UIDebuggingInformationOverlay prepareDebuggingOverlay]:
-> 0x7fff483f32de <+12>: test al, al
0x7fff483f32e0 <+14>: je 0x7fff483f333d ; <+107>
0x7fff483f32e2 <+16>: mov rdi, qword ptr [rip + 0x41483f2f] ; (void *)0x00007fff87d0e358: NSNotificationCenter
0x7fff483f32e9 <+23>: mov rsi, qword ptr [rip + 0x41431750] ; "defaultCenter"
Target 0: (Watermark) stopped.
(lldb) p/x $al
(unsigned char) $6 = 0x00
(lldb) po $al = 0xff
我们验证一下,通过si
来到下一步。
(lldb) si
Process 8075 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
frame #0: 0x00007fff483f32e0 UIKitCore`+[UIDebuggingInformationOverlay prepareDebuggingOverlay] + 14
UIKitCore`+[UIDebuggingInformationOverlay prepareDebuggingOverlay]:
-> 0x7fff483f32e0 <+14>: je 0x7fff483f333d ; <+107>
0x7fff483f32e2 <+16>: mov rdi, qword ptr [rip + 0x41483f2f] ; (void *)0x00007fff87d0e358: NSNotificationCenter
0x7fff483f32e9 <+23>: mov rsi, qword ptr [rip + 0x41431750] ; "defaultCenter"
0x7fff483f32f0 <+30>: mov r14, qword ptr [rip + 0x3e388029] ; (void *)0x00007fff513f7780: objc_msgSend
Target 0: (Watermark) stopped.
如果在test
指令执行的时候,AL
寄存器的值是0x0
,那么je 0x7fff483f333d ; <+107>
告诉我们会跳转到偏移量为107
的地方;如果AL
寄存器的值不是0x0
,那么会继续执行。
我们再往下执行一步。
(lldb) si
Process 8075 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
frame #0: 0x00007fff483f32e2 UIKitCore`+[UIDebuggingInformationOverlay prepareDebuggingOverlay] + 16
UIKitCore`+[UIDebuggingInformationOverlay prepareDebuggingOverlay]:
-> 0x7fff483f32e2 <+16>: mov rdi, qword ptr [rip + 0x41483f2f] ; (void *)0x00007fff87d0e358: NSNotificationCenter
0x7fff483f32e9 <+23>: mov rsi, qword ptr [rip + 0x41431750] ; "defaultCenter"
0x7fff483f32f0 <+30>: mov r14, qword ptr [rip + 0x3e388029] ; (void *)0x00007fff513f7780: objc_msgSend
0x7fff483f32f7 <+37>: call r14
Target 0: (Watermark) stopped.
我们没有来到偏移量为107
的地方,说明我们操作成功了,可以执行continue
继续运行了。
那么+ [UIDebuggingInformationOverlay prepareDebuggingOverlay]
到底干了什么呢?
+ (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];
}
}
这个逻辑是处理在状态栏窗口的双指点击事件的。当事件发生时,触发UIDebuggingInformationOverlayInvokeGestureHandler
这个mainHandler
单例执行_handleActivationGesture:
方法。
那- [UIDebuggingInformationOverlayInvokeGestureHandler _handleActivationGesture:]
又干了什么呢?
(lldb) disassemble -n "-[UIDebuggingInformationOverlayInvokeGestureHandler _handleActivationGesture:]" -c20
UIKitCore`-[UIDebuggingInformationOverlayInvokeGestureHandler _handleActivationGesture:]:
0x7fff483f2f67 <+0>: push rbp
0x7fff483f2f68 <+1>: mov rbp, rsp
0x7fff483f2f6b <+4>: push r14
0x7fff483f2f6d <+6>: push rbx
0x7fff483f2f6e <+7>: mov rbx, rdi
0x7fff483f2f71 <+10>: mov rsi, qword ptr [rip + 0x41432228] ; "state"
0x7fff483f2f78 <+17>: mov rdi, rdx
0x7fff483f2f7b <+20>: call qword ptr [rip + 0x3e38839f] ; (void *)0x00007fff513f7780: objc_msgSend
0x7fff483f2f81 <+26>: cmp rax, 0x3
0x7fff483f2f85 <+30>: jne 0x7fff483f30a8 ; <+321>
0x7fff483f2f8b <+36>: cmp byte ptr [rbx + 0x8], 0x0
0x7fff483f2f8f <+40>: jne 0x7fff483f306b ; <+260>
0x7fff483f2f95 <+46>: mov rdi, qword ptr [rip + 0x41489944] ; (void *)0x00007fff898dacb0: _UIPrototypingMenuViewController
0x7fff483f2f9c <+53>: call 0x7fff486252fa ; symbol stub for: objc_opt_class
0x7fff483f2fa1 <+58>: lea rdi, [rip + 0x3e415d78] ; @"Prototyping"
0x7fff483f2fa8 <+65>: mov rsi, rax
0x7fff483f2fab <+68>: call 0x7fff483f30ad ; UIDebuggingViewControllerAtTopLevel
0x7fff483f2fb0 <+73>: mov rdi, rax
0x7fff483f2fb3 <+76>: call 0x7fff48625372 ; symbol stub for: objc_unsafeClaimAutoreleasedReturnValue
0x7fff483f2fb8 <+81>: mov rdi, qword ptr [rip + 0x41489929] ; (void *)0x00007fff898da5d0: UIDebuggingInformationHierarchyViewController
UITapGestureRecognizer
实例对象会通过RDI
寄存器传递,获取它的state
值和0x3
进行比较。如果等于0x3
继续执行,否则跳转到函数的末尾。
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible,
UIGestureRecognizerStateBegan,
UIGestureRecognizerStateChanged,
UIGestureRecognizerStateEnded,
UIGestureRecognizerStateCancelled,
UIGestureRecognizerStateFailed, UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};
对比声明,我们可以知道0x3
是UIGestureRecognizerStateEnded
。也就是说,UIKit
的开发者不仅加了UIDebuggingInformationOverlay
类的访问控制,还在状态栏加了一个双指点击的“彩蛋”来执行配置操作。
- 总结一下整个过程
- 我们先找到
UIDebuggingOverlayIsEnabled.onceToken
的内存地址。(lldb) image lookup -vs UIDebuggingOverlayIsEnabled.onceToken
- 通过
memory write
操作或直接用一个long
指针赋值将它的值设置为-1
。(lldb) po *(long *) 0x00007fff8992b8e0 = -1
- 对
UIDebuggingOverlayIsEnabled.__overlayIsEnabled
执行1、2步同样的操作。(lldb) image lookup -vs UIDebuggingOverlayIsEnabled.__overlayIsEnabled (lldb) po *(long *) 0x00007fff8992b8d8 = -1
- 在
_UIGetDebuggingOverlayEnabled()
上设置一个断点。(lldb) b _UIGetDebuggingOverlayEnabled
- 执行
+ [UIDebuggingInformationOverlay prepareDebuggingOverlay]
。(lldb) exp -i 0 -O -- [UIDebuggingInformationOverlay prepareDebuggingOverlay]
- 修改
_UIGetDebuggingOverlayEnabled()
的返回值。(lldb) finish (lldb) po $al = 0xff
- 恢复运行。
(lldb) continue
这只是很多方法中的一种,绕过iOS 11
以后苹果防止你使用这些私有类而设置检测的方式。
1.3 Method Swizzling
method swizzling
就是运行时动态地改变一个OC方法行为的过程。编译好的代码是在二进制代码的__TEXT
段,我们没法修改。然而,执行OC代码时,实际是通过objc_msgSend
进行方法调用的。
method swizzling
有很多作用,但经常人们只是策略性修改一个参数或者返回值。或者,他们可以窥探一个方法是怎么执行的,而不需要研究汇编代码。实际上,苹果甚至自己都用method swizzling
来实现KVO
等技术。这里我们不详细讨论method swizzling,如果你想看可以去链接里面详细了解。
下面,我们直接开始。简单在视图中间展示了一个按钮,点击后弹出UIDebuggingInformationOverlay
的视图。
我们在NSObject+UIDebuggingInformationOverlayInjector.m
文件中开始写代码。
//声明一个NSObject扩展,主要是解决调用私有方法过不了编译
//告诉编译器我们实现了_setWindowControlsStatusBarOrientation:方法
@interface NSObject()
- (void)_setWindowControlsStatusBarOrientation:(BOOL)orientation;
@end
//UIDebuggingInformationOverlay的父类是UIWindow
//声明一个继承与UIWindow的FakeWindowClass
@interface FakeWindowClass : UIWindow
@end
@implementation FakeWindowClass
- (instancetype)initSwizzled
{
if (self= [super init]) {
[self _setWindowControlsStatusBarOrientation:NO];
}
return self;
}
@end
//完成NSObject的UIDebuggingInformationOverlayInjector扩展
@implementation NSObject (UIDebuggingInformationOverlayInjector)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//确保我们可以拿到UIDebuggingInformationOverlay类
Class cls = NSClassFromString(@"UIDebuggingInformationOverlay");
NSAssert(cls, @"DBG Class is nil?");
//把UIDebuggingInformationOverlay类的init方法,换成我们的FakeWindowClass类的initSwizzled方法
//就把init中的检测全部去掉了
[FakeWindowClass swizzleOriginalSelector:@selector(init) withSizzledSelector:@selector(initSwizzled) forClass:cls isClassMethod:NO];
//把UIDebuggingInformationOverlay类的prepareDebuggingOverlay方法,换成我们NSObject类的prepareDebuggingOverlaySwizzled方法
//然后我们来跳过之前提到的检测
[self swizzleOriginalSelector:@selector(prepareDebuggingOverlay) withSizzledSelector:@selector(prepareDebuggingOverlaySwizzled) forClass:cls isClassMethod:YES];
});
}
//重新实现prepareDebuggingOverlay方法
//或者说我们要复写原来prepareDebuggingOverlay的部分汇编代码
+ (void)prepareDebuggingOverlaySwizzled {
//获取到UIDebuggingInformationOverlay类
Class cls = NSClassFromString(@"UIDebuggingInformationOverlay");
//因为实现了方法交换,我们需要拿到原来方法的选择器
//所以我们用它交换后的名字prepareDebuggingOverlaySwizzled
SEL sel = @selector(prepareDebuggingOverlaySwizzled);
//通过拿到的类实例和选择器实例获取方法
Method m = class_getClassMethod(cls, sel);
//通过方法获取到具体的实现
IMP imp = method_getImplementation(m);
//hack的核心部分,我们会详细分析
void (*methodOffset) = (void *)((imp + (long)16));
void *returnAddr = &&RETURNADDRESS;
__asm__ __volatile__(
"pushq %0\n\t"
"pushq %%rbp\n\t"
"movq %%rsp, %%rbp\n\t"
"pushq %%r14\n\t"
"pushq %%rbx\n\t"
"jmp *%1\n\t"
:
: "r" (returnAddr), "r" (methodOffset));
RETURNADDRESS: ;
}
//Method Swizzling的核心交换方法的实现
+ (void)swizzleOriginalSelector:(SEL)originalSelector withSizzledSelector:(SEL)swizzledSelector forClass:(Class)class isClassMethod:(BOOL)isClassMethod
{
Method originalMethod;
Method swizzledMethod;
if (isClassMethod) {
originalMethod = class_getClassMethod(class, originalSelector);
swizzledMethod = class_getClassMethod([self class], swizzledSelector);
} else {
originalMethod = class_getInstanceMethod(class, originalSelector);
swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
}
NSAssert(originalMethod, @"originalMethod should not be nil");
NSAssert(swizzledMethod, @"swizzledMethod should not be nil");
method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end
- 最难的部分
void (*methodOffset) = (void *)((imp + (long)16)); //1
void *returnAddr = &&RETURNADDRESS; //2
__asm__ __volatile__( //3
"pushq %0\n\t" //3.1
"pushq %%rbp\n\t" //3.2
"movq %%rsp, %%rbp\n\t" //3.3
"pushq %%r14\n\t" //3.4
"pushq %%rbx\n\t" //3.5
"jmp *%1\n\t" //3.6
:
: "r" (returnAddr), "r" (methodOffset)); //3.7
RETURNADDRESS: ; //4
- 首先我们通过方法的指针和
(long)16
拿到了一个方法偏移指针。
(lldb) disassemble -n "+[UIDebuggingInformationOverlay prepareDebuggingOverlay]" -c10
UIKitCore`+[UIDebuggingInformationOverlay prepareDebuggingOverlay]:
0x7fff483f32d2 <+0>: pushq %rbp
0x7fff483f32d3 <+1>: movq %rsp, %rbp
0x7fff483f32d6 <+4>: pushq %r14
0x7fff483f32d8 <+6>: pushq %rbx
0x7fff483f32d9 <+7>: callq 0x7fff483f4018 ; _UIGetDebuggingOverlayEnabled
0x7fff483f32de <+12>: testb %al, %al
0x7fff483f32e0 <+14>: je 0x7fff483f333d ; <+107>
0x7fff483f32e2 <+16>: movq 0x41483f2f(%rip), %rdi ; (void *)0x00007fff87d0e358: NSNotificationCenter
0x7fff483f32e9 <+23>: movq 0x41431750(%rip), %rsi ; "defaultCenter"
0x7fff483f32f0 <+30>: movq 0x3e388029(%rip), %r14 ; (void *)0x00007fff513f7780: objc_msgSend
还记得prepareDebuggingOverlay
的汇编代码么,我们用方法偏移直接跳到了+16
这一行,那么+7
、+12
、+14
的检测逻辑就跳过了。
- 我们要伪装成一个
call
指令,那么久需要一个返回地址。这里我们通过拿到一个伪指令(label)的地址来实现。伪指令(label)不是一个普通开发者常用的特性,它的作用就是允许你jmp
到函数的任何一个地方。如今,在代码中使用伪指令(label)是一个很糟糕的实践,因为if/for/while
实现一样的效果。但我们在hack
,不是么😈。 - 就是我们的汇编代码了。我们这里只能使用
AT&T
的格式来写x86_64
的汇编代码。__volatile__
是告诉编译器不要试图优化这段代码。你可以把这个看做是一个类似printf
的代码,%0
和%1
只是指代我们稍后要传入的变量。- 3.1意思是我们要把函数的返回地址入栈,是不是让你想到的
call
指令😈。 - 3.2~3.5是直接从prepareDebuggingOverlay的汇编代码中超过来的,即偏移量
+7
之前的程序序言。 - 3.6我们直接绕过
+7
、+12
、+14
的检测逻辑,跳转到+16
的逻辑。 - 3.7的
r
告诉汇编你的汇编指令可以用任何寄存器来读取这些值。
- 3.1意思是我们要把函数的返回地址入栈,是不是让你想到的
-
RETURNADDRESS
伪指令的声明。后面的分号是必须的,因为C的语法要求的。
最后我们看看我们VC中实现了什么。
class ViewController: UIViewController {
//检测我们是否完成初始初始化
var hasPerformedSetup: Bool = false
// 按钮点击回调
@IBAction func overlayButtonTapped(_ sender: Any) {
//确保UIDebuggingInformationOverlay存在
guard let cls = NSClassFromString("UIDebuggingInformationOverlay") as? UIWindow.Type else {
print("UIDebuggingInformationOverlay class doesn't exist!")
return
}
//没有完成初始化,则调用UIDebuggingInformationOverlay的prepareDebuggingOverlay方法
if !hasPerformedSetup {
cls.perform(NSSelectorFromString("prepareDebuggingOverlay"))
hasPerformedSetup = true
}
//伪装一个tap事件
let tapGesture = UITapGestureRecognizer()
tapGesture.state = .ended
//拿到UIDebuggingInformationOverlayInvokeGestureHandler的单例mainHandler
//然后直接调用_handleActivationGesture:传入伪装的点击
let handlerCls = NSClassFromString("UIDebuggingInformationOverlayInvokeGestureHandler") as! NSObject.Type
let handler = handlerCls.perform(NSSelectorFromString("mainHandler")).takeUnretainedValue()
let _ = handler.perform(NSSelectorFromString("_handleActivationGesture:"), with: tapGesture)
}
}
我们来测试一下🎉🎉🎉
点击按钮之后的效果
网友评论