Apple高级调试与逆向工程

(六) Method Swizzling

2020-03-07  本文已影响0人  收纳箱

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 9iOS 10中,开启这个类是非常容易的。只需要在LLDB中输入下面两句代码就可以开启。

(lldb) po [UIDebuggingInformationOverlay prepareDebuggingOverlay]
(lldb) po [[UIDebuggingInformationOverlay overlay] toggleVisibility]

我们在iOS 11模拟器上附着LLDBPhotos上。

~> 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]"

这里做了大致的翻译工作:

@implementation UIDebuggingInformationOverlay
- (instancetype)init {
    if (self = [super init]) {
        [self _setWindowControlsStatusBarOrientation:NO];
    }
    return self;
}
@end
@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

从上面的汇编代码来看,我们有两个办法来获得我们感兴趣的内存地址。

  1. 你可以通过RIP来访问这个变量。比如,我们例子中是[rip + 0x41538710]。我们知道RIP中存储的是下一条命令的执行地址。所以,实际[rip + 0x41538710] = [0x7fff483f31d0 + 0x41538710] = 0x00007fff8992b8e0

  2. 你可以用image lookup方法与verbosesymbol选项来找到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这个就是我们想得到的符号名了。

(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

我们看到注意到偏移量71214执行了一个叫_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
};

对比声明,我们可以知道0x3UIGestureRecognizerStateEnded。也就是说,UIKit的开发者不仅加了UIDebuggingInformationOverlay类的访问控制,还在状态栏加了一个双指点击的“彩蛋”来执行配置操作。

  1. 我们先找到UIDebuggingOverlayIsEnabled.onceToken的内存地址。
    (lldb) image lookup -vs UIDebuggingOverlayIsEnabled.onceToken
    
  2. 通过memory write操作或直接用一个long指针赋值将它的值设置为-1
    (lldb) po *(long *) 0x00007fff8992b8e0 = -1
    
  3. UIDebuggingOverlayIsEnabled.__overlayIsEnabled执行1、2步同样的操作。
    (lldb) image lookup -vs UIDebuggingOverlayIsEnabled.__overlayIsEnabled
    (lldb) po *(long *) 0x00007fff8992b8d8 = -1
    
  4. _UIGetDebuggingOverlayEnabled()上设置一个断点。
    (lldb) b _UIGetDebuggingOverlayEnabled
    
  5. 执行+ [UIDebuggingInformationOverlay prepareDebuggingOverlay]
    (lldb) exp -i 0 -O -- [UIDebuggingInformationOverlay prepareDebuggingOverlay]
    
  6. 修改_UIGetDebuggingOverlayEnabled()的返回值。
    (lldb) finish
    (lldb) po $al = 0xff
    
  7. 恢复运行。
    (lldb) continue
    

这只是很多方法中的一种,绕过iOS 11以后苹果防止你使用这些私有类而设置检测的方式。

1.3 Method Swizzling

method swizzling就是运行时动态地改变一个OC方法行为的过程。编译好的代码是在二进制代码的__TEXT段,我们没法修改。然而,执行OC代码时,实际是通过objc_msgSend进行方法调用的。

method swizzling有很多作用,但经常人们只是策略性修改一个参数或者返回值。或者,他们可以窥探一个方法是怎么执行的,而不需要研究汇编代码。实际上,苹果甚至自己都用method swizzling来实现KVO等技术。这里我们不详细讨论method swizzling,如果你想看可以去链接里面详细了解。

下面,我们直接开始。简单在视图中间展示了一个按钮,点击后弹出UIDebuggingInformationOverlay的视图。

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
  1. 首先我们通过方法的指针和(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的检测逻辑就跳过了。

  1. 我们要伪装成一个call指令,那么久需要一个返回地址。这里我们通过拿到一个伪指令(label)的地址来实现。伪指令(label)不是一个普通开发者常用的特性,它的作用就是允许你jmp到函数的任何一个地方。如今,在代码中使用伪指令(label)是一个很糟糕的实践,因为if/for/while实现一样的效果。但我们在hack,不是么😈。
  2. 就是我们的汇编代码了。我们这里只能使用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. 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)
  }
}

我们来测试一下🎉🎉🎉


点击按钮之后的效果
上一篇 下一篇

猜你喜欢

热点阅读