47、Mach-O文件修改实战:修改加载命令、动态库注入、fishhook使用

说实话,Mach-O文件格式是iOS逆向绕不开的坎。你想想看,无论是做越狱开发、应用加固,还是搞点“小动作”,最终都要落到对Mach-O的修改上。今天这一讲,我带你手把手过一遍三个核心操作:修改加载命令、动态库注入、以及fishhook的使用。嗯,都是实战中高频用到的东西。

Mach-O文件结构速览

先简单回顾下Mach-O的结构。它主要由三部分组成:

  • Header:文件头,标识CPU类型、文件类型、加载命令数量等。
  • Load Commands:加载命令区,告诉dyld怎么加载这个二进制。比如依赖哪些动态库、入口地址在哪、段信息等。
  • Data:真正的代码和数据,按Segment组织(__TEXT, __DATA, __LINKEDIT等)。

我们今天要动刀的地方,主要就是Load Commands和__DATA段。

核心思路:修改Mach-O本质上就是二进制级别的“增删改查”。你不需要每次都从零写解析器,用现成的工具(比如jtool、MachOView、或者自己写Python脚本)就能搞定。

实战一:修改加载命令

为什么要改加载命令?最常见的场景是:你想让一个App加载你写的动态库,或者你想修改它的依赖库路径。举个例子,我在项目中遇到过需要把某个App的LC_LOAD_DYLIB指向的旧库换成新版本,但不想重新签名整个包——直接改Mach-O里的路径就行。

操作步骤

  1. 定位加载命令:用MachOView打开二进制,找到LC_LOAD_DYLIB或LC_LOAD_WEAK_DYLIB。
  2. 修改路径:直接改字符串内容。注意新路径长度不能超过原路径,否则会破坏后续命令的偏移。
  3. 更新偏移:如果路径长度变了,需要调整后续所有加载命令的偏移量。这一步容易出错,我建议用工具自动处理。

我的习惯:用Python的macholib库来修改。它封装好了偏移计算,比手撸二进制安全得多。

# 示例:修改动态库路径
from macholib.MachO import MachO
from macholib.mach_o import LC_LOAD_DYLIB

macho = MachO('YourApp')
for header in macho.headers:
    for idx, (lc, cmd, data) in enumerate(header.commands):
        if lc.cmd == LC_LOAD_DYLIB:
            old_path = data.name.decode('utf-8')
            new_path = '/usr/lib/libCustom.dylib'
            # 确保新路径长度 <= 原路径长度
            if len(new_path) <= len(old_path):
                data.name = new_path.encode('utf-8').ljust(len(old_path), b'\x00')
                print(f'替换: {old_path} -> {new_path}')
macho.write('YourApp_patched')

注意:修改后必须重新签名,否则iOS会拒绝运行。我吃过这个亏,改完直接闪退,查了半天才发现是签名问题。

实战二:动态库注入

动态库注入是逆向分析的“瑞士军刀”。你想hook某个函数、监控网络请求、或者注入调试代码,都可以通过往Mach-O里加一个LC_LOAD_DYLIB命令来实现。

注入原理

说白了,就是在加载命令区末尾追加一条LC_LOAD_DYLIB,指向你的dylib。dyld在加载主二进制时,会顺带把你的库也加载进来,然后执行它的构造函数(__attribute__((constructor)))。

手动注入步骤

  1. 准备好你的dylib(编译成fat binary,支持arm64)。
  2. 用工具(比如insert_dylib)把LC_LOAD_DYLIB命令插入到Mach-O中。
  3. 把dylib放到App的Frameworks目录下,或者用install_name_tool修改路径。
  4. 重新签名。
# 使用insert_dylib工具(越狱环境常用)
insert_dylib --inplace --all-yes @executable_path/Frameworks/MyTweak.dylib YourApp

