27、移动应用逆向(iOS):IPA结构、Mach-O分析、Theos与Cycript、越狱检测绕过

iOS逆向,说实话,比Android要「封闭」得多。但封闭不代表不能搞。我这些年做iOS安全评估,最深的感受就是:你只要搞懂了IPA的皮、Mach-O的骨、以及越狱检测的套路,大部分App在你面前就是透明的。今天我们就把这几个核心点掰开揉碎讲清楚。

一、IPA结构:App的「压缩包」里到底藏了什么?

IPA本质上就是个zip包。你把它后缀改成.zip,解压就能看到内部结构。我刚开始学逆向时,以为IPA里就一个二进制文件加一堆资源,后来发现远不止这么简单。

一个典型的IPA解压后,你会看到这样的目录结构:

Payload/
  └── WeChat.app/
        ├── WeChat                  # 主Mach-O可执行文件
        ├── Info.plist              # 应用元信息
        ├── embedded.mobileprovision # 描述文件(签名信息)
        ├── Frameworks/             # 动态库(.dylib或.framework)
        ├── PlugIns/                # 扩展插件(如Today Widget)
        ├── _CodeSignature/        # 代码签名目录
        └── Assets.car             # 资源文件(图标、图片等)

这里我重点提几个关键文件:

  • Info.plist:App的身份证。里面藏着Bundle ID、版本号、权限声明(如摄像头、定位)、以及是否允许JIT等关键开关。我曾在Info.plist里发现过硬编码的API密钥——开发者以为没人会解包看这个文件。
  • embedded.mobileprovision:描述文件。它决定了这个App能在哪些设备上跑、能用哪些服务。越狱检测绕过时,有时需要修改这个文件里的设备UDID列表。
  • _CodeSignature/CodeResources:签名校验文件。App启动时系统会校验这个目录里的哈希值。如果你改了Mach-O文件但没重签名,App直接闪退。
我的习惯:拿到一个IPA后,我第一件事不是看代码,而是先看Info.plist里的UIBackgroundModesLSApplicationQueriesSchemes。前者能告诉你App在后台偷偷干了什么,后者能暴露它调用了哪些第三方App。

二、Mach-O分析:iOS可执行文件的「骨架」

Mach-O是iOS和macOS的可执行文件格式。说白了,它就是一堆二进制数据,但系统靠它知道「这个程序该怎么加载、代码在哪儿、依赖哪些库」。

一个Mach-O文件的结构大致如下:

+------------------+
|  Mach-O Header   |  ← 魔数、CPU类型、文件类型
+------------------+
|  Load Commands   |  ← 告诉加载器:代码段在哪、依赖哪些库
+------------------+
|  __TEXT 段       |  ← 代码段(只读,可执行)
|    __text        |    实际指令
|    __cstring     |    字符串常量
|    __const       |    常量数据
+------------------+
|  __DATA 段       |  ← 数据段(可读写)
|    __data        |    全局变量
|    __objc_*      |    Objective-C运行时信息
|    __cfstring    |    CFString常量
+------------------+
|  __LINKEDIT      |  ← 链接信息(符号表、字符串表)
+------------------+

我个人最常用的是otoolMachOView这两个工具。举个例子,你想看一个App依赖了哪些动态库:

otool -L WeChat

# 输出示例:
WeChat:
  /usr/lib/libSystem.B.dylib
  /System/Library/Frameworks/UIKit.framework/UIKit
  /System/Library/Frameworks/Foundation.framework/Foundation
  @rpath/AFNetworking.framework/AFNetworking
  ...

这里有个坑:如果App用了@rpath开头的库路径,说明它加载的是Frameworks目录下的动态库。我曾在逆向一个金融App时,发现它把核心加密逻辑放在了一个自定义的动态库里,然后通过@rpath加载。你只要把这个动态库dump出来,就能直接分析它的加密算法。

关键点:Mach-O的__objc_* section里存着所有Objective-C的类名、方法名、协议名。这意味着——只要App是用OC写的,你就能通过class-dump或nm命令导出它的所有接口。Swift写的App虽然会混淆符号,但依然可以通过__swift_* section找到部分信息。

三、Theos与Cycript:越狱环境下的「瑞士军刀」

这两个工具,是我做iOS逆向时最离不开的。一个用来写Tweak(插件),一个用来动态调试。

3.1 Theos:写Tweak的脚手架

Theos本质上是一个Makefile系统,帮你自动处理编译、签名、打包。你只需要写一个.xm文件,里面用Logos语法hook住目标方法就行。

举个例子,我想hook住微信的-[WeChatAppDelegate application:didFinishLaunchingWithOptions:]方法,在启动时弹个窗:

%hook WeChatAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    %log; // 打印调用日志
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Hooked!"
                                                    message:@"启动成功"
                                                   delegate:nil
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles:nil];
    [alert show];
    return %orig; // 调用原始方法
}

%end

然后执行make package install,Tweak就会被安装到越狱设备上。我当年第一次成功hook住一个系统App时,那种感觉就像打开了新世界的大门。

注意:Theos编译出来的Tweak是.deb包,需要越狱设备才能安装。如果你没有越狱设备,可以用frida-ios-dump配合Frida来做类似的事情,但Theos在写复杂逻辑时更顺手。

3.2 Cycript:运行时「手术刀」

Cycript是JavaScript和Objective-C的混合体。你可以在越狱设备上通过SSH连接,然后动态修改App的内存、调用任意方法、甚至替换实现。

我常用的几个命令:

# 附加到微信进程
cycript -p WeChat

# 查看当前UI层级
UIApp.keyWindow.recursiveDescription().toString()

# 找到某个类的所有实例
var instances = [UIApplication sharedApplication].delegate

# 动态调用方法
[instances performSelector:@selector(somePrivateMethod)]

# 替换方法实现(Hook)
var orig = MyClass.messages['- originalMethod'];
MyClass.messages['- originalMethod'] = function(self, cmd) {
    console.log("Hooked!");
    return orig(self, cmd);
};

我记得有一次,一个App在启动时弹了个「网络异常」的对话框,但明明网络是通的。我用Cycript hook了UIAlertViewshow方法,打印出调用栈,发现是某个私有方法里写死了判断条件。改掉那个条件后,App就正常了。

避坑指南:Cycript在iOS 12以上版本兼容性不太好。我建议你优先用Frida的frida-tracefrida-ps来做动态调试,Cycript更适合做快速验证。

四、越狱检测绕过:一场「猫鼠游戏」

越狱检测,说白了就是App检查设备上有没有越狱痕迹。常见的检测点有:

检测类型 检测方法 绕过思路
文件检测 检查/Applications/Cydia.app/bin/bash等是否存在 MSHookFunction hook住access()stat(),返回假值
沙盒检测 尝试fork子进程,看是否成功 hook fork(),返回-1并设置errno
动态库检测 检查dlopen()是否能加载越狱相关dylib hook dlopen(),对特定路径返回NULL
符号检测 通过dlsym()查找越狱工具的函数地址 hook dlsym(),对敏感符号返回NULL
环境变量检测 检查DYLD_INSERT_LIBRARIES是否被设置 在Tweak启动时清空该环境变量

我见过最狠的检测方式,是__attribute__((constructor))里做检测。这意味着App还没进入main函数,就已经开始检查了。如果你用常规的MSHookFunction去hook,可能根本来不及——因为你的Tweak加载时,检测代码已经跑完了。

怎么破?我的经验是:fishhook在更早的阶段hook。fishhook可以重绑定Mach-O的懒加载符号表,在系统库函数被调用之前就替换掉。比如:

// 在Tweak的构造函数里,提前hook access()
static int (*orig_access)(const char *, int);
int my_access(const char *path, int amode) {
    if (strstr(path, "Cydia") || strstr(path, "bash")) {
        return -1; // 假装文件不存在
    }
    return orig_access(path, amode);
}

__attribute__((constructor))
void init() {
    // 用fishhook重绑定access
    rebind_symbols((struct rebinding[]){{"access", my_access, (void**)&orig_access}}, 1);
}
核心原则:越狱检测绕过的本质是「谁先加载,谁就赢」。你的Tweak必须比App的检测代码更早执行。所以,__attribute__((constructor))fishhook是你的好朋友。

五、知识体系总览

下面这张图,是我自己总结的iOS逆向知识体系。你可以把它当作一个「地图」,每次遇到新App时,按这个流程走一遍:

iOS逆向知识体系 1. 获取IPA 砸壳 / 抓包 / 备份 2. 解包分析 Info.plist / 资源文件 3. Mach-O分析 otool / class-dump 4a. 静态分析 IDA / Hopper / 反汇编 4b. 动态分析 Theos / Cycript / Frida 5. 越狱检测绕过 fishhook / 构造函数 / 环境变量 Tweak / 脱壳 / 协议分析

嗯,这张图基本覆盖了iOS逆向的完整链路。你从左上角开始,拿到IPA后解包,然后分析Mach-O,接着根据需求选择静态或动态分析,最后处理越狱检测。每一步都有对应的工具和技巧。

我个人建议:新手先别急着写Tweak,先把Mach-O分析透。你只要能用otoolclass-dump把App的类结构摸清楚,后面写Tweak就是水到渠成的事。