第三十讲:移动应用逆向(iOS)——从IPA到Runtime的全链路分析

iOS逆向,说实话,比Android要麻烦不少。我最早接触iOS逆向是在2015年,那时候越狱设备还比较普遍,现在虽然环境变了,但核心思路没变。这一讲,我会带你走一遍完整的iOS逆向流程——从拿到一个IPA文件开始,到用Frida做动态Hook结束。

IPA文件结构:拆开看看里面有什么

IPA本质上就是个zip包。你直接把后缀改成.zip,解压就能看到内容。我习惯用命令行操作:

unzip target.ipa -d ipa_output

解压后,典型的目录结构是这样的:

路径 说明
Payload/xxx.app 核心bundle,里面是Mach-O可执行文件
Payload/xxx.app/Info.plist 应用元信息,Bundle ID、版本号等
Payload/xxx.app/embedded.mobileprovision 签名描述文件,包含设备UDID列表
Payload/xxx.app/CodeResources 代码签名校验文件
iTunesMetadata.plist App Store下载信息

嗯,这里要注意一点:如果你拿到的IPA是加密的(从App Store下载的通常都加密),那Payload里的Mach-O文件是加过密的。你需要先用工具解密,才能进行后续分析。我常用的解密工具是Clutch或者dumpdecrypted

避坑提醒:我曾经在分析一个金融类App时,直接拿加密的Mach-O去反编译,结果Hopper里全是乱码。折腾了两小时才发现是没解密。所以第一步先确认文件是否加密,用otool -l xxx | grep crypt查看cryptid字段,1表示已加密。

Mach-O文件格式:iOS可执行文件的骨架

Mach-O是iOS和macOS的原生可执行格式。说白了,它就是一个二进制文件的组织规范。你反编译一个iOS App,本质上就是在解析Mach-O。

Mach-O的结构分为三部分:

  • Header:文件头,标识CPU架构、文件类型(可执行/动态库/对象文件)
  • Load Commands:加载命令,告诉dyld(动态链接器)如何加载这个文件
  • Data:实际代码和数据段,包括__TEXT(代码段)、__DATA(数据段)、__LINKEDIT(链接信息)

我个人习惯用otoolMachOView来查看Mach-O细节。比如查看所有加载命令:

otool -l Payload/xxx.app/xxx

你会看到一堆LC_SEGMENT_64、LC_MAIN、LC_LOAD_DYLIB之类的命令。其中LC_MAIN指定了程序入口点,LC_LOAD_DYLIB列出了所有依赖的动态库。

为什么理解这个很重要?因为你在做逆向时,经常需要定位某个函数的地址,或者修改某个段的权限。不懂Mach-O结构,你连函数在哪个段都找不到。

核心知识点:__TEXT段是只读可执行的,__DATA段是可读写不可执行的。如果你要做代码注入,通常需要修改__DATA段或者新增一个段。

使用Hopper/IDA反编译:把二进制变成可读代码

拿到解密后的Mach-O,下一步就是反编译。我主要用两个工具:

  • Hopper Disassembler:轻量级,启动快,适合快速分析。我个人比较喜欢它的伪代码生成功能。
  • IDA Pro:重型武器,插件生态丰富,适合复杂分析。但价格贵,启动慢。

操作流程其实差不多:

  1. 把Mach-O拖进工具,选择ARM64架构(现在基本没有32位的iOS App了)
  2. 等待自动分析完成,工具会识别函数、字符串、交叉引用
  3. 从入口点或者关键字符串入手,逐步追踪逻辑

举个例子,你想找某个按钮点击后的处理逻辑。在Hopper里搜索按钮标题对应的字符串,然后查看交叉引用,就能定位到处理函数。

// Hopper伪代码示例
int __cdecl -[ViewController buttonClicked:] (void *self, SEL sel, id arg2) {
    r0 = [self someMethod];
    if (r0 != 0x0) {
        r0 = [self doSomething];
    }
    return r0;
}

伪代码虽然不一定完全准确,但足够帮你理解逻辑。我一般先看伪代码,需要精确分析时才看汇编。

小技巧:在Hopper里按Shift+E可以导出伪代码为C文件。我经常把关键函数导出,然后对照着看,比在工具里翻来翻去方便多了。

Objective-C/Swift Runtime分析:理解消息机制

iOS逆向绕不开Runtime。Objective-C是动态语言,方法调用本质上是发消息。这意味着你可以在运行时替换方法实现——这就是Hook的基础。

Objective-C的类结构大概是这样:

// 每个Objective-C对象都有一个isa指针,指向它的类
struct objc_object {
    Class isa;
};

