乐于分享
好东西不私藏

iOS 软件破解的技术原理

iOS 软件破解的技术原理

做了快十年 iOS 开发,逆向这块虽然不是我的主业,但工作中多少都会接触到一些安全加固、反调试的需求。正好这个问题问的是技术原理,我就从 iOS 移动端的角度,把破解的底层逻辑和历史脉络捋一遍。不涉及具体操作教程,纯聊原理。

先说结论:iOS 上所有破解手段,归根到底都在利用同一个矛盾——加密的代码必须解密到内存里才能运行,而这个解密后的瞬间,就是所有破解工具下手的窗口。 苹果十八年来叠了无数层防护,本质上都是在提高「接触到那个窗口」的成本。

FairPlay DRM:一切故事的起点

2008 年 App Store 上线,苹果给所有上架 App 套了一层 FairPlay DRM 加密。这套加密机制的技术实现其实没那么神秘。

每个从 App Store 下载的二进制文件(Mach-O 格式),里面有一个叫 LC_ENCRYPTION_INFO_64 的 Load Command,记录了三个关键字段:cryptoff(加密区域的文件偏移,通常是 0x4000)、cryptsize(加密区域的长度)、以及最关键的 cryptid(1 表示已加密,0 表示未加密)。苹果只加密 __TEXT 段,也就是存放编译后指令代码的那部分,其他段像 __DATA__LINKEDIT 和各种头信息都是明文。

App 启动时,内核的 Mach-O 加载器检测到 cryptid != 0,就会调用设备上的 FairPlay 密钥(绑定 Apple ID 和 Secure Enclave 硬件)把 __TEXT 段解密到进程的虚拟内存里。磁盘上的文件还是加密的,但内存里的代码是明文

说白了,这就像一个保险箱,里面的东西要用的时候必须拿出来摆在桌上。你箱子锁得再好,东西摆出来的那一刻就能被看见。整个 iOS 破解历史,都是围绕「怎么在东西摆到桌上的时候把它拍照存下来」展开的。

美团技术团队的 FairPlay 分析文章把内核层和用户态的交互流程写得非常清楚,涉及到 FairplayIOKit 驱动和 fairplayd 守护进程通过 MIG 调用配合解密,有兴趣的可以去读原文。

砸壳工具的五代进化

中文安全圈管 App 解密叫砸壳,非常形象。这个领域的工具迭代了五代,每一代都对应着 iOS 安全机制的升级。

最早期(2008-2011),人们用 GDB 脚本手动调试、手动 dump 内存,效率极低,而且需要很深的逆向功底。2011 年 Stefan Esser 发布了 dumpdecrypted,思路非常巧妙:利用 DYLD_INSERT_LIBRARIES 环境变量注入一个动态库,这个库的构造函数(__attribute__((constructor)))在 main() 之前执行。此时内核已经把代码解密好了,构造函数直接从自己进程的地址空间读出明文字节,写到新文件里,顺手把 cryptid 改成 0。整个过程就像搭了个顺风车,内核帮你解密,你负责抄一份。

后来 Clutch 换了个思路,fork 目标进程后用 task_for_pid() 拿到 Mach task 端口,再通过 vm_read() 跨进程读取已解密的内存。到了 2017 年左右,AloneMonkey(后来就职于阿里巴巴,著有《iOS应用逆向与安全》一书)开发的 frida-ios-dump 成了主流。它把 Frida 的 JavaScript 引擎注入到运行中的进程里,直接用 Memory.readByteArray() 读取解密区域,通过 SSH 传回 Mac。这个工具到现在还是 OWASP 移动安全测试指南里推荐的标准方案。

说到 AloneMonkey,iOS 逆向圈的人基本都叫他「猴神」。我早期入门逆向就是看他那本《iOS应用逆向与安全》,照着书一步步搭环境、砸壳、写 Hook。2018 年那阵我还真拿一个手游练了手,用 Method Swizzling hook 掉了血量计算的方法实现锁血,再改了攻击伤害的倍率做倍攻。第一次在游戏里看到自己的角色打不死、一刀秒的时候,那种「我居然能控制另一个程序的行为」的感觉相当上头。虽然后来没有继续深入逆向方向,但那段经历让我对 Objective-C 的 runtime 机制有了非常直觉化的理解,这在后来做正向开发时反而成了优势。

不过 frida-ios-dump 只是猴神对 iOS 逆向生态贡献的一小部分,他真正改变游戏规则的作品是后面要讲的 MonkeyDev。

其实从原理上看,五代工具的核心逻辑从未变过:等系统把代码解密到内存里,然后想办法把内存里的内容读出来。变的只是「想办法」的具体手段,因为苹果不断在收紧进程间读取内存的权限。

