水文一篇
今天在群友逆向企业微信的时候,发现了一个比较有意思的现象了,发现对于NSObject
添加的load
的方法执行了两次,导致原本意图的Swizzle
出现了问题。
之前在个人的理解中,load
和initialize
函数有所不同,load
是在加载二进制程序的时候,将这些二进制程序中的类中包含的load
方法进行一一调用,调用过程中不会有调用父类的情况。而initialize
则不同,是在类第一次使用的过程中进行调用,同时也会有过程中调用父类的情况。
所以,今天一开始这个情况有点懵逼啊,来看看究竟是为啥。
准备工作(略)
- PP助手上下载一个企业微信
- 重签名 -> Build
写一个诸如下面这么简单的
NSObject Category
,并实现+(void)Load
方法@implementation NSObject (injectLocation) + (void)load { NSLog(@"我好弱"); } @end
排查过程
按照我们对load
函数的理解,程序加载开始的时候,会通过libobjc
的call_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 0x196767fac
和b.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
方法,建议还是做是否是重入的判断或者保护,不然很可能出现与预期不相符的结果。