普通视图

发现新文章,点击刷新页面。
昨天以前首页

Fishhook原理深度剖析:从Mach-O到运行时函数Hook

作者 sweet丶
2026年1月20日 12:07

前言

在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系统底层机制的绝佳案例,也是许多调试和监控工具的基础。

通过本文的详细解析,我们希望读者能够:

  1. 深入理解Mach-O文件结构和动态链接机制
  2. 掌握Fishhook的工作原理和使用方法
  3. 了解不同Hook技术的适用场景
  4. 在实际开发中正确应用Hook技术

记住,强大的工具也意味着重大的责任。Hook技术应该用于正当的调试、监控和优化目的,而不是破坏系统安全或侵犯用户隐私。


参考资源:

❌
❌