Hook Objective-C中的block

前言

iOS的方法交换能为我们 hook 实例方法,也能为我们 hook 类方法,但是对于 Block 却无能为力,原因很简单,Block并不是一个方法,而是一个函数指针。但是如果你了解了 Block 底层结构,又熟悉了iOS中的消息转发机制。想要 Hook OC 的Block还是能做到的。有关Hook OC Block的文章有许多,目前有两种比较常见的方法来Hook Block:

  • 一种是通过引入 Libffi ,利用 Libffi 在运行时动态定义|调用函数的强大特性,来实现Block的hook。参考文章 Hook Objective-C Block with Libffi。 这里引用原该方案作者的一段原理说明(感谢):
  1. 根据 block 对象的签名,使用 ffi_prep_cif 构建 block->invoke 函数的模板 cif
  2. 使用 ffi_closure,根据 cif 动态定义函数 replacementInvoke,并指定通用的实现函数为 ClosureFunc
  3. block->invoke 替换为 replacementInvoke,原始的 block->invoke 存放在 originInvoke
  4. ClosureFunc 中动态调用 originInvoke 函数和执行 hook 的逻辑。
  • 另一种是通过消息转发的方式,利用 runtime 函数 _objc_msgForward 来实现对Block的hook。参考文章 Block hook 正确姿势? 它的原理比较取巧,看一下原作者的原理说明(感谢):
  1. 保存原来block的副本,因为不影响原有的微信业务逻辑,在hook注入我们自己业务逻辑之后,我们需要回过头响应原有的微信block逻辑;
  2. 强制启动block的消息转发机制;
  3. 在消息转发最后一步,将副本和hook block取出包装成NSInvocation进行调用;
  • 第三种方案?

那么有没有既不需要用到Libffi,又不用方法交换使用_objc_msgForward的其他方法呢?答案是有的。源码跳转

原理

通过 hook Block的回调函数invoke(void *p,...),替换为我们自定义的回调函数,在这个自定义的回调函数 _ff_invoke(void *p,...)内,注入其他逻辑,然后再以OC灵活的消息发送机制 NSInvocation去触发原来的block的回调及完成了对Block的hook。

实现

思考一下? 要想实现对Block的hook,需要解决以下几点:

  1. 如何获取block底层的回调函数,并且替换为自己的回调函数
  2. 如何将block的入参传入到自己的回调中,并触发自己的回调
  3. 在block的回调被替换后,如何触发原block的回调
  4. 如何处理hook链导致的回调循环问题

1、获取block底层结构Block_layout

想要获取Block的底层回调函数,首先要知道Block的底层数据结构。这里直接从源码处节选:

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

其中 struct Block_layout 就是真正的block底层结构,分别存了如下信息:

  • isa:指向Block具体的类型,__NSStackBlock____NSMallockBlock____NSGlobalBlock__

  • flags:定义了下列枚举中的信息,通过 Block_layout->flags 获取具体值

enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};
  • reserved:预留字段,作用未知

  • invoke:block的回调函数指针

  • descriptor:block的具体描述,这有三个结构体,非别为Block_descriptor_1,Block_descriptor_2,Block_descriptor_3,编译器会根据 falgs 生成不同结构的 Block_layout

通过如下方式将block强转成底层结构 Block_layout

// block为外部传入的block对象
struct Block_layout *b = (__bridge struct Block_layout *)block;

看下图,我们想要的block的回调函数就是下面的 invoke 指针。

invoke指针

2、交换invoke函数的实现

在上一步中,我们转换block为底层结构,获取到了回调函数指针 invoke,接下来就是将其替换为我们自定义的回调函数,这样block在执行时会进入我们自定义的函数体内。

// iOS 13 后,GlobalBlock 对象所占的内存是只读的,这就导致 Hook 过程中无法对 invoke 函数指针做写操作,直接 crash。
// 首先需要判断下 invoke 指针对应的地址有没有写权限,如果没有写权限则需要提权
vm_prot_t prot = changeAddressToWritable(invokeAddress);
// 将block的回调函数换成自己的,注意参数形式保持一致
b->invoke = _ff_invoke;
setOriginProtection(invokeAddress, prot);

内存提权代码实现(参考):

