运行时Hook全部Block方法调用的技术实现

本技术实如今YSBlockHook中。git

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

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

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

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

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

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

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

Block类层次结构图

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

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

//须要注意的是下面两个只是模板,具体的每一个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从定义到调用是如何实现的。就如下面的源代码为例:spa

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(testblock);
   
    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(...);
}
//模拟器下若是返回类型是结构体而且大于16字节那么第一个参数是返回值保存的内存地址,block对象变为第二个参数
void blockhook_stret(void *pret, void *obj, ...)
{
   struct Block_layout *layout = (struct Block_layout*) obj;
   //调用原始的invoke函数
   layout->descriptor->reserved(...);
}

//执行Block对象的方法替换处理
void replaceBlockInvokeFunction(const void *blockObj)
{
   struct Block_layout *layout = (struct Block_layout*)blockObj;
   if (layout != NULL && layout->descriptor != NULL){
         int32_t BLOCK_USE_STRET = (1 << 29);  //若是模拟器下返回的类型是一个大于16字节的结构体,那么block的第一个参数为返回的指针,而不是block对象。
         void *hookfunc = ((layout->flags & BLOCK_USE_STRET) == BLOCK_USE_STRET) ? blockhook_stret : blockhook;
         if (layout->invoke != hookfunc){
                layout->descriptor->reserved = layout->invoke;
                layout->invoke = hookfunc;
            }
    }
}

void *(*__NSStackBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSStackBlock_retain_new(void *obj, SEL cmd)
{
    replaceBlockInvokeFunction(obj);
    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);
    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);
    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_stret只是伪代码实现,由于任何一个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:

复制代码

对于x86_64/arm32位系统来讲,若是block函数的返回是一个结构体而且长度超过16字节(arm32是8字节)。那么block对象里面的flags属性就会设置为BLOCK_USE_STRET。而x86_64/arm32位系统对于这种返回类型的函数就会将返回值存放到第一个参数所指向的内存中,同时会把本来的block对象变化为第二个参数,所以须要对这种状况进行特殊处理。

关于在运行时Hook全部Block方法调用的技术实现原理就介绍到这里了。固然一个完整的系统可能须要其余一些能力:

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


欢迎你们访问欧阳大哥2013的github地址简书地址

相关文章
相关标签/搜索