Block hook 正確姿式?

前言

最近在作一個項目,裏面涉及到一些Mac逆向的內容,例如反編譯出微信一下功能API,經過運行時攔截將咱們本身的功能注入到微信中。在之中遇到這麼一個難點,須要攔截微信某個功能回調,而這個回調是一個block【蘋果在iOS4開始引入的對C語言的擴展,用來實現匿名函數的特性】,咱們須要hook【勾住】這個block進行咱們的邏輯注入,且不影響原有block邏輯。 Mac/iOS等蘋果平臺開發的主力語言是Objective-C,Objective-C有很強的動態性,依賴它的運行時機制,咱們很容易攔截某個已實現的方法調用進行替換或者從新轉發。放到咱們當前這個業務來說,攔截注入微信任何一個方法較容易,可是攔截block卻沒那麼簡單。並且網上關於block hook內容很是很是少,也沒有一個相對成熟的框架或者工具來幫咱們實現block hook。那今天這篇文章就來教你們如何正確進行block hook。git

block hook 前提

要完成block hook有兩個關鍵因素: 一、block也是對象,支持消息轉發機制,block hook選擇在消息轉發時機進行操做; 二、block支持以NSInvocation的形式調用,保證block hook以後能正常響應舊block;github

block也支持消息轉發

Objective-C的運行時機制中最重要的一個應用場景就是消息轉發。在Objective-C中,一個對象調用某個方法,嚴格意義上來講他不叫調用,叫發消息。Objective-C不像C/C++,在編譯器就肯定內部函數的地址,而是到運行時的時候才找到函數的調用地址進行調用。任何Objective-C的方法調用,編譯器實際上把它轉換成objc_msgSend(對象,方法名,...)這樣的C函數調用。經過objc_msgSend函數,運行時機制會根據方法名在對象的方法列表裏面查找方法實現,若是沒有到父類中查找,一直到根類。若是沒有查找到方法實現的地址,就會進入消息轉發,若是消息轉發沒有作處理,則會拋出一個doesNotRecognizeSelector的異常。在這裏我要要重點理解消息轉發有什麼做用。消息轉發的做用就是當一個對象調用一個沒有實現的方法時,給它機會去解決這個方法沒法響應的問題以防止出現奔潰。 這種機制應用場景應用對象調用方法,而block也是一種對象,咱們能夠看下block的源碼結構(如下源碼是最新的libclosure-67版本):objective-c

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的消息轉發機制: bash

圖上的意思就是當一個對象的某個方法沒實現的實現,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 基本步驟

一、保存原來block的副本,由於不影響原有的微信業務邏輯,在hook注入咱們本身業務邏輯以後,咱們須要回過頭響應原有的微信block邏輯;
二、強制啓動block的消息轉發機制;
三、在消息轉發最後一步,將副本和hook block取出包裝成NSInvocation進行調用;工具

block hook 具體操做

我這邊設計一個block hook框架WBHookBlock,這個框架提供各類姿式給block hook,你能夠在origin block前調用你注入的邏輯,或者在origin block後調用,甚至是替換origin block,API以下:ui

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:spa

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終端相關知識能夠前往終端雜談

相關文章
相關標籤/搜索