Hook 技术:运行时的手术刀

砸壳解决的是「看到代码」的问题,而 Hook(钩子)解决的是「改变代码行为」的问题。前面提到我在游戏里实现锁血和倍攻,用的就是这套技术。这才是真正实现功能绕过的核心手段。

Objective-C 的消息派发机制天然适合被 Hook。每次方法调用编译后都是 objc_msgSend(receiver, selector, args...),运行时维护着一张可修改的派发表,映射 selector(方法名)到 IMP(函数指针)。Method Swizzling 做的事情就是调用 method_exchangeImplementations() 把两个方法的 IMP 互换。互换之后,调用原方法名实际执行的是你的替换函数,调用你的方法名反而执行原函数。这就是为什么 swizzle 代码里看似「递归调用自己」实际不会死循环,因为 IMP 已经交叉了。

对于 C 函数层面的 Hook,Facebook 开源的 fishhook 只有大约 250 行代码,却极其精妙。iOS 的 Mach-O 二进制对外部符号使用位置无关代码,函数调用经过 stub 跳转,stub 里读取的函数指针存放在 __DATA 段的 __la_symbol_ptr(延迟绑定)和 __nl_symbol_ptr(非延迟绑定)区域。动态链接器 dyld 在加载时把真实地址写进去。fishhook 顺着间接符号表的链条找到目标符号名,直接改写 __DATA 段里的指针值指向替换函数。因为修改的是可写的数据页,不会违反 W^X 策略或代码签名。不过 fishhook 有个关键限制:它只能 hook 动态链接的外部符号,同一个二进制内部定义的函数它动不了。

越狱环境下的 Cydia Substrate(saurik 开发)就没这个限制了。它的 MSHookFunction 直接在汇编层面操作:把目标函数开头的几条指令替换成一个跳转指令(trampoline),跳到替换函数;同时在另一块可执行内存里保存被替换的原始指令加上一个跳回去的跳转,这样替换函数里还能调用原始实现。这需要内核补丁来绕过 W^X 保护,所以只有越狱设备才能用。

Swift 对这套体系造成了很大冲击。Swift 默认使用静态派发,struct、final class、private 方法全部在编译期确定调用地址,运行时根本没有可以替换的派发表。只有显式标记 @objc dynamic 的 NSObject 子类方法才走 Objective-C 消息派发,才能被 swizzle。这也是为什么纯 Swift 项目天然比 OC 项目更难逆向。

从命令行到 Xcode:iOS 逆向的工程化之路

上面聊的砸壳、Hook 都是单点技术,但真正做逆向分析的人面对的是一个完整的工程问题:怎么把砸壳后的 App 跑起来?怎么把自己写的 Hook 代码注入进去?怎么调试?怎么管理依赖?这条工具链的演进,本身就是一部从「手工作坊」走向「工业化生产」的历史。

最早的越狱插件开发靠 Theos,一个纯命令行的越狱开发包。你用 nic.pl 脚本创建项目模板,在 Makefile 里配置编译参数,用 Logos 语法(.xm 文件)写 Hook 代码,然后 make package install 编译成 .deb 包部署到越狱手机上。整个流程全在终端里完成,没有代码补全,没有断点调试,没有可视化 UI 检查。写错一个方法名只能靠运行时崩溃来发现。对于习惯了 Xcode 开发体验的 iOS 工程师来说,这套工作流的效率和体验都很原始。

iOSOpenDev 是第一个尝试把逆向开发搬进 Xcode 的工具。它在 Xcode 里注册了项目模板,让你可以直接在 Xcode 中创建 Tweak 项目,用 Xcode 的编辑器写代码、用 Xcode 的构建系统编译。想法是好的,但这个项目后来停止维护了,对新版本 Xcode 的兼容性越来越差,安装过程也出了名的容易翻车,社区里到处是安装失败的求助帖。

2017 年,猴神发布了 MonkeyDev(GitHub 6.7k star),彻底改变了 iOS 逆向开发的工作方式。MonkeyDev 自称是 iOSOpenDev 的升级版,但实际做的事情远超「升级」二字。它解决的核心问题是:把 iOS 逆向分析从一堆零散的命令行工具,变成了一个完整的、基于 Xcode 的 IDE 级开发体验

具体来说,MonkeyDev 安装后会在 Xcode 的新建项目菜单里注册四个模板。其中最核心的是 MonkeyApp:你新建一个 MonkeyApp 项目,把砸壳后的 IPA 拖进 TargetApp 文件夹,配好开发证书,按 ⌘R 就能把目标 App 连带你的 Hook 代码一起编译、注入动态库、自动重签名、安装到非越狱手机上运行。整个过程和你平时开发自己的 App 几乎一样。