static vm_prot_t changeAddressToWritable(void *address) {
    vm_address_t addr = (vm_address_t)address;
    vm_size_t vmsize = 0;
    mach_port_t object = 0;
#if defined(__LP64__) && __LP64__
    vm_region_basic_info_data_64_t info;
    mach_msg_type_number_t infoCnt = VM_REGION_BASIC_INFO_COUNT_64;
    kern_return_t ret = vm_region_64(mach_task_self(), &addr, &vmsize, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &infoCnt, &object);
#else
    vm_region_basic_info_data_t info;
    mach_msg_type_number_t infoCnt = VM_REGION_BASIC_INFO_COUNT;
    kern_return_t ret = vm_region(mach_task_self(), &addr, &vmsize, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &infoCnt, &object);
#endif
    if (ret != KERN_SUCCESS) {
        NSLog(@"vm_region block invoke pointer failed! ret:%d, addr:%p", ret, address);
        return VM_PROT_NONE;
    }
    vm_prot_t protection = info.protection;
    if ((protection&VM_PROT_WRITE) == 0) {
        ret = vm_protect(mach_task_self(), (vm_address_t)address, sizeof(address), false, protection|VM_PROT_WRITE);
        if (ret != KERN_SUCCESS) {
            NSLog(@"vm_protect block invoke pointer VM_PROT_WRITE failed! ret:%d, addr:%p", ret, address);
            return VM_PROT_NONE;
        }
    }
    return protection;
}

static bool setOriginProtection(void *address, vm_prot_t originProtection) {
    if (originProtection == VM_PROT_NONE) return false;
    if ((originProtection&VM_PROT_WRITE) == 0) {
        kern_return_t ret = vm_protect(mach_task_self(), (vm_address_t)address, sizeof(address), false, originProtection);
        if (ret != KERN_SUCCESS) {
            return  false;
        }
    }
    return YES;
}

自定义的回调函数,注意参数格式类型保持一致。

void _ff_invoke(void *p, ...) {
    //... 
}

3、在自定义回调函数中调用原始block的回调函数以及自己注入的逻辑回调

block支持以NSInvocation的方式触发,而要做到这种方式则需要先获取到block的函数签名。这样我们才能通过构建出一个NSInvocation实例,+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

block的方法qianm在哪里获取?我们在回到block的底层结构上,其中有个 Block_descriptor_3 的结构体,里面有个 signature 的成员变量就是我们要的方法签名。

struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

但是需要注意一点,Block_descriptor_3的生成需要flgs中有BLOCK_HAS_SIGNATURE,也就是需要满足 flags & BLOCK_HAS_SIGNATUREtrue。相应的,Block_descriptor_2的生成需要flags中有BLOCK_HAS_COPY_DISPOSE,即满足 flags & BLOCK_HAS_COPY_DISPOSEtrue

再通过指针偏移的方式来获取到signture

const char *getBlockSignture(struct Block_layout *layout) {
    const char *csignature = NULL;
    void *desc1 = layout->descriptor;
    if (layout->flags & BLOCK_HAS_SIGNATURE) {
        desc1 += sizeof(struct Block_descriptor_1);
        if (layout->flags & BLOCK_HAS_COPY_DISPOSE) {
            desc1 += sizeof(struct Block_descriptor_2);
        }
        csignature = (*(const char **)desc1);
    }
    return csignature;
}

csignature = (*(const char **)desc1); 这段代码略微讲解下,void *类型的desc1指针强转成指向char *类型的指针,再通过*操作符获取到指针指向的值就是csignature

拿到了signture后,我们就可以初始化一个NSMethodSignature出来,用于进一步创建对象NSInvocation。还记得我们是在函数_ff_invoke(void *p,...)中吗,外部传入的参数都在 void *p 中,那么使用NSInvocation发消息的参数、方法签名都全了。主动触发block的方式如下:

const char *bsignature = getBlockSignture(b);
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:bsignature];
NSUInteger argsCount = signature.numberOfArguments;
            
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = block;
            
va_list va;
va_start(va, p);
for (int i = 0; i < argsCount - 1; i++) {
    void * arg = va_arg(va, void *);
    [invocation setArgument:&arg atIndex:i+1];
}
va_end(va);
            
[invocation invoke];

如果需要触发自己注入的block,也是同样的方式,这里不多复述。具体请看源码

4. 如何处理hook链带来的循环回调问题?

其实走到这一步时,一般的block hook已经初步完成了。但是一旦对同一个block多次hook就会出现回调地狱,你会发现__ff_invoke函数深陷回调不可自拔~

那么如何处理?

还是先思考,我们hook多次后,其实最终触发block时,原block回调只有一次,而自己注入的block逻辑根据hook的次数而定,因此统计一个block的hook次数,当回调次数超过hook次数时,退出__ff_invoke函数,这样就避免了回调循环。并且需要注意,我们只有在最后一次的__ff_invoke回调中,才触发原始block的回调,也就是将block的invoke指针给替换回原来的回调函数。至此,hook链已能正常工作。

if (callbackCount == descs.count) { // 最后一次回调才会触发原始block
            void *invokeAddress = &(b->invoke);
            void *originInvoke = (__bridge void *)(objc_getAssociatedObject(block, &k_invokes_bind_key));
            vm_prot_t prot = changeAddressToWritable(invokeAddress);
            b->invoke = originInvoke;
            setOriginProtection(invokeAddress, prot);
        }

