欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

程序员文章站 2022-11-30 18:38:33
黑客技术点击右侧关注,了解黑客的世界!Java开发进阶点击右侧关注,掌握进阶之路!Python开发点击右侧关注,探讨技术话题!作者 | satanwoo来源 | htt......

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

黑客技术

点击右侧关注,了解黑客的世界!

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

Java开发进阶

点击右侧关注,掌握进阶之路!

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

Python开发

点击右侧关注,探讨技术话题!

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

作者 | satanwoo 来源 | http://satanwoo.github.io/

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

本文发表于 2017.9

随着iOS11的正式发布,手淘/天猫也开始逐步用Xcode 9开始编译。在调试过程中,很多同事发现经常报许多API会报线程使用错误的问题。摸索了下,发现是Xcode 9里面带上了一个叫libMainThreadChecker.dylib的动态库,在运行时提供了主线程检查的功能,今天就从探究苹果的实现开始讲起。

0x1 苹果的实现

把苹果的动态库拖入hopper里面看看,基本上扫一眼以后,比较可疑的是__library_initializer和__library_deintializer。

我看反汇编,第一直觉就是猜,然后都试一把。

我们来看看其伪代码实现,可以分为几个部分来探究:


1.1 环境变量

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

从图中不难看出,libMainThreadChecker的运行依赖于许多的环境变量,我们可以在Xcode->Scheme->Arguments里面一个个输入这些变量进行测试,我发现比较重要的是MTC_VERBOSE这个参数,使用后,可以输出究竟对于哪些类进行了线程监控。


可以看出,苹果会在启动前对于这些类进行所谓的线程监控。


1.2 逻辑

看完了输出,我们来看看其中的逻辑实现,如下所示:

CFAbsoluteTimeGetCurrent();
   var_270 = intrinsic_movsd(var_270, xmm0);
   *_indirect__main_thread_checker_on_report = dlsym(0xfffffffffffffffd, "__main_thread_checker_on_report");
   if (objc_getClass("UIView") != 0x0) {
           *_XXKitImage = dyld_image_header_containing_address(objc_getClass("UIView"));
           *_CoreFoundationImage = dyld_image_header_containing_address(_CFArrayGetCount);
           rax = objc_getClass("WKWebView");
           rax = dyld_image_header_containing_address(rax);
           *_WebKitImage = rax;
           *_InlineCallsMachHeaders = *_XXKitImage;
           *0x1ec3e8 = *_CoreFoundationImage;
           *0x1ec3f0 = rax;
           *___CATransaction = objc_getClass("CATransaction");
           *___NSGraphicsContext = objc_getClass("NSGraphicsContext");
           *_SEL_currentState = sel_registerName("currentState");
           *_SEL_currentContext = sel_registerName("currentContext");
           *_MyOwnMachHeader = dyld_image_header_containing_address(___library_initializer);
           *_classesToSwizzle = CFArrayCreateMutable(0x0, 0x200, 0x0);
           var_240 = objc_getClass("UIView");
           _FindClassesToSwizzleInImage(*_XXKitImage, &var_240, 0x2);
           if (*_WebKitImage != 0x0) {
                   var_230 = objc_getClass("WKWebView");
                   *(&var_230 + 0x8) = objc_getClass("WKWebsiteDataStore");
                   *(&var_230 + 0x10) = objc_getClass("WKUserScript");
                   *(&var_230 + 0x18) = objc_getClass("WKUserContentController");
                   *(&var_230 + 0x20) = objc_getClass("WKScriptMessage");
                   *(&var_230 + 0x28) = objc_getClass("WKProcessPool");
                   *(&var_230 + 0x30) = objc_getClass("WKProcessGroup");
                   *(&var_230 + 0x38) = objc_getClass("WKContentExtensionStore");
                   _FindClassesToSwizzleInImage(*_WebKitImage, &var_230, 0x8);
           }
           rcx = CFArrayGetCount(*_classesToSwizzle);
           if (rcx != 0x0) {
                   rax = 0x0;
                   var_278 = rcx;
                   do {
                           var_288 = rax;
                           rax = CFArrayGetValueAtIndex(*_classesToSwizzle, rax);
                           var_258 = rax;
                           rbx = objc_getClass(rax);
                           var_290 = dyld_image_header_containing_address(rbx);
                           var_230 = 0x0;
                           var_280 = rbx;
                           r14 = class_copyMethodList(rbx, &var_230);
                           if (var_230 != 0x0) {
                                   rbx = 0x0;
                                   do {
                                           r13 = *(r14 + rbx * 0x8);
                                           r12 = method_getName(r13);
                                           r15 = sel_getName(r12);
                                           if ((((((((((((((((*(int8_t *)r15 != 0x5f) && (dyld_image_header_containing_address(method_getImplementation(r13)) == var_290)) && (((*(int8_t *)_envIgnoreRetainRelease == 0x0) || (((strcmp(r15, "retain") != 0x0) && (strcmp(r15, "release") != 0x0)) && (strcmp(r15, "autorelease") != 0x0))))) && (((*(int8_t *)_envIgnoreDealloc == 0x0) || ((strcmp(r15, "dealloc") != 0x0) && (strcmp(r15, ".cxx_destruct") != 0x0))))) && (((*(int8_t *)_envIgnoreNSObjectThreadSafeMethods == 0x0) || ((((strcmp(r15, "description") != 0x0) && (strcmp(r15, "debugDescription") != 0x0)) && (strcmp(r15, "self") != 0x0)) && (strcmp(r15, "class") != 0x0))))) && (strcmp(r15, "beginBackgroundTaskWithExpirationHandler:") != 0x0)) && (strcmp(r15, "beginBackgroundTaskWithName:expirationHandler:") != 0x0)) && (strcmp(r15, "endBackgroundTask:") != 0x0)) && (strcmp(r15, "lockFocus") != 0x0)) && (strcmp(r15, "lockFocusIfCanDraw") != 0x0)) && (strcmp(r15, "lockFocusIfCanDrawInContext:") != 0x0)) && (strcmp(r15, "unlockFocus") != 0x0)) && (strcmp(r15, "openGLContext") != 0x0)) && (strncmp(r15, "webThread", 0x9) != 0x0)) && (strncmp(r15, "nsli_", 0x5) != 0x0)) && (strncmp(r15, "nsis_", 0x5) != 0x0)) {
                                                   if (*_userSuppressedClasses != 0x0) {
                                                           rax = CFStringCreateWithCStringNoCopy(0x0, var_258, 0x8000100, *_kCFAllocatorNull);
                                                           var_244 = CFSetContainsValue(*_userSuppressedClasses, rax) != 0x0 ? 0x1 : 0x0;
                                                           CFRelease(rax);
                                                   }
                                                   else {
                                                           var_244 = 0x0;
                                                   }
                                                   if (*_userSuppressedSelectors != 0x0) {
                                                           rax = CFStringCreateWithCStringNoCopy(0x0, r15, 0x8000100, *_kCFAllocatorNull);
                                                           var_250 = rax;
                                                           if (CFSetContainsValue(*_userSuppressedSelectors, rax) != 0x0) {
                                                                   var_244 = 0x1;
                                                           }
                                                           CFRelease(var_250);
                                                   }
                                                   if (*_userSuppressedMethods != 0x0) {
                                                           rax = CFStringCreateWithFormat(0x0, 0x0, @"-[%s %s]");
                                                           var_250 = CFSetContainsValue(*_userSuppressedMethods, rax);
                                                           CFRelease(rax);
                                                           rax = var_250 | var_244;
                                                           if (rax == 0x0) {
                                                                   _addSwizzler(r13, r12, var_258, r15, 0x1);
                                                           }
                                                           else {
                                                                   *_userSuppressionsCount = *_userSuppressionsCount + 0x1;
                                                           }
                                                   }
                                                   else {
                                                           if (var_244 != 0x0) {
                                                                   *_userSuppressionsCount = *_userSuppressionsCount + 0x1;
                                                           }
                                                           else {
                                                                   _addSwizzler(r13, r12, var_258, r15, 0x1);
                                                           }
                                                   }
                                           }
                                           rbx = rbx + 0x1;
                                   } while (rbx < var_230);
                           }
                           _objc_flush_caches(var_280);
                           free(r14);
                           rax = var_288 + 0x1;
                           rcx = var_278;
                   } while (rax != rcx);
           }
           *_totalSwizzledClasses = rcx;
           if (*(int8_t *)_envVerbose != 0x0) {
                   rdx = *_totalSwizzledMethods;
                   fprintf(*___stderrp, "Swizzled %zu methods in %zu classes.
", rdx, rcx);
           }

代码乍一看很多,其实逻辑非常简单,概述如下:

• 通过获取UIView的类实体(不理解类实体的去看runtime)所在的地址来反推所在的image(二进制产物,基本是动态库),这里基本能猜测是UIKit。

• 从UIKit中获取所有继承自UIView和UIApplication的类及其子类(这也是你为什么会在刚刚上文提到的输出中发现UIIBApplication这种不知道啥类的原因),过滤到带_的私有类,然后对剩下的类的所有的方法进行Swizzle。

• 对于需要Swizzle的方法,要额外判断是不是真正属于UIKit这个动态库的。比如我们在调试的时候,Xcode会加载libViewDebugging.dylib等不会用于用于线上的动态库,里面会给UIView填上很多奇奇怪怪的方法。

• 过滤如下的方法,以及以nsli_和nsis_开头的方法。


• 可选,如果还要检查WebKit相关的方法,还可以Hook如下这些类的子类:


0x2 自己实现

当时看到这,关于苹果的实现我觉得实在是太简单了,即使不用私有API,结合现在Github上的*我自己造一个估计1、2个小时就解决了。现在回想起来,自己还是too simple, sometimes native

大致代码获取UIKit中UIView和UIApplication所有子类的代码如下:


2.1 现有方案Hook的缺陷

到这,我们就只差把这些类的方法都Hook掉就行了。传统的Method Swizzling肯定不行,那样我们需要对每个方法对应实现一个新的方法进行替换,工作量太大。所以我们需要一个思路能够中心重定向整个过程。

之前跟着网易iOS大佬刘培庆(现已入职阿里)学习iOS的时候,了解到了AnyMethodLog,听说能监控所有类所有方法的执行,于是我就直接套用了这个框架,嘿嘿,使用起来真方便,看起来大功告成了,Build & Run。

卧槽,怎么运行了就启动崩了,一脸懵逼。

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

没事,我换个开源库BigBang改改。卧槽,还是崩了。这下必须要开下源码分析下原因了。

从AnyMethodLog的实现来看,如下所示:


作者的意图比较简单,主要可以概述为如下几点:

• 把每个类的forwardInvocation,替换成自己实现的一个C函数。

• 把需要Hook原来selector获取的method的IMP指向objc_msgForward,通过其触发消息转发,也就是触发forwardInvocation;

• 对每个需要重定向的selector,生成一个特定的格式的新selector,将其IMP指向原来method的IMP。

• 对于刚刚重定向的C函数,通过NSInvocation获取要调用的target和selector,再次将这个selector生成特定格式的新selector,反射调用。

为啥能把OC的函数forwardInvocation换成C函数,原因就在于只要补上OC函数隐式的前两个参数self, selector,让其的函数签名一致即可。

读到这,看起来没有啥问题吧?为什么会崩溃呢!

原因在于这种调用方式,缺少了super上下文。

假设我们现在对UIView、UIButton都Hook了initWithFrame:这个方法,在调用[[UIView alloc] initWithFrame:]和[[UIButton alloc] initWithFrame:]都会定向到C函数qhd_forwardInvocation中,在UIView调用的时候没问题。但是在UIButton调用的时候,由于其内部实现获取了super initWithFrame:,就产生了循环定向的问题。

问题本质的原因是,由于我们对于父类、子类同名的方法都换成了同一个IMP,那么不论是走objc_msgSend抑或是objc_msgSendSuper2,获取到的IMP都是一致的。而在Hook之前,objc_msgSendSuper2拿到的是super_imp, objc_msgSend拿到是imp,从而不会有问题。

2.2 基于桥的全量Hook方法

好,上面的一个小节我们说,如果我们把所有方法都重定向到一个IMP上的时候,就会丧失关于继承关系之间的父子上下文关系,导致重定向循环。所以,我们需要一个思路,能够正确解决上下文的问题。

首先我们来回顾下runtime的消息转发机制:

1) 调用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允许用户在此时为该 Class 动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。这一次对象会响应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的动作。

2) 调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非 nil 对象。否则返回 nil ,继续下面的动作。注意,这里不要返回 self ,否则会形成死循环。

