一、Hook 技术深度解析
HOOK(挂钩)技术是逆向工程与安全研究的核心基石。它的本质是劫持程序执行流,使程序在执行特定功能前(或后),先执行我们可以控制的代码。这让我们能够查看、修改函数的参数和返回值,甚至完全改变函数的行为。
在 iOS/macOS 开发中,常见的 Hook 方式主要有三种:
-
Method Swizzling: 利用 Objective-C Runtime 动态交换方法实现,适用于 OC 方法,安全且简单。
-
PLT Hook (fishhook): 修改 Mach-O 文件的懒加载表(Lazy Symbol Pointers),适用于系统库函数(如 printf, open),但无法 Hook App 内部的 C 函数。
-
Inline Hook: 直接修改内存中的汇编指令,理论上可以 Hook 任何函数(C/C++/OC/Swift),功能最强大,但实现难度最高。本文将重点探讨这种方式。
二、ARM64 Inline Hook 核心原理
ARM64 架构下,所有指令的长度固定为 32 位(4 字节)。

1. 劫持入口 (The Patch)
我们需要在目标函数的入口处,写入一段“跳转指令”,强制 CPU 跳到我们编写的 Hook 函数去执行。由于 ARM64 的指令编码限制,无法通过单条指令跳转任意 64 位地址(B 指令仅支持 ±128MB 跳转范围),因此通用的做法是使用 16 字节的指令序列实现长跳转:
// ARM64 绝对跳转 Payload (16 Bytes)
ldr x17, .+8 ; 1. 从当前 PC + 8 的位置加载数据到 x17 寄存器
br x17 ; 2. 无条件跳转到 x17 寄存器指向的地址
.quad 0x100008888 ; 3. 这里存储实际的 Hook 函数地址 (64位)
为什么使用 x17 寄存器? 在 ARM64 调用约定(AAPCS64)中,x16 和 x17 是过程间调用暂存寄存器(Intra-procedure-call scratch registers)。编译器假设这两个寄存器的值在函数调用时是不被保存的,因此在函数入口处覆盖它们通常是安全的。
缓存一致性 (Cache Coherency): CPU 拥有指令缓存 (I-Cache) 和数据缓存 (D-Cache)。我们通过数据写入指令(D-Cache),但 CPU 执行时从 I-Cache 读取。若不手动刷新 I-Cache,CPU 可能执行旧指令导致崩溃。
2. 指令修复与跳板 (Trampoline)
当我们覆盖了原函数的前 4 条指令(16字节)后,原函数的逻辑就被破坏了。为了保证 Hook 后还能调用原函数,我们需要构建一个跳板(Trampoline)。
指令重定位 (Instruction Relocation): 如果被“偷走”的 16 字节中包含 PC 相关 的指令(如 ADR, LDR (literal), B (cond)),直接将其复制到跳板会导致执行出错。因为这些指令的寻址是基于当前程序计数器(PC)的相对偏移,当它们被移动到新的内存地址(跳板)执行时,PC 值发生了变化,导致计算出的目标地址错误。 解决方案: 必须对这些指令进行“重定位”修复,将其转换为不依赖 PC 的绝对地址计算指令序列,或调整其偏移量以确保逻辑正确。
跳板的内存布局如下:
区域
|
内容
|
作用
|
Stolen Bytes
|
原函数前 16 字节指令
|
补回被覆盖的逻辑,让原函数能正常运行。
|
Back Jump
|
LDR x17, ...
BR x17
|
执行完被偷的指令后,跳转回原函数 Entry + 16 的位置继续执行。
|
它是一个跨平台、多架构的 Inline Hook 框架,支持 Windows/macOS/iOS/Android/Linux 以及 X86/ARM/ARM64 架构。
核心功能:
-
自动指令修复 (Relocation): 自动处理被 Hook 指令中与 PC 相关的指令(如 adr, ldr, b),避免手动修复的繁琐和错误。
-
通用性强: 统一的 API 设计,使得 Hook 代码可以在不同操作系统和架构间复用。
-
功能完备: 支持函数替换 (Replace) 和动态插桩 (Instrumentation)。
三、跨进程注入 (Remote Hook) 实战
- 获取权限 (Task Port)
- 使用
sysctl 获取进程名对应的 PID,再task_for_pid(mach_task_self(), pid, &task) 获取目标进程的 kern_return_t。
- 远程内存分配 (Allocate)
- 使用
mach_vm_allocate 在目标进程开辟内存空间,用于存放我们的 Shellcode 和跳板数据。
- 代码注入 (Write)
- 使用
mach_vm_write 将本地编写好的机器码写入目标进程。
- 内存权限修改 (Protect)
- 写入完成后,必须使用
mach_vm_protect 将该内存区域标记为 VM_PROT_READ | VM_PROT_EXECUTE,否则 CPU 拒绝执行数据段的代码(NX 位保护)。
- Remap 技术 (高级技巧)
- 使用
vm_remap ,先在本地分配内存并写入 Patch,然后将其“映射”覆盖到目标进程的内存页上。
实战代码结构

