IosBX's Blog

IosbX's Blog
记录自己的每一次进步
  1. 首页
  2. Mac
  3. 正文

MacOS逆向 - 跨进程函数Hook

2025年 12月 4日 459点热度 2人点赞 0条评论

一、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 的位置继续执行。

推荐一个强大的 Hook 框架:jmpews/Dobby

它是一个跨平台、多架构的 Inline Hook 框架,支持 Windows/macOS/iOS/Android/Linux 以及 X86/ARM/ARM64 架构。

核心功能:

  • 自动指令修复 (Relocation): 自动处理被 Hook 指令中与 PC 相关的指令(如 adr, ldr, b),避免手动修复的繁琐和错误。

  • 通用性强: 统一的 API 设计,使得 Hook 代码可以在不同操作系统和架构间复用。

  • 功能完备: 支持函数替换 (Replace) 和动态插桩 (Instrumentation)。

三、跨进程注入 (Remote Hook) 实战

  1. 获取权限 (Task Port)
    • 使用 sysctl 获取进程名对应的 PID,再task_for_pid(mach_task_self(), pid, &task) 获取目标进程的 kern_return_t。
  2. 远程内存分配 (Allocate)
    • 使用 mach_vm_allocate 在目标进程开辟内存空间,用于存放我们的 Shellcode 和跳板数据。
  3. 代码注入 (Write)
    • 使用 mach_vm_write 将本地编写好的机器码写入目标进程。
  4. 内存权限修改 (Protect)
    • 写入完成后,必须使用 mach_vm_protect 将该内存区域标记为 VM_PROT_READ | VM_PROT_EXECUTE,否则 CPU 拒绝执行数据段的代码(NX 位保护)。
  5. 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;
}

Dome:IOSBX/Remote_Shell_Code_MacOS

实现效果:https://www.bilibili.com/video/BV1SF2YBsEbw/?share_source=copy_web&vd_source=37e9383d225a75453a9592159ef5e7f4

 

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2025年 12月 4日

IosBX

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2026 IosBX's Blog. ALL RIGHTS RESERVED.