再不发就过时了——逆向 iOS 16 的 dyld_shared_cache
2023-6-5 10:30:57 Author: mp.weixin.qq.com(查看原文) 阅读量:13 收藏

最近文章水得厉害。想当旅游博主,文笔稀烂。其实差点还想水一篇去巴塞罗那看酷玩演唱会的日志和梅西差了一天,不然可以吹牛说和梅老板一起看的。泰酷辣!

万人 KTV 之 Viva La Vida

算了还是回归技术主题。虽说也是流水账,但是起码有点信息量。


2.5 年前本号已经写过一篇 IDA Pro 分析 dyld_shared_cache,当然这篇文章已经过时了。

dsc 格式是 iOS 私有的,不需要考虑任何第三方的兼容性。因此格式迭代随心所欲,每一个大版本的 iOS 都会改。

截至目前最新版的 IDA Pro 8.2 正式版虽然可以载入 dyld_shared_cache,但是并没有处理好数据段指针的 rebase,导致大量无效的地址引用。函数的交叉引用也是坏的。

Hopper 我没有付费,不知道;Ghidra 基本不可用,也可能是我姿势错了;Binary Nina 实际上依赖一个我接下来要提到的 python 库,后文展开说。

前阵子 OffensiveCon 上(惊现 IDA Pro 娘!一起逛 OffensiveCon 2023)找 Hex Rays 的人抱怨过,据说很快 8.3 beta 就会处理掉这个问题。

但是 WWDC 还有不到 24 小时,马上 iOS 17 就要出来了。

如果又改格式,IDA Pro 绝对赶不上趟。本文再不赶紧水出来,过几个小时又作废了

先来看几个可行的方案。

首先是 dyld_shared_cache 文件的提取。

下载 ipsw 固件之后,当作 zip 解压出文件系统对应的 dmg,挂载,找到对应的路径直接复制出来即可。

iOS 16 的 dyld_shared_cache 已经不是单一文件,包含多个 dyld_shared_cache_[arch].[序号],以及 .dylddata、.dyldlinkedit、.symbols 等相关文件。

建议直接使用这个 go 语言的工具,可以完成从 ipsw 下载到解压逆向一条龙服务:

https://github.com/blacktop/ipsw

ipsw extract -d iPhone_AAAAAAAAA_Restore.ipsw

正如作者所言,他想做一个 iOS 安全研究的瑞士军刀。这个工具还是 go 语言编写,如果信任作者,直接下载可执行文件就可以用。

这个工具还有很多用法,请参考文档。本文后续也会提到一些。


调试器

无论动态链接器实现得多么鬼畜,用了什么奇怪的二进制格式或者重定向机制,最后都要作为机器码加载到内存的。

最直接的办法当然是 Xcode 新建一个空的 app 真机调试,然后就可以随便看任何系统框架函数的的反汇编了。符号什么都不成问题,就直接读汇编门槛有点高。

个人觉得 arm64 的代码和 intel 比起来简洁易读很多,可是 F5 伪代码就是香啊。


简单版

Xcode 自带的模拟器会带一个 Runtime,路径在 

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot

代码和真实设备有差异,但是很多情况下够用。如果记不住这个路径也不要紧,搜一下就出来了:

find /Applications/Xcode.app -name RuntimeRoot -type d

然后挑选最喜欢的反编译工具,没有什么门槛——除了缺失很多真机上的代码和框架。


IDA Pro

IDA Pro 从 7.2 开始增强了 dyld_shared_cache 的支持,允许先载入单个 dylib 后,手动在缺失地址上右键载入依赖的模块或者段(section)。

IDA Pro 对 iOS 16 的 dsc 支持其实没有太大问题。

使用 single module(单一模块模式)载入文件后,所有涉及其他模块的函数调用都会变成 bl 无效地址。

在 iOS 16 上,此类地址指向模块之外的 stub island 来实现长距离跳转。一个 stub island 通常包含三条指令,adrp 和 add 将目标地址指针载入寄存器,然后 br(开启了 PAC 的设备上是 braa)执行真正的跳转。

当然我们不需要考虑这么多,直接右键 Load __stubs(或 __auth_stubs),IDA 就会自动识别跳转的目标并修复对应的符号。

