运行时Hook所有Block方法调用的技术实现

作者 : 开心源码 本文共6896个字,预计阅读时间需要18分钟 发布时间: 2022-05-12 共202人阅读

本技术实现在YSBlockHook中。

1.方法调用的几种Hook机制

iOS系统中一共有:C函数、Block、OC类方法三种形式的方法调用。Hook一个方法调用的目的一般是为了监控阻拦或者者统计少量系统的行为。Hook的机制有很多种,通常良好的Hook方法都是以AOP的形式来实现的。

当我们想Hook一个OC类的某些具体的方法时可以通过Method Swizzling技术来实现、当我们想Hook动态库中导出的某个C函数时可以通过修改导入函数地址表中的信息来实现(可以使用开源库fishhook来完成)、当我们想Hook所有OC类的方法时则可以通过替换objc_msgSend系列函数来实现。。。

那么对于Block方法呢而言呢?

2.Block的内部实现原理和实现机制简介

这里假定你对Block内部实现原理和运行机制有所理解,假如不理解则请参考文章《深入解构iOS的block闭包实现原理》或者者自行通过搜索引擎搜索。

源程序中定义的每个Block在编译时都会转化为一个和OC类对象布局类似的对象,每个Block也存在着isa这个数据成员,根据isa指向的不同,Block分为__NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三种类型。也就是说从某种程度上Block对象也是一种OC对象。下面的类图形容了Block类的层次结构。

Block类层次结构图

Block类以及其派生类在CoreFoundation.framework中被定义和实现,并且没有对外公开。

每个Block对象在内存中的布局,也就是Block对象的存储结构被定义如下(代码出自苹果开源出来的库实现libclosure中的文件Block_private.h):

//需要注意的是下面两个只是模板,具体的每个Block定义时总是按这个模板来定义的。//Block形容,每个Block一个形容并定义在全局数据段struct Block_descriptor_1 {    uintptr_t reserved;   //记住这个变量和结构体,它很重要!!    uintptr_t size;};//Block对象的内存布局struct Block_layout {    void *isa;    volatile int32_t flags; // contains ref count    int32_t reserved;    uintptr_t invoke;   //Block对象的实现函数    struct Block_descriptor_1 *descriptor;    // imported variables,这里是每个block对象的特定数据成员区域};

这里要关注一下struct Block_descriptor_1中的reserved这个数据成员,尽管系统没有用到它,但是下面就会用到它而且很重要!

在理解了Block对象的类型以及Block对象的内存布局后,再来考察一下一个Block从定义到调用是如何实现的。就以下面的源代码为例:

