注意系统库的坑之load函数调用多次

水文一篇

今天在群友逆向企业微信的时候,发现了一个比较有意思的现象了,发现对于NSObject添加的load的方法执行了两次,导致原本意图的Swizzle出现了问题。

之前在个人的理解中,loadinitialize函数有所不同,load是在加载二进制程序的时候,将这些二进制程序中的类中包含的load方法进行一一调用,调用过程中不会有调用父类的情况。而initialize则不同,是在类第一次使用的过程中进行调用,同时也会有过程中调用父类的情况。

所以,今天一开始这个情况有点懵逼啊,来看看究竟是为啥。

准备工作(略)

  1. PP助手上下载一个企业微信
  2. 重签名 -> Build
  3. 写一个诸如下面这么简单的NSObject Category,并实现+(void)Load方法

    @implementation NSObject (injectLocation)
    + (void)load
    {
        NSLog(@"我好弱");
    }
    @end
    

排查过程

按照我们对load函数的理解,程序加载开始的时候,会通过libobjccall_load_methods遍历逐一执行所有的load方法,如下图印证:

一开始当我在使用iOS 10.3.3的设备进行测试的时候,这就是唯一一次调用,没有二次重入的状况。

于是我按照群友的提示换了iOS 11的设备,果不其然,iOS 11的企业微信在登录过程中,会再次调用我这个分类的load方法,让我们一起来看看调用栈:

卧槽,又从WebThread这个类里面进行了调用了load,匪夷所思啊。

lldb调试下,结果如下:

frame #0: 0x0000000107a2558c libZXLQYWechatDylib.dylib`+[NSObject(self=SKUIMetricsAppLaunchEvent, _cmd="load") load] at TestCategory.m:15
frame #1: 0x0000000196767f9c StoreKitUI`+[SKUIMetricsAppLaunchEvent load] + 44
frame #2: 0x00000001807fa91c libobjc.A.dylib`call_load_methods + 184
frame #3: 0x00000001807fba84 libobjc.A.dylib`load_images + 76
frame #4: 0x00000001074e6170 dyld`dyld::notifySingle(dyld_image_states, ImageLoader const*, ImageLoader::InitializerTimingList*) + 488
frame #5: 0x00000001074f6ce8 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 348
frame #6: 0x00000001074f6c90 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 260
frame #7: 0x00000001074f6c90 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 260
frame #8: 0x00000001074f6c90 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 260
frame #9: 0x00000001074f5d40 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 136
frame #10: 0x00000001074f5dfc dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 84
frame #11: 0x00000001074e979c dyld`dyld::runInitializers(ImageLoader*) + 88
frame #12: 0x00000001074f0324 dyld`dlopen + 976
frame #13: 0x0000000180ccf4d4 libdyld.dylib`dlopen + 116
frame #14: 0x0000000189caec58 WebCore`initWebFilterEvaluator() + 36

从上述链路看起来:WebCore通过dlopen加载了/System/Library/PrivateFrameworks/StoreKitUI.framework/StoreKitUI这个动态库,然后动态库加载完成后,执行了和主二进制一样的call_load_methods过程。

逐一执行load的过程中,会调用到这个类SKUIMetricsAppLaunchEvent,然后这个类执行的汇编我们看看:

StoreKitUI`+[SKUIMetricsAppLaunchEvent load]:
->  0x196767f70 <+0>:  sub    sp, sp, #0x20             ; =0x20 
    0x196767f74 <+4>:  stp    x29, x30, [sp, #0x10]
    0x196767f78 <+8>:  add    x29, sp, #0x10            ; =0x10 
    0x196767f7c <+12>: str    x0, [sp]
    0x196767f80 <+16>: adrp   x8, 108130
    0x196767f84 <+20>: ldr    x8, [x8, #0xff8]
    0x196767f88 <+24>: str    x8, [sp, #0x8]
    0x196767f8c <+28>: adrp   x8, 108114
    0x196767f90 <+32>: ldr    x1, [x8, #0xf70]
    0x196767f94 <+36>: mov    x0, sp
    0x196767f98 <+40>: bl     0x1902ccaac
    0x196767f9c <+44>: adrp   x8, 111804
    0x196767fa0 <+48>: ldr    x8, [x8, #0x6e0]
    0x196767fa4 <+52>: cmn    x8, #0x1                  ; =0x1 
    0x196767fa8 <+56>: b.ne   0x196767fb8               ; <+72>
    0x196767fac <+60>: ldp    x29, x30, [sp, #0x10]
    0x196767fb0 <+64>: add    sp, sp, #0x20             ; =0x20 
    0x196767fb4 <+68>: ret    
    0x196767fb8 <+72>: adrp   x0, 111804
    0x196767fbc <+76>: add    x0, x0, #0x6e0            ; =0x6e0 
    0x196767fc0 <+80>: adrp   x1, 93832
    0x196767fc4 <+84>: add    x1, x1, #0xf60            ; =0xf60 
    0x196767fc8 <+88>: bl     0x19684f598               ; symbol stub for: __copy_helper_block_.236
    0x196767fcc <+92>: b      0x196767fac               ; <+60>

看起来没有关键字stub for objc_msgSend之类的关键字,那我们就重点关注几个跳转指令对应的地址。

排除掉 b 0x196767facb.ne 0x196767fb8,因为这两地址就属于本函数。

通过lldb一查询看看剩下的0x1902ccaac是干啥的,卧槽,没结果。那干脆断这个地址试试,然后继续执行,得到如下结果:

0x1902ccaac: b      0x1886362ac
0x1902ccab0: b      0x188637ae8
0x1902ccab4: b      0x1886362e8
0x1902ccab8: b      0x1886365a4
0x1902ccabc: b      0x18863ada8
0x1902ccac0: b      0x1886365b4
0x1902ccac4: b      0x18863889c
0x1902ccac8: b      0x188636b2c

好吧,看起来这是运行时创建的桥(trampoline)。继续断0x1886362ac,然后执行:

0x1886362ac: b      0x18080c620               ; objc_msgSendSuper2
0x1886362b0: b      0x180814250               ; objc_release
0x1886362b4: b      0x180814190               ; objc_retain
0x1886362b8: b      0x1808165f0               ; objc_retainAutorelease
0x1886362bc: b      0x180816558               ; objc_retainAutoreleaseReturnValue
0x1886362c0: b      0x180816588               ; objc_retainAutoreleasedReturnValue
0x1886362c4: b      0x180802fa8               ; class_addMethod
0x1886362c8: b      0x18080157c               ; class_getInstanceMethod

哈哈,看到我们想要的代码了:

0x1886362ac: b 0x18080c620 ; objc_msgSendSuper2

从这段汇编不难看出,在+[SKUIMetricsAppLaunchEvent load]方法里面,会调用[super load]这样的代码。

为啥iOS 10上没有问题

在iOS 10上其实也有同样的问题,但是由于WebCore不会主动把对应的StoreKitUI加载进来,所以也就没出触发这样的问题,但是如果我们主动通过dlopen加载这个系统库,也一样有问题:

__attribute__((constructor)) void load_private()
{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        void *libHandleIMD = dlopen("/System/Library/PrivateFrameworks/StoreKitUI.framework/StoreKitUI", RTLD_LAZY);
        NSLog(@"libHandleIMD is %p", libHandleIMD);
        if (!libHandleIMD) {
            printf("error is %s\n", dlerror());
        }
    });
}

提醒

对于在系统类上添加的load方法,建议还是做是否是重入的判断或者保护,不然很可能出现与预期不相符的结果。