这背后 MonkeyDev 自动完成了一长串操作:调用内置的 class-dump 导出目标 App 的所有类头文件,调用 restore-symbol 恢复被 strip 掉的符号表(这对调试至关重要),自动集成 Reveal(可视化 UI 层级检查)和 Cycript(运行时交互式调试),把你写的 Hook 代码编译成动态库注入到目标 App 中,最后用你的开发证书重签名整个包。Debug 模式下自动集成调试工具,Release 模式下自动移除,连这个都帮你想好了。

MonkeyDev 的项目结构里有几个值得注意的目录:AntiAntiDebug 目录预置了反反调试代码(对付前面提到的 ptrace(PT_DENY_ATTACH) 等防护);fishhook 模块自动集成,可以直接 hook C 函数;LLDBTools 提供了方便的 LLDB 调试辅助命令。你写 Hook 代码支持三种风格:标准的 OC runtime 写法、Theos 的 Logos 语法(写在 .xm 文件里),以及 CaptainHook 的宏定义写法。对于从 Theos 迁移过来的人,.xm 文件可以无缝复用。

更有意思的是 MonkeyDev 引入了 CocoaPods 集成。它维护了一个非越狱插件的私有 Specs 仓库,你在 Podfile 里加一行 source 就能像管理正常依赖一样引入逆向插件。比如做微信相关的逆向分析,社区里有人把现成的 Hook 封装成了 Pod,直接 pod install 就能用。这套机制说白了就是用 CocoaPods 搭了一个非越狱插件的分发平台。对于做 iOS 开发的人来说,这完全是熟悉的工作流,心智负担几乎为零。

其实吧,MonkeyDev 的意义不只是让逆向变得「方便」了,而是降低了从正向开发切换到逆向分析的门槛。我 2018 年搞那个游戏破解,用的就是 MonkeyDev。以前你想分析一个 App 的实现,得先学一堆命令行工具、配各种环境、折腾 SSH 连接,光搭环境就能劝退大部分人。有了 MonkeyDev 之后,在 Xcode 里新建项目、拖进 IPA、⌘R 运行,然后像调试自己项目一样打断点、看变量、inspect UI。当时我就是在 Xcode 里单步跟进游戏的血量计算逻辑,找到关键方法后写了个 swizzle 替换掉,整个过程和平时写业务代码的体验几乎没有区别。做 iOS 开发的人都知道,LLDB 断点调试和 print log 调试,那是两个世界的效率差距。

当然 MonkeyDev 也有局限。它最后一次大更新是 2022 年,对新版 Xcode 的兼容需要社区 fork 来维护。对越狱设备它还支持直接通过 Frida 自动砸壳;但对非越狱设备,你得自己先搞到砸壳后的 IPA。随着苹果签名机制的持续收紧,重签名安装本身也越来越受限。但它建立的这套「Xcode + CocoaPods + 一键部署」的逆向开发范式,已经深刻影响了整个 iOS 安全研究社区的工作方式。

苹果十八年的防线升级

说完攻击手段,再看防守方。苹果的安全架构演进堪称教科书级别,每一层都是在「堵上一代攻击的路」。

早期 iPhone(2007)跑的所有进程都是 root 权限,没有沙箱,没有代码签名。George Hotz 十七岁就用烙铁和吉他拨片完成了第一台 iPhone 的硬件解锁。iOS 4.3(2011)引入 ASLR,让代码和数据的内存地址随机化,从此攻击者不能硬编码地址了。iOS 6(2012)加入 KASLR,把内核基址也随机化。配合栈金丝雀(函数返回前验证栈上的随机值)和 W^X(内存页不能同时可写可执行),单一的缓冲区溢出攻击被升级成了需要信息泄漏 + ROP 链 + 代码签名绕过的多阶段工程。

2018 年的 A12 仿生芯片是安全架构最大的分水岭,一次性带来了四项硬件级防护。PAC(Pointer Authentication Code) 利用 64 位指针的高位空闲比特存放密码学签名,由硬件密钥和上下文(比如栈指针)共同计算。指针被篡改后签名验证不过,直接触发异常。Google Project Zero 的 Brandon Azad 研究后发现,苹果的 PAC 实现比 ARM 标准规范更严格,打破了跨密钥的对称性。PPL(Page Protection Layer) 在内核中划出特权内存区域,只有从 __PPLTEXT 段执行的代码才能修改页表和信任缓存,哪怕你拿到了完整的内核读写权限也改不了。CoreTrust 在内核层验证 CMS 签名必须包含苹果的真实证书。CTRR 把只读内核保护扩展到了协处理器固件。