// 类结构体
struct objc_class {
    Class isa;           // 元类
    Class super_class;   // 父类
    const char *name;    // 类名
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;        // 成员变量列表
    struct objc_method_list **methodLists; // 方法列表
    struct objc_cache *cache;            // 方法缓存
    struct objc_protocol_list *protocols; // 协议列表
};

Swift虽然更静态,但和Objective-C混编时,很多类仍然暴露在Runtime中。我分析Swift App时,会先用nm命令查看导出的符号:

nm Payload/xxx.app/xxx | grep "OBJC_CLASS"

这会列出所有暴露的Objective-C类。如果是纯Swift类,符号名会带模块前缀,比如_TtC7MyApp11ViewController

为什么会这样?因为Swift编译器为了性能,默认会把大部分方法静态化。但只要类标记了@objc或者继承了NSObject,就会暴露在Runtime中。

实战经验:我在分析一个Swift写的社交App时,发现它的网络请求类没有暴露任何方法名。后来我用class-dump导出头文件,发现它用了@objcMembers修饰整个类——这下所有方法都暴露了。所以别急着放弃,多试几种方式。

使用Frida/Theos进行动态Hook:运行时修改行为

静态分析只能看到代码逻辑,动态Hook才能真正验证你的猜想。我主要用两个工具:

  • Frida:跨平台,脚本化,适合快速验证。用JavaScript写Hook逻辑,实时注入。
  • Theos:越狱设备上的开发套件,可以编译成dylib注入。适合做长期使用的Tweak。

Frida快速上手

Frida的基本用法是attach到进程,然后Hook指定方法。比如我想Hook一个Objective-C方法:

// Frida脚本
if (ObjC.available) {
    var className = "ViewController";
    var methodName = "- buttonClicked:";
    
    var hook = ObjC.classes[className][methodName];
    Interceptor.attach(hook.implementation, {
        onEnter: function(args) {
            console.log("[+] buttonClicked called");
            console.log("[+] self: " + args[0]);
            console.log("[+] selector: " + args[1]);
            // 可以修改参数
        },
        onLeave: function(retval) {
            console.log("[+] return value: " + retval);
            // 可以修改返回值
        }
    });
}

运行方式:

frida -U -f com.example.app -l hook.js --no-pause

嗯,这里要注意:Frida需要手机端运行frida-server,而且iOS版本和frida-server版本要匹配。我曾经因为版本不匹配,折腾了一下午才发现是这个问题。

Theos制作Tweak

如果你需要长期Hook某个App,用Theos编译一个dylib更稳定。Theos的Tweak语法基于Logos,非常简洁:

%hook ViewController
- (void)buttonClicked:(id)sender {
    %log; // 打印调用日志
    NSLog(@"[Tweak] buttonClicked called");
    
    // 调用原始实现
    %orig;
    
    // 或者完全替换
    // UIAlertView *alert = [[UIAlertView alloc] init...];
    // [alert show];
}
%end

编译后用dpkg-deb打包成deb,通过Cydia或Sileo安装到越狱设备上。

重要提醒:动态Hook在非越狱设备上也能做,但需要配合开发者证书和dyld_insert_libraries环境变量。不过操作起来比较麻烦,我建议初学者先搞一台越狱设备,省心很多。

知识体系总览

下面这张图总结了iOS逆向的核心链路,从IPA文件到动态Hook的完整流程:

iOS逆向核心链路图 IPA文件 解密 → 解包 → 提取Mach-O Clutch / dumpdecrypted Mach-O文件分析 Header | Load Commands | __TEXT/__DATA/__LINKEDIT 静态反编译 Hopper / IDA Pro Runtime分析 ObjC/Swift 消息机制 动态Hook Frida / Theos 理解App行为 → 修改逻辑

这张图把整个流程串起来了。你从IPA出发,经过解密、Mach-O分析,然后分三条路走——静态反编译看代码逻辑,Runtime分析理解消息机制,动态Hook验证和修改行为。三条路最终汇合,帮你彻底理解一个App的内部运作。

我的建议:初学者先走静态分析路线,用Hopper看几个简单App的代码,熟悉ARM64汇编和Objective-C的调用约定。等静态分析熟练了,再上手Frida做动态Hook。别一上来就搞动态,容易迷失方向。

iOS逆向是个需要耐心的活。我做了这么多年,每次遇到新App还是会有卡住的时候。但只要你把基础打牢——IPA结构、Mach-O格式、Runtime机制、Hook工具——大部分问题都能找到突破口。嗯,今天就到这里,剩下的靠你自己动手了。


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