Files
knowledge-kit/Chapter1 - iOS/1.88.md
2022-05-24 13:00:23 +08:00

6.7 KiB
Raw Blame History

fishhook 原理

先看看怎么用

经常会遇到 hook oc 方法,但是遇到像 NSLog、objc_msgSend 等方法的时候 OC Runtime 就不满足了,有了 fishhook 神器hook “c 函数”已不是难题。

为什么对 “c 函数”加了引号,带着问题往下看

Hook NSLog上 Demo

static void (*SystemLog)(NSString *format, ...);
- (void)viewDidLoad {
    [super viewDidLoad];
    struct rebinding NSLogRebinding = {
        "NSLog",
        lbpLog,
        (void *)&SystemLog
    };
    struct rebinding rebs[1] = {NSLogRebinding};
    rebind_symbols(rebs, 1);
    NSLog(@"沙沙");
}
void lbpLog(NSString *format, ...) {
    format = [NSString stringWithFormat:@"fishhook 探索 - %@", format];
    SystemLog(format);
}
@end
// fishhook 探索 - 沙沙

可以看到 hook 成功了。

struct rebinding {
  const char *name;     // 需要 hook 的函数名称c 字符串
  void *replacement;    // 新函数地址
  void **replaced;      // 原始函数地址的指针
};

原理窥探

我们知道 NSLog 的函数实现在 Foundation 库中,而我们开发自己写的其他函数则在自身可执行文件中,也就是 Mach-O。

这里稍微展开谈谈静态链接和动态链接。

链接分为静态链接和动态链接。早期计算机都是采用静态链接这种方式的。静态链接存在缺点:

  • 对于计算机内存和磁盘浪费很严重。想象下,每个程序内部保留了 printf、scanf 等公用库函数,还有很多其他库函数和所需要的数据结构。

  • 程序的开发和发布很不方便。比如应用 A使用的 Lib.o 是一个第三方厂商提供的,当 lib.o 修复 bug 或者升级,开发都需要将应用 A 重新链接再发布,整个周期很不方便。

要解决空间浪费和更新困难最简单的办法就是把程序的模块拆分,形成独立文件,而不再将他们静态地链接在一起。而是等到程序运行起来才进行链接,也就是动态链接。

动态链接涉及运行时的链接以及多个文件的装载,必须有操作系统级别的支持。此时还有个角色叫做动态链接库。所有应用都可以在运行时使用它。

程序与 lib 动态库之间的链接工作是由动态链接器完成的,而不是静态链接器 ld 完成的。也就是动态链接是把链接这个过程由程序装载前被推迟到了装载的时候。

但也带来了坏处因为都是程序每次装载的时候进行重新链接。有解决方案叫做延迟绑定Lazy binding可使得动态链接对性能的影响减的最小。据估算动态链接相比静态链接存在大约5%的性能损失,但换来程序在空间上的节省和程序构建和升级的灵活性,是值得的。

地址无关代码PIC

装载时重定位是解决动态模块中有绝对地址引用的方案之一。但其存在一个很大缺点,是指令部分无法在多个进程间共享,这样就是失去动态链接节省内存这一优势。我们还需要一种更优雅的方案,希望程序模块中的共享指令部分在装载时不需要因为装载地址改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这个方案就是 地址无关代码Position Independent CodePIC 技术。

写的业务代码里面假如某一行调用了 NSLog,那么在编译阶段,使用 NSLog 只是 IDE 提供了功能,让你可以看到声明而已。编译后的可执行文件,还是不知道 NSLog 的具体函数地址。这个底层是如何工作的呢?

在工程编译阶段,所产生的 Mach-O 可执行文件中会预留出一段空间,这个空间被叫做符号表,存放在 _DATA 数据段中,且数据段是可读可写的。

工程中所有引用了动态库共享缓存区中的系统符号,其指向的地址设置成符号地址。比如工程中 NSLog那么编译时就会在 Mach-O 中创建一个 NSLog 符号,工程中的 NSLog 就指向这个符号

当 dyld 将 Mach-O 加载到内存中时,读取 header 中 load command 信息,找出需要加载哪些库文件,去做绑定的操作。比如 dyld 会找到 Foundation 中的 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 对应的符号上。

当 DYLD 加载当前可执行文件的时候,才将这个表每个编号对应的函数地址去填上去,这个动作叫做符号绑定

它指向了一个表(类似一个应用程序的外部函数名,函数真正实现地址),去这个表里面找地址,这个表叫做符号表

当真正去调用 NSLog 函数的时候才去这个符号表中去寻找函数地址,去调用实现。

调用外部函数(在内部找不到方法实现)的时候,在 Mach-O 的数据段生成一个区域,叫做符号表。符号表的 key 就是方法名,比如 NSLog。

fishHook 做的事情就是 rebind将 NSLog 真正实现的地址指向到其他我们生成的函数地址去hook

PIC 技术。

编号(符号) 地址
NSLog 0xaabbcc
... ...

image-20200810201822593

fishhook 做的事情就是将系统的符号表,将符号表中的特定符号对应的地址,修改为自定义的函数地址。起到了 hook 作用。也就是说外部的 c 函数,在 iOS 中的调用属于动态调用

知道了 fishhook 的工作原理,我们就知道 fishhook 是 hook 不了自己写的 c 函数了。因为自定义函数是没有通过符号去寻找函数真正地址的这个过程。而系统库是通过符号去绑定真实地址的。可以通过 rebind_symbols 这个名称得以印证。

纠错

之前同事问了个问题fishhook为什么能hook系统库的c方法不能hook c++

  1. FishHook 的原理是 ASRL + Lazy Symbol Table。系统库 NSLog 地址不确定的,会随机偏移,当 DYLD 加载后根据 offset 动态计算(也就是 rebinding、rebase
  2. Data 段可读可写NSLog 位于 Data 段,自定义函数位于 Text 段只读。所以C/C++ FishHook 可以hook 系统库/动态库共享缓存这些符号
  3. 知道机制后也就可以说:自定义符号是在 Text 段Read Only所以不能被 FishHook hook。另外系统库很多都是 c 实现。要是某个库是 C++ 实现,也可以 hook

总结版FishHook 基于 ASRL + Lazy Symbol Table 运行,另外能不能 hook 要看代码是落在 Data 段(RW) 还是 TextRO