int main(int argc, char *argv[]){   //定义    int a = 10;    void (^testblock)(void) = ^(){        NSLog(@"Hello world!%d", a);    };        //执行    testblock();    return 0;}

在将OC代码翻译为C语言代码后每个Block的定义和调用将变成如下的伪代码:

//testblock的形容信息struct Block_descriptor_1_fortestblock {    uintptr_t reserved;     uintptr_t size;};//testblock的布局存储结构体struct Block_layout_fortestblock {    void *isa;    volatile int32_t flags; // contains ref count    int32_t reserved;    uintptr_t invoke;   //Block对象的实现函数    struct Block_descriptor_1_fortestblock *descriptor;    int m_a;  //外部的传递进来的数据。};//testblock函数的实现。void main_invoke_fortestblock(struct Block_layout_fortestblock *cself){      NSLog(@"Hello world!%d", cself->m_a);}//testblock对象形容的实例,存储在全局内存区struct Block_descriptor_1_fortestblock  _testblockdesc = {0, sizeof(struct Block_layout_fortestblock)};int main(int argc, char *argv[]){   //定义部分    int a = 10;    struct Block_layout_fortestblock testblock = {            .isa = __NSConcreteStackBlock,            .flags =0,            .reserved = 0,            .invoke = main_invoke_fortestblock,            .descriptor = & _testblockdesc,            .m_a = a    };   //调用部分   testblock.invoke();       return 0;}

可以看出Block对象的生成和调用都是在编译期间就已经固定在代码中了,它不像其余OC对象调用方法时需要通过runtime来执行间接调用。并且线上程序中所有关于Block的符号信息都会被strip掉。所以上述的所详情的几种Hook方法都无法Hook住一个Block对象的函数调用。

假如想要Hook住系统的所有Block调用,需要处理如下几个问题:
a. 如何在运行时将所有的Block的invoke函数替换为一个统一的Hook函数。
b. 这个统一的Hook函数如何调用原始Block的invoke函数。
c. 如何构建这个统一的Hook函数。

3.实现Block对象Hook的方法和原理

一个OC类对象的实例通过引用计数来管理对象的生命周期。在MRC时代当对象进行赋值和拷贝时需要通过调用retain方法来实现引用计数的添加,而在ARC时代对象进行赋值和拷贝时就不再需要显示调用retain方法了,而是系统内部在编译时会自动插入相应的代码来实现引用计数的增加和减少。不论如何只需是对OC对象执行赋值拷贝操作,最终内部都会调用retain方法。

Block对象也是一种OC对象!!

每当一个Block对象在需要进行赋值或者者拷贝操作时,也会激发对retain方法的调用。由于Block对象赋值操作一般是发生在Block方法执行之前,因而我们可以通过Method Swizzling的机制来Hook 类的retain方法,而后在重写的retain方法内部将Block对象的invoke数据成员替换为一个统一的Hook函数!

通过考察__NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三个类的实现发现这三个类都重载了NSObject的retain方法,这样在执行Method Swizzling时就不需要对NSObject的retain方法执行替换,而只需对上述三个类的retain执行替换就可。

你可以说出为什么这三个派生类都会对retain方法进行重载吗?答案可以从这三种Block的类型定义以及所表示的意义中去寻觅。

Block技术不仅可以用在OC语言中,LLVM对C语言进行的扩展也能使用Block,比方gcd库中大量的使用了Block。在C语言中假如对一个Block进行赋值或者者拷贝系统需要通过C库函数:

//函数公告在Block.h头文件汇总// Create a heap based copy of a Block or simply add a reference to an existing one.// This must be paired with Block_release to recover memory, even when running// under Objective-C Garbage Collection.BLOCK_EXPORT void *_Block_copy(const void *aBlock)    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

来实现,这个函数定义在libsystem_blocks.dylib库中,并且库实现已经开源:libclosure。因而可以借助fishhook库来对__Block_copy这个函数进行替换解决,而后在替换的函数函数中将一个Block的原始的invoke函数替换为统一的Hook函数。

另外一个C语言函数objc_retainBlock,也是实现了对Block进行赋值时的引用计数添加,这个函数内部就是简单的调用__Block_copy方法。因而我们也可以增加对objc_retainBlock的替换解决。

处理了第一个问题后,接下来再处理第二个问题。还记得上面提到过的struct Block_descriptor_1中的reserved这个数据成员吗? 当我们通过上述的方法对所有Block对象的invoke成员替换为一个统一的Hook函数前,可以将Block对象的原始invoke函数保存到这个保留字段中去。而后即可以在统一的Hook函数内部读取这个保留字段中的保存的原始invoke函数来执行真实的方法调用了。

由于一个Block对象函数的第一个参数其实是一个隐藏的参数,这个隐藏的参数就是Block对象本身,因而很容易即可以从隐藏的参数中来获取到对应的保留字段。

下面的代码将展现通过方法交换来实现Hook解决的伪代码

struct Block_descriptor {    void *reserved;    uintptr_t size;};struct Block_layout {    void *isa;    int32_t flags; // contains ref count    int32_t reserved;    void  *invoke;    struct Block_descriptor *descriptor;};//统一的Hook函数,这里以伪代码的形式提供void blockhook(void *obj, ...){   struct Block_layout *layout = (struct Block_layout*) obj;   //调用原始的invoke函数   layout->descriptor->reserved(...);}//执行Block对象的方法替换解决void replaceBlockInvokeFunction(const void *blockObj, void *newFunction){   struct Block_layout *layout = (struct Block_layout*)blockObj;   if (layout != NULL && layout->descriptor != NULL){           layout->descriptor->reserved = layout->invoke;           layout->invoke = newFunction;    }}void *(*__NSStackBlock_retain_old)(void *obj, SEL cmd) = NULL;void *__NSStackBlock_retain_new(void *obj, SEL cmd){    replaceBlockInvokeFunction(obj, blockhook);    return __NSStackBlock_retain_old(obj, cmd);}void *(*__NSMallocBlock_retain_old)(void *obj, SEL cmd) = NULL;void *__NSMallocBlock_retain_new(void *obj, SEL cmd){    replaceBlockInvokeFunction(obj, blockhook);    return __NSMallocBlock_retain_old(obj, cmd);}void *(*__NSGlobalBlock_retain_old)(void *obj, SEL cmd) = NULL;void *__NSGlobalBlock_retain_new(void *obj, SEL cmd){    replaceBlockInvokeFunction(obj, blockhook);    return __NSGlobalBlock_retain_old(obj, cmd);}int main(int argc, char *argv[]){      //由于类名和方法名都不能直接使用,所以这里都以字符串的形式来转换获取。    __NSStackBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSStackBlock"), sel_registerName("retain"), (IMP)__NSStackBlock_retain_new, nil);    __NSMallocBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSMallocBlock"), sel_registerName("retain"), (IMP)__NSMallocBlock_retain_new, nil);    __NSGlobalBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSGlobalBlock"), sel_registerName("retain"), (IMP)__NSGlobalBlock_retain_new, nil);    return 0; }

处理了第二个问题后,就需要处理第三个问题。上面的统一Hook函数blockhook只是一个伪代码实现,由于任何一个Block中的函数的参数类型和个数是不一样的,而且统一Hook函数也需要在适当的时候调用原始的默认Block函数实现,并且不能破坏参数信息。为理解决这些问题就使得这个统一的Hook函数不能用高级语言来实现,而只能用汇编语言来实现。下面就是在arm64位体系下的实现代码:

.text.align 5.private_extern _blockhook   _blockhook:   //为了不破坏原有参数,这里将所有参数压入栈中  stp q6, q7, [sp, #-0x20]!  stp q4, q5, [sp, #-0x20]!  stp q2, q3, [sp, #-0x20]!  stp q0, q1, [sp, #-0x20]!  stp x6, x7, [sp, #-0x10]!  stp x4, x5, [sp, #-0x10]!  stp x2, x3, [sp, #-0x10]!  stp x0, x1, [sp, #-0x10]!  stp x8, x30, [sp, #-0x10]!    //这里可以增加任意逻辑来进行hook解决。  //这里将所有参数复原  ldp x8, x30, [sp], #0x10  ldp x0, x1, [sp], #0x10  ldp x2, x3, [sp], #0x10  ldp x4, x5, [sp], #0x10  ldp x6, x7, [sp], #0x10  ldp q0, q1, [sp], #0x20  ldp q2, q3, [sp], #0x20  ldp q4, q5, [sp], #0x20  ldp q6, q7, [sp], #0x20  ldr x16, [x0, #0x18]   //将block对象的descriptor数据成员取出  ldr x16, [x16]         //获取descriptor中的reserved成员  br x16                 //执行reserved中保存的原始函数指针。LExit_blockhook:

关于在运行时Hook所有Block方法调用的技术实现原理就详情到这里了。当然一个完整的系统可能需要其余少量能力:

  • 假如你只想Hook可执行程序中定义的Block,那么请参考我的文章:深入iOS系统底层之映像操作API详情 中的内容来实现Hook函数的过滤解决。
  • 假如你不想借助Block_descriptor中的reserved来保存原始的invoke函数,那么可以参考我的文章:Thunk程序的实现原理以及在iOS中的应用(二)中详情的技术来实现统一Hook函数以及完成对原始invoke函数的调用技术。

具体完整的代码可以访问我的github中的项目:YSBlockHook。这个项目以AOP的形式实现了真机arm64位模式下对可执行程序中所有定义的Block进行Hook的方法,Hook所做的事情就是在所有Block调用前,打印出这个Block的符号信息。


欢迎大家访问欧阳大哥2013的github地址和简书地址

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 运行时Hook所有Block方法调用的技术实现

发表回复