最近在作一个项目,里面涉及到一些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也是对象,支持消息转发机制,block hook选择在消息转发时机进行操做; 二、block支持以NSInvocation的形式调用,保证block hook以后能正常响应旧block;github
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
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
复制代码
forwardingTargetForSelector
能够为了方法指定一个可以响应这个方法的备选对象,由于hook处理不在这个时机,因此咱们能够不用去实现这个方法指定一个备选者。 methodSignatureForSelector
返回一个方法的签名,签名包含方法入参信息、返回值等信息,对于block来讲就是block的签名,后面源码会分析怎么获取block的签名。 forwardInvocation
会根据上一步返回的签名生成一个NSInvocation对象,它包含方法调用全部信息,而咱们的hook关键也在这一步,从新包装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也是同理。那怎么判断呢?先看下面的枚举:markdown
// 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逻辑;
二、强制启动block的消息转发机制;
三、在消息转发最后一步,将副本和hook block取出包装成NSInvocation进行调用;函数
我这边设计一个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:oop
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的运行时相关函数,咱们很容易实现方法的替换,详见宏定义。咱们这里主要是为了替换实现两个消息转发的方法methodSignatureForSelector
和forwardInvocation
, 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的调用。