3) 调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:。

4) 调用forwardInvocation:方法,将第3步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非ni。

5) 调用doesNotRecognizeSelector: ,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。

对于我们来说,我们至少要在第四步之前(确切的是第三步之前),我们就要保留好super上下文。一旦到了forwardInvocation函数,留给我们的又只有self这样的残缺信息了。

哎,我就是卡在这思考了一天,最终我想出了一个思路。

• 提供一个桩WZQMessageStub,这个桩保留了class和selector,拼接成不一样的函数名,这样就能区分UIButton和UIView的同名initWithFrame:方法,因为不同的selector找到的IMP肯定不一样。

• 在NSObject里面实现forwardingTargetForSelector,在消息转发的时候指定把消息全部转发给WZQMessageStub。

• WZQMessageStub实现methodSignatureForSelector和forwardInvocation:方法,承担真正的方法反射调用的职责。

好,思路确定了,难点还剩一个。

对于forwardingTargetForSelector这个函数来说,能拿到的参数也是target和selector。在super和self调用场景下,这个参数毫无价值,因此我们需要从selector上着手。如果不做任何改变,我们这里拿到的selector肯定是诸如initWithFrame:的old selector,所以我们需要在这之前桥一下,可以按照下述流程理解:

每个方法置换到不同的IMP桥上 -> 从桥上反推出当前的调用关系(class和selector)-> 构造一个中间态新名字 -> forwardingTargetForSelector(self, 中间态新名字) OK,大功告成。具体桥的实现我待会再单独开篇博客讲一讲。

嘿嘿,看起来很简单的任务也学习到了不少新知识。一会把代码开源到Github上。

0x3 遗留问题

我在开启Main Thread Chekcer后,build了一次产物,但是在通过Mach-O文件中Load Commands部分的时候,却没有发现libMainThreadChecker.dylib的踪影,如下所示:

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

符号断点dlopen也并没有发现这个动态库调用的踪影,所以非常好奇苹果是怎么加载这个动态库的,有大佬知道请赐教。

 推荐↓↓↓ 

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

?16个技术公众号】都在这里!

涵盖:程序员大咖、源码共读、程序员共读、数据结构与算法、黑客技术和网络安全、大数据科技、编程前端、Java、Python、Web编程开发、Android、iOS开发、Linux、数据库研发、幽默程序员等。

基于桥的全量方法Hook方案 - 探究苹果主线程检查实现

万水千山总是情,点个 “在看” 行不行

本文地址:https://blog.csdn.net/olsQ93038o99S/article/details/100588429