之前在实现 组件化分发生命周期 - AOP 方案 过程中用到了 Aspects 这个库,所以当时又仔细看了看源码,借鉴了些消息分发的写法。后来又为了性能考虑用 libffi 这个库替换了 Aspects 的实现 ,参考了 Stinger 这个库的实现。本质上都是在做 Hook 操作,加上最近看了些 fishhook 的原理,涉及了很多 Mach-O 等底层原理,所以打算把 iOS 里的一些 Hook 方案列一下,做个笔记总结,就是这篇文章的初衷了。
看了一段时间的底层原理就有些倦怠了,所以计划了一场组内分享驱动着看完了 fishhook 相关的知识。Demo 和 keynote 放在了 GitHub上,地址在这:https://github.com/gonghonglou/HookDemo
Preview
1、Method Swizzling2、Message Forwarding3、libffi4、fishhook5、静态库插桩
6、基于桥的全量方法 Hook 方案 TrampoLineHook7、Dobby / Frida
小试牛刀:Method Swizzling这是 iOS 里最基础最原生的 Hook 方法了,当然也是性能最好的选择。本质上就是交换两个方法的 IMP(函数指针),即:
常见的写法也很简单:
123456789101112131415161718192021222324252627@implementation HookDemoObj (HK)+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(loopLogWithCount:); SEL swizzledSelector = @selector(hook_loopLogWithCount:); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (success) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } });}- (void)hook_loopLogWithCount:(NSInteger)count { [self hook_loopLogWithCount:count]; NSLog(@"hook after count: %d", count);}
需要注意的也就是以下几点:
1、为什么 load 时机 可以成功执行?
1.1、启动 dyld(the dynamic link editor)将应用程序加载到二进制中1.2、Runtime 向 dyld 中注册回调函数1.3、通过 ImageLoader 将所有的 image 加载到内存中1.4、dyld 在 image 发生改变时,主动调用回调函数1.5、Runtime 接收到 dyld 的函数回调,开始 map_images、load_images等操作,并回调 +load 方法1.6、调用 mian 函数
其中 3、4、5会执行多次,在 ImageLoader 加载新的 image 进内存后就会执行一次ImageLoader 是 image 的加载器,image 可以理解为编译后的二进制
2、dispatch_once 保证,避免方法被多次交换。
3、调用原方法,因为方法已经被交换过了,所以这里调用 [self hook_loopLogWithCount:count]; 即执行的是原函数(originalIMP)。
穿针引线:Message Forwarding主要是 Aspects 的原理,让目标方法在被执行时直接进入快速消息转发流程,在最后的 forwardInvocation: 方法里拿到 NSInvocation 对象,即包含了被调用方法的所有信息,主要是参数个数,参数值。然后在调用原方法之前或之后调用传入的 Block。
看一下 Aspects 的使用:
12345678910HookDemoObj *forwardingObj = [HookDemoObj new];// 无参数的 block[forwardingObj aspect_hookSelector:@selector(logString:) withOptions:AspectPositionAfter usingBlock: ^{ NSLog(@"Aspects after"); // Aspects after} error:nil];// 有参数的 block[forwardingObj aspect_hookSelector:@selector(logString:) withOptions:AspectPositionAfter usingBlock: ^(id
需要注意的有以下几点:
1、isa-swizzling 的应用2、消息转发流程。如何不重写 “methodSignatureForSelector:” 方法也能进入 “forwardInvocation:” 阶段?3、如何获取 block NSMethodSignature?并且在不定参数情况下执行 block?
关于isa-swizzling 的应用:isa-swizzling 的原理如下图。即 KVO 的基本原理:原来的对象 isa 指向一个 class,通过 runtime 动态创建一个 class 的子类,并将原来对象的 isa 指向这个新建的子类,重写这个子类 getter、setter 即可。
Aspects 的应用:
12345678910111213141516171819202122232425// Default case. Create dynamic subclass.const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;Class subclass = objc_getClass(subclassName);// 判断目标类是否创建过if (subclass == nil) { // 动态创建子类 subclass = objc_allocateClassPair(baseClass, subclassName, 0); if (subclass == nil) { NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName]; AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc); return nil; } // 交换 forwardInvocation: 方法 aspect_swizzleForwardInvocation(subclass); // 重写 class 方法 aspect_hookedGetClass(subclass, statedClass); aspect_hookedGetClass(object_getClass(subclass), statedClass); // 注册动态创建的类 objc_registerClassPair(subclass);}// 修改 isa 指针指向新创建的子类object_setClass(self, subclass);
关于消息转发流程:正常的消息转发流程如下:
12345678910111213141516171819+ (BOOL)resolveInstanceMethod:(SEL)sel { NSLog(@"1:%@", NSStringFromSelector(_cmd)); return [super resolveInstanceMethod:sel];}- (id)forwardingTargetForSelector:(SEL)aSelector { NSLog(@"2:%@", NSStringFromSelector(_cmd)); return [super forwardingTargetForSelector:aSelector];}- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSLog(@"3:%@", NSStringFromSelector(_cmd)); return [super methodSignatureForSelector:aSelector];}- (void)forwardInvocation:(NSInvocation *)anInvocation { NSLog(@"4:%@", NSStringFromSelector(_cmd)); [super forwardInvocation:anInvocation];}
注意,必须实现 methodSignatureForSelector: 方法返回 NSMethodSignature 才会进入 forwardInvocation: 方法。但 Aspects 并没有实现 methodSignatureForSelector: 方法。Aspects 的做法是:把要 hook 的方法通过 class_replaceMethod() 接口指向 _objc_msgForward,这是一个全局 IMP,OC 调用方法不存在时都会转发到这个 IMP 上,直接把方法替换成这个 IMP,这样调用 hook 方法时就会走到 forwardInvocation::
123Method targetMethod = class_getInstanceMethod(klass, selector);const char *typeEncoding = method_getTypeEncoding(targetMethod);class_replaceMethod(klass, selector, _objc_msgForward, typeEncoding);
关于 Block NSMethodSignature 和调用:
这里 Block_layout 是 Block 的源码,Block 的结构体内容是:
12345678910111213141516171819202122232425262728#define BLOCK_DESCRIPTOR_1 1struct Block_descriptor_1 { uintptr_t reserved; uintptr_t size;};#define BLOCK_DESCRIPTOR_2 1struct Block_descriptor_2 { // requires BLOCK_HAS_COPY_DISPOSE BlockCopyFunction copy; BlockDisposeFunction dispose;};#define BLOCK_DESCRIPTOR_3 1struct 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; BlockInvokeFunction invoke; struct Block_descriptor_1 *descriptor; // imported variables};
isa 指向 Block 对象的类;flags 决定 Block 包含的 Block_descriptor_1、Block_descriptor_2、Block_descriptor_3 信息;reserved 预留字段
Aspects 获取 Block signature 的方式:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// 这里自己定义了和系统 Block_layout 布局一致的 block 结构体// Block internals.typedef NS_OPTIONS(int, AspectBlockFlags) { AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25), AspectBlockFlagsHasSignature = (1 << 30)};typedef struct _AspectBlock { __unused Class isa; AspectBlockFlags flags; __unused int reserved; void (__unused *invoke)(struct _AspectBlock *block, ...); struct { unsigned long int reserved; unsigned long int size; // requires AspectBlockFlagsHasCopyDisposeHelpers void (*copy)(void *dst, const void *src); void (*dispose)(const void *); // requires AspectBlockFlagsHasSignature const char *signature; const char *layout; } *descriptor; // imported variables} *AspectBlockRef;// 用该方法获取 block 的 signaturestatic NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) { // 将 block 强转为自己定义的结构体:AspectBlockRef,和系统 Block_layout 定义的一致 AspectBlockRef layout = (__bridge void *)block; if (!(layout->flags & AspectBlockFlagsHasSignature)) { NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block]; AspectError(AspectErrorMissingBlockSignature, description); return nil; } void *desc = layout->descriptor; // 1、偏移 reserved、size 两个 Int 类型的大小 desc += 2 * sizeof(unsigned long int); if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) { // 2、偏移 copy、dispose 两个 指针 类型的大小 desc += 2 * sizeof(void *); } if (!desc) { NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block]; AspectError(AspectErrorMissingBlockSignature, description); return nil; } // 3、即拿到了 signature const char *signature = (*(const char **)desc); return [NSMethodSignature signatureWithObjCTypes:signature];}
将这些信息保存下来,在方法进入 forwardInvocation: 阶段,拿到方法的参数个数和入参值等信息分发到传入的 Block 上:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546- (BOOL)invokeWithInfo:(id
移花接木:libffilibffi 相当于 C 语言上的 runtime,拥有动态调用 C 方法及 OC 方法的能力。简单介绍下用法,例如,调用 C 函数:
123456789101112131415161718int c_func(int a , int b) { int sum = a + b; return sum;}- (void)libffi_call_c_func { ffi_cif cif; ffi_type *argTypes[] = {&ffi_type_sint, &ffi_type_sint}; ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, &ffi_type_sint, argTypes); int a = 1; int b = 2; void *args[] = {&a, &b}; int retValue; ffi_call(&cif, (void *)c_func, &retValue, args); // retValue = 3 NSLog(@"libffi_call_c_func, retValue:%d", retValue);}
调用 OC 函数:
12345678910111213141516171819202122- (int)oc_func:(int)a b:(int)b { int sum = a + b; return sum;}- (void)libffi_call_oc_func { SEL selector = @selector(oc_func:b:); NSMethodSignature *signature = [self methodSignatureForSelector:selector]; ffi_cif cif; ffi_type *argTypes[] = {&ffi_type_pointer, &ffi_type_pointer, &ffi_type_sint, &ffi_type_sint}; ffi_prep_cif(&cif, FFI_DEFAULT_ABI, (uint32_t)signature.numberOfArguments, &ffi_type_sint, argTypes); int arg1 = 1; int arg2 = 2; void *args[] = {(__bridge void *)(self), selector, &arg1, &arg2}; int retValue; IMP func = [self methodForSelector:selector]; ffi_call(&cif, (void *)func, &retValue, args); // retValue = 3 NSLog(@"libffi_call_oc_func, retValue:%d", retValue);}
函数调用 ffi_call 方法需要传入:1、函数模版(ffi_cif),2、函数指针,3、返回值,4、参数数组 这四个参数字段。重点在于 函数模版(ffi_cif)。用 ffi_cif 生成一套函数模版,这个模版定义了调用一个函数时,这个函数的:1、参数个数(int),2、返回值类型(ffi_type),3、包含各个入参类型(ffi_type)的数组
这样,libffi 的 ffi_call 方法就可以根据这个传入的 函数模版(ffi_cif)去调用函数了。
ffi_type 类型对应着系统的 Type Encodings 类型
除了动态调用 C & OC 方法外,libffi 提供的另外一个关键的能力是:通过 ffi_closure_alloc 方法创建一个关联着函数指针(IMP)的闭包(closure)通过 ffi_prep_closure_loc 函数给 cif 关联上这个闭包,并传入一个函数实体(fun)和刚才闭包里的 IMP
这样,当 IMP 被执行的时候,就会执行函数实体(fun),并且能在这个函数里拿到:1、cif,2、返回值,3、参数地址,4、自定义的关联数据(userdata),例如:
12345678910void *newIMP = NULL;ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&newIMP);ffi_status prepClosureStatus = ffi_prep_closure_loc(closure, cif, holo_lifecycle_ffi_closure_func, (__bridge void *)info, newIMP);if (prepClosureStatus != FFI_OK) { return;}static void holo_lifecycle_ffi_closure_func(ffi_cif *cif, void *ret, void **args, void *userdata) {}
有了这样的能力,我们就可以将上述的 newIMP 与我们要 hook 的方法 IMP 交换,这样调用 hook 方法的时候,就会执行到 holo_lifecycle_ffi_closure_func 方法,在这个方法里可以拿到原方法的参数个数、参数类型能信息,我们只要在这个方法里再调用一下原方法的 IMP,并在之前和之后分别调用要转发的目标对象的同名方法即可。
实现了一份简单版的基于 libffi 的 hook 方法代码:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247@implementation NSObject (LibffiHook)- (void)hook_method:(SEL)sel withBlock:(id)block { libffi_hook_func(self, sel, block);}@end// =================== LibffiHookInfo@interface LibffiHookInfo : NSObject { @public Class cls; SEL sel; void *_originalIMP; NSMethodSignature *_signature; id _block; ffi_cif *_block_cif; void *_block_IMP;}@end@implementation LibffiHookInfo@end// =================== LibffiHooktypedef void *LibffiHookBlockIMP;typedef struct LibffiHookBlock_layout LibffiHookBlock;struct LibffiHookBlock_layout { void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock volatile int flags; // contains ref count int reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 *descriptor; // imported variables};@implementation LibffiHookvoid libffi_hook_func(id obj, SEL sel, id block) { NSString *selStr = [@"libffi_hook_" stringByAppendingString:NSStringFromSelector(sel)]; const SEL key = NSSelectorFromString(selStr); if (objc_getAssociatedObject(obj, key)) { return; } LibffiHookInfo *info = [LibffiHookInfo new]; info->cls = [obj class]; info->sel = sel; info->_block = block; objc_setAssociatedObject(obj, key, info, OBJC_ASSOCIATION_RETAIN_NONATOMIC); Method method = class_getInstanceMethod([obj class], sel); const char *typeEncoding = method_getTypeEncoding(method); NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:typeEncoding]; info->_signature = signature; const unsigned int argsCount = method_getNumberOfArguments(method); // 1、构造参数类型列表 ffi_type **argTypes = calloc(argsCount, sizeof(ffi_type *)); for (int i = 0; i < argsCount; ++i) { const char *argType = [signature getArgumentTypeAtIndex:i]; ffi_type *arg_ffi_type = libffi_hook_ffi_type(argType); NSCAssert(arg_ffi_type, @"LibffiHook: can't find a ffi_type: %s", argType); argTypes[i] = arg_ffi_type; } // 2、返回值类型 ffi_type *retType = libffi_hook_ffi_type(signature.methodReturnType); // 3、准备 cif // 需要在堆上开辟内存,否则会出现内存问题 (LibffiHookInfo 释放时会 free 掉) ffi_cif *cif = calloc(1, sizeof(ffi_cif)); // 生成 ffi_cfi 模版对象,保存函数参数个数、类型等信息,相当于一个函数原型 ffi_status prepCifStatus = ffi_prep_cif(cif, FFI_DEFAULT_ABI, argsCount, retType, argTypes); if (prepCifStatus != FFI_OK) { NSCAssert(NO, @"LibffiHook: ffi_prep_cif failed: %d", prepCifStatus); return; } // 4、生成新的 IMP void *newIMP = NULL; ffi_closure *closure = ffi_closure_alloc(sizeof(ffi_closure), (void **)&newIMP); ffi_status prepClosureStatus = ffi_prep_closure_loc(closure, cif, libffi_hook_ffi_closure_func, (__bridge void *)info, newIMP); if (prepClosureStatus != FFI_OK) { NSCAssert(NO, @"LibffiHook: ffi_prep_closure_loc failed: %d", prepClosureStatus); return; } // 5、替换 IMP 实现 Class hookClass = [obj class]; SEL aSelector = method_getName(method); if (!class_addMethod(hookClass, aSelector, newIMP, typeEncoding)) { IMP originIMP = method_setImplementation(method, newIMP); if (info->_originalIMP != originIMP) { info->_originalIMP = originIMP; } } // 收集 block 信息(hook 的时候准备好,执行的时候会快点) // block 没有 SEL,所以比普通方法少一个参数 uint blockArgsCount = argsCount - 1; ffi_type **blockArgTypes = calloc(blockArgsCount, sizeof(ffi_type *)); // 1、构造参数类型列表 // 第一个参数是 block 自己,肯定为指针类型 blockArgTypes[0] = &ffi_type_pointer; for (NSInteger i = 2; i < argsCount; ++i) { blockArgTypes[i - 1] = libffi_hook_ffi_type([info->_signature getArgumentTypeAtIndex:i]); } // 2、准备 cif ffi_cif *callbackCif = calloc(1, sizeof(ffi_cif)); if (ffi_prep_cif(callbackCif, FFI_DEFAULT_ABI, blockArgsCount, &ffi_type_void, blockArgTypes) == FFI_OK) { info->_block_cif = callbackCif; } else { NSCAssert(NO, @"ffi_prep_cif failed"); } // 3、获取 block IMP LibffiHookBlock *blockRef = (__bridge LibffiHookBlock *)block; info->_block_IMP = blockRef->invoke;}static void libffi_hook_ffi_closure_func(ffi_cif *cif, void *ret, void **args, void *userdata) { LibffiHookInfo *info = (__bridge LibffiHookInfo *)userdata; // 1、before // NSLog(@"LibffiHook before, class: %@, sel: %@", NSStringFromClass(info->cls), NSStringFromSelector(info->sel)); // 2、call original IMP ffi_call(cif, info->_originalIMP, ret, args); // 3、after 回调 block // block 没有 SEL,所以比普通方法少一个参数 void **callbackArgs = calloc(info->_signature.numberOfArguments - 1, sizeof(void *)); // 第一个参数是 block 自己 callbackArgs[0] = (__bridge void *)(info->_block); // 从 index = 2 位置开始把 args 中的数据拷贝到 callbackArgs中 (从 index = 1 开始,第 0 个位置留给 block 自己) memcpy(callbackArgs + 1, args + 2, sizeof(*args)*(info->_signature.numberOfArguments - 2));// for (NSInteger i = 2; i < info->_signature.numberOfArguments; ++i) {// callbackArgs[i - 1] = args[i];// } ffi_call(info->_block_cif, info->_block_IMP, NULL, callbackArgs); free(callbackArgs);}NS_INLINE ffi_type *libffi_hook_ffi_type(const char *c) { switch (c[0]) { case 'v': return &ffi_type_void; case 'c': return &ffi_type_schar; case 'C': return &ffi_type_uchar; case 's': return &ffi_type_sshort; case 'S': return &ffi_type_ushort; case 'i': return &ffi_type_sint; case 'I': return &ffi_type_uint; case 'l': return &ffi_type_slong; case 'L': return &ffi_type_ulong; case 'q': return &ffi_type_sint64; case 'Q': return &ffi_type_uint64; case 'f': return &ffi_type_float; case 'd': return &ffi_type_double; case 'F':#if CGFLOAT_IS_DOUBLE return &ffi_type_double;#else return &ffi_type_float;#endif case 'B': return &ffi_type_uint8; case '^': return &ffi_type_pointer; case '@': return &ffi_type_pointer; case '#': return &ffi_type_pointer; case ':': return &ffi_type_pointer; case '{': { // http://www.chiark.greenend.org.uk/doc/libffi-dev/html/Type-Example.html ffi_type *type = malloc(sizeof(ffi_type)); type->type = FFI_TYPE_STRUCT; NSUInteger size = 0; NSUInteger alignment = 0; NSGetSizeAndAlignment(c, &size, &alignment); type->alignment = alignment; type->size = size; while (c[0] != '=') ++c; ++c; NSPointerArray *pointArray = [NSPointerArray pointerArrayWithOptions:NSPointerFunctionsOpaqueMemory]; while (c[0] != '}') { ffi_type *elementType = NULL; elementType = libffi_hook_ffi_type(c); if (elementType) { [pointArray addPointer:elementType]; c = NSGetSizeAndAlignment(c, NULL, NULL); } else { return NULL; } } NSInteger count = pointArray.count; ffi_type **types = malloc(sizeof(ffi_type *) * (count + 1)); for (NSInteger i = 0; i < count; i++) { types[i] = [pointArray pointerAtIndex:i]; } types[count] = NULL; // terminated element is NULL type->elements = types; return type; } } return NULL;}@end
libffi_hook_ffi_type 方法直接拷贝自:Stinger 里的 ffi_type *_st_ffiTypeWithType(const char *c) 方法
偷梁换柱:fishhook以 hook NSLog 为例,使用方式如下:
123456789101112131415161718192021222324252627- (void)fishhook_nslog { NSLog(@"fishhook before"); // fishhook before struct rebinding rebindingLog; // 需要 hook 的方法名 rebindingLog.name = "NSLog"; // 用哪个方法来替换 rebindingLog.replacement = myLog; // 保存原本函数指针 rebindingLog.replaced = (void **)&sys_nslog; struct rebinding rebindings[] = {rebindingLog}; rebind_symbols(rebindings, 1); NSLog(@"fishhook after"); // fishhook after---->🍺🍺🍺}// 函数指针,用来保存原来的函数static void (*sys_nslog)(NSString *format, ...);// 新函数(注意:不定参数未处理)void myLog(NSString * _Nonnull format, ...) { NSString *message = [format stringByAppendingString:@"---->🍺🍺🍺"]; (*sys_nslog)(message);}
fishhook 是 FaceBook 出品的能够 hook 动态库方法的框架(注意:仅能 hook 系统动态库方法),其源代码仅仅不足两百行,但是要搞明白这两百行代码的工作原理所需要的基础知识却是非常多的。
以下是 App 启动的大致过程,大致分为:开辟进程、加载可执行文件、加载 Dyld、Dyld 加载各个动态库、Rebase(因为 ALSR 技术进行基址重定位)、Bind(动态库的符号绑定)、加载 OC 类,分类、init 方法、调用 Main 函数、调用 UIApplicationMain 函数、起一个主线程的 runloop。
fishhook 的工作原理就是 Bind 阶段:符号重绑定。
普及一些涉及的基础概念:
为什么要动态链接?远古时代,所有源代码都在一个文件上(想象下开发一个App,所有源代码都在main.m上,这个 main.m 有几百万行代码。多人协同开发、如何维护、复用、每次编译几分钟….)。为了解决上面问题,于是有了静态链接。像我们平时开发,每个人开发自己的模块功能,最后编译链接在一起。解决了协同开发、可维护、可复用、编译速度也很快了(未改动的模块用编译好的缓存)。
静态链接好像已经很完美了。我们平时开发 App,都会用到 UIKit、Foundation 等许多系统库。假如都是通过静态链接的,手机里每个App都包含了一份这些系统库,每个App包体积变大了,占用磁盘空间;每个 App 运行时都要在内存里分别加载这些库,占用内存。
假设 UIKit 里某个函数有bug,需要更新,所有 App 都要重新静态链接最新的 UIKit 库,然后发版。
为了这些问题,于是产生了动态链接。
position-independent code (PIC 地址无关代码)产生地址无关代码原因:dylib 在编译时候,是不知道自己在进程中的虚拟内存地址的。因为 dylib 可以被多个进程共享,比如进程 1 可以在空闲地址 0x1000-0x2000 放共享对象 a,但是进程 2 的 0x1000-0x2000 已经被主模块占用了,只有空闲地址 0x3000-0x4000 可以放这个共享对象 a。所以共享对象 a 里面有一个函数,在进程 1 中的虚拟内存地址是 0x10f4,在进程 2 中的虚拟内存地址就成了 0x30f4。那机器指令就不能包含绝地地址了(动态库代码段所有进程共享;可修改的数据段,每个进程有一个副本,私有的)。
PIC原理:为了解决 dylib 的代码段能被共享,PIC(地址无关代码)技术就产生了。PIC 原理就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分是每个进程都有一个副本。
ALSR:在计算机科学中,地址空间配置随机加载(英语:Address space layout randomization,缩写ASLR,又称地址空间配置随机化、地址空间布局随机化)是一种防范内存损坏漏洞被利用的计算机安全技术。ASLR通过随机放置进程关键数据区域的地址空间来防止攻击者能可靠地跳转到内存的特定位置来利用函数。现代操作系统一般都加设这一机制,以防范恶意程序对已知地址进行Return-to-libc攻击。
Mach-O 文件在了解 fishhook 的具体原理之前还要熟悉下 Mach-O 文件:MacOS 上的可执行文件格式,类似于 Linux 和大部分 UNIX 的原生格式 ELF(Extensible Firmware Interface)。
1、Header:magic 魔数,内核识别 MachO2、Load Commands:存储 Mach-O 的布局信息3、Data:包含实际的代码和数据,Data 被分割为多个 Segment。每个 Segment 被分割为多个 Section,分别存放不同的数据
标准的三个 Segment:TEXT、DATA、LINKEDIT
3.1、TEXT:代码段,只读可执行,存储函数的二进制代码(__text),常量字符串(__cstring),Objective C 的类/方法名等信息3.2、DATA:数据段,读写,存储 Objective C 的字符串(__cfstring),以及运行时的元数据:class/protocol/method…3.3、LINKEDIT:启动 App 需要的信息,如 bind & rebase 的地址,代码签名,符号表…
Mach-O 中 __DATA 段有两个 Section 与动态符号绑定有关系
__nl_symbol_ptr :存储了 non-lazily 绑定的符号,这些符号在 Mach-O 加载的时候绑定完成__la_symbol_ptr :存储了 lazy 绑定的方法,这些方法在第一次调用时,由 dyld_stub_binder 进行绑定
为了实现系统动态库的共用,有了上文提到的动态链接。PIC 原理里提到了把那些需要共用的符号放在了 DATA 段,DATA 段的权限是可读写的,fishhook 就是在运行期修改 DATA 段里的数据,把系统符号绑定的地址重新绑定位我们自己定义的 hook 函数地址。
比如 NSLog 就是懒加载的,在第一次访问 NSLog 符号的时候先去 stub,stub 告诉从 __la_symbol_ptr 查找,__la_symbol_ptr 表示还没有 NSLog 符号真实函数地址,需要动态绑定,于是去 __nl_symbol_ptr 查找 dyld_stub_binder 函数的地址,进行查找真实的 NSLog 地址。找到后调用 NSLog 函数,并把这个地址保存进 __la_symbol_ptr。
下次调用 NSLog 函数的时候在 __la_symbol_ptr 就能得到真实地址进行跳转。
fashhook 工作流程:下图是 fashhook 在 GitHub 上 README.md 里的图,非常清晰的介绍了 fashhook 的工作流程:
以 hook NSLog 方法为例:
1、在 Lazy Symbol Pointer Table 找到 NSLog 顺序2、按上面的顺序在 Indirect Symbol Table 找到 NSLog3、把 Indirect Symbol Table 中 NSLog 的 data 值转为 10 进制,作为角标在 Symbols Table -> Symbols 中查找4、把 Symbols 表中 NSLog 的 data 值加上 String Table 中的第一条数据(base value)的值,确认找到了目标符号
把 Lazy Symbol Pointer Table 里的角标位置上的值修改为我们自己函数的地址,即完成了符号重绑定过程。
核心代码如下:
123456789101112131415161718192021222324252627282930313233343536373839404142434445// dyld 在 image 发生改变时,主动调用回调函数_dyld_register_func_for_add_image(_rebind_symbols_for_image);// slide 即 ALSR 产生的随机偏移量static void _rebind_symbols_for_image(const struct mach_header *header, intptr_t slide) { rebind_symbols_for_image(_rebindings_head, header, slide);}static void perform_rebinding_with_section(struct rebindings_entry *rebindings, section_t *section, intptr_t slide, nlist_t *symtab, char *strtab, uint32_t *indirect_symtab) { uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); for (uint i = 0; i < section->size / sizeof(void *); i++) { uint32_t symtab_index = indirect_symbol_indices[i]; if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL || symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) { continue; } uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; char *symbol_name = strtab + strtab_offset; struct rebindings_entry *cur = rebindings; while (cur) { for (uint j = 0; j < cur->rebindings_nel; j++) { if (strlen(symbol_name) > 1 && strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { if (cur->rebindings[j].replaced != NULL && indirect_symbol_bindings[i] != cur->rebindings[j].replacement) { *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; } // 找到目标位置,将该角标下的函数地址修改为我们自己的 hook 函数地址 indirect_symbol_bindings[i] = cur->rebindings[j].replacement; goto symbol_loop; } } cur = cur->next; } symbol_loop:; }}
李代桃僵:静态库插桩主要是 静态插桩的方式来实现Hook Method 这个文化里提到的技术,
文章里主要是操作的 _objc_msgSend 方法。基本原理大概是:
把自己的组件打成静态库,编译阶段因为不知道引用的外部符号的具体地址,只在符号表里做了标记,需要在链接阶段再查找外部符号的引用进行绑定。
通过脚本手动替换掉 .a 文件里的符号(_objc_msgSend)为我们自定义的符号(_hook_msgSend),注意两个符号必须等长。
再自己在 text 段定义一个 _hook_msgSend 函数,这样,链接阶段查找外部符号就绑定成了自己定义的函数。
脚本代码如下,需要对 Mach-O 格式非常熟悉:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233# -*- coding: utf-8 -*import osimport reimport structfrom pathlib import Path'''静态库结构1、魔数 8个字节magic(8) = '!
在 text 段定义一个 _hook_msgSend 函数:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970#ifdef __arm64__#include
简单解释下以上 _hook_msgSend 的汇编代码:
ARM64 有 31 个通用寄存器,每个寄存器可以存取一个 64 位的数据。我们可以通过 X0 - X30 来对这些寄存器进行寻址。对应 X0 - X30,W0 - W30 对应的就是相同单元数的低 32 位。W0 - W30 当进行写入操作时,会将高 32 位清零。
每一个寄存器具体的作用:
X0 - X7:这 8 个寄存器主要 用来存储传递参数 。如果参数超过 8 个,则会通过栈来传递 ;X0 也用来存放上文方法的返回值
X29 :即我们通常所说的帧指针 FP(Frame Pointer),指向当前方法栈的底部
X30 :即链接寄存器 LR(Link Register)。为什么叫做链接,是因为这个寄存器会记录着当前方法的调用方地址 ,即当前方法调用完成时应该返回的位置。例如我们遇到 Crash 要获取方法堆栈,其本质就是不断的向上递归每一个 X30 寄存器的记录状态(也就是栈上 X30 寄存器的内容)来找到上层调用方。
除了这些通用寄存器,还有一个最重要的 SP 寄存器:
SP 寄存器:即我们通常说的栈帧 SP(Stack Pointer)。指向当前方法栈的顶部。
这里 _hook_msgSend 方法里因为要用到 X0 - X7 等参数寄存器,所以每次保存下这些寄存器,调用原 _objc_msgSend 方法前再回复这些寄存器的内容,以保证上下文环境不被污染。
镜花水月:基于桥的全量方法 Hook 方案 TrampolineHook这是 五子棋 开源的中心重定向框架:TrampolineHook,用法示例:
1234567891011121314151617void myInterceptor() { printf("调用了 myInterceptor\n");}- (void)trampolineHook { THInterceptor *interceptor = [[THInterceptor alloc] initWithRedirectionFunction:(IMP)myInterceptor]; Method m = class_getInstanceMethod([HookDemoObj class], @selector(logString:)); IMP imp = method_getImplementation(m); THInterceptorResult *interceptorResult = [interceptor interceptFunction:imp]; if (interceptorResult.state == THInterceptStateSuccess) { method_setImplementation(m, interceptorResult.replacedAddress); // 设置替换的地址 } // 执行到这一行时,会调用 myInterceptor 方法 HookDemoObj *obj = [HookDemoObj new]; [obj logString:@"abc"];}
因为对 TrampolineHook 里的汇编代码还没完全看懂,之前 靛青 曾经写过一篇:TrampolineHook 学习笔记 这里就不错过多原理解析了。
除了以上提到的 Hook 方案,剩下还有 Dobby / Frida 逆向领域的 Hook 手段等,因为才疏学浅就不再继续解析了。