Fishhook原理深度剖析:从Mach-O到运行时函数Hook
前言
在iOS/macOS开发中,函数Hook是一项强大的技术,它允许我们在运行时修改函数行为,广泛应用于调试、监控、性能分析等领域。Fishhook作为Facebook开源的一个轻量级Hook框架,其设计精巧、实现简洁,是理解macOS/iOS系统动态链接机制的绝佳案例。本文将深入剖析Fishhook的工作原理,解答一系列核心问题。
一、Fishhook是什么?
Fishhook是一个基于动态链接特性的函数Hook库,专门针对macOS和iOS平台设计。它的核心思想是通过修改Mach-O文件中的符号绑定表来实现C函数的Hook。
核心特点:
- 轻量级,仅几百行代码
- 专注于C函数Hook
- 基于合法的dyld API
- 主要用于调试和监控场景
二、核心问题:Fishhook为什么只能Hook动态链接函数?
1. Mach-O文件结构回顾
要理解Fishhook的限制,首先要了解Mach-O文件结构:
Mach-O Header
Load Commands
- LC_SEGMENT_64
- LC_SYMTAB (符号表)
- LC_DYSYMTAB (动态符号表)
- LC_DYLD_INFO (绑定信息)
Data
- __TEXT (代码段,只读)
- __DATA (数据段,可写)
- __la_symbol_ptr (惰性绑定指针)
- __nl_symbol_ptr (非惰性绑定指针)
- __got (全局偏移表)
- __LINKEDIT (链接信息)
2. 动态链接 vs 静态链接
// 静态链接函数(无法Hook)
static void internal_function() {
// 编译时直接嵌入二进制
// 调用方式:call 0x100000f00 (直接地址)
}
// 动态链接函数(可以Hook)
void printf(const char *format, ...) {
// 来自libSystem.dylib
// 调用方式:call [__la_symbol_ptr + offset] (间接寻址)
}
关键区别: 动态链接函数通过符号指针表间接调用,Fishhook正是通过修改这个指针表中的地址来实现Hook。
3. 为什么只能Hook系统库函数?
这是一个常见的误解!Fishhook不仅可以Hook系统库函数,也可以Hook自定义动态库函数。
// 自定义动态库中的函数也可以Hook
__attribute__((visibility("default")))
void my_dynamic_function() {
// 这个函数可以被Fishhook Hook
}
// 只要满足以下条件:
// 1. 函数来自动态库(不是静态链接)
// 2. 函数具有外部可见性(不是static)
// 3. 函数通过符号表引用
三、符号绑定机制深度解析
1. 惰性绑定(Lazy Binding) vs 非惰性绑定
惰性绑定(__la_symbol_ptr)
; 第一次调用printf时的流程
call printf ; 实际调用桩代码
; __stubs中的桩代码
___stub_printf:
jmp *___la_symbol_ptr[0] ; 第一次跳转到dyld_stub_binder
; 绑定后的状态
___la_symbol_ptr[0] = 0x7fff12345678 (真实printf地址)
非惰性绑定(__nl_symbol_ptr)
// 程序启动时立即绑定的函数
// 现代iOS/macOS中,__nl_symbol_ptr已较少使用
// 取而代之的是__got(全局偏移表)
// 使用__got的示例
void *function_ptr __attribute__((section("__DATA,__got"))) = &some_function;
现代绑定机制变化
在现代 iOS/macOS 中,传统的 __nl_symbol_ptr / __la_symbol_ptr 的角色已被弱化,
编译器和 dyld 更多通过 GOT-like 的间接寻址方式以及 chained fixups 机制来完成符号绑定;因此使用时需要注意是否能正确实现hook.
# 传统方式的缺点
# 1. 重定位表可能很大
# 2. 加载时需要大量随机内存访问
# 3. 不支持新硬件特性
# 4. 安全保护有限
# Chained Fixups 的优势
# 1. 更小的二进制大小(减少30-50%重定位数据)
# 2. 更快的启动速度
# 3. 支持指针认证(PAC)
# 4. 更好的安全性和性能
// 寻址方式对比
; 传统方式(x86_64)
; 通过 __la_symbol_ptr 调用 printf
lea rdi, [rip + format_string]
call qword ptr [rip + ___la_symbol_ptr_printf]
; __la_symbol_ptr 节区内容:
___la_symbol_ptr_printf:
.quad ___dyld_stub_binder_printf
; 现代方式(arm64)
; 通过 GOT-like 间接寻址
adrp x0, _printf@GOTPAGE ; 获取 GOT 页
ldr x1, [x0, _printf@GOTPAGEOFF] ; 从 GOT 加载地址
blr x1 ; 间接跳转
; 实际上,编译器可能生成更优化的代码
; 直接使用 PC 相对寻址,不需要显式的 GOT 节区
2. dyld_stub_binder的工作原理
当第一次调用动态链接函数时,系统如何查找真实地址?
// dyld_stub_binder的简化流程
void* dyld_stub_binder(uint32_t lazy_binding_info_offset) {
// 1. 从__LINKEDIT获取绑定信息
LazyBindingInfo *info = get_binding_info(offset);
// 2. 提取符号名和库序号
const char *symbol_name = info->symbol_name;
int library_ordinal = info->library_ordinal;
// 3. 在指定动态库中查找符号
void *address = find_symbol(symbol_name, library_ordinal);
// 4. 修改符号指针表
void **symbol_ptr = info->symbol_pointer;
*symbol_ptr = address; // 关键步骤!
// 5. 跳转到目标函数
return address;
}
3. ASLR(地址空间布局随机化)的影响
ASLR是现代操作系统的安全特性,每次启动时系统模块的加载地址都随机变化:
// 没有ASLR:
// printf地址固定为0x7fff12345678
// 有ASLR:
// 第一次启动:printf地址为0x7fff12345678
// 第二次启动:printf地址为0x7fff56789abc
// 第三次启动:printf地址为0x7fff9abcdef0
// Mach-O通过位置无关代码支持ASLR:
// 所有外部引用都通过指针表间接访问
// 指针表地址在运行时由dyld填充
四、Fishhook的核心实现原理
1. 核心数据结构:rebinding
struct rebinding {
const char *name; // 要Hook的函数名
void *replacement; // 替换函数的地址
void **replaced; // 保存原始函数指针
};
2. Fishhook的工作流程
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
// 1. 遍历所有镜像(动态库)
for (int i = 0; i < _dyld_image_count(); i++) {
const struct mach_header *header = _dyld_get_image_header(i);
// 2. 查找__DATA段中的符号指针表
struct segment_command *seg = find_segment(header, "__DATA");
if (!seg) continue;
// 3. 查找__la_symbol_ptr和__got节
struct section *la_section = find_section(seg, "__la_symbol_ptr");
struct section *got_section = find_section(seg, "__got");
// 4. 处理每个符号指针
perform_rebinding_with_section(la_section, rebindings, rebindings_nel);
perform_rebinding_with_section(got_section, rebindings, rebindings_nel);
}
return 0;
}
3. 关键函数:查找和替换符号
static void perform_rebinding_with_section(struct section *section,
struct rebinding rebindings[],
size_t rebindings_nel) {
// 1. 获取间接符号表
uint32_t *indirect_symbol_indices = indirect_symbol_table + section->reserved1;
// 2. 遍历符号指针表中的每个条目
for (uint32_t i = 0; i < section->size / sizeof(void*); i++) {
uint32_t symtab_index = indirect_symbol_indices[i];
// 3. 在动态符号表中查找符号信息
struct nlist_64 *symbol = &symtab[symtab_index];
uint32_t strtab_offset = symbol->n_un.n_strx;
const char *symbol_name = strtab + strtab_offset;
// 4. 检查是否是需要Hook的函数
for (size_t j = 0; j < rebindings_nel; j++) {
if (strcmp(symbol_name, rebindings[j].name) == 0) {
// 5. 找到目标!进行Hook
void **symbol_ptr = (void**)(section->addr + i * sizeof(void*));
// 保存原始函数指针
if (rebindings[j].replaced != NULL) {
*rebindings[j].replaced = *symbol_ptr;
}
// 替换为新的函数地址
*symbol_ptr = rebindings[j].replacement;
break;
}
}
}
}
五、实际应用场景和限制
1. 支持的函数类型
| 函数类型 | 是否支持 | 说明 |
|---|---|---|
| C函数(动态库) | ✅ 完全支持 | Fishhook的主要目标 |
| Objective-C方法 | ❌ 不支持 | 使用Method Swizzling |
| Swift函数(@_cdecl) | ✅ 条件支持 | 需要C接口导出 |
| 静态链接函数 | ❌ 不支持 | 编译时直接链接 |
| 内联函数 | ❌ 不支持 | 编译时展开 |
2. iOS上的特殊限制
// 在非越狱iOS设备上:
// ✅ 可以Hook自己的函数和系统公开API
// ❌ 不能Hook系统私有函数(受代码签名保护)
// 在越狱iOS设备上:
// ✅ 可以Hook所有函数
// 验证方法:
#include <fishhook.h>
#include <stdio.h>
// 可以Hook的系统公开函数
static int (*original_printf)(const char *, ...);
int hooked_printf(const char *format, ...) {
// Hook实现
return original_printf("[HOOKED] %s", format);
}
__attribute__((constructor))
static void setup_hook() {
struct rebinding rebind = {"printf", hooked_printf, (void**)&original_printf};
rebind_symbols(&rebind, 1);
}
3. 自定义动态库的Hook
// MyDynamicFramework.framework
__attribute__((visibility("default")))
void framework_function() {
NSLog(@"Original framework function");
}
// 主工程中的Hook
static void (*original_framework_func)() = NULL;
void hooked_framework_func() {
NSLog(@"Before framework function");
original_framework_func();
NSLog(@"After framework function");
}
// 安装Hook
struct rebinding rebind = {
"framework_function",
hooked_framework_func,
(void**)&original_framework_func
};
rebind_symbols(&rebind, 1);
六、与Method Swizzling的对比
1. Fishhook(C函数Hook)
// 适用于C函数
void hook_c_function() {
struct rebinding rebind = {"printf", hooked_printf, &original_printf};
rebind_symbols(&rebind, 1);
}
2. Method Swizzling(Objective-C方法Hook)
// 适用于Objective-C方法
+ (void)hookObjectiveCMethod {
Method original = class_getInstanceMethod([UIView class], @selector(setBackgroundColor:));
Method swizzled = class_getInstanceMethod([self class], @selector(hooked_setBackgroundColor:));
method_exchangeImplementations(original, swizzled);
}
3. 选择指南
| 场景 | 推荐技术 |
|---|---|
| Hook C系统函数 | Fishhook |
| Hook C自定义函数 | Fishhook |
| Hook Objective-C方法 | Method Swizzling |
| Hook Swift方法 | Method Swizzling(通过@objc暴露) |
| 底层系统监控 | Fishhook |
七、实战案例
1. 性能监控
// 监控内存分配
static void *(*original_malloc)(size_t);
static void (*original_free)(void *);
void *hooked_malloc(size_t size) {
void *ptr = original_malloc(size);
// 记录大内存分配
if (size > 1024 * 1024) {
printf("[MEMORY] Large malloc: %zu bytes\n", size);
}
return ptr;
}
void hooked_free(void *ptr) {
// 可以在这里添加内存释放监控
original_free(ptr);
}
八、常见问题解答
Q1: Fishhook能Hook静态库函数吗?
A: 取决于静态库的链接方式:
- 如果静态库被完全复制到动态库中,可以Hook
- 如果静态库保持外部引用,需要在链接静态库的地方Hook
- 静态库的私有函数(static)无法Hook
Q2: 为什么OC和Swift方法不能用Fishhook Hook?
A: Objective-C和Swift的方法调用机制完全不同:
- Objective-C使用消息发送机制(objc_msgSend)
- Swift有自己的分发机制(虚函数表、直接调用等)
- 它们不通过符号绑定表调用,因此Fishhook无法修改
Q3: Hook的时机是什么?
A: Fishhook可以在运行时任何时间点进行Hook,但最佳实践是:
// 1. 程序启动时(推荐)
__attribute__((constructor))
static void setup_hooks() {
// 在main()函数之前执行
}
// 2. 运行时动态Hook
void dynamic_hook() {
// 可以在需要时动态安装或移除Hook
}
// 3. 使用dispatch_once保证只Hook一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 安装Hook
});
Q4: 在MachOView中为什么看不到__nl_symbol_ptr?
A: 在现代iOS/macOS系统中:
-
__nl_symbol_ptr已被__got(全局偏移表)替代 - 所有需要立即绑定的符号现在都放在
__got中 - 这是为了更好的位置无关代码支持和性能优化
九、Fishhook的局限性
1. 技术限制
- 只能Hook通过符号表调用的函数
- 无法Hook内联函数和静态链接函数
- 对C++函数支持有限(名称重整问题)
- 线程安全问题需要开发者自己处理
2. 平台限制
- iOS非越狱设备受限较多
- 某些系统函数受代码签名保护
- 不同iOS版本可能有行为差异
3. 性能考虑
- 第一次Hook需要遍历符号表,有性能开销
- 频繁的动态Hook/Unhook可能影响性能
- 需要考虑多线程环境下的安全性
十、未来展望
1. 替代方案
// 1. Dobby(原DobbyHook)
// 支持inline hook,更强大
#include <dobby.h>
DobbyHook((void *)printf, (void *)hooked_printf, (void **)&original_printf);
// 2. substrate(越狱设备)
// iOS越狱环境下的完整Hook框架
MSHookFunction((void *)printf, (void *)hooked_printf, (void **)&original_printf);
2. 发展趋势
- Apple正在加强系统安全性
- 静态分析和运行时保护机制增强
- 对动态代码修改的限制可能增加
- 开发者需要更多关注官方提供的调试和监控API
结语
Fishhook通过精巧地利用Mach-O文件的动态链接机制,实现了轻量级的函数Hook功能。虽然它有一定的局限性,但仍然是理解macOS/iOS系统底层机制的绝佳案例,也是许多调试和监控工具的基础。
通过本文的详细解析,我们希望读者能够:
- 深入理解Mach-O文件结构和动态链接机制
- 掌握Fishhook的工作原理和使用方法
- 了解不同Hook技术的适用场景
- 在实际开发中正确应用Hook技术
记住,强大的工具也意味着重大的责任。Hook技术应该用于正当的调试、监控和优化目的,而不是破坏系统安全或侵犯用户隐私。
参考资源: