文章总结: 文档分析了2026年iOS/macOS系统中dyldsharedcache反编译的持续挑战,指出主流工具虽已支持但存在加载缓慢、内存溢出等问题。作者通过Rust重写工具恢复跨模块链接信息,重点解决chainedfixups结构修复、ARM64指令地址引用修正等关键技术难点,并利用AI辅助实现性能优化和回归测试,最终生成可独立使用的Mach-O文件。
综合评分: 85
文章分类: 逆向分析,二进制安全,安全工具,移动安全,安全开发
2026 年了,还在折腾 dyld_shared_cache
原创
0xcc
0xcc
非尝咸鱼贩
2026年5月1日 08:10
芬兰
在小说阅读器读本章
去阅读
今晚两个消息,Google Chrome VRP 大幅降低 bounty 价格,Claude 向企业用户开放 Claude Security 工具的 beta 测试。
装作没看见,依然写老掉牙的题材。虽然实在没劲,倒也是一直有人问。
iOS 的绝大多数用户态框架都在文件系统上找不到,而是预链接了几千个 dylib 作为一个巨大的共享缓存(dyld_shared_cache)。macOS 从 BigSur 开始也引入了这个机制。想反编译点代码还挺麻烦。
都已经 2026 年了,各大反编译工具如 IDA Pro / Binary Ninja / Ghidra / Hooper / radare2 都内置了 dyld_shared_cache 的支持。
说是支持,体验起来还是难受。
毕竟文件体积摆在那里,你只想看一个库,还得载入很多不想干的文件。漫长等待、爆内存、xref 不能用……各种问题。
拆分成单个文件?系统自带了 dsc_extractor.bundle,但是拆出来的文件也不能用,反编译一看全是红色的地址。
dyld_shared_cache 里的每个文件都被 dyld 预处理过了。很多原本属于单个 Mach-O 的信息,被挪到了 cache 的全局区域;很多指针已经按共享缓存地址空间修正过;跨库的符号引用不再使用 bind,而是直接变成 dsc 里的重定向地址。
所以反编译器看到的是一个损坏的文件:指针指向文件外,符号表缺失,__LINKEDIT 不完整,GOT 和 stub 对不上,ObjC 元数据里到处是共享缓存地址。
等等,上面的这个对比图是怎么来的?当然是写了个工具搞定了。
我几年前就用 C++ 手写过一个版本,需要串联 dsc_extractor.bundle 的输出结果和原始 dsc 文件一起处理。大体能用,但限制也很明显:只支持 iOS 18 以后的 arm64e 缓存,而且实际用起来经常遇到样本处理不完整的问题,只能哪里坏了补哪里。
现在当然要随波逐流:用 Rust 重写;叫 AI 用 Rust 重写。
这算是我用 vibe coding 做过的相对复杂的一个工作。当然,AI 不会一句话直接变出一个能用的方案,前提是我之前已经对 dyld 内部的不少机制比较熟悉,一开始能设计一个方案,并且解决不少性能和设计上的问题。
我不打算开源,所以这里简单介绍一下思路,哪位读者实在是需要,可以考虑自己复现。
这个工具的本质是恢复跨模块的链接信息:符号绑定和重定向。对应到现在的 dyld,主要就是恢复 chained fixups 结构信息,同时处理很多边角问题如二进制重排、Objective-C 运行时信息恢复,以及 ARM64 指令里那些已经被 dyld 重定位过的地址引用。
第一步当然是复刻 dsc_extractor.bundle 的行为。最简单的办法,是直接动态链接 Xcode 里自带的实现。
而 dyld 本身是开源的,都 2026 年了,当然也可以让 AI 参考 dyld 的实现,把相关逻辑移植到 Rust。然后提供几个不同版本的 iOS dyld_shared_cache 样本和 macOS 样本,让它在真实文件上自行修复、迭代。
macOS 系统和 LLVM 工具链本身就提供了很多检查 Mach-O 的工具,比如 nm、otool、dyld_info、objdump。这些工具可以作为 harness 的一部分,用来检查导出的 Mach-O 是否结构正常;同时明确要求 segment 地址、顺序、大小等关键布局尽量和原始输出保持一致,作为回归指标。
于是现在有了一个跨平台的 dsc_extractor,甚至可以部署在 NAS 上。还有一个好处是原版的并不让用户控制有选择地解压个别路径,现在可以只提取一两个库,即使是全量解压也比原版快很多。
接下来这个文件到底坏在哪里?
主要有这么些症状:
- 分支跳转指令指向共享的 stub island 或者直接跳转到另一个库的 __TEXT 段
- 数据区里的指针已经被 dyld 预先重定位过,指向文件外。比如 GOT、Objective-C class refs、selector refs、常量区里的函数指针,本来应该通过 bind 绑定外部符号,或使用 rebase 指向同一文件内部地址
- Objective-C 元数据指向 __OBJC_RO 和 __OBJC_RW 段,不在文件内
- GOT 被清零,间接绑定的 stub 地址对不上
- arm64e 架构的文件还能看到指针被编码,混有 PAC 相关信息、diversity、key、next 等字段。对 dyld 来说这些是 chained fixups 的一部分;对单独拆出来的文件来说,就会变成一堆不像地址也不像符号的数值
dyld_shared_cache 里有 slide info,描述哪些页面、哪些位置存在需要重定位的指针。系统加载 cache 时,dyld 会沿着这些信息把指针修到正确的共享地址空间里。
关键就是逆向这个行为,找出当前 dylib 里有哪些指针需要恢复到 chained fixups 里。
目前的 dsc 依然大发慈悲保留了 exports trie 结构,虽然链接器本身已经不再使用。我们可以通过这个解析某个 dylib 所有导出的符号和地址,再反过来用地址去匹配符号。
slide info 结构迭代了五个版本。读者可以自行古法查找源代码,或者让 AI 代劳找出来。遍历 slide info 信息获取指针的所在地址和目标地址,如果目标地址在当前 dylib 范围内,生成一个 rebase;反之则反查符号索引,生成一个 bind。
当然实际情况会脏很多。地址可能带 Swift 或 arm64e 的额外 bit;目标可能落在符号中间,需要记录 addend;有些地址指向的不是函数,而是 ObjC 的 class、selector、protocol 或常量数据;还要递归处理 re-export 的情况。
在程序中会遇到大量“获取某个地址上 rebase 之后的数据”的操作。而 dsc 的 slide info 是以内存页为单位,用链表保存重定位信息。
dyld 在这里做了一个很有意思的设计:链表本身就保存在相应的地址上。重定位指针的时候,原来的指针值会被编码成一种特殊格式,其中一部分 bit 表示目标地址,另一部分 bit 表示到下一个 fixup 的距离。
页面里没有额外放一张“重定位表”。每个需要 fixup 的指针 slot 自己就是链表节点。dyld 处理某个页面时,只需要从 page start 指定的位置开始,读取这个 slot 上的 encoded pointer,解码出当前指针应该变成什么值,同时得到下一个节点距离当前位置多远。然后跳到下一个 slot,继续重复这个过程,直到 next == 0。
但对我们这种离线恢复工具来说,它有一个副作用:如果想知道某个随机地址是不是 fixup,不能只看一眼 metadata 就结束。你得找到它所在页面的 fixup 链,从 page start 一路走过去,才能知道这个地址在不在链上,以及它被 dyld rebase 之后应该是什么值。
实际运行会产生大量随机访问,重复解析多次产生严重的性能问题。
解决方法很简单,把文件以 copy on write 方式映射到内存,直接照着 dyld 的行为原地重定向即可。再简单加一层按需访问缓存,用 bitmap 标记状态,如果没有 rebase 过就走一遍。
所有地方都走同一个读取接口,只要页面被访问过,里面的指针就已经是“dyld 处理过”的状态。对于 Objective-C 元数据遍历、GOT 分析、ARM64 地址追踪这类会大量随机读指针的逻辑,性能会稳定很多。如果需要读取原始文件内容,再 mmap 一个只读副本就好了。
class 列表的修复
CFString 的修复
最后一步是最麻烦的,如何修复指令当中对地址的无效引用。
需要处理如下情况:
- B/BL 目标地址是 stub island,需要替换成当前 dylib 的 __auth_stub 当中的动态绑定符号
- ADRP 的处理,一般会遇到 ADRP 和 ADD(如字符串、selector 的引用)或者 ADRP 和 LDR 的组合
所以工具会做一层很轻量的指令扫描,不追求完整反编译,只识别几种模式。
第一种最明显的分支跳转,如果是共享的 stub island,就在当前 dylib 里找一个合适的 stub/GOT 位置,把这条分支改成跳到本文件内的 stub。
第二种是 ADRP + ADD,最常见就是 selector 和 class 的引用。替换成本地的 __objc_methname 或者外部绑定地址。
第三种是 ADRP + LDR,一般是 GOT、指针、ObjC 引用等。
以上两种 ADRP 都可以做一个快速路径处理,在识别到 ADRP 的时候,不需要解析后续的 ADD 或者 LDR,已经能获取到内存页的基地址,就能快速判断是不是外部的引用。ADRP 在 arm64 程序当中出现频次非常高,如果不加优化会导致运行极其缓慢。
这里还藏着一些坑。dyld 合并之后的 cache 有时候会出现同一个内存页包含两个 dylib 的结束和起始地址的情况……而这还不是最大的问题。
默认 ADRP 紧跟 ADD 或 LDR 当然最容易处理,但真实代码当中编译器经常会把它们隔开几条指令,中间穿插一些寄存器移动,甚至条件分支指令。
所以这里实现了一个简单的状态追踪,并不是真正的数据流分析。遇到类似如下指令
ADRP Xn, page
就记录一个状态,接下来线性扫描分支跳转指令简单记录控制流,如果遇到接下来匹配的操作就计算最终地址。只要发现 Xn 被其他指令覆盖,就清掉这个寄存器的状态。再结合 LC_FUNCTION_STARTS 限制函数边界,实际测试扫描的窗口不会很夸张。
在具体交给 agent 实现的时候,一开始我给的文档提到了主要思路和踩过的坑。但是生成的版本非常不满意,主要原因还是缺少回归测试的标准。
直到我偶然提到了一次 IDA,agent 给我加了一个判定的方法。生成新的 dylib 之后调用 idat 生成 .asm 文件,grep 有没有类似的无效地址引用:
0x1FAC66B18@PAGEOFF
再检查其他 Objective-C 的区段是不是正确地还原成了符号,而不是地址。代码里面每次遇到不合理的条件就直接输出日志作为反馈。这样测试覆盖率突然就上去了,挂机一晚自主解决了不少 bug。反正我手工验收,还没发现有问题的样本。
先这样吧,反正也不开源 
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:非尝咸鱼贩 0xcc
0xcc《2026 年了,还在折腾 dyldsharedcache》