(六) Method Swizzling

作者: 收纳箱 | 来源:发表于2020-03-07 11:21 被阅读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]"
    

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

    • 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

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

    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这个就是我们想得到的符号名了。

    • 通过修改内存来绕过检测
      我们先来看看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
    

    我们看到注意到偏移量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)
      }
    }
    

    我们来测试一下🎉🎉🎉


    点击按钮之后的效果

    相关文章

      网友评论

        本文标题:(六) Method Swizzling

        本文链接:https://www.haomeiwen.com/subject/lwpwrhtx.html