最终实现效果

执行代码:

self.block = ^(int a, int b){
        NSLog(@"=============>");

        NSLog(@"a=%d",a);
        NSLog(@"b=%d",b);

        NSLog(@"+++++ %d",a+b);
    };



    [FFBlockHook hookBlock:self.block optional:FFBlockHookOptionAfter|FFBlockHookOptionBefore usingCustomAction:^(int a,int b, int c, int d) {
        NSLog(@"+++a=%d",a);
        NSLog(@"+++b=%d",b);
        NSLog(@"+++c=%d",c);
        NSLog(@"+++d=%d",d);
    }];
    
    [FFBlockHook hookBlock:self.block optional:FFBlockHookOptionInstead usingCustomAction:^{
        NSLog(@"直接替换");
    }];

    self.block(3,4);
    
    
    
    void (^strBlock) (NSString *, id, int ) = ^(NSString *s,id obj, int i) {
        NSLog(@"=========>");

        NSLog(@"s= %@",s);
        NSLog(@"obj=%@",obj);
        NSLog(@"i=%d",i);
    };



    [FFBlockHook hookBlock:strBlock optional:FFBlockHookOptionBefore usingCustomAction: ^int (NSString *s) {
        
        NSLog(@"第一次hook s=%@",s);
        
        return 10;
    }];
    [FFBlockHook hookBlock:strBlock optional:FFBlockHookOptionBefore usingCustomAction: ^ (void) {

        NSLog(@"第二次对strBlock hook");
    }];
    
    [FFBlockHook hookBlock:strBlock optional:FFBlockHookOptionAfter usingCustomAction: ^ (void) {

        NSLog(@"第3次对strBlock hook");
    }];
    
    [FFBlockHook hookBlock:strBlock optional:FFBlockHookOptionAfter|FFBlockHookOptionBefore usingCustomAction: ^ (void) {

        NSLog(@"第4次对strBlock hook");
    }];
    
    strBlock(@"hehe", @[@"1",@"2"], 10);
    
    
    [FFBlockHook hookBlock:strBlock optional:FFBlockHookOptionAfter usingCustomAction: ^ (void) {

        NSLog(@"第5次对strBlock hook");
    }];
    
    strBlock(@"我的天啊,这名🦅吗", @[], 123456);

输出:

2021-06-20 22:54:15.175775+0800 FFBlockHook[3501:71203] +++a=3
2021-06-20 22:54:15.175976+0800 FFBlockHook[3501:71203] +++b=4
2021-06-20 22:54:15.176111+0800 FFBlockHook[3501:71203] +++c=0
2021-06-20 22:54:15.176232+0800 FFBlockHook[3501:71203] +++d=0
2021-06-20 22:54:15.176395+0800 FFBlockHook[3501:71203] 直接替换
2021-06-20 22:54:15.176543+0800 FFBlockHook[3501:71203] +++a=3
2021-06-20 22:54:15.176673+0800 FFBlockHook[3501:71203] +++b=4
2021-06-20 22:54:15.176776+0800 FFBlockHook[3501:71203] +++c=0
2021-06-20 22:54:15.176983+0800 FFBlockHook[3501:71203] +++d=0
2021-06-20 22:54:15.177440+0800 FFBlockHook[3501:71203] 第一次hook s=hehe
2021-06-20 22:54:15.177750+0800 FFBlockHook[3501:71203] 第二次对strBlock hook
2021-06-20 22:54:15.178113+0800 FFBlockHook[3501:71203] 第4次对strBlock hook
2021-06-20 22:54:15.178513+0800 FFBlockHook[3501:71203] =========>
2021-06-20 22:54:15.178926+0800 FFBlockHook[3501:71203] s= hehe
2021-06-20 22:54:15.179344+0800 FFBlockHook[3501:71203] obj=(
1,
2
)
2021-06-20 22:54:15.179642+0800 FFBlockHook[3501:71203] i=10
2021-06-20 22:54:15.180030+0800 FFBlockHook[3501:71203] 第4次对strBlock hook
2021-06-20 22:54:15.180423+0800 FFBlockHook[3501:71203] 第3次对strBlock hook
2021-06-20 22:54:15.180934+0800 FFBlockHook[3501:71203] =========>
2021-06-20 22:54:15.235841+0800 FFBlockHook[3501:71203] s= 我的天啊,这名🦅吗
2021-06-20 22:54:15.236076+0800 FFBlockHook[3501:71203] obj=(
)
2021-06-20 22:54:15.236221+0800 FFBlockHook[3501:71203] i=123456
2021-06-20 22:54:15.236380+0800 FFBlockHook[3501:71203] 第5次对strBlock hook

源码传送门


参考:

Block hook 正确姿势?

Hook Objective-C Block with Libffi

MABlockClosure

BlockHook学习记录

Block签名信息的使用

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容