在 QEMU 中模拟 iPhone
芯片安全测试
新的高级分析工具、增加的自定义选项和改进的集成能力。
ALL-IN-ONE PLATFORM
esDynamic 在一个强大且协作的平台中管理你的攻击工作流程。
Expertise Modules 攻击和技术的执行目录。
Infrastructure 集成你的实验室设备并远程管理你的工作台。
Lab equipments 使用最新的硬件技术升级你的实验室。
PHYSICAL ATTACKS
Side Channel Attacks 从数据采集到结果可视化评估密码学算法。
Fault Injection Attacks 使用激光、电磁或 Glitch 来利用物理中断。
Photoemission Analysis 检测来自你的 IC 的光子发射,以观察其运行期间的行为。
EXPERTISE SERVICES
Evaluation Lab 我们的团队已准备好提供你的硬件的专家分析。
Starter Kits 通过在现代芯片上开发的内置用例来构建专业知识。
Cybersecurity Training 通过教练指导的实践培训模块来增长专业知识。
二进制安全分析
开始使用 𝗲𝘀𝗥𝗲𝘃𝗲𝗿𝘀𝗲:用于高级软件二进制分析的协作平台。
ALL-IN-ONE PLATFORM
esReverse 在一个强大且协作的平台中进行静态、动态和压力测试。
Extension: Intel x86, x64 使用专用模拟框架对 x86/x64 二进制文件进行动态分析。
Extension: ARM 32, 64 使用专用模拟框架对 ARM 二进制文件进行动态分析。
DIFFERENT USAGES
Penetration Testing 在一个平台上识别和利用系统漏洞。
Vulnerability Research 更快、更有效地发现和解决安全漏洞。
Code Audit & Verification 有效地检测和消除有害软件。
Digital Forensics 协作分析数据以确保彻底调查。
EXPERTISE SERVICES
Software Assessment 我们的团队已准备好提供你的二进制代码的专家分析。
Cybersecurity training 通过教练指导的实践培训模块来增长专业知识。
资源
通过网络安全,使团队能够掌握知识,从而构建一个更安全的世界。 加入我们的使命。
INDUSTRIES
Semiconductor
Automotive
Security Lab
Gov. Agencies
Academics
Defense
Healthcare
Energy
ABOUT US
Why eShard?
Our team
Careers
FOLLOW US
Linkedin
Twitter
Youtube
Gitlab
Github
Blog Contact us Back to all articles Binary Analysis
使用 QEMU 模拟 iPhone: 一段旅程
17 分钟阅读
由 Georges Gagnerot 编辑 • 2025 年 4 月 4 日
分享
旅程的开始
我们从研究现有的开源解决方案开始了我们的 iOS 模拟之旅。我们之前已经成功运行过 alephsecurity/xnu-qemu-arm64,但是该项目是只读的,这令人担忧。
然后我们尝试了 TrungNguyen1909/qemu-t8030,它具有许多有趣的功能:
- 实际恢复 iOS 的能力(使用第二个“伴侣”QEMU 进行 USB 连接)
- 运行 iOS 14
- 更新版本的 QEMU
- 一个关于如何启动模拟器的不错的 wiki
通过该项目,我们通过修改 System/Library/xpc/launchd.plist
快速获得了一个 shell 和 ssh 连接,所以这是一个很好的起点。
我们将长期目标设定为获得一个功能性的 iOS 模拟环境,具有 UI 并且至少能够执行一些应用程序。
t8030
项目首先困扰我们的是他们在 QEMU 本身中添加代码来修补 xnu 内核。 我们知道我们可能需要更多的修补,并且想要一种更简洁的方法来做到这一点。 因为我们有一些使用越狱的真实 iPhone 的经验,所以我们研究了使用 Pongo 来应用 checkra1n 补丁,因为这可以让我们删除在 QEMU 中完成的任何修补。
在越狱场景中,在被 checkmate 攻破后,PongoOS 被注入 SRAM 中,并且 checkra1n-kpf 模块通过 USB 发送。 我们选择增加模拟手机上的 SRAM,并使用带有 checkra1n 的 KPF 模块的 PongoOS,而不是麻烦的早期 USB。
首先,执行 PongoOS 并非没有问题,任何通常由 bootrom 或 iboot 完成的引导代码都会丢失,例如在执行任何 double/float 指令之前设置 FPU。 浏览 ARM 文档(第 5.4 节) 和一些 Googling 提供了帮助。
A13 及更高版本设备引入的 Pongo 不支持的功能破坏了一些补丁的模式匹配。 例如,指针身份验证 (PAC) 指令添加了 autda
/ xpacd
,并且 Apple 使用了不同的 slide。
以下示例使用了臭名昭著的 task_for_pid (tfp0):
% ipsw macho info kernelcache.release.iphone10b.decompressed
000: LC_SEGMENT_64 sz=0x006d8000 off=0x00000000-0x006d8000 addr=0xfffffff007004000-0xfffffff0076dc000 r-x/r-x __TEXT
Previous version (iphone-X (14.0_18A373_GM))
- tfp0 address 0xfffffff0076e9e70
0xfffffff0076e9e70 - 0ffffffff007004000 = 0x6e5e70
- binary
% hexdump -s 0x6e5e70 -n 8 kernelcache.release.iphone10b.decompressed
06e5e70 7f70 07ec fff0 0017
Raw: 0x0017_fff0_07ec_7f70
Ghidra: 0xffff_fff0_70ec_7f70
Later version (iphone-11 (14.0_18A5351d))
- tfp0 address 0x0xfffffff0076c9e40
0xfffffff0076c9e40 - 0ffffffff007004000 = 0x6c5e40
- binary
% hexdump -s 0x6c5e40 -n 8 kernelcache.research.iphone12b.decompressed
06c5e40 1d70 00f0 307a 8010
Raw: 0x8010_307a_00f0_1d70
Ghidra: 0xffff_fff0_07f0_5d70
Pongo 允许我们访问多个 iOS 版本的现有 checkra1n 补丁,虽然动态应用程序很有趣,但它不容易阅读、修改或共享。 我们想要一种更声明性的方法,就像实际的代码补丁一样。
所以我们制作了一些工具,允许我们在两个 Mach-O
之间进行差异比较,并生成一个包含汇编差异的文本补丁文件。 另一个程序将获取此补丁文件并简单地应用到二进制文件。
然后我们使用 Pongo 启动,并使用 QEMU 监视器转储 Pongo 修补的内存部分,然后重新组装一个修补过的内核,最后生成一个包含所有修改的补丁文件。 然后将大补丁拆分并正确注释,从而允许我们审查和控制内核中修补的内容。
里面一片漆黑
我们知道在现代 iPhone 上,每个图形渲染最终都会通过他们的 Metal
API 进行,然后需要一个实际的 GPU。 我们认为模拟 Apple Silicon GPU 会过于复杂,因此我们想到了两种解决方案:
- 使用软件渲染:看起来在旧版本的 iOS 中是可行的(使用 gpu=0 bootarg)
- 将 Metal 调用转发到能够进行渲染的设备,例如真实的 iPhone 或可能运行 OSX 的 Mac
软件渲染似乎容易得多,所以我们首先研究了它。 不幸的是,XNU
内核 bootarg 选项在 iOS 14 中已消失。 在使用 Ghidra 查看 QuartzCore
框架后,似乎只有在没有 Metal
渲染器可用时,才会调用软件渲染作为后备方案。
为了确认软件渲染确实可用,我们在真实的越狱 iPhone 上工作,并修补了 Quartzcore
以使用软件渲染。 而且我们确实确认这是可行的! 通过这些修改,UI 变得慢得多,并且在可能直接需要 Metal
进行渲染的部分上存在伪影。
经过这些实验,我们知道我们可以在 QEMU 上获得软件渲染,适用于任何不直接使用 Metal
或 OpenGL
的东西(所以基本上是所有 UIKit
应用程序)。
我们还探索了代理 metal 调用的替代方案,与 2 个物理 iPhone 配合使用。 我们所做的是:
- 使用 LLVM 解析所有 iOS 标头
- 服务器上的所有 objective C 对象指针都是客户端上的 stub 指针
- 生成自动代码以交换结构和指针
- Hook 所有函数和方法
- 将每个调用转发到服务器,执行它们并返回结果
我们让一些基本调用来回进行 Metal 初始化,但我们意识到要让某些东西真正发挥作用还有很长的路要走。Objetive C 语言和 Metal
API 非常复杂,并且具有许多功能,使得这项工作非常复杂。
我们最终推迟了此解决方案,并认为从软件渲染开始,即使它更具限制性,也可以帮助我们更快地解决其他问题。
此外,我们发现 iOS 框架实际上公开了公共标头中不存在的私有 API。 尽管有一些方法可以解析这些并生成标头,但它们在大多数时候都无法直接使用并且使事情更加复杂。
寻找 IOSurface
在尝试使软件渲染工作后,我们决定仍然至少需要一个 framebuffer 设备,而原始的 t8030 QEMU 没有实现它。 然而,我们找到了 项目的一个分支,它显然正在开发 IOMFB 支持,并决定尝试使用它来调试显示。
而且,在使用该版本恢复 iOS 时,我们确实可以看到 Apple 徽标和进度条。 但是在正常启动时,显示仍然完全是黑色的,所以是时候进行调试了!
在 Ghidra 中查看 IOMFB kext 和 QEMU 中的 framebuffer 实现,似乎有两种模式是可能的:
- 可以使用固定硬件地址的原始 framebuffer(我们猜测是用于早期显示)
- 一种更复杂的 API,使用寄存器来配置多个平面,并使用 dma 写入表面数据
我们首先开始尝试使用原始 framebuffer(我们后来发现 Pongo
就是这样显示内容的)。 使用它,我们可以显示任意 ARGB 表面,但是在启动时,该 framebuffer 永远不会被系统写入。
因此,我们开始研究第二种显示模式。 通过在 QEMU 中的 framebuffer 实现中启用跟踪,我们可以看到内核将使用寄存器设置图形平面,然后什么也没有发生。
此时,我们需要调试为什么在启动后没有任何显示,即使 framebuffer 似乎已实现并被内核检测到。
地址随机化
即使我们有 SSH 访问权限,我们也可以在运行系统上观察到的内容也很快受到了限制。 我们需要能够使用 GDB 调试内核和用户空间组件。
对于内核随机化,它实际上是在 t8030 板初始化中设置的,并且允许完全关闭它,所以这很容易。
对于 userland,我们有两种情况,可执行文件的随机化和 dyld 缓存中的动态库的随机化。 对于可执行文件,只需修补 _load_machfile 内核函数就足以禁用它。
对于动态库,我们最终以不同的方式处理它。 首先要知道的是,每个库都包含在一个名为 dyld 缓存的大型二进制 blob 中(位于 /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64e
)。
来自此缓存的所有库(称为框架)在启动时加载一次,然后映射到进程内存空间中,即使 dlopen 是使用文件系统上的路径调用的(例如 /System/Library/Frameworks/QuartzCore
)。
我们注意到地址随机化实际上只在启动时发生一次,然后每个可执行文件始终在相同的地址加载库。
所以我们所要做的就是编写一些 C 工具,它将 dlopen 每个框架库,然后使用 _dyld*
函数列出加载的图像并获取它们的偏移量。
使用此解决方案(以及在使用来自 GDB 的地址进行调试时的反向过程),我们可以轻松地调试 dyld 缓存中的任何库。 我们对 IOMFB
kext、backboardd
和 SpringBoard
守护进程以及 QuartzCore
框架特别感兴趣。
debugserver localhost:1111 –attach backboardd
iproxy 1111:1111
gdb-multiarch -x "set architecture arch" -x "target remote localhost:1111"
Note2: 我们后来发现如何通过修补内核来禁用 dyld 缓存。 这样做允许直接在主机上的 dyld 缓存中查找虚拟地址(我们使用来自 Gimli 项目的伟大 object Rust 库来完成)。
Note1: 要调试用户空间,你需要在访客中有一个 gdb 服务器(例如,请参阅来自 Procursus 的 debugserver 包)。 我们开始在主机上使用 gdb,但它在某种程度上受到限制或存在错误,而 lldb 似乎更好。
请与我交谈
有了可以工作的 GDB,我们可以看到 backboardd
似乎正在正常启动,但我们意识到系统日志对于了解正在发生或未发生的事情会有很大的帮助。
在真实的 iPhone 上,你可以使用工具 idevicesyslog
获取系统日志(在通过 USB 与你的计算机配对后)。 此配对过程涉及生成一个密钥对,其中私钥存储在手机上。 lockdownd
使用它来验证计算机的身份(在用户在 UI 中授权一次后)。
在我们的例子中,虽然我们可以通过 USB 与手机进行交互,但 lockdownd
无法正常工作。 经过一些 Ghidra 会话后,我们意识到 lockdownd
试图使用 keybag
来存储私钥,这将需要我们缺少的 SEP。
为了更进一步,我们创建了一个 shellcode,注入到一些大概无用的现有函数的位置。 该代码从文件系统中读取预先生成的私钥/公钥对,