dyld_shared_cache 的 __stubs(__auth_stubs)有很多个,不过模块内基本上只会用到同一个 stub island,所以手动加载一次就够了。

目前仍然有一个 bug,虽然符号就在代码里躺着,但是不能交叉引用。

此外 IDA 没有很好支持 dsc 的 rebase, Objective-C 相关的代码尤为明显。

Class 的引用只显示一个无效地址:

selector 没法看:

首先可以阅读 arm64 而不是 arm64e 的设备固件,处理起来相比支持 PAC 的要简单一些。

诸如这样的地址,高位直接抹掉即可。例如 0x201E1DE6318 -> 0x1E1DE6318。然后就可以右键载入缺失的内容(例如 selector 位于 libobjc.dylib 的 __OBJC_RO)

以上是手动在菜单中选择 Edit / Patch Program / Change Byte 实现的。可以写 IDA Python 干这种脏活。

IDA Pro 号称可以只分析单个模块,然后按需加载数据。实际上这样生成的 idb 会非常庞大。多看几个模块,硬盘有点吃不消。


分离单个 framework

老生常谈的 dsc_extractor.bundle,网上一大堆教程,懒得重复了。

这个办法分离单个模块速度非常快,分割成小文件后反编译起来也方便。缺点也很明显,其中凡是有跨模块的函数调用都会变成无效的地址。效果等同于前文 IDA Pro 载入单个模块,但是这时候已经不能右键按需载入依赖了。

也有人自行 fork dyld 的代码来做 extractor。例如

https://worthdoingbadly.com/dscextract/

缺点是 dyld 一直改,这个早就不能用了。


DyldExtractor

下面隆重介绍这个开源项目。

https://github.com/arandomdev/DyldExtractor

这个项目把 dyld 相关的重定位、符号解析等逻辑用 python 重新实现,不依赖 Xcode 什么乱七八糟的东西。

提取单个模块的命令:

dyldex -e UIKitCore dyld_shared_cache_arm64e

也可以使用批处理模式一次性分割所有的框架。

这个工具目前抽取单个模块效果是最好的,在反编译工具中阅读体验很不错。仅有少数 Objective-C 相关的段(section)修复有问题。

具体来说,有一些 Objective-C 运行时的信息,例如对另一个模块的 class 的引用。如图所示,__objc_classrefs 当中保存了 class 指针的。

在 unslide 之后,一部分 class 指向同一个模块内的符号,无需进一步处理;而图中红色的无效地址,则是其他模块的 class。

对于这些无效地址,DyldExtractor 的策略是生成一个名为 __EXTRA_OBJC_hidden 的数据段,并将对应的数据结构复制到这个 dylib 当中。

其实这里完全可以引用导入的符号,没有必要将数据也复制过来。这样还会影响 IDA Pro 等工具对 Runtime 信息的解析。


ipsw 提取模块

前文提到的 blacktop/ipsw 工具也实现了 DyldExtractor 的功能。示例如下:

ipsw dyld extract dyld_shared_cache_arm64e UIKitCore --objc --slide --stubs

生成的文件部分结构有问题,在 IDA 一打开就会有一个警告 The input file has invalid section file offsets.

不过不影响使用。

然而这个工具实现的修复效果有限,随便浏览几个函数,仍然会看到大量无效的地址引用。

不要相信生成的 Objective-C 方法的名字。由于修复数据结构的时候 selector 指向错误地址,图中的 -[UIViewPropertyAnimator x] 是一个错误的符号。

虽然这个工具直接修复 dylib 效果不怎么样,在解析 dsc 格式上还是非常强大的。

前文提到 iOS 16 使用 branch island 来实现长距离跳转。如下命令可以打印出整个 dsc 当中的 __auth_stubs 对应的符号和地址(后面我们会用到)。

ipsw dyld stubs dyld_shared_cache_arm64e

对于任意一个在反汇编中出现的红色未知地址,都可以用 ipsw dyld a2s 命令来获取虚拟地址对应的符号信息。

ipsw dyld a2s dyld_shared_cache_arm64e 0x1937a4bd0

所以理论上可以写一个 IDA Python 脚本,直接分析 dsc_extractor.bundle 生成的,缺失各种符号的 dylib,将所有的无效地址直接扔给 ipsw 来符号化即可。

可惜这样做性能完全不可接受。ipsw 反向搜索符号虽然准确,但是效率很低。

其原理是首先遍历 dyld_shared_cache 所有的符号和地址,将其缓存到一个字典文件中。第一次生成这个字典文件非常耗时,此后虽然有缓存可以提速,但每反查一个符号都要耗费数秒时间。

具体代码可以参考其中的

cmd/ipsw/cmd/dyld/dyld_a2s.go

要是修复所有出现的无效地址,脚本跑起来就遥遥无期了。


改良轮子

前面已经提到了一种思路。dsc_extractor.bundle 是最稳定的办法,只是缺符号。

无论 dsc 格式怎么改,思路都是提前链接好代码,在 cache 文件里直接用地址,而不走符号绑定。

这样一来在 __TEXT 段中出现无效地址的指令基本上就是:

  1. 调用函数变成 bl [branch island]。stub 不属于任何一个 dylib;

  2. 访问其他模块导出的数据符号,例如字符串常量。变成 adrp 和 ldr/str 两条指令,地址指向某个不属于任何一个 dylib 的 __AUTH_CONST 段中;

  3. 对 selector 的引用,换成 selector 在 libobjc.dylib.__OBJC_RO 的一个副本,使用 adrp 和 add 两条指令

  4. 其他数据段相关的地址引用,稍微有些复杂

这里以 arm64e 架构的可执行文件为例。

引用到其他 dylib 的函数符号,都会出现在 __auth_stubs 这个区段中。因为不再需要动态绑定,这些 stubs 会从一个 __AUTH_CONST 的地址 ldr 一个(有 PAC 签名)的函数指针,再用一条 braa 指令跳转过去。

__auth_stubs:000000018A19BD94 _ADClientAddValueForScalarKey__auth_stubs:000000018A19BD94                 ADRP            X17, #0x1E22341C8@PAGE__auth_stubs:000000018A19BD98                 ADD             X17, X17, #0x1E22341C8@PAGEOFF__auth_stubs:000000018A19BD9C                 LDR             X16, [X17]__auth_stubs:000000018A19BDA0                 BRAA            X16, X17

__auth_stubs 可以直接通过 section 的名字查询,也可以解析间接符号表。假定读者有一定的基础,此处不再 LC_DYSYMTAB 如何解析,不然就变成烂大街的 iOS 面试高级指南系列了。可以通过搜索引擎找到大量此类文章。

解析 adrp、add 指令,可以获取到 stub 指向的真正的目标函数地址。而在 __text 段的代码,确是用一条 bl 指令跳转到另一个结构类似的 stub island 中。

通过解析这些指令,生成一个 __auth_stubs 和真实地址的查找表。最后一个 pass 遍历 __text 中所有的 bl 指令,替换成同一个 binary 的 __auth_stubs 的地址。

这样一来是不是顺眼多了?

另一个需要解决的是数据指针的修复。

正确的方式在 blacktop/ipsw 当中有实现:

https://github.com/blacktop/ipsw/blob/7d9c7c89c/pkg/dyld/file.go#L754

即根据 dyld_cache_slide_info 结构体对指针 rebase。使用 ipsw 命令可以打印所有的指针不过小心,输出的数据会非常庞大。

ipsw dyld slide dyld_shared_cache_arm64e

来一个粗暴的实现。

  1. __got

  2. __objc_classlist

  3. __objc_nlclslist

  4. __objc_catlist

  5. __objc_catlist2

  6. __objc_protolist

  7. __objc_nlcatlist

  8. __objc_superrefs

  9. __objc_classrefs

  10. __objc_protorefs

  11. __objc_arraydata

这些 section 里的指针一旦满足形如 0x80001DE295670,直接位运算屏蔽掉 0x8000000000000 即可。

接下来获得的地址如果在模块范围内,则说明不是外部的符号,不做处理;反之则需要替换成一个导入符号的地址。

本文主要为了让 IDA Pro 的结果更顺眼,所以只照顾这一款反编译工具。

在 IDA Pro 当中,点开 exports 标签,有二进制所有导入的符号和地址。同时生成一个虚拟的 UNDEF 区段,来保存外部符号和地址的映射关系。实际上这个 UNDEF 并不存在于 MachO 格式中,而且地址居然是 IDA Pro 自己生成的。

例如图中所谓的 UNDEF 基地址,是选取 Mach-O 当中最后一个 section 的结尾然后按照字节对齐。接下来根据 LC_SYMTAB 命令,将符号表中导入的符号按照 library ordinal 分组排序。每一个符号的地址累加一个指针的大小。

Binary Ninja 也会生成一个虚拟的 .extern section。两者对比可以发现符号的顺序一致,但虚拟的基地址不同。

回到刚才说的数据地址引用(如 __objc_classrefs 区段)。为了取悦 IDA Pro,直接将计算出来的 UNDEF 的地址的值写入区段中,就可以建立一个导入符号的映射——虽然这样不满足真正的 MachO 格式规范。

我们的需求只是 F5,又不打算能运行,所以无所谓。

如下是保存 CFSTR 字面量的区段。

以 0x80156AE15D5427C0 为例,应该指向 ___CFConstantStringClassReference。

其中有几位 0x6AE1 保存的是指针的 discriminator,PAC 加签名的时候会用到,表示这是一个 isa 指针。把高位屏蔽掉之后得到 0x5D5427C0,再加上基地址 0x180000000 即可得到真正的地址 0x1dd5427c0

验证一下。

$ ipsw dyld a2s dyld_shared_cache_arm64e 0x1dd5427c0   • Loading symbol cache file...   • Address location          dylib=/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation section=__DATA_DIRTY.__objc_data0x1dd5427c0: class___NSCFConstantString

等一下,这个 0x180000000 怎么来的?当然是 dyld 源码里写死的。

#define ARM64_SHARED_REGION_START   0x180000000ULL

注意,本文提到的对 PAC 指针的处理都是不科学的暴力做法,只是为了不解析结构体而做的短平快处理

前面提到所有的 selector 都会被换成 libobjc.dylib.__OBJC_RO 的一个副本,但 dylib 本身不会删掉这段内容,__objc_methname 还是保留的。

遍历 __objc_selrefs 当中的地址,除掉高位,然后读取 libobjc.dylib 对应虚拟地址的字符串,搜索同一 dylib 的 __objc_methname 即可。需要注意的是可能有多个 selector 具有同样后缀的情况,所以一定要确保搜索到的字符串前面是 \x00,否则可能匹配到其他 selector 的尾巴。

这样一来能处理掉大部分的情况。除了 swift。可能 swift 设计之初就想恶心逆向工程,把 ABI 和字面量设计得非常奇葩。更神奇的是前文搜索 bl 指令替换成 LC_DSYMTAB 项目的思路不起作用,一些符号根本就没有出现在导入表中。


笔者非常懒,甚至不想自己费力去解析 dsc 的格式,直接用 ipsw dump 命令来读取 dsc 特定地址的内容。我在几个 python 库(lief 解析 MachO 格式,capstone 反编译,keystone 生成补丁代码)的基础上实现了一个小工具,不过并不打算放出来。

这种东西只会有修不完的 bug,自用就好。反正马上再等等 iOS 17 就出来了,说不定就没用了呢 

我的 GitHub 堆了好些 issue 要修,都没什么动力去改。最近才来了一个 Sponsor,天天都是些有的没的 issue。

群友挖苦我天天整这些没用的,不如开个知识星球。现在都说国际化,咱好歹也得弄个 OnlyFans 吧。

几年前有老外私信问我,说我放的工具很好用,有没有开打赏。我当时还在墙内,嫌弃注册那些平台太麻烦,就婉拒了。前阵子我们在会议上见到了。他说一开始搞 app pentest,后来转研究系统漏洞,然后拿出手机给我看他几年刷 Apple Security Bounty 出来的真·海景房。一时不知道应该感到高兴还是该狠狠酸一把🍋

题图生成自 MidJourney


文章来源: https://mp.weixin.qq.com/s?__biz=Mzk0NDE3MTkzNQ==&mid=2247484849&idx=1&sn=5bce36a46f47434ec07639d38d3a965c&chksm=c329fb41f45e725717b08c4c1f413a516cabfd24576cbf4a5b38d5d2e96f378c2774f6428915&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh