1.准备测试环境,使用 hook_nslog 替换 系统NSLog
//
// ViewController.m
// fishhook
//
// Created by LEE on 4/14/22.
//
#import "ViewController.h"
#import "fishhook.h"
@interface ViewController ()
@end
@implementation ViewController
//原函数指针变量
static void (*sys_nslog)(NSString *format, ...);
//hook新函数
void hook_nslog(NSString *format, ...) {
format = [format stringByAppendingString:@"❤️ ( ⚫︎ー⚫︎ ) balalala~"];
sys_nslog(format);
}
- (void)viewDidLoad {
[super viewDidLoad];
//提出需求->分解需求->完成需求->测试验收
//【需求】hook NSLog
//1. 点击屏幕输出日志;
//2.fishhook hook NSLog让日志内容发生点变化!!!
//定义hook的函数的结构体变量
struct rebinding nslog_reb;
nslog_reb.name = "NSLog";
nslog_reb.replacement = hook_nslog;
nslog_reb.replaced = (void *)&sys_nslog;
//定义需要hook的函数的结构体数组变量
struct rebinding rebs[] = {nslog_reb};
//很简单,传递结构体数组地址及其成员变量数目
rebind_symbols(rebs, 1);
}
//点击屏幕输出日志
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"click!!!");
}
@end
2.构建替换的结构体
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
* frame #0: 0x00000001000c0274 fishhook`prepend_rebindings(rebindings_head=0x00000001000c5560, rebindings=0x000000016fd411c0, nel=1) at fishhook.c:80:22
frame #1: 0x00000001000c0654 fishhook`rebind_symbols(rebindings=0x000000016fd411c0, rebindings_nel=1) at fishhook.c:249:16
frame #2: 0x00000001000c0104 fishhook`-[ViewController viewDidLoad](self=0x0000000109e09460, _cmd="viewDidLoad") at ViewController.m:38:5

3.执行符号地址替换

4.几个关键问题
4.1.运行时的可执行文件不是不可以修改吗,为什么这里 使用 vm_protect 可以修改 引用的动态符号?
为什么可以修改动态符号引用?
你的理解有一个误区。可执行文件的代码段确实不能修改,但是 fishhook 修改的不是代码段,而是数据段中的符号指针表。
1. 动态符号绑定的工作原理
在 Mach-O 文件中,动态符号的绑定是通过以下机制实现的:
代码段:包含实际的机器代码,通常是只读的
数据段:包含符号指针表(__la_symbol_ptr 和 __nl_symbol_ptr),这些指针指向实际的函数地址
2. fishhook 修改的是什么?
从代码中可以看到,fishhook 修改的是:
err = vm_protect (mach_task_self (), (uintptr_t)indirect_symbol_bindings, section->size, 0, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY);
这里的 indirect_symbol_bindings 指向的是符号指针表,位于数据段中,而不是代码段。
3. vm_protect 的作用
vm_protect 是 macOS/iOS 的虚拟内存管理 API,它的作用是:
修改内存页面的保护属性:将原本只读的内存页面改为可写
参数说明:
mach_task_self():当前进程
(uintptr_t)indirect_symbol_bindings:要修改的内存地址
section->size:要修改的内存大小
0:设置为新的保护属性
VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY:新的保护属性(读、写、写时复制)
4. 为什么需要 VM_PROT_COPY?
VM_PROT_COPY 是关键,它启用了写时复制(Copy-on-Write)机制:
当进程尝试写入共享的只读内存页面时,系统会创建一个该页面的私有副本
这样既保持了原始页面的完整性,又允许进程修改自己的副本
4.2.VM_PROT_COPY 是关键,它启用了写时复制(Copy-on-Write)机制: 系统会创建一个该页面的私有副本,那么需要动态找到外部符号的时候,是如何知道要去新页面去找的喃?
写时复制机制的工作原理
1. 虚拟内存映射机制
在 macOS/iOS 中,每个进程都有自己的虚拟地址空间,但多个进程可能映射同一个物理页面。当使用 VM_PROT_COPY 时:
进程A的虚拟地址 0x1000 → 物理页面P (只读)
进程B的虚拟地址 0x1000 → 物理页面P (只读)
2. 写时复制的触发
当进程尝试写入共享的只读页面时:
// 这行代码会触发写时复制
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
系统会:
检测到写操作:发现进程试图写入只读页面
创建私有副本:分配新的物理页面,复制原始内容
更新页表:将进程的虚拟地址重新映射到新的物理页面
设置写权限:新页面具有读写权限
修改前:
进程A的虚拟地址 0x1000 → 物理页面P (只读)
修改后:
进程A的虚拟地址 0x1000 → 物理页面P' (读写,私有副本)
进程B的虚拟地址 0x1000 → 物理页面P (只读,保持不变)
3. 符号查找的连续性
关键在于:虚拟地址保持不变!
// 这行代码获取符号指针表的地址
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
section->addr 是符号指针表在虚拟地址空间中的固定偏移
slide 是 ASLR (Address Space Layout Randomization) 的随机偏移
最终的虚拟地址 indirect_symbol_bindings 在写时复制前后完全相同
4. 动态符号解析过程
当程序需要调用外部函数时:
; 假设这是调用 printf 的代码
call [rip + offset_to_symbol_ptr] ; 间接调用
CPU 执行间接调用:从符号指针表读取函数地址
符号指针表地址:indirect_symbol_bindings[i] 指向的虚拟地址
读取函数地址:从该地址读取实际的函数指针
跳转到函数:执行函数调用
5. 写时复制后的效果
写时复制后,符号指针表的内容发生了变化:
// 修改前:指向原始的 printf 函数
indirect_symbol_bindings[i] = 0x7fff12345678; // 原始 printf 地址
// 修改后:指向我们的 hook 函数
indirect_symbol_bindings[i] = 0x1000123456; // 我们的 hook 函数地址
6. 为什么不需要"知道新页面"
程序不需要知道页面是否被复制,因为:
虚拟地址不变:indirect_symbol_bindings[i] 的地址始终相同
内存访问透明:CPU 和内存管理单元 (MMU) 自动处理物理地址转换
页表自动更新:操作系统自动维护虚拟地址到物理地址的映射
7. 具体的内存访问流程
程序执行:call [0x1000] (间接调用)
1. CPU 访问虚拟地址 0x1000
2. MMU 查询页表,发现 0x1000 → 物理页面P'
3. 从物理页面P' 读取数据:0x1000123456 (hook函数地址)
4. CPU 跳转到 0x1000123456 执行
总结
写时复制机制的美妙之处在于:
对程序透明:程序代码不需要任何修改
地址不变:符号指针表的虚拟地址保持不变
内容改变:符号指针表的内容被替换为 hook 函数地址
自动处理:操作系统自动处理物理页面的复制和映射
这就是为什么 fishhook 能够在不修改代码段的情况下,成功 hook 动态符号调用的原理!
fishhook原理及关键问题解析
738

被折叠的 条评论
为什么被折叠?



