BlockHook学习记录

导言

block作为oc极为重要的一部分从来都是面试和开发乐此不疲的话题与工具。

关于block的数据结构和实现原理网上大大小小真的已经很多了,这里不再叙述,最近有业务需求需要hook block 进行一些无埋点的监控,所以顺便记录下学习过程。

方案

当前blockhook的方案大致有两种:

1.基于libffi的blockhook

libffi的原理和用法这里不予赘述了。

大致流程:

  1. 根据 block 对象的签名,使用 ffi_prep_cif 构建block->invoke函数的模板 cif
  2. 使用 ffi_closure,根据 cif 动态定义函数 replacementInvoke,绑定到ClosureFunc(一个具体实现的函数实体上)
  3. block->invoke替换为 replacementInvoke,原始的 block->invoke 存放在 originInvoke
  4. ClosureFunc 中通过hook位置动态调用 originInvoke 函数和执行 hook 的逻辑。

2.基于方法转发的blockhook

大致流程:

  1. 通过runtime交换block对象的方法转发的方法。
  2. block->invoke替换为(IMP)_objc_msgForward,重新拷贝原block生成newBlock,通过关联对象进行保存(block地址为key)。
  3. 在自定义的方法转发方法中拿到newBlockoriginInvoke 函数进行调用和执行 hook 的逻辑。

问题总结

1.其实两种方法在思路是差不多的,都是通过'曲线救国'的方式,找到那个中间的桥接函数,进行替换。区别在于实现方法不同。

上述的第一种方法,优点是目前比较稳定,功能强大,且兼容了在各种环境下的很多问题,缺点是要引libffi。

第二种的方式较轻量,且不用引libffi,但没有像第一种经过大量的验证,兼容性有待测试。

2.在第二种方式中,我发现作者对于原originInvoke的处理麻烦了,不需要重新new一份新的内存和拷贝。直接保存originInvoke的通过关联对象保存原函数指针,在调用过程中再换回来就可以。

伪代码:

-(void)blockhook{
    //交换方法..
    ...
    //保存block原实现
        [self saveOriginInvoke:block->invoke];  
    //替换block实现
    block->invoke = _objc_msgForward; 
}

-(void)bh_forwardInvocation:(NSInvocation *)invocation{
    
    //hook逻辑
    ...
    //替换回原实现
    block->invoke = [self getOriginInvoke];
    //执行原逻辑函数
    [invocation invokeWithTarget:block];
}

3.将target设置为iOS13,运行时GlobalBlock类型会出现invoke替换问题,而另外两张类型的block没问题。

image.png

开始也是百思不得其解,想了各种办法,想试着通过结构体的地址偏移量去修改invoke依然换不掉,开始觉得可能是底层偷偷把地址换了,看了汇编把寄存器值打印出来发现地址也对,但写入就是坏地址访问。

image.png

走投无路之际看到了第一位作者的文章才恍然大悟,不得不佩服大佬的才华。

BlockHook and Memory Safety 文中提到如何解决 GlobalBlock 没有写权限的问题。用到了虚拟内存的一些相关api去修改内存页的读写权限,经过代码运行,我也证实了确实苹果在iOS13将GlobalBlock权限进行了限制,只有只读权限,至于为啥求大佬们解惑。以下是解决方案

vm_prot_t protectInvokeVMIfNeed(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;
}

核心函数是下面这个,直接贴文档。

Function - Set access privilege attribute for a region of virtual memory.

SYNOPSIS
kern_return_t   vm_protect
                 (vm_task_t           target_task,(需要修改的内存空间区域)
                  vm_address_t            address,(起始地址)
                  vm_size_t                  size,(地址大小)
                  boolean_t           set_maximum, (这个没太看懂)
                  vm_prot_t        new_protection); (赋予的新的权限)
PARAMETERS
target_task
[in task send right] The port for the task whose address space contains the region.
address
[in scalar] The starting address for the region.
size
[in scalar] The number of bytes in the region.
set_maximum
[in scalar] Maximum/current indicator. If true, the new protection sets the maximum protection for the region. If false, the new protection sets the current protection for the region. If the maximum protection is set below the current protection, the current protection is also reset to the new maximum.
new_protection
[in scalar] The new protection for the region. Valid values are obtained by or'ing together the following values:
VM_PROT_READ
Allows read access.
VM_PROT_WRITE
Allows write access.
VM_PROT_EXECUTE
Allows execute access.

至此在iOS13也可以开心的玩耍了。

剩下的实现包括读取参数,获取方法签名包装这些相关文档已经太多了,就不多赘述了。

看别人的方案就是这样。内心的os都是:哇还可以这么玩,为什么我想不到😭

我自己也基于第二种方法,并且结合以上的问题,做了些修改,撸了一个分类。目前自己在项目中debug用用感觉还不错

超简易版BlockHook

本次学习非常感谢:

BlockHook

FishBind

BlockHook and Memory Safety

Hook Objective-C Block with Libffi

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