最新的大招是 2025 年随 iPhone 17(A19 芯片)公布的 MIE(Memory Integrity Enforcement),结合了同步模式的增强内存标记扩展、安全分配器和标记保密执行。苹果自己说这是消费级操作系统历史上对内存安全最大的升级,内部安全团队在 MIE 上跑之前已知的漏洞利用链,全部无法复现。

后越狱时代:TrollStore 与服务端迁移

2019 年 axi0mX 公开了 checkm8 漏洞,这是一个 BootROM 中 USB DFU 栈的 use-after-free,影响 A5 到 A11 芯片。BootROM 是出厂时烧录在芯片里的,苹果用软件更新补不了。基于此的 checkra1n 越狱工具支持任意 iOS 版本,对老设备来说相当于永久越狱。但 A12 以上的设备完全不受影响。

TrollStore(opa334 开发)开辟了另一条路线:利用 CoreTrust/AMFI 的签名验证漏洞,在不越狱的情况下永久安装 IPA。装上去的 App 被系统当成系统级应用,重启后依然存在,还能拥有任意 entitlements。TrollStore 2.0 用的漏洞还是 Google 威胁分析组在一个间谍软件攻击链中独立发现的。不过它支持的范围有限(iOS 14.0-16.6.1 和 17.0),iOS 18 以上因为苹果修复了非 root 进程生成 root 进程的机制,永远不会被支持。

其实吧,现在让传统破解越来越没意义的,不是苹果把墙砌得多高,而是整个软件行业都在把核心逻辑往服务端迁移。订阅制 App 需要持续联网验证,AI 功能在云端运行,你把本地二进制砸壳改得天花乱坠也没用,因为关键计算根本不在你手机上。API 驱动的 App 离了服务器就是个空壳,破解一个 Instagram 的二进制毫无意义,你没有认证过的服务端通信什么也干不了。

有人总结得很到位:任何本地授权验证,最终都会归结为一个 if 判断,没有任何客户端软件能真正免疫破解。 唯一靠谱的保护就是把关键功能放在服务端。这也是为什么一键越狱 + 随意装盗版 App 的时代已经彻底过去了。留下来的是一个高度专业化的技术领域,理解这些机制对做安全加固、渗透测试、防御性工程的价值,远远大于它对盗版的价值。

跨平台通用的绕过原理

最后补充几个跨平台通用的技术原理。

二进制补丁是最古老也最直接的手段。ARM64 的指令固定 4 字节长,改起来很方便:把验证函数的条件跳转指令从 TBZ(为零跳转)改成 TBNZ(非零跳转),或者直接把整个检查函数改成 MOV W0, #1; RET(永远返回 true)。x86 上更简单,JNZ(0x75)改成 JZ(0x74)或者无条件 JMP(0xEB),一个字节的事。说白了,所有的授权验证在二进制层面都会对应一条条件跳转指令,找到它,反转它,验证就形同虚设。

内存操控是运行时修改数据的手段。经典的操作流程是:搜索一个已知值(比如你当前有 100 金币),在程序里改变它(花掉 10 个变成 90),再筛选变成新值的地址,反复缩小范围直到锁定唯一地址,然后直接改写。Windows 上的 Cheat Engine 就是干这个的,macOS 上通过 Mach 内核的 task_for_pid + vm_read / vm_write 实现同样的效果。

网络拦截针对的是需要联网验证的软件。mitmproxy 这类中间人代理可以实时生成伪造证书,拦截并篡改 App 和授权服务器之间的通信。软件做了证书锁定(Certificate Pinning)?用 Frida hook 掉 SSL 上下文的初始化函数就行。不过正如前面说的,越来越多的 App 把核心逻辑也放在了服务端,光拦截网络流量已经不够了。

以上就是 iOS 破解原理的技术全貌。从 2007 年 Geohot 拿烙铁焊 iPhone,到 2025 年苹果在硬件层面实现内存完整性执行,攻防双方都在不断升级。但底层逻辑始终没变:代码要运行就得解密,行为要改变就得找到跳转点。

回头看这段历史,最让我感慨的倒不是攻防技术本身,而是逆向工程的工具化进程。从 Theos 的命令行时代到 MonkeyDev 的 Xcode 集成,从手动 GDB dump 内存到 frida-ios-dump 一行命令搞定,每一步都在降低技术门槛。防守方能做的是不断提高攻击成本,直到只有国家级别的资源才负担得起。而对我们 iOS 开发者来说,理解这些原理最大的价值在于知道怎么做好防守:关键逻辑放服务端,本地验证多做混淆和完整性校验,利用好苹果提供的每一层安全机制。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » iOS 软件破解的技术原理

评论 抢沙发

5 + 7 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