feature: App 逆向防护

This commit is contained in:
杭城小刘
2024-07-15 20:03:01 +08:00
parent 13f7457be9
commit 83fefff66b
109 changed files with 2549 additions and 672 deletions

View File

@@ -1,33 +1,29 @@
# fishhook 原理
## 先看看怎么用
## hook 分类
经常会遇到 hook oc 方法,但是遇到像 NSLog、objc_msgSend 等方法的时候 OC Runtime 就不满足了,有了 fishhook 神器hook “c 函数”已不是难题。
- Method Swizzle:利用 OC runtime,动态改变 SEL方法编号、IMP方法实现的对应关系达到 OC 方法调用流程改变的目的,主要用于 OC 方法
- fishhook是 Facebook 提供的一个动态修改链接 Mach-o 文件的工具,利用 Mach-O 文件加载原理通过修改懒加载和非懒加载2个表的指针达到 c 函数 hook 的目的。
- Cydia Substrate: 原名为 Mobile Substrate主要作用是针对 OC 方法、C 函数以及函数地址进行 hook当然并不是仅针对 iOS 而设计Android 也可使用。官方地址http://www.cydiasubstrate.com
hook只有二种:
- `inline hook`:直接修改函数入口代码或函数内某处代码跳转到自己代码
- 地址替换:包含入口表地址替换、出口表地址替换、结构体内地址替换等。这类最简单但不一定有效,不通过地址表的调用 hook 不到
## 应用
经常会遇到 hook oc 方法,但是遇到像 NSLog、objc_msgSend 等方法的时候 OC Runtime 就不满足了 ,有了 fishhook 神器hook “c 函数”已不是难题。
为什么对 “c 函数”加了引号,带着问题往下看
Hook NSLog上 Demo
```objectivec
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 系统 c 函数
以 NSLog 为例。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookWithCFunction.png" style="zoom:30%" />
可以看到 hook 成功了。
@@ -39,10 +35,49 @@ struct rebinding {
};
```
### Hook 自定义 c 函数
自定义一个 c 函数 `handleTouchAction` ,发现没有 hook 成功。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookWithUserCFunction.png" style="zoom:30%" />
不禁令人好奇,同样是 c 函数,为什么系统 c 函数可以 hook自定义的 c 函数无法 hook带着问题继续探究
## 原理窥探
FishHook 是 FaceBook 提供的一个可以动态修改链接 Mach-O 文件的工具。利用 Mach-O 文件的加载原理通过修改懒加载和非懒加载2个表的指针达到 hook 系统 C 函数的目的。
### Mach-O 文件权限
Mach-O 分为代码段、数据段...
- 代码段:可读、可执行、不可写
- 数据段:可读、可写、不可执行
### 系统共享缓存
我们知道 NSLog 的函数实现在 Foundation 库中,而我们开发自己写的其他函数则在自身可执行文件中,也就是 Mach-O。
iOS 共享缓存在iOS 3.1及以后的版本中Apple 将系统库文件打包合并成一个大的缓存文件,存放在 `/System/Library/Caches/com.apple.dyld/ ` 目录下,以减少冗余并优化内存使用
- 所有的进程均可访问
- 缓存文件的架构特定性:共享缓存文件根据不同的处理器架构有不同的版本,例如 `dyld_shared_cache_arm64` 针对的是 ARM 64 位架构的处理器
- 动态库的加载优化通过共享缓存iOS 系统可以在程序运行时更高效地加载动态库,因为不需要每个应用程序重复加载相同的库文件,从而加快了启动速度并提高了性能
App 对应的 Mach-O 被 dyld 装载进内存的时候,`NSLog ` 地址还不确定,其位于 `Foundation` 框架中,也就是位于共享缓存中。
这带来一个问题:代码在 Mach-O 文件上策马奔腾,在愉快的执行,当遇到 `NSLog` 的时候,该如何确定位于 `Foundation` 框架中 NSLog 真实函数地址呢编译阶段clang 可以知道任何设备iPhone14、iPhone6s任何架构arm64、armv7上 Foundation 真实的内存吗?显然不可能。
这里稍微展开谈谈静态链接和动态链接。
链接分为静态链接和动态链接。早期计算机都是采用静态链接这种方式的。静态链接存在缺点:
@@ -59,48 +94,100 @@ struct rebinding {
但也带来了坏处因为都是程序每次装载的时候进行重新链接。有解决方案叫做延迟绑定Lazy binding可使得动态链接对性能的影响减的最小。据估算动态链接相比静态链接存在大约5%的性能损失,但换来程序在空间上的节省和程序构建和升级的灵活性,是值得的。
地址无关代码PIC
装载时重定位是解决动态模块中有绝对地址引用的方案之一。但其存在一个很大缺点,是指令部分无法在多个进程间共享,这样就是失去动态链接节省内存这一优势。我们还需要一种更优雅的方案,希望程序模块中的共享指令部分在装载时不需要因为装载地址改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这个方案就是 地址无关代码Position Independent CodePIC 技术。
如何解决?
编译阶段给 NSLog 一个默认地址,在应用启动时,通过重定向,把真正的地址重新写入到 Mach-O 中,但效率很低。 后来诞生了 `PIC` 技术
### PIC 技术
装载时重定位是解决动态模块中有绝对地址引用的方案之一。但其存在一个很大缺点,是指令部分无法在多个进程间共享,这样就是失去动态链接节省内存这一优势。
我们还需要一种更优雅的方案,希望程序模块中的共享指令部分在装载时不需要因为装载地址改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这个方案就是 地址无关代码Position Independent CodePIC 技术。
Position-Independent Code即位置无关代码。这是一种编译技术允许生成的代码在内存中的任何位置执行而不需要任何修改。这在动态链接库dylib中非常重要因为它允许系统动态地将库加载到内存中的不同位置而不需要重新链接。
优点是:
- 共享代码:多个应用程序可以共享同一个动态库的实例,节省内存并减少启动时间。
- 动态链接器dyld可以优化符号绑定过程提高应用程序的启动速度
- 支持代码重定位,使得应用程序更新和修补更加灵活
写的业务代码里面假如某一行调用了 `NSLog`,那么在编译阶段,使用 NSLog 只是 IDE 提供了功能,让你可以看到声明而已。编译后的可执行文件,还是不知道 NSLog 的具体函数地址。这个底层是如何工作的呢?
在工程编译阶段,所产生的 Mach-O 可执行文件中会预留出一段空间,这个空间被叫做符号表,存放在 `_DATA` 数据段中,且数据段是可读可写的。
有了 PIC 之后,工作流程为:
工程中所有引用了动态库共享缓存区中的系统符号,其指向的地址设置成符号地址。比如工程中 NSLog那么编译时就会在 Mach-O 中创建一个 NSLog 符号,工程中的 NSLog 就指向这个符号
- 编译期,在 Mach-O 中数据段生成一块区域,该区域叫符号表。数据段可读可写。
当 dyld 将 Mach-O 加载到内存中时,读取 header 中 load command 信息,找出需要加载哪些库文件,去做绑定的操作。比如 dyld 会找到 Foundation 中的 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 对应的符号上。
工程中所有引用了动态库共享缓存区中的系统符号,其指向的地址设置成符号地址。比如工程中 NSLog那么编译时就会在 Mach-O 中创建一个 NSLog 符号,工程中的 NSLog 就指向这个符号
当 DYLD 加载当前可执行文件的时候,才将这个表每个编号对应的函数地址去填上去,这个动作叫做**符号绑定**
- dyld 加载 Mach-O符号绑定。
它指向了一个表(类似一个应用程序的外部函数名,函数真正实现地址),去这个表里面找地址,这个表叫做**符号表**
当 dyld 将 Mach-O 加载到内存中时,读取 header 中 load command 信息,找出需要加载哪些库文件,去做绑定的操作。比如 dyld 会找到 Foundation 中的 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 对应的符号上
当真正去调用 NSLog 函数的时候才去这个符号表中去寻找函数地址,去调用实现。
调用外部函数(在内部找不到方法实现)的时候,在 Mach-O 的数据段生成一个区域,叫做符号表。符号表的 key 就是方法名,比如 NSLog。
fishHook 做的事情就是 rebind将 NSLog 真正实现的地址指向到其他我们生成的函数地址去hook
### 实践探索
PIC 技术。
做个实验验证下,完整流程
第一步:可以看到 NSLog 位于 Lazy Symbol Pointers 里的第一个。lazy 说明只有在用到的时候才去绑定。下断点验证下
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookMachO.png" style="zoom:40%" />
第二步:在 `NSLog` 处打断点,触发后 LLDB 模式下 `image list` 查看所有的 image。可以看到第一个 image 就是 App 主程序镜像,且 image 开始地址为 `0x0000000100da5000`
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookDemoImageList.png" style="zoom:40%" />
第三步:进入 LLDB 模式,根据 image base 地址 + offset 计算 NSLog 的地址。即 `memory read 0x0000000102eec000+0xC000`,查看下内存信息
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/NSLogFakeAddress.png" style="zoom:40%" />
第四步:断点过下一行,即执行过一次 NSLog。然后再通过 LLDB 根据地址查看汇编代码,`dis -s addr` 查看
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/LLDBNSLogAddressSymbol.png" style="zoom:40%" />
第五步:继续过断点,等执行完 `rebind_symbols` 再看看内存信息。可以看到再 rebind 之后,地址变了。然后根据地址查看汇编代码,发现已经是我们自定义的函数了。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishhookResult.png" style="zoom:40%" />
第一步: 在 `Lazy Symbol Pointers` 懒加载符号表中看到第一个符号 `NSLog`索引为1。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookMachO1.png" style="zoom:30%" />
第二步:根据索引,在 `Dynamic Symbol Tables` 动态符号表中看到第一条数据,是 NSLog 相关的。其 Data 值 `00000084` 是十六进制的,换算为十进制就是 132。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookMachO2.png" style="zoom:30%" />
第三步:根据第二步得到的角标,在 `Symbol Table` 符号表中查找第132个位置。可以看到其 Data 值 `000000AA` 是偏移值。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookMachO3.png" style="zoom:30%" />
第四步:在 `String Table` 中,第一个位置 `0000CFE4` 加上偏移值 `0xAA` ,等于 `0xD08E`,如下图所示,就是 `NSLog` 符号真实的地址。
<img src="https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/FishHookMachO4.png" style="zoom:30%" />
`NSLog``dispatch_once`stub 代码指向 `lazy Symbol Pointers` 部分,`lazy Symbol Pointers` 中又指向 `stub_helper`,默认又到了 `dyld_stub_binder`,在首次调用时再绑定真实调用地址。
fishhook 正是利用上面这点,将 `lazy Symbol Pointer` 中的符号替换成自己的函数,从而实现 hook这也是为什么 fishhook 不能 hook 二进制文件中自定义的 C 函数
| 编号(符号) | 地址 | |
| ------ | -------- | --- |
| NSLog | 0xaabbcc | |
| ... | ... | |
![image-20200810201822593](https://github.com/FantasticLBP/knowledge-kit/raw/master/assets/mage-20200810201822593.png)
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
4.
总结版FishHook 基于 ASRL + Lazy Symbol Table 运行,另外能不能 hook 要看代码是落在 Data 段(RW) 还是 TextRO
知道了 fishhook 的工作原理,我们就知道 fishhook 是 hook 不了自己写的 c 函数了。因为自定义函数是没有通过符号去寻找函数真正地址的这个过程。而系统库是通过符号去绑定真实地址的。