避坑指南:我曾经在非越狱设备上尝试注入,结果发现iOS 13+对动态库加载限制很严。非越狱环境下,你只能注入到已经加载了你的dylib的进程,或者通过Framework注入(需要修改Info.plist)。说白了,越狱环境更自由,非越狱环境需要配合其他手段。

实战三:fishhook使用

fishhook是Facebook开源的一个工具,专门用来hook Mach-O中的C函数。它的原理很巧妙:通过修改__DATA段的懒加载符号表指针,把系统函数调用重定向到你的函数。

为什么需要fishhook?

你想想看,Objective-C的method swizzling只能hook OC方法,但系统底层很多函数是C写的(比如openwritedlopen)。fishhook就是用来干这个的。

典型用法:hook open函数

#import "fishhook.h"

// 保存原始函数指针
static int (*original_open)(const char *, int, ...);

// 替换函数
int my_open(const char *path, int flags, ...) {
    NSLog(@"拦截到open调用: %s", path);
    // 可以在这里做过滤或记录
    return original_open(path, flags);
}

// 在初始化时绑定
__attribute__((constructor))
void setup() {
    struct rebinding rebind;
    rebind.name = "open";
    rebind.replacement = my_open;
    rebind.replaced = (void *)&original_open;
    
    rebind_symbols((struct rebinding[1]){rebind}, 1);
}

个人经验:fishhook只能hook那些通过动态链接解析的符号。如果函数是静态链接进二进制里的,fishhook就无能为力了。我在项目中遇到过这种情况,最后只能改用inline hook。

知识体系图

下面这张图梳理了Mach-O修改的核心流程和工具链,方便你对照理解:

Mach-O 修改实战知识体系 Mach-O 文件 修改加载命令 动态库注入 fishhook 使用 LC_LOAD_DYLIB 路径修改 偏移量计算与修复 insert_dylib 工具 重签名与部署 符号表指针重绑定 C函数 Hook 工具链:MachOView + jtool + fishhook + insert_dylib

综合实战:注入dylib并用fishhook拦截系统调用

把上面三个技术串起来,就是一个完整的逆向工具链。我举个例子:

  1. 写一个dylib,里面用fishhook hook了NSLog函数,把所有日志输出重定向到文件。
  2. insert_dylib把这个dylib注入到目标App的Mach-O里。
  3. 重新签名,运行App,所有日志都被你捕获了。
// dylib 核心代码
#import "fishhook.h"

static void (*original_NSLog)(NSString *format, ...);

void my_NSLog(NSString *format, ...) {
    va_list args;
    va_start(args, format);
    NSString *message = [[NSString alloc] initWithFormat:format arguments:args];
    // 写入文件
    NSData *data = [[message stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding];
    [data writeToFile:@"/tmp/log.txt" atomically:YES];
    va_end(args);
    
    // 仍然调用原函数
    original_NSLog(@"%@", message);
}

__attribute__((constructor))
void init() {
    struct rebinding rb = {"NSLog", my_NSLog, (void *)&original_NSLog};
    rebind_symbols(&rb, 1);
}

重要提醒:在非越狱设备上,注入dylib需要绕过代码签名和AMFI(Apple Mobile File Integrity)。常用的方法包括:利用Xcode调试器附加进程后注入、或者通过Framework注入配合修改Info.plist。越狱设备上则简单得多,直接用Cydia Substrate或libhooker就行。

总结

Mach-O修改是iOS逆向的硬功夫。我个人觉得,掌握这三个操作后,大部分动态分析需求都能覆盖了。修改加载命令让你能控制dyld的行为,动态库注入给了你代码执行的能力,fishhook则让你能拦截系统函数调用。三者配合,基本上可以“为所欲为”了。

嗯,最后说一句:动手实践比看一百遍文章都管用。找个App练练手,从修改一个加载命令开始,慢慢你就会发现Mach-O其实没那么神秘。


公众号:蓝海资料掘金营,微信 deep3321