Inline Hook 就是在运行的流程中插入跳转指令来抢夺运行流程的一个方法。大体分为三步:
以上的 N 有多大,取决于你的跳转指令写得有多大(占用了多少指令)。
相较与 Inline Hook, fishhook 使用的是很 Trick 的方式,通过劫持 stub 从而达到替换的目的。
通过查看 objc_msgSend ,我们知道 Runtime 的 Method Swizzling 并不适用,因为它并不是 Objective-C 方法,调用时并不会有我们经常说的“消息转发”;
通过查看 Runtime 源码,我们发现 objc_msgSend 是使用纯汇编实现函数,通过汇编文件我们可以看到以下定义:
ENTRY _objc_msgSend
在文件中搜索 ENTRY 我们找到了这么一个宏:
.macro ENTRY /* name */
.text
.align 5
.globl $0
$0:
.endmacro
这里定义了一个汇编宏,表示在 text 段定义一个 global 的 _objc_msgSend ,$0 其实就是这个宏传入的参数,也就是一个方法入口。我们可以手动将这个宏来展开:
.text
.align 5
.globl _objc_msgSend
; ...
这里我们发现,在第三行的位置通过 C 的 name mangling 命名规则,将符号 _objc_msgSend 映射为 C 的全局方法符号。
也就是说,这段汇编可以通过头文件声明,便已完成了 C 的函数定义。
我们在后续处理的时候可以将其视为 C 方法。
当然我们也可以使用 MachOView 来验证这个符号名:
既然 objc_msgSend 已经视作 C 方法,那么我就可以使用 fishhook 来完成 Inline Hook 的第一步:跳到 Hook 方法。
Apple 自身的共享缓存库其实不会编译进我们自己的 Mach-O 中的,而是在 App 启动后的动态链接才会去做重绑定操作。
这里我们以某个APP举例,通过nm -n命令查看所有方法的符号以及对应地址:
> nm -n xxxxx
...
U _NSLocalizedRecoverySuggestionErrorKey
U _NSLog
U _NSLogv
U _NSMallocException
...
U _objc_msgSend
U _objc_msgSendSuper
U _objc_msgSendSuper2
...
0000000100000000 T __mh_execute_header
0000000100af3cb8 T __ZN5folly6detail15str_to_floatingIdEENS_8ExpectedIT_NS_14ConversionCodeEEEPNS_5RangeIPKcEE
...
在这里我们就可以发现,其实 NSLog 方法其实并没有地址,这些系统库函数并不会打入到我们的 App 包中;当我们使用它们时,dyld 就要从共享的动态库中查找对应方法,然后将具体的函数地址绑定到之前声明的地方,从而实现系统库方法的调用。
对于这种可在主存中任意位置正确地执行,并且不受其绝对地址影响的技术,在计算机领域称之为 PIC(Position Independent Code)技术。
Mach-O 中 __DATA 段有两个 Section 与动态符号绑定有关系:
既然 __la_symbol_ptr 存储了所有 lazy 绑定的方法,那也就是说在这些位置应该存储了对应方法的地址。
写一段代码验证一下:
#import "ViewController.h"
#import "fishhook.h"
@implementation ViewController
static void (*ori_nslog)(NSString * format, ...);
void new_nslog(NSString * format, ...) {
//自定义的替换函数
format = [format stringByAppendingFormat:@" FISHHOOK "];
ori_nslog(format);
}
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"hello world");
struct rebinding nslog;
nslog.name = "NSLog";
nslog.replacement = new_nslog;
nslog.replaced = (void *)&ori_nslog;
rebind_symbols((struct rebinding[1]){nslog}, 1);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"hello world");
}
@end
控制台输出如下:
2020-08-05 16:42:32.698782+0800 Fish[7569:336213] hello world
2020-08-05 16:42:34.032334+0800 Fish[7569:336213] hello world FISHHOOK
2020-08-05 16:42:34.776257+0800 Fish[7569:336213] hello world FISHHOOK
我们在第一个 hello world 的位置增加断点:
使用 image list 命令来获取 App 的基地址:
可以看到App 基地址为 0x0000000106f9a000。
然后我们使用 MachOView 来查看NSLog在 __la_symbol_str 中的偏移量,偏移量为0x5000。
我们使用lldb的x命令来查看一下 基地址+偏移量 位置的数据,发现在此位置的数据是 0x0106f9c3f0 (这一步看不懂的请看上一篇博文里面的大端序和小端序)。
使用反汇编 dis 命令, 来看对应地址所指向的代码段:
那么这段代码到底是我们的 NSLog 的代码吗?我们可以直接看断点的汇编代码:
可以看到汇编中 callq 命令对应对地址是 0x106f9c354 。
对这个地址再次进行 dis -s 反汇编来查看:
可以看到上图中方框圈起来的地址就是上面 基地址+偏移量 这个位置存储的地址。
在这之后,我们再对点击事件中的 NSLog 方法下一个断点,并且点击一下模拟器屏幕来触发一下。
我们再使用 x 和 dis -s 两个命令来查看一下 基地址+偏移量 中的新数据:
可以很清楚的看到此时的NSLog函数的地址已经被替换了:
至此,fishhook Hook C 方法已经完成。
fishhook 的 Hook 思路,也就是我们上述所描述的,当第一次调用系统动态库中 C 方法时,去替换掉 __la_symbol_str 的指针。
但是它的逻辑要比这个思路还是要复杂一些,比如 fishhook 要解决以下问题:
如果想看具体的实现,推荐去阅读源码(不到两百行)。
归纳一下 fishhook 来修改 C 方法的本质,dyld 更新 Mach-O 二进制的 __DATA segment 的 __la_symbol_str 中的指针,使用 rebind_symbol 方法更新两个符号位置来进行符号的重新绑定。
无fishhook:
有fishhook: