12

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

 3 years ago
source link: http://satanwoo.github.io/2017/11/02/load-twice/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

今天在群友逆向企业微信的时候,发现了一个比较有意思的现象了,发现对于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方法,如下图印证:

load_twice_2.png?raw=true

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

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

load_twice_1.png?raw=true

卧槽,又从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方法,建议还是做是否是重入的判断或者保护,不然很可能出现与预期不相符的结果。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK