大约一年前,我开始使用QEMU 模拟软件模拟 iPod Touch 1G 。经过数月的逆向工程、弄清各种硬件组件的规格以及使用 GDB 进行无数次调试运行,我现在拥有一个功能齐全的 iPod Touch 模拟,其中包括显示渲染和多点触控支持。模拟设备运行 Apple 为 iPod Touch 发布的第一个固件:iPhoneOS 1.0,版本 3A101a。模拟器运行 iBoot(引导加载程序)、XNU 内核,然后执行 Springboard。Springboard 呈现主屏幕并负责启动其他应用程序,如 Safari 和日历。我没有对引导加载程序、内核或正在加载的其他二进制文件进行任何修改。所有源代码都可以在我的 QEMU 分支中找到。注意:模拟器需要自定义 NOR 和 NAND 映像(本文后面将详细介绍)。我计划很快发布另一篇博客文章,其中包含有关如何生成这些自定义映像的详细说明。

下面的视频展示了模拟器在启动设备以及浏览各种应用程序时的运行情况:

为了实现上述目标,我借鉴了其他人在 iOS / Apple 设备模拟方面的一些工作:

该项目最复杂的部分是模拟 iPod Touch 中包含的许多硬件组件。我必须让这些组件中的大多数运行起来,而这些组件的规格都是专有的且未记录在案,因此有时很难正确模拟它们。不过,我确实认为这是第一款模拟的 Apple 产品,它不仅是开源的,而且还具有完整的显示支持和多点触控功能(尽管CorrelliumCorrellium 也提供虚拟化的 iPhone,但它是商业的和闭源的。在这篇博文中,我将概述我遇到的一些挑战,描述启动过程中采取的步骤,并列出一些可以使模拟变得更好的未来任务。我确实很喜欢使用这个模拟器,并且学到了很多关于移动设备内部结构的新知识。

我特意决定专注于模拟运行有史以来第一个 iOS 版本的 iPod Touch 1G。我这样做有两个原因:首先,旧设备的硬件组件比新设备少,因此更容易构建有用的设备模拟器。当代 Apple 设备包含许多额外的硬件组件,例如神经引擎、安全区域和各种传感器,这些组件将使此类设备的模拟变得更加困难和耗时。第二个原因是旧版 iPhoneOS/iOS 几乎没有实施任何安全措施,例如信任缓存。通过专注于最原始的 iPhoneOS 版本,我不需要规避任何安全机制。

当前项目状态

执行 iBoot、XNU 内核、Springboard 和预装的 iPhoneOS 应用程序所需的所有硬件组件均已正常运行。这些硬件组件包括:

  • AES 加密引擎

  • SHA1 哈希引擎

  • 芯片识别模块

  • 硬件时钟和计时器

  • GPIO 控制器

  • LCD 显示屏和帧缓冲器

  • NAND 控制器和纠错码 (ECC) 模块

  • 闪存控制器 (FMC),用于与 NAND 存储器通信

  • 多点触控设备

  • 电源管理单元和集成实时时钟

  • SDIO 控制器

  • SPI 控制器

  • I2C 控制器

  • 矢量中断控制器 (VIC) 和 GPIO 中断控制器

  • 直接内存访问 (DMA) 控制器

  • UART 控制器

以下硬件组件尚未发挥作用,但对于完全启动 iPod Touch 也不是必需的:

  • USB OTG/Synopsys 设备

  • 音频设备

  • 802.11 WiFi 控制器

  • PowerVR MBX图形处理器

  • 视频编码器/解码器引擎

  • 加速器和光传感器

iPod Touch 的启动过程

下图显示了启动 iPod Touch 到用户应用程序的所有五个步骤:

Bootrom 和低级引导加载程序

iPod Touch 1G 使用 ArmV6(Little Endian)指令集。该项目的验证第一步涉及设置带有 CPU 的 QEMU 机器,以便我们可以执行一些代码。幸运的是,QEMU 支持 CPUARM1176和所需的指令集。初始化 QEMU 机器并初始化一些内存后,我们就可以将二进制文件加载到内存中并执行一些代码了!

启动 iPod Touch 时执行的第一段代码是bootrom代码,据推测是三星在推出 iPod Touch 1G 时设计的。bootrom 是只读的,无法通过软件修改。因此,bootrom 中的漏洞备受追捧,因为此类漏洞无法通过软件修复(Checkm8是此类漏洞的最后一个)。可以从此网站下载 bootrom 代码的转储。我最初尝试在我的 QEMU 机器中加载和执行 bootrom 代码。但是,我很快发现 bootrom 跳转到一些代码,这些代码可能也融合在设备中,而我使用的 bootrom 转储中缺少这些代码(缺少的代码似乎位于偏移量0x22000000在内存中)。由于我在这个项目开始时没有实体的 iPod Touch 1G,所以我无法获取这个缺失的代码。低级引导加载程序(LLB,上图中的步骤 2)也会跳转到这个神秘的代码,所以我将重点转移到执行 iBoot 上(上图中的步骤 3)。

使用 iBoot 引导加载程序的乐趣

iBoot 引导加载程序的主要功能是初始化设备外围设备并加载和执行内核映像。iBoot 还可以进入恢复模式,从而可以使用 iTunes 重新安装 iPhoneOS。幸运的是,openiBoot 项目已经做了很多工作来重新实现 iBoot 提供的大部分功能。此源代码对我理解 iBoot 中的主要逻辑和程序很有帮助。由于 iBoot 初始化并与各种硬件组件通信,因此我还必须专注于启动和运行这些组件,以便 iBoot 运行。

我研究的第一个硬件组件是矢量中断控制器 (VIC)。该组件注册来自其他硬件组件的中断请求,并在发生中断时通知 CPU。iPod Touch 1G 似乎配备了 PL192,该 PL192 有据可查。在 VIC 启动并运行后,我开始将内核生成的打印语句重定向到 QEMU 控制台,这有助于调试过程。下面您可以看到 iBoot 的控制台输出,直到 iBoot 加载和解密 XNU 内核为止:

iis_init()spi_init()power supply type battbattery voltage Reading PMU register 87errorSysCfg: version 0x00010001 with 4 entries using 200 of 8192 bytesBDEV: protecting 0x2000-0x8000image 0x1802bd20: bdev 0x1802b6a8 type dtre offset 0x10800 len 0x7d28image 0x1802c170: bdev 0x1802b6a8 type batC offset 0x18d40 len 0x101e1image 0x1802c5c0: bdev 0x1802b6a8 type logo offset 0x29a80 len 0x1c3aimage 0x1802ca10: bdev 0x1802b6a8 type nsrv offset 0x2bfc0 len 0x4695image 0x1802ce60: bdev 0x1802b6a8 type batl offset 0x30d00 len 0xc829image 0x1802d2b0: bdev 0x1802b6a8 type batL offset 0x3e240 len 0xe9d2image 0x1802e888: bdev 0x1802b6a8 type recm offset 0x4d780 len 0xb594display_init: displayEnabled: 0otf clock divisor 5fps set to: 59.977SFN: 0x600, Addr: 0xfe00000, Size: 0x14001e0, hspan: 0x500, QLEN: 0x140merlot_init() -- Universal code version 08-29-07Merlot Panel ID (0x71c200):
   Build:          PVT1 
   Type:           TMD 
   Project/Driver: M68/NSC-Merlot ClcdInstallGammaTable: No Gamma table found for display_id: 0x0071c200power supply type battbattery voltage errorpower supply type battbattery voltage errorusb_menu_init()vrom_late_init: unknown image crc: 0x66a3fbbf=======================================:::: iBoot, Copyright 2007, Apple Inc.::::	BUILD_TAG: iBoot-204::::	BUILD_STYLE: RELEASE::=======================================[FTL:MSG] Apple NAND Driver (AND) 0x43303032[NAND] Device ID           0xa514d3ad[NAND] BANKS_TOTAL         8[NAND] BLOCKS_PER_BANK     4096[NAND] SUBLKS_TOTAL        4096[NAND] USER_SUBLKS_TOTAL   3872[NAND] PAGES_PER_SUBLK     1024[NAND] PAGES_PER_BANK      524288[NAND] SECTORS_PER_PAGE    4[NAND] BYTES_PER_SPARE     64[FTL:MSG] FIL_Init			[OK][FTL:MSG] BUF_Init			[OK][FTL:MSG] VFL_Init			[OK][FTL:MSG] FTL_Init			[OK][FTL:MSG] VFL_Open			[OK][FTL:MSG] FTL_Open			[OK]Boot Failure Count: 0	Panic Fail Count: 0Delaying boot for 0 seconds. Hit enter to break into the command prompt...HFSInitPartition: 0x1802b8f0Reading 8900 header with length 2048 at address 0x0b000000Will decrypt 8900 image at address 0x0b000000 (len: 3319392 bytes)Loading kernel cache at 0xb000000...data starts at 0xb000180

从上面的日志中可以看到,iBoot 首先初始化各种硬件组件;然后从 NOR 闪存读取多个映像,初始化 LCD 屏幕,初始化电源管理单元 (PMU) 以读取电池状态,然后从 NAND 闪存读取内核映像。最后,它将执行权释放给内核。如果启动因任何原因失败,iBoot 将跳转到恢复模式,允许通过 UART 接口执行多个调试命令。

iPod Touch 1G 包含两种持久内存:NOR 和 NAND。NOR 内存是一种相对较小的块设备。主文件系统持久保存在 NAND 内存中,对于 iPod Touch 1G,其大小为 8-32 GB,具体取决于型号。为了使模拟器正常工作,我们需要模拟这些块设备并确保引导加载程序/内核可以正确读取它们。

构建 NOR 图像

在启动过程中,iBoot 引导加载程序会读取存储在 NOR 闪存中的多个文件。这些文件包括设备启动时显示的 Apple 徽标、恢复模式屏幕、低电量屏幕和设备树。NOR 内存还包含 NVRAM 和 SysCfg 分区,用于存储各种设备属性,例如序列号、MAC 地址、内核的启动参数和崩溃日志。我编写了一个自定义工具,用于从 IPSW 文件中包含的文件构建有效的 NOR 内存映像,并在启动 QEMU 时提供了此自定义内存映像。构建此 NOR 映像的源代码可在此 GitHub 存储库中找到。

构建 NAND 映像

iBoot 的职责之一是将 XNU 内核加载到内存中并将执行权交给它。iBoot 可以通过两种方式加载内核映像:它要么从 NAND 内存中的文件系统读取映像,要么加载位于特定内存偏移量的映像。由于我希望模拟尽可能接近实际的启动过程,因此我专注于启动和运行 NAND I/O。乍一看,这听起来很简单,因为 NAND 存储分为不同的页面,并且每个页面都有编号。因此,当 iBoot 或内核请求页面时,我们的模拟器可以简单地返回页面中的相应数据。然而,在底层,NAND 设备要复杂得多,主要是因为 NAND 内存需要用于磨损均衡的算法。这是必要的,因为 NAND 中的每个物理块只能可靠地擦除和写入一定次数,否则性能会下降。NAND 驱动程序还包含其他算法,例如用于纠错码、坏块管理和垃圾收集的算法。因此,NAND 内存中页面的物理布局与这些页面的逻辑组织完全不同。

幸运的是,Openiboot 包含了iPod Touch 1G 中的NAND 驱动程序的实现。这不仅帮助我了解了 NAND 内存的物理布局,还帮助我了解了与 NAND 内存的 I/O 交互。我还查看了泄露的 iBoot 源代码版本,其中包含 NAND 驱动程序的源代码。与 NOR 映像类似,我编写了各种脚本来构建 NAND 映像,该映像可由 NAND 驱动程序读取。源代码可在此 GitHub 存储库中找到。NAND 映像是从 IPSW 固件文件中包含的根文件系统构建的。

解密并加载内核映像

此时,iBoot 正确地从 NAND 存储(位于文件系统中/System/Library/Caches/com.apple.kernelcaches/kernelcache.s5l8900xrb)加载内核映像。但是,此内核映像使用专有的 8900 加密方案加密,并且 iBoot 跳转到内存中的解密过程,而我没有这些指令。为了仍然能够解密映像,我在跳转到的加密函数的开头实现了一个回调,并在 QEMU 逻辑中解密内核映像。然后我将解密的内核映像留在内存中,之后 iBoot 跳转到内核映像的入口方法。

在 iBoot 加载内核之前,我必须启动并运行一些其他硬件组件。这些组件包括电源管理单元 (PMU)、DMA 控制器、硬件定时器和时钟以及 LCD 显示屏。

模拟 XNU 内核

我的大部分逆向工程工作都用于理解 XNU 内核和模拟内核使用的硬件组件。尽管 XNU 内核大部分是开源的,但 Apple 似乎为 iPod Touch 和 iPhone 等 Apple 设备中包含的内核维护了一个私有分支。将 iOS 中附带的内核与开源内核代码进行比较,似乎 Apple 对 iOS 内核进行了各种更改,以确保它可以在 ARM CPU 上运行。此外,开源内核实现中没有针对硬件组件的设备特定驱动程序的源代码。

XNU 内核首先初始化几个BSD 子系统,包括内存管理逻辑、调度程序和线程支持。随后,内核读取 NOR 映像中包含的设备树。设备树是一种数据结构,描述特定设备的所有硬件组件。内核使用设备树为所有这些组件加载适当的驱动程序,并使用正确的设置初始化这些组件。iPod Touch 1G 使用的设备树的转储可在此处找到如您所见,它包含了很多信息!设备树还可以揭示不同组件之间的依赖关系信息。例如,它表明与多点触摸屏的通信通过由 SPI 控制器控制的 SPI 接口进行。

设备树节点中最重要的字段可能是组件的内存地址。大多数硬件组件使用一种称为内存映射 IO 或 MMIO 的技术。使用 MMIO,相同的地址空间用于寻址主内存和 I/O 设备。因此,内核可以简单地从主内存读取和写入以与硬件组件通信。在 QEMU 中实现对内存映射 I/O 的支持相对简单。但是,某些硬件组件不使用 MMIO,必须使用不同的硬件通信协议(例如 SPI、I2C 或 SDIO)进行访问。

在初始化 BSD 子系统后,内核启动IOKit 框架并开始加载设备树中包含的硬件组件的驱动程序。由于内核加载了相当多的驱动程序(大约 30 个),确保所有这些驱动程序都正确启动花了我几个月的时间。启动过程偶尔会卡住,因为它在等待我尚未正确模拟的硬件组件给出特定响应。下面您可以看到一些反编译驱动程序的屏幕截图:

以下是我的 QEMU 仓库中的一些文件

在执行过程中的某个时刻,内核开始从 NAND 中的文件系统读取二进制文件。尽管我已经拥有完整的 NAND 支持以使 iBoot 正常运行,但内核还是通过闪存控制器 (FMC) 从 NAND 存储中读取数据。这原来是我必须模拟的最具挑战性的硬件组件之一。FMC 也是我必须模拟的第一个硬件组件,没有任何文档或源代码可用。我花了数周的时间反复试验,才弄清楚 FMC 执行的不同 I/O 操作并确保读取正确的 NAND 页面。目前,FMC 的 NAND 读取操作应该可以正常工作,但我还没有添加对 NAND 写入操作的支持。

初始化完所有驱动程序后,内核就开始执行launchd应用程序了。launchd是内核启动的第一个程序,顾名思义,它负责启动其他应用程序和启动脚本(它也以 PID 1 运行)。launchd启动后,内核启动即视为完成。从此时起,执行的应用程序launchd在用户空间而不是内核空间中运行。launchd正常运行后,下一步是启动管理 iPod Touch 主屏幕的标准应用程序:Springboard。

启动 Springboard

应用launchd程序在文件系统的目录中查找启动脚本/System/Library/LaunchDaemons并执行这些脚本。这些启动脚本包括音频控制、地址簿和蓝牙支持的守护进程。其中一个启动脚本com.apple.SpringBoard.plist包含启动Springboard.app应用程序的指令。不幸的是,由于我尚未实现显示渲染,Springboard 在启动后不久就卡住了。

让那里有显示

Springboard.App包含渲染主屏幕的逻辑,包括应用程序图标、对话框屏幕和状态栏。iPod Touch(或任何移动设备)上的显示渲染通常由硬件图形处理器加速。通过逆向工程,我已经看到这个硬件组件非常复杂,内核和图形处理器之间的通信协议很复杂。作为替代方案,我开始寻找暂时禁用图形处理器的方法。幸运的是,的启动脚本Springboard.App允许我添加环境变量LK_ENABLE_MBX2D=0成功禁用图形处理器。使用此选项,所有显示渲染都由内核执行,这也比在专用硬件上进行渲染慢得多。尽管没有硬件加速渲染功能,但模拟设备中的动画非常流畅,正如博客文章开头的视频所示。

此时模拟设备成功启动 Springboard 并呈现主屏幕 ???

实现多点触控支持

对我来说,下一步是添加通过触摸屏幕导航用户界面的支持。我的想法是使用与 Xcode 中包含的 iPhone 模拟器相同的方法,将鼠标点击转换为屏幕上的触摸。这似乎是一个相对简单的问题 - 检测用户按下屏幕的位置,将此触摸转换为 (x, y) 坐标对并将其传递给内核 - 实际上是一个非常具有挑战性的问题。这项于 2007 年授予 Apple 的专利描述了准确记录用户触摸和手势所需的一些步骤。总之,多点触控设备会生成由内核中的多点触摸驱动程序读取。每帧都包含一个触摸事件,其中包含以省略号形式表示的触摸详细信息(例如,参见链接专利中的图 3)。

在某一时刻,内核开始初始化HID 设备,其中也包括多点触控设备。多点触控设备的初始化过程大致如下:

  1. 上传校准数据:内核将校准数据上传到多点触控设备并校准设备。此校准数据包含在文件系统中,也嵌入在设备树中。

  2. 上传固件数据:内核将一些 Zephyr2 固件数据上传到多点触控设备。这些固件数据包含在文件系统中,也嵌入在设备树中。

  3. 读取设备信息:内核从多点触控设备获取各种状态报告。这些报告包括有关多点触控设备的多个方面的信息,例如版本信息和触摸表面水平/垂直方向上的触摸点数量。

内核通过 SPI 接口与多点触控设备通信。为了确保多点触控设备生成的帧成功传输到内核,我必须启动并运行 SPI 控制器。多点触控设备生成 GPIO 中断以通知内核帧的可用性,例如,是否有触摸或其他需要处理的事件。为了获取有关包含触摸事件的帧结构的更多信息,我修改了 openiboot 以初始化多点触控设备,对其进行编译,并记录帧中的所有字段,如下面的屏幕截图所示:

通过仔细分析各种触摸和滑动产生的帧,我弄清楚了如何将 QEMU 窗口中的鼠标点击转换为多点触控设备的触摸和帧。与触摸事件相关的每个帧还包括有关滑动速度的信息。例如,在滚动垂直列表或调整水平滑块时会使用此速度。为了确保这些滚动操作正常工作,我还必须在触摸产生的每个帧中提供水平和垂直速度。我通过将上一个鼠标事件的 x/y 坐标与当前鼠标事件的 x/y 坐标进行比较来计算这些速度。

最后,我添加了对主页按钮(按“H”键激活)和电源按钮(按“P”键激活)的支持。这一步非常简单。此时,我有一个功能齐全的 iPod Touch,它可以启动到主屏幕,并且可以通过鼠标点击和键盘进行导航。

我还发现一些应用程序崩溃是因为缺少关键资源文件。这些文件丢失的原因是我从 IPSW 中提供的根文件系统生成 NAND 存储。但是,在恢复或安装 iPhoneOS 时,这个干净的文件系统会填充各种文件。在我的模拟中,我没有执行恢复脚本。我还必须从实际设备复制激活记录以绕过设备激活。

浏览预装的 iPhoneOS 应用程序时的一些其他屏幕截图:

已知问题和后续步骤

虽然我现在有一个可以运行的 iPod Touch 模拟器,但仍然有不少问题:

  • 设备在尝试显示键盘时崩溃。这似乎是因为libicucore.dylib(负责 Unicode 支持的库)未正确加载到内存中,但我还没弄清楚为什么会发生这种情况。

  • 有一些与 USB 驱动程序和闪存控制器相关的偶发崩溃。我怀疑它们是竞争条件引起的,因为 QEMU 中的硬件通信比实际设备上的硬件通信快得多,这可能违反了内核逻辑中的一些基本假设。

  • 不支持高级手势,例如捏合和放大。

  • 亮度控制尚未起作用。

  • NAND存储器不具备持久性。

  • 当设备关闭或进入自动锁定模式时,会出现各种故障。

有时很难调试并找出设备上发生了什么。大多数调试都是通过将 GDB 调试器附加到 QEMU 客户机来完成的。如果有一个交互式 shell 运行会很有帮助。我尝试bash在模拟设备上编译和运行,但我还没有让它运行。

努力实现统一的基础设施以模拟其他几代 iPhone、iPod Touch、Apple TV 甚至 Apple Watch 也是一件好事。但是,所有这些设备的硬件和软件规格都不同,模拟它们可能非常耗时。下一步,我想尝试让 iPod Touch 2G 正常运行。

我希望这篇博文能让您深入了解模拟 iPod Touch 1G 的过程。还有很多细节我没有写到,但我可能会在其他博文中写到。在我的下一篇博文中,我将提供有关编译 QEMU、生成自定义 NOR/NAND 映像以及运行 QEMU 模拟的说明。同时,如果您对此项目有任何想法、建议或问题,请告诉我!