OC的runtime想必大家都用过,我们可以通过hook的方式,将Method的IMP的指向修改或替换从而达到方法交换替换的目的
关于OC的方法交换原理和注意点,可以参照这篇文章方法替换注意点;
那么OC的方法交换我们知道原理,那么C语言的函数了,我们能否对它也进行交换了?我们知道OC能交换是因为OC的方法他的IMP指针的指向是运行时才绑定确定的;
假设1
如果C语言函数指针的地址也是在某个时机绑定的,而不是编译完成就确定地址的话,那么理论也是可以进行方法hook的。
我们假设C函数的地址是编译时期确定的,那么系统的动态库的c函数的地址就需要是固定的,那么就是编译时期分配给这些函数的手机的内存地址都是预留给这些函数占用了;这显然不符合设计规范,iOS系统的动态库,所有的app在使用的时候就是共享的,并且是按需加载到内存的,没有app使用的是不会加载进去造成浪费的;
苹果系统在动态库的c函数调用的时候,编译阶段会给一个符号地址,在运行时首次调用该函数的时候才会去对符号进行绑定,映射到它真正的函数地址,这种手段专业的术语
PIC(Position Independent Code)
对PIC感兴趣的可以查阅该文章GCC -fPIC option;
Position Independent Code means that the generated machine code is not dependent on being located at a specific address in order to work.
由于PIC的存在,那么我们就可以修改符号的绑定来达到方法替换的目的,听着跟OC的runtime方法替换原理似曾相识;facebook开源的fishhook库就能对动态库的c函数进行hook
fishhook简介
fishhook is a very simple library that enables dynamically rebinding symbols in Mach-O binaries running on iOS in the simulator and on device. This provides functionality that is similar to using DYLD_INTERPOSE on OS X. At Facebook, we've found it useful as a way to hook calls in libSystem for debugging/tracing purposes (for example, auditing for double-close issues with file descriptors).
fishhook是一个非常简单的库,它支持在模拟器和设备的iOS上运行的Mach-O二进制文件中动态地重新绑定符号
Mach-O就是iOS app的可执行文件,在程序启动的时候,dyld读取Mach-O文件的配置,进行动态库的加载,对动态链接库进行 rebase 指针调整和 bind 符号绑定等操作。
fishhook rebinding定义如下,这个后面会用到,所以讲一下
struct rebinding {
const char *name; // 需要替换的函数名
void *replacement; // 新函数的指针
void **replaced; // 老函数的新指针-被替换的函数
};
rebinding需要用到的函数定义如下:
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
关于fishhook源码的解析可以参照该文章从fishhook看runtime,hook系统C函数;讲述的很清晰了,源码的注视在demo中也有,感兴趣的可以学习下。
hook系统库c函数
我们hook系统动态库的c函数,一般步骤:
- 声明并定义新函数
- 声明一个函数指针用来存储老函数的地址,如果hook的函数内需要调用原始函数
- 初始化
struct rebinding
- 调用
rebind_symbols
上代码,hook系统的objc_setAssociatedObject
static char kAssociatedKey;
static void (*system_objc_setAssociatedObject)(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy);
- (void)testHookCocoaFrameworkMethod {
NSLog(@"before objc_setAssociatedObject hook");
objc_setAssociatedObject(self, &kAssociatedKey, @(111), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
struct rebinding rebindingStruct;
rebindingStruct.name = "objc_setAssociatedObject";
rebindingStruct.replacement = (void *)hook_objc_setAssociatedObject;
rebindingStruct.replaced = (void **)&system_objc_setAssociatedObject;
rebind_symbols(&rebindingStruct, 1);
NSLog(@"after objc_setAssociatedObject hook");
objc_setAssociatedObject(self, &kAssociatedKey, @(222), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
void hook_objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy) {
NSLog(@"%s", __FUNCTION__);
system_objc_setAssociatedObject(object, key, value, policy);
}
为了验证我们前面的假设1
-
下个符号断点
-
打开debug,查看汇编选项
可以看到调用的时候是个symbols stub for objc_setAssociatedObject
查看汇编代码,看到函数真实的调用地址是0x0000000100b4eaa3
(lldb) dis -s 0x104395e58
RuntimeLearning`objc_setAssociatedObject:
0x104395e58 <+0>: nop
0x104395e5c <+4>: ldr x16, #0x64e4 ; (void *)0x0000000214001fc8: objc_setAssociatedObject
0x104395e60 <+8>: br x16
RuntimeLearning`objc_setProperty_atomic_copy:
0x104395e64 <+0>: nop
0x104395e68 <+4>: ldr x16, #0x64e0 ; (void *)0x0000000104396170
0x104395e6c <+8>: br x16
RuntimeLearning`objc_setProperty_nonatomic_copy:
0x104395e70 <+0>: nop
0x104395e74 <+4>: ldr x16, #0x64dc ; (void *)0x000000010439617c
(lldb) dis -s 0x0000000214001fc8
libobjc.A.dylib`objc_setAssociatedObject:
0x214001fc8 <+0>: b 0x214003cc8 ; _object_set_associative_reference
libobjc.A.dylib`objc_removeAssociatedObjects:
0x214001fcc <+0>: cbz x0, 0x214001fe4 ; <+24>
0x214001fd0 <+4>: tbnz x0, #0x3f, 0x214001fe8 ; <+28>
0x214001fd4 <+8>: ldr x8, [x0]
0x214001fd8 <+12>: and x8, x8, #0x3
0x214001fdc <+16>: cmp x8, #0x1 ; =0x1
0x214001fe0 <+20>: b.ne 0x214001fe8 ; <+28>
0x214001fe4 <+24>: ret
(lldb)
执行rebinding之后再次调用系统的objc_setAssociatedObject
可以看到
我们这个时候再查看objc_setAssociatedObject
符号地址0x104395e58
的汇编
(lldb) dis -s 0x104395e58
RuntimeLearning`objc_setAssociatedObject:
0x104395e58 <+0>: nop
0x104395e5c <+4>: ldr x16, #0x64e4 ; (void *)0x000000010437f81c: hook_objc_setAssociatedObject at /Users/hechao/Documents/Demos/RuntimeLearning/RuntimeLearning/fishhook/FishhookUsage.m:45
0x104395e60 <+8>: br x16
RuntimeLearning`objc_setProperty_atomic_copy:
0x104395e64 <+0>: nop
0x104395e68 <+4>: ldr x16, #0x64e0 ; (void *)0x0000000104396170
0x104395e6c <+8>: br x16
RuntimeLearning`objc_setProperty_nonatomic_copy:
0x104395e70 <+0>: nop
0x104395e74 <+4>: ldr x16, #0x64dc ; (void *)0x000000010439617c
(lldb)
可以看到函数的符号的指向变成了0x104395e5c <+4>: ldr x16, #0x64e4 ; (void *)0x000000010437f81c: hook_objc_setAssociatedObject at /Users/hechao/Documents/Demos/RuntimeLearning/RuntimeLearning/fishhook/FishhookUsage.m:45
说明rebinding之后函数符号保存的真实调用的地址被修改为我们定义的函数了。
hook自定义动态库c函数
既然系统的动态库的c函数可以hook,那么我们自定义的动态库的函数是否也可以了?
我们建一个动态库,添加一个c方法
int calculate_add(int a, int b) {
return a + b;
}
hook代码如下:
static int (*libadd_calculate_add)(int a, int b);
- (void)testHookSelfDefineDynamicFrameworkMethod {
int sum = calculate_add(2, 3);
NSLog(@"before calculate_add hook: 2 + 3 = %d", sum);
struct rebinding rebindingStruct;
rebindingStruct.name = "calculate_add";
rebindingStruct.replacement = (void *)hook_calculate_add;
rebindingStruct.replaced = (void **)&libadd_calculate_add;
rebind_symbols(&rebindingStruct, 1);
int sum1 = calculate_add(2, 3);
NSLog(@"after calculate_add hook: 2 + 3 = %d", sum1);
}
int hook_calculate_add(int a, int b) {
NSLog(@"%s", __FUNCTION__);
int sum = libadd_calculate_add(a, b);
NSLog(@"正确的和是:%d", sum);
return 100;
}
在hook前后分别对calculate_add
下断点
hook前:
(lldb) dis -s 0x10bacfc38
RuntimeLearning`calculate_add:
0x10bacfc38 <+0>: jmpq *0x64f2(%rip) ; (void *)0x000000010bdeafa0: calculate_add at /Users/hechao/Documents/Demos/RuntimeLearning/RuntimeLearning/Dynamic/calculate.c:15
RuntimeLearning`ceil:
0x10bacfc3e <+0>: jmpq *0x64f4(%rip) ; (void *)0x000000010bad00e6
RuntimeLearning`class_addMethod:
0x10bacfc44 <+0>: jmpq *0x64f6(%rip) ; (void *)0x000000010c3d59f4: class_addMethod
RuntimeLearning`class_copyMethodList:
0x10bacfc4a <+0>: jmpq *0x64f8(%rip) ; (void *)0x000000010bacfe98
RuntimeLearning`class_getClassMethod:
0x10bacfc50 <+0>: jmpq *0x64fa(%rip) ; (void *)0x000000010c3cbb9e: class_getClassMethod
(lldb) dis -s 0x000000010bdeafa0
libadd.a`calculate_add:
0x10bdeafa0 <+0>: pushq %rbp
0x10bdeafa1 <+1>: movq %rsp, %rbp
0x10bdeafa4 <+4>: movl %edi, -0x4(%rbp)
0x10bdeafa7 <+7>: movl %esi, -0x8(%rbp)
-> 0x10bdeafaa <+10>: movl -0x4(%rbp), %esi
0x10bdeafad <+13>: addl -0x8(%rbp), %esi
0x10bdeafb0 <+16>: movl %esi, %eax
0x10bdeafb2 <+18>: popq %rbp
0x10bdeafb3 <+19>: retq
0x10bdeafb4: addl %eax, (%rax)
0x10bdeafb6: addb %al, (%rax)
0x10bdeafb8: sbbb $0x0, %al
0x10bdeafba: addb %al, (%rax)
0x10bdeafbc: addb %al, (%rax)
0x10bdeafbe: addb %al, (%rax)
(lldb)
可以看到自定义的动态库的c函数也是symbol,那么就也可以像系统的那样rebinding symbol达到hook的目的;
执行rebinding之后,再次调用calculate_add
的指向已经修改为hook_calculate_add
;这说明hook成功了
(lldb) dis -s 0x10bacfc38
RuntimeLearning`calculate_add:
0x10bacfc38 <+0>: jmpq *0x64f2(%rip) ; (void *)0x000000010bab8d10: hook_calculate_add at /Users/hechao/Documents/Demos/RuntimeLearning/RuntimeLearning/fishhook/FishhookUsage.m:63
RuntimeLearning`ceil:
0x10bacfc3e <+0>: jmpq *0x64f4(%rip) ; (void *)0x000000010bad00e6
RuntimeLearning`class_addMethod:
0x10bacfc44 <+0>: jmpq *0x64f6(%rip) ; (void *)0x000000010c3d59f4: class_addMethod
RuntimeLearning`class_copyMethodList:
0x10bacfc4a <+0>: jmpq *0x64f8(%rip) ; (void *)0x000000010bacfe98
RuntimeLearning`class_getClassMethod:
0x10bacfc50 <+0>: jmpq *0x64fa(%rip) ; (void *)0x000000010c3cbb9e: class_getClassMethod
(lldb)
这时候执行了hook的方法
总结
fishhook就是利用编译器的PIC,符号的绑定是运行时去绑定的,那么修改符号的绑定就能达到hook动态库的c函数。