50d71d79fa6b823b4ad41a4ec5d9bc52
Block hook 正确姿势?

前言

最近在做一个项目,里面涉及到一些Mac逆向的内容,例如反编译出微信一下功能API,通过运行时拦截将我们自己的功能注入到微信中。在之中遇到这么一个难点,需要拦截微信某个功能回调,而这个回调是一个block【苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性】,我们需要hook【勾住】这个block进行我们的逻辑注入,且不影响原有block逻辑。
Mac/iOS等苹果平台开发的主力语言是Objective-C,Objective-C有很强的动态性,依赖它的运行时机制,我们很容易拦截某个已实现的方法调用进行替换或者重新转发。放到我们当前这个业务来讲,拦截注入微信任何一个方法较容易,但是拦截block却没那么简单。而且网上关于block hook内容非常非常少,也没有一个相对成熟的框架或者工具来帮我们实现block hook。那今天这篇文章就来教大家如何正确进行block hook。

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的消息转发机制:

图上的意思就是当一个对象的某个方法没实现的实现,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的形式调用


从苹果官方文档,我们知道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
};

通过flags与BLOCK_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);
}
top Created with Sketch.