Block底层hook

作者: 6ffd6634d577 | 来源:发表于2019-08-02 10:48 被阅读0次

    Block内存关系
    Block经典问题循环引用&解决
    Block底层分析
    Block底层HooK

    前言

    如何反编译出微信一下功能API,通过运行时拦截将我们自己的功能注入到微信中。在之中遇到这么一个难点,需要拦截微信某个功能回调,而这个回调是一个block【苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性】,我们需要hook【勾住】这个block进行我们的逻辑注入,且不影响原有block逻辑。 Mac/iOS等苹果平台开发的主力语言是Objective-C,Objective-C有很强的动态性,依赖它的运行时机制,我们很容易拦截某个已实现的方法调用进行替换或者重新转发。拦截注入微信任何一个方法较容易,但是拦截block却没那么简单。

    方法调用的几种Hook机制

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

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

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

    block hook 前提

    要完成block hook有两个关键因素:
    1、block也是对象,支持消息转发机制,block hook选择在消息转发时机进行操作;
    2、block支持以NSInvocation的形式调用,保证block hook之后能正常响应旧block;

    block也支持消息转发

    Objective-C的运行时机制中最重要的一个应用场景就是消息转发。在Objective-C中,一个对象调用某个方法,严格意义上来说他不叫调用,叫发消息。Objective-C不像C/C++,在编译器就确定内部函数的地址,而是到运行时的时候才找到函数的调用地址进行调用。任何Objective-C的方法调用,编译器实际上把它转换成objc_msgSend(对象,方法名,...)这样的C函数调用。通过objc_msgSend函数,运行时机制会根据方法名在对象的方法列表里面查找方法实现,如果没有到父类中查找,一直到根类。如果没有查找到方法实现的地址,就会进入消息转发,如果消息转发没有做处理,则会抛出一个doesNotRecognizeSelector的异常。在这里我要要重点理解消息转发有什么作用。消息转发的作用就是当一个对象调用一个没有实现的方法时,给它机会去解决这个方法无法响应的问题以防止出现奔溃。 这种机制应用场景应用对象调用方法,而block也是一种对象,我们可以看下block的源码结构(以下源码是最新的libclosure-67版本):

    
    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
    };
    

    Block_layout就是block实际的源码结构,在Objective-C里,如果包含isa指针,说明这个结构类型则属于对象类型。我们可以从源码看到Block_layout包含一个isa指针,说明block是一个对象。block调用实际上是block对象调用了自己的函数实现【invoke指针,是一个函数指针,指向block的实现】,所以block也支持消息转发机制。 一个正常结构的block被响应是不会触发消息转发机制,因为消息转发机制是为了解决调用没实现方法这种异常情况。所以我们用一个比较trick的方式强制启动block的消息转发机制:

    image.png

    图上的意思就是当一个对象的某个方法没实现的实现,Objective-C会将该方法实现指向一个特殊的函数指针_objc_msgForward,之后方法会就进入消息动态绑定或消息转发流程。我们从这里得到启发,也就是我们强行将block的函数指针(invoke)强行指向_objc_msgForward,启动它的消息转发。启动消息转发后,我们需要实现以下函数来辅助我们对这个block进行后继的处理:

    - (id)forwardingTargetForSelector:(SEL)aSelector;
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    
    

    forwardingTargetForSelector可以为了方法指定一个能够响应这个方法的备选对象,因为hook处理不在这个时机,所以我们可以不用去实现这个方法指定一个备选者。
    methodSignatureForSelector返回一个方法的签名,签名包含方法入参信息、返回值等信息,对于block来说就是block的签名,后面源码会分析怎么获取block的签名。
    forwardInvocation会根据上一步返回的签名生成一个NSInvocation对象,它包含方法调用所有信息,而我们的hook关键也在这一步,重新包装NSInvocation对象进行响应,后面我会将具体怎么操作。

    block支持以NSInvocation的形式调用

    image.png

    从苹果官方文档,我们知道NSInvocation是一条消息的对象包装,这里消息指的是我们的方法调用,同样也适用block。NSInvocation构造的关键是签名,方法的签名获取比较简单,通过[NSString instanceMethodSignatureForSelector:@selector(method:)]获取。block签名的获取则没那么直接,我们再来看下block的源码结构:

    #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
    };
    
    

    可以看到Block_descriptor_3这个结构体包含signature这个字段,这也是我们需要的签名。我们从Block_layout结构中看到,好像没有访问Block_descriptor_3的方法,它只有一个Block_descriptor_1的指针,这是因为并不是所有block有Block_descriptor_3这个结构体,编译器根据flags上的值判断block是何种类型,生成不同的Block_layout结构。Block_descriptor_2也是同理。那怎么判断呢?先看下面的枚举:

    // 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
    };
    
    

    通过flagsBLOCK_HAS_SIGNATURE做一次与操作,如果值不为0,则说明当前这个block有Block_descriptor_3这个结构,这样就可以取到里面的签名信息。
    接着通过[NSMethodSignature signatureWithObjCTypes]生成签名对象,再通过[NSInvocation invocationWithMethodSignature]构造NSInvocation对象,给NSInvocation对象指定消息的响应者,block响应者当然是自己本身,再调invoke方法就可以完成block的调用。

    block hook 基本步骤

    1、保存原来block的副本,因为不影响原有的微信业务逻辑,在hook注入我们自己业务逻辑之后,我们需要回过头响应原有的微信block逻辑;
    2、强制启动block的消息转发机制;
    3、在消息转发最后一步,将副本和hook block取出包装成NSInvocation进行调用;

    block hook 具体操作

    我这边设计一个block hook框架WBHookBlock,这个框架提供各种姿势给block hook,你可以在origin block前调用你注入的逻辑,或者在origin block后调用,甚至是替换origin block,API如下:

    typedef NS_ENUM(NSUInteger, WBHookBlockPosition) {
        WBHookBlockPositionBefore = 0,
        WBHookBlockPositionAfter,
        WBHookBlockPositionReplace,
    };
    
    @interface WBHookBlock : NSObject
    
    + (void)hookBlock:(id)originBlock alter:(id)alterBlock position:(WBHookBlockPosition)position;
    
    @end
    
    

    因为这需要访问block源码层面的数据(现有没有API提供访问入口),所以我仿造官方源码构造一个源码结构体的block:

    typedef NS_OPTIONS(int, WBFishBlockFlage) {
        WBFish_BLOCK_DEALLOCATING =      (0x0001),  // runtime
        WBFish_BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
        WBFish_BLOCK_NEEDS_FREE =        (1 << 24), // runtime
        WBFish_BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
        WBFish_BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
        WBFish_BLOCK_IS_GC =             (1 << 27), // runtime
        WBFish_BLOCK_IS_GLOBAL =         (1 << 28), // compiler
        WBFish_BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
        WBFish_BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
        WBFish_BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
    };
    
    struct WBFishBlock_layout {
        void *isa;
        volatile int32_t flags;
        int32_t reserved;
        void (*invoke)(void *, ...);
        struct WBFishBlock_descriptor_1 *descriptor;
    };
    typedef struct WBFishBlock_layout  *WBFishBlock;
    
    struct WBFishBlock_descriptor_1 {
        uintptr_t reserved;
        uintptr_t size;
    };
    
    struct WBFishBlock_descriptor_2 {
        void (*copy)(void *dst, const void *src);
        void (*dispose)(const void *);
    };
    
    struct WBFishBlock_descriptor_3 {
        const char *signature;
        const char *layout;
    };
    
    

    可能命名跟源码里的名字不一样,但这不影响,因为结构体结构和数据偏移是一样,这能够保证正确访问block内的数据(例如flags、invoke指针、des描述信息)

    + (void)hookBlock:(id)originBlock alter:(id)alterBlock position:(WBHookBlockPosition)position{
        WBFishBlock u_originBlock = (__bridge WBFishBlock)originBlock;
        WBFishBlock u_alterBlock = (__bridge WBFishBlock)alterBlock;
        
        wbhook_setPosInfo(u_originBlock, position);
        
        wbhook_setHookBlock(u_originBlock, u_alterBlock);
        
        wbhook_block(originBlock);
    }
    
    

    先做bridge桥接将Objective-C block转化为结构体形式的block。wbhook_setPosInfo将位置信息跟origin block关联起来,wbhook_setHookBlock将alter block[自己业务逻辑的block]跟origin block关联起来,目的是先保存起来以方便后继使用,关联对象的存取如下:

    static void wbhook_setHookBlock(WBFishBlock block, WBFishBlock hookBlock) {
        objc_setAssociatedObject((__bridge id _Nonnull)(block), @"wbhook_block_hookBlock", (__bridge id _Nullable)(hookBlock), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        
    }
    
    static id wbhook_getHookBlock(WBFishBlock block) {
        return objc_getAssociatedObject((__bridge id _Nonnull)(block), @"wbhook_block_hookBlock");
    }
    
    static void wbhook_setPosInfo(WBFishBlock block, NSUInteger pos) {
        objc_setAssociatedObject((__bridge id _Nonnull)(block), @"wbhookblock_pos", @(pos), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    static NSNumber* wbhook_getPosInfo(WBFishBlock block) {
        return objc_getAssociatedObject((__bridge id _Nonnull)(block), @"wbhookblock_pos");
    }
    

    接下来看wbhook_block的逻辑:

    wbblock_hook_once();
        WBFishBlock block = (__bridge WBFishBlock)(obj);
        if (!wbhook_block_getTmpBlock(block)) {
            //先copy一份,目的是为了复制外部变量
            _wbFish_block_deepCopy(block);
            struct WBFishBlock_descriptor_3 *desc_3 = get_WBFishBlock_descriptor_3(block);
            //设置block的invoke指针为_objc_msgForward为了调用block触发消息转发。
            block->invoke = _objc_msgForward;
        }
    
    

    wbblock_hook_once里主要做消息转发那些方法的swizzle操作,这个等下后面详细讲。
    wbhook_block_getTmpBlock用于获取origin block的副本,如果是第一次hook这个block是没有这个副本,所以我们需要通过_wbFish_block_deepCopy拷贝一份副本保存起来。这时候也许会有人问为什么要拷贝一份副本,先不着急,我先将这里的整体逻辑讲完再细细道来。最后一步我们将block的invoke指针强行指向_objc_msgForward,有上文知道invoke指针指向block的实现,指向_objc_msgForward会启动block的消息转发。现在我们就来说明为什么需要拷贝一份副本,因为origin block已经被指向_objc_msgForward启动消息转发,后面在消息转发最后一个阶段如果还需要调用origin block的逻辑,我们不能直接再调origin block,因为再调用origin block会再次进入消息转发,这就变成一个死循环,所以我们需要保持一个origin block的副本用于调起origin block的逻辑,因为副本是通过深拷贝出来的,跟origin block是相互独立,所以origin block强制消息转发不会影响副本,也就不会进入死循环。
    那block如何做深拷贝?这里需要分情况:

    static void _wbFish_block_deepCopy(WBFishBlock block) {
        struct WBFishBlock_descriptor_2 *desc_2 = get_WBFishBlock_descriptor_2(block);
        //如果捕获的变量存在对象或者被__block修饰的变量时,在__main_block_desc_0函数内部会增加copy跟dispose函数,copy函数内部会根据修饰类型(weak or strong)对对象进行强引用还是弱引用,当block释放之后会进行dispose函数,release掉修饰对象的引用,如果都没有引用对象,将对象释放
    
        if (desc_2) {
            WBFishBlock newBlock = malloc(block->descriptor->size);
            if (!newBlock) {
                return;
            }
            memmove(newBlock, block, block->descriptor->size);
            newBlock->flags &= ~(WBFish_BLOCK_REFCOUNT_MASK|WBFish_BLOCK_DEALLOCATING);
            newBlock->flags |= WBFish_BLOCK_NEEDS_FREE | 2;  // logical refcount 1
            
            (desc_2->copy)(newBlock, block);
            wbhook_block_setTmpBlock(block, newBlock);
        } else {
            WBFishBlock newBlock = malloc(block->descriptor->size);
            if (!newBlock) {
                return;
            }
            memmove(newBlock, block, block->descriptor->size);
            newBlock->flags &= ~(WBFish_BLOCK_REFCOUNT_MASK|WBFish_BLOCK_DEALLOCATING);
            newBlock->flags |= WBFish_BLOCK_NEEDS_FREE | 2;  // logical refcount 1
            wbhook_block_setTmpBlock(block, newBlock);
        }
    }
    
    

    基本操作就是先声明一个新block,申请内存,执行memmove内存拷贝操作,将旧block的内容拷贝到新block上,flags的配置参考官方block_copy的源码,主要目的是标识block的类型和引用计数,这里一步不同就是如果block结构中存在WBFishBlock_descriptor_2,什么类型的block会存在WBFishBlock_descriptor_2呢?如果一个block是一个堆block,且捕获对象类型的变量或者__block修饰的变量时,这时候的block会多一个WBFishBlock_descriptor_2描述信息,里面包含两个内存辅助函数指针,用于辅助捕获变量的内存管理。对于这种类型的block的拷贝,还需要调用WBFishBlock_descriptor_2的copy函数进行捕获变量的内存管理的拷贝,这里也是参考官方block_copy的源码。拷贝结束后通过wbhook_block_setTmpBlock将拷贝的副本与origin block关联保存起来:

    static void wbhook_block_setTmpBlock(WBFishBlock block, WBFishBlock tmpBlock) {
        objc_setAssociatedObject((__bridge id)block, @"wbhook_block_TmpBlock", (__bridge id)tmpBlock, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    static id wbhook_block_getTmpBlock(WBFishBlock block) {
        return objc_getAssociatedObject((__bridge id)block, @"wbhook_block_TmpBlock");
    }
    
    

    我们再回到wbblock_hook_once这个函数,上面说了里面主要做一些消息转发函数的重定义的操作,还记得那几个消息转发函数吗?

    #define WBFish_StrongHookMethod(selector, func) { Class cls = NSClassFromString(@"NSBlock");Method method = class_getInstanceMethod([NSObject class], selector); \
    BOOL success = class_addMethod(cls, selector, (IMP)func, method_getTypeEncoding(method)); \
    if (!success) { class_replaceMethod(cls, selector, (IMP)func, method_getTypeEncoding(method));}}
    
    static void wbblock_hook_once() {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            WBFish_StrongHookMethod(@selector(methodSignatureForSelector:), wb_block_methodSignatureForSelector);
            //在forwardInvocation:中执行完自己的逻辑后,将invocation的target设置为刚刚copy的block,执行invoke。完成Hook
            WBFish_StrongHookMethod(@selector(forwardInvocation:), wb_block_forwardInvocation);
        });
    }
    
    

    利用Objective-C的运行时相关函数,我们很容易实现方法的替换,详见宏定义。我们这里主要是为了替换实现两个消息转发的方法methodSignatureForSelectorforwardInvocation, methodSignatureForSelector用于返回block的签名,forwardInvocation是我们做转发逻辑的关键

    static NSMethodSignature *wb_block_methodSignatureForSelector(id self, SEL _cmd, SEL aSelector) {
        struct WBFishBlock_descriptor_3 *desc_3 = get_WBFishBlock_descriptor_3((__bridge void *)self);
        return [NSMethodSignature signatureWithObjCTypes:desc_3->signature];
    }
    
    static void wb_block_forwardInvocation(id self, SEL _cmd, NSInvocation *invo) {
        WBFishBlock block = (__bridge void *)invo.target;
        
        NSUInteger originArgNum = invo.methodSignature.numberOfArguments;
        NSUInteger hookArgNum = invo.methodSignature.numberOfArguments;
        
        //block转invoation
        WBFishBlock hookBlock = (__bridge void*)wbhook_getHookBlock(block);
        struct WBFishBlock_descriptor_3 * hookBlock_des_3 = get_WBFishBlock_descriptor_3(hookBlock);
        NSMethodSignature *hookBlockMethodSignature = [NSMethodSignature signatureWithObjCTypes:hookBlock_des_3->signature];
        NSInvocation *hookBlockInv = [NSInvocation invocationWithMethodSignature:hookBlockMethodSignature];
        
        if (originArgNum != hookArgNum) {
            NSLog(@"arguments count is not fit");
            return;
        }
    
        if (hookArgNum > 1) {
            void *tmpArg = NULL;
            for (NSUInteger i = 1; i < hookArgNum; i++) {
                const char *type = [invo.methodSignature getArgumentTypeAtIndex:i];
                NSUInteger argsSize;
                NSGetSizeAndAlignment(type, &argsSize, NULL);
                if (!(tmpArg = realloc(tmpArg, argsSize))) {
                    NSLog(@"fail allocate memory for block arg");
                    return;
                }
                [invo getArgument:tmpArg atIndex:i];
                [hookBlockInv setArgument:tmpArg atIndex:i];
            }
        }
        
        WBFishBlock tmpBlock = (__bridge void *)wbhook_block_getTmpBlock(block);
        
        NSNumber *pos = wbhook_getPosInfo(block);
        NSUInteger posInx = [pos unsignedIntegerValue];
        switch (posInx) {
            case 0:
                [invo invokeWithTarget:(__bridge id _Nonnull)(tmpBlock)];
                [hookBlockInv invokeWithTarget:(__bridge id _Nonnull)(hookBlock)];
                break;
            case 1:
                [hookBlockInv invokeWithTarget:(__bridge id _Nonnull)(hookBlock)];
                [invo invokeWithTarget:(__bridge id _Nonnull)(tmpBlock)];
                break;
            case 2:
                [hookBlockInv invokeWithTarget:(__bridge id _Nonnull)(hookBlock)];
                break;
            default:
                [invo invokeWithTarget:(__bridge id _Nonnull)(tmpBlock)];
                [hookBlockInv invokeWithTarget:(__bridge id _Nonnull)(hookBlock)];
                break;
        }
    }
    
    

    我们看下在消息转发最后一步,我们怎么完成hook注入我们的逻辑,我们从消息转发得到NSInvocation的target中得到origin block,再从origin block的关联对象中取到保存的副本tmp block、alter block【注入的业务逻辑】及位置信息,将tmp block和alter block转化为NSInvocation对象,根据位置信息先后调用invoke实现两个block的调用。

    示例源码

    demo源码
    想了解更多iOS终端相关知识可以前往向大家推荐[终端杂谈] (https://xiaozhuanlan.com/dane?rel=5336027142)

    原文链接:https://juejin.im/post/5c653921e51d457fa676eafc

    相关文章

      网友评论

        本文标题:Block底层hook

        本文链接:https://www.haomeiwen.com/subject/jidkdctx.html