输出 1,输出 1 + 1 = 2,

在 IDA 中可以看到 _add 函数在0x600 的位置,
先用 sysctl 函数获取进程的 pid,再用 task_for_pid 获取 kern_return_t,
mach_port_t get_remote_task(const char *process_name) {
pid_t pid = -1;
size_t length = 0;
static const int name[] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0};
int err = sysctl((int *)name, (sizeof(name) / sizeof(*name)) - 1, NULL, &length, NULL, 0);
if (err == -1) err = errno;
if (err == 0) {
struct kinfo_proc *procBuffer = (struct kinfo_proc *)malloc(length);
if(procBuffer == NULL) return MACH_PORT_NULL;
sysctl((int *)name, (sizeof(name) / sizeof(*name)) - 1, procBuffer, &length, NULL, 0);
int count = (int)(length / sizeof(struct kinfo_proc));
for (int i = 0; i < count; ++i) {
const char *procname = procBuffer[i].kp_proc.p_comm;
NSString *procNameStr = [NSString stringWithFormat:@"%s",procname];
if([procNameStr isEqualToString:[NSString stringWithUTF8String:process_name]]) {
pid = procBuffer[i].kp_proc.p_pid;
NSLog(@"[IosBX] Found %s pid:%d", process_name, pid);
free(procBuffer);
break;
}
}
if (pid == -1 && procBuffer) free(procBuffer);
}
if (pid == -1) return MACH_PORT_NULL;
mach_port_t task;
kern_return_t kret = task_for_pid(mach_task_self(), pid, &task);
if (kret == KERN_SUCCESS) {
return task;
} else {
NSLog(@"[IosBX] task_for_pid failed: %d", kret);
return MACH_PORT_NULL;
}
}
再用 mach_vm_region_recurse 获取基地址,
uint64_t get_remote_image_header(mach_port_t task) {
mach_vm_address_t address = 0;
mach_vm_size_t size = 0;
uint32_t depth = 0;
struct vm_region_submap_info_64 info;
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
while (1) {
kern_return_t kr = mach_vm_region_recurse(task, &address, &size, &depth, (vm_region_recurse_info_t)&info, &count);
if (kr != KERN_SUCCESS) break;
if (info.protection & VM_PROT_EXECUTE) {
return address;
}
address += size;
}
return 0;
}
基地址加 0x600 偏移
void *remote_target = (void*)(header + 0x600);
创建实现 1 + 1 = 3 的 uint8_t 类型 shellcode 数组,
uint8_t shellcode[] = {
0x00, 0x00, 0x01, 0x0b, // add w0, w0, w1
0x00, 0x04, 0x00, 0x11, // add w0, w0, #1
0xc0, 0x03, 0x5f, 0xd6 // ret
};
用mach_vm_allocate函数在目标进程 task 的虚拟内存空间中,申请一块大小为 sizeof(shellcode) 的内存,用mach_vm_write函数将本地变量 code 中的数据,拷贝到刚才在目标进程申请的mach_vm_address_t,用mach_vm_protect函数将申请的内存的权限修改为可读 + 可执行 ( RX )。
void* inject_remote_code(mach_port_t task, const void *code, size_t size) {
if (task == MACH_PORT_NULL || !code || size == 0) return NULL;
mach_vm_address_t remote_addr = 0;
kern_return_t kr = mach_vm_allocate(task, &remote_addr, size, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS) {
printf("mach_vm_allocate failed: %d\n", kr);
return NULL;
}
kr = mach_vm_write(task, remote_addr, (vm_offset_t)code, (mach_msg_type_number_t)size);
if (kr != KERN_SUCCESS) {
printf("mach_vm_write failed: %d\n", kr);
mach_vm_deallocate(task, remote_addr, size);
return NULL;
}
kr = mach_vm_protect(task, remote_addr, size, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
if (kr != KERN_SUCCESS) {
printf("mach_vm_protect failed: %d\n", kr);
mach_vm_deallocate(task, remote_addr, size);
return NULL;
}
return (void*)remote_addr;
}
在完成了代码注入之后,接下来步骤就是实现 Hook,
int simple_hook(void *target, void *replacement, void **original, mach_port_t target_task) {
if (!target || !replacement) return -1;
if (target_task == MACH_PORT_NULL) target_task = mach_task_self();
// 1. 处理 PAC (Pointer Authentication Code)
// 在 Apple Silicon (arm64e) 架构上,函数指针可能包含签名。
// 在读取内存前,必须使用 ptrauth_strip 去除签名,获取真实的内存地址。
void *real_target = ptrauth_strip(target, ptrauth_key_asia);
size_t patch_size = sizeof(AbsJump);
size_t page_size = sysconf(_SC_PAGESIZE);
// 2. 准备跳板 (Trampoline)
// 跳板用于保存原函数被覆盖的指令,并跳转回原函数继续执行。
// 必须在目标进程中分配内存。
mach_vm_address_t trampoline = 0;
kern_return_t kr = mach_vm_allocate(target_task, &trampoline, page_size, VM_FLAGS_ANYWHERE);
if (kr != KERN_SUCCESS) return -1;
// 3. 构建跳板内容 (在本地构建然后写入)
uint8_t *local_trampoline = (uint8_t *)malloc(page_size);
memset(local_trampoline, 0, page_size);
// 3.1 读取原函数前 16 字节 (Stolen Bytes)
mach_vm_size_t read_size = 0;
kr = mach_vm_read_overwrite(target_task, (mach_vm_address_t)real_target, patch_size, (mach_vm_address_t)local_trampoline, &read_size);
if (kr != KERN_SUCCESS) {
free(local_trampoline);
return -4;
}
// 3.2 追加跳转回原函数+16 的指令
// 执行完被偷走的指令后,必须跳回原函数继续执行后续代码。
AbsJump *jump_back = (AbsJump *)(local_trampoline + patch_size);
AbsJump back_jump_inst;
back_jump_inst.addr = (uint64_t)((uintptr_t)real_target + patch_size);
*jump_back = back_jump_inst;
// 3.3 将构建好的跳板写入目标进程
kr = mach_vm_write(target_task, trampoline, (vm_offset_t)local_trampoline, page_size);
free(local_trampoline);
if (kr != KERN_SUCCESS) return -5;
// 4. 设置跳板为可执行权限 (R-X)
set_mem_exec(target_task, (void*)trampoline, page_size);
// 5. 生成 Hook Patch 数据
// 这是一个跳转到 replacement 函数的绝对跳转指令。
AbsJump hook_patch;
hook_patch.addr = (uint64_t)replacement;
// 6. 写入 Hook (覆盖原函数入口)
// 使用 vm_remap 技术规避内存写保护,将跳转指令写入原函数入口。
if (patch_code_via_remap(target_task, real_target, &hook_patch, patch_size) != 0) {
return -6;
}
// 7. 返回跳板地址
// 如果调用者需要调用原函数,可以通过这个地址调用。
if (original) {
*original = (void*)trampoline;
}
return 0;
}
文章评论