啰啰嗦嗦的简介

关于这个“项目”只是23年的时候突然来的灵光一闪,可能因为之前玩iOS越狱和玩机的缘故,也挺喜欢折腾刷机的,就想想能不能在别的设备上跑iOS(或者arm64 macOS),类似黑苹果那样,结果找到了这个仓库,就想着能不能魔改下玩玩。最后做下来发现挺有趣的,但是离跑完整的系统还差的很远很远,而且网上找不到几个能参考的,写这个文章是想记录一下过程(上次折腾这个“项目”隔了有半年了)。第一次写博客,可能内容比较琐碎(会不会成黑历史)。

我目前的进度也只仅仅是能在设备上跑起一个基本的Bash,真的很慢很慢。最初选择了XNU内核开源代码中就有的Raspberry Pi 3B作为运行设备,但是没有实体开发板,使用QEMU进行模拟的。后来尝试了小米 Note 3测试运行内核,能跑,不过因为没有写EMMC的驱动所以没继续整了。

目前先用Raspberry Pi 3B来跑,现阶段能用的功能很少,只有一个SD卡能驱动,好多东西都是东拼西凑写起来的,而且整个内核跑起来有个很严重的BUG,在跑进用户态之后,不知道是什么部分导致了一些Kext的代码区域被填充成了0,可能是Kext补丁并不完善,将错误的地址当成了数据区。

编译内核

首先编译内核部分(其实有想过直接从iOS固件内的内核用进行补丁的方式来增减,但是没去试验,感觉很麻烦)。之前在做实验的时候一开始是选的当时的开源的最新版xnu内核,版本为xnu-8792.81.2(对应iOS 16),但是发现太新了有些部分有点麻烦,然后又转头跑去整了xnu-4903.270.47(对应iOS 12)。

总的来说编译部分不是特别麻烦,就是官方开源的源码并不能完全开箱即用,需要做一些修改才能编译。修改后的xnu源码我会上传至我的Github仓库。

环境

这里我搭建的是虚拟机环境来进行编译,使用的是macOS 10.15.7,Xcode 11.7(由于Kext的因素最后用了macOS 12,Xcode 14,内核好像也可以编译)。

源码下载

编译内核所需的源代码都在苹果的开源仓库中可以找到,可以在这个网站通过对应的macOS系统版本来找到对应版本的源码,tar.gz包可以通过wget或者curl下载:

编译

该版本的内核编译命令主要参考自这篇文章,就是使用的版本不一样。因为会修改Xcode的SDK文件,在做下面这些操作之前,建议把iPhoneOS SDK备份一份,以免编译其他项目的时候出现问题。

CTF工具

tar -xzvf dtrace-284.250.4.tar.gz
cd dtrace-dtrace-284.250.4
mkdir obj sym dst
xcodebuild install -sdk macosx \
    -target ctfconvert -target ctfdump -target ctfmerge \            
    ARCHS=x86_64 SRCROOT=$PWD OBJROOT=$PWD/obj \
    SYMROOT=$PWD/sym DSTROOT=$PWD/dst \
    HEADER_SEARCH_PATHS="$PWD/compat/opensolaris/** $PWD/lib/**"
sudo ditto \
    $PWD/dst/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain \
    /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain`
cd ..

AvailabilityVersions

tar -xzvf AvailabilityVersions-33.200.7.tar.gz
cd AvailabilityVersions-AvailabilityVersions-33.200.7
mkdir dst
make install SRCROOT=$PWD DSTROOT=$PWD/dst
sudo ditto \
    $PWD/dst/usr/local/libexec \
    $(xcrun -sdk iphoneos -show-sdk-path)/usr/local/libexec
cd ..

libplatform 头文件

tar -xzvf libdispatch-1008.270.1.tar.gz
cd libplatform-libplatform-177.270.1
sudo mkdir -p \
    $(xcrun -sdk iphoneos -show-sdk-path)/usr/local/include/os/internal
sudo ditto $PWD/private/os/internal \
    $(xcrun -sdk iphoneos -show-sdk-path)/usr/local/include/os/internal
cd ..

XNU 头文件

cd xnu-4903.270.47
make LOGCOLORS=y SDKROOT=iphoneos ARCH_CONFIGS=ARM64 MACHINE_CONFIGS=BCM2837 \
     KERNEL_CONFIGS=RELEASE ARCH_STRING_FOR_CURRENT_MACHINE_CONFIG=arm64 \
     installhdrs
sudo ditto $PWD/BUILD/dst $(xcrun -sdk iphoneos -show-sdk-path)
sudo cp $(xcrun -sdk iphoneos -show-sdk-path)/usr/include/TargetConditionals.h \
    $(xcrun -sdk iphoneos -show-sdk-path)/System/Library/Frameworks/Kernel.framework/Versions/A/Headers/TargetConditionals.h
cd ..

编译libfirehose_kernel

cd libplatform-libplatform-177.270.1
awk '/include "<DEVELOPER/ {next;} /SDKROOT =/ {print "SDKROOT = macosx"; next;} {print $0}' \
    xcodeconfig/libdispatch.xcconfig > .__tmp__ && \
    mv -f .__tmp__ xcodeconfig/libdispatch.xcconfig
awk '/#include / { next; } { print $0 }' \
    xcodeconfig/libfirehose_kernel.xcconfig > .__tmp__ && \
    mv -f .__tmp__ xcodeconfig/libfirehose_kernel.xcconfig
xcodebuild install -sdk iphoneos -target libfirehose_kernel \
    SRCROOT=$PWD OBJROOT=$PWD/obj SYMROOT=$PWD/sym DSTROOT=$PWD/dst \
    ENABLE_BITCODE=no
sudo ditto $PWD/dst/usr/local \
    $(xcrun -sdk iphoneos -show-sdk-path)/usr/local
cd ..

编译内核

这里源码不能直接编译,要进行一些修改才能完全编译过。

cd xnu-4903.270.47
make SDKROOT=iphoneos ARCH_STRING_FOR_CURRENT_MACHINE_CONFIG=arm64 ARCH_CONFIGS=ARM64 \
    MACHINE_CONFIGS=BCM2837 KERNEL_CONFIGS=DEVELOPMENT BUILD_WERROR=0 -j16 LOGCOLORS=y

好像这个编译指令并不会生成System.kext,需要添加install_config才会生成。

设备树

设备树是基于这个编写的,我照着编写了一份Raspberry Pi 3B的设备树,也放在仓库里了,直接使用make就可以生成了。

引导程序

XNU内核使用的是一种叫Mach-O类型的可执行文件格式,所以需要一个引导程序将内核文件加载到内存中,Mach-O的结构也比较简单,这里就不多介绍了,主要就是把内核的每一个段复制到对应的虚拟地址即可。

中通尝试过几个引导方法,基本上大同小异,只是实现方式不一样。有用过GenericBooter、裸程序、u-boot,现阶段还是用的u-boot来作为引导。我参考GenericBooter来编写了u-boot的引导xnu命令,代码已上传至仓库中,使用示例我在README里面写了。

初次启动

准备好了以上东西,我们可以先简单的跑一下内核看看。由于我手头上并没有实体的树莓派开发板,所以使用了QEMU来进行模拟。

引导分区

由于使用的u-boot来进行引导内核,我们需要为其创建一个引导分区,首先需要创建一个磁盘镜像文件:

qemu-img create -f raw disk.img 8G 

然后将disk.img映射到loop0上:

sudo losetup /dev/loop0 -P disk.img

使用分区工具(如fdisk、cfdisk等)为其分配一个100M的引导分区,并格式化为FAT32文件系统:

sudo mkfs.vfat -F 32 /dev/loop0p1 

然后挂载分区:

sudo mount /dev/loop0p1 bootmnt 

最后将内核文件和设备树拷贝进去即可:

sudo cp mach.development.bcm2837 bootmnt/kernel
sudo cp DeviceTrees/Raspi3B.devicetree bootmnt/dt

启动

使用以下命令启动qemu:

qemu-system-aarch64 -M raspi3b -kernel u-boot/u-boot.bin -serial null -serial stdio -sd disk.img

然后可以看到终端开始输出串口日志,证明已经进入u-boot,随后输入以下命令将内核和设备树载入内存,并设置引导参数:

load mmc 0 0x3000000 kernel
load mmc 0 0x3D00000 dt
setenv bootargs "rd=disk0s2 debug=0x8 -v -pi3 serial=3 -disable_aslr cs_enforcement_disable=1 dyld_flags=0x00000008 -enable_kprintf_spam kextlog=0xffff fips_mode=0xC nointr_consio=1"

最后输入以下命令即可启动内核:

loadxnu 0x3000000 0x3D00000 0x1000000

可以看到内核成功地跑了起来,并触发了一个内核恐慌:

屏幕截图 2025-03-08 112701.png

Kext

引起恐慌的原因是因为缺少平台的相关驱动,需要编写一个驱动来让内核继续执行。xnu内核使用kext来实现驱动的加载,查了一下网上相关的项目,很少有去搞驱动部分的。与其相同内核的macOS有一些开源驱动,但也不是很多,所以难度还挺大的。

编译

我先简单创建了个kext试了试,编译的话试了下Xcode 11不知道为什么不能直接编译,提示“error: unable to resolve product type 'com.apple.product-type.kernel-extension' for platform 'iphoneos'”,换了个环境,Xcode 14可以,打算后面再解决这个问题。

编译命令如下:

xcodebuild -arch arm64 -sdk iphoneos CODE_SIGNING_ALLOWED=NO

编译好的kext默认在build\Release-iphoneos目录下,为一个名叫*.kext的文件夹,里面包含了一个可执行文件和一个Info.plist。每一个kext都是单独进去编译的,这个目前还没有弄什么比较自动化的东西,我可能考虑后续弄一下这个。

加载

kext是编译出来了,可是怎么把它加载到内核里呢?这里我真的折腾了好久。

查看了一下xnu源码,内核有两种加载kext的方式,一种是预链接在内核中,另一种是使用引导程序将kext加载值内存中并提供地址信息给设备树。这两种方式通过判断__PRELINK_INFO.__info段的大小来选择,实现代码位于readStartupExtensions函数中:

	/* If the prelink info segment has a nonzero size, we are prelinked
	 * and won't have any individual kexts or mkexts to read.
	 * Otherwise, we need to read kexts or the mkext from what the booter
	 * has handed us.
	 */
	prelinkInfoSect = getsectbynamefromheader(mh, kPrelinkInfoSegment, kPrelinkInfoSection);
	if (prelinkInfoSect->size) {
		readPrelinkedExtensions(mh, KCKindPrimary);
	} else {
		readBooterExtensions();
	}

一开始我对这个kext加载玩意是完全没头绪的,就最先尝试过使用非预链接的形式进行加载,但是效果不是很好,故不详细说明了。

预链接

iOS使用预链接的方式加载kext,并将其打包和内核一起成内核缓存的形式,我选择对应内核版本的iOS 12.4作为参考。

使用jtool2工具可以查看相关的段结构:

LC 07: LC_SEGMENT_64         	 Mem: 0xfffffff005ca0000-0xfffffff006128000	__PRELINK_TEXT
	Mem: 0xfffffff005ca0000-0xfffffff006128000		__PRELINK_TEXT.__text	
LC 08: LC_SEGMENT_64         	 Mem: 0xfffffff0077e4000-0xfffffff0079e8000	__PRELINK_INFO
	Mem: 0xfffffff0077e4000-0xfffffff0079e8000		__PRELINK_INFO.__info	
LC 09: LC_SEGMENT_64         	 Mem: 0xfffffff006128000-0xfffffff006dec000	__PLK_TEXT_EXEC
	Mem: 0xfffffff006128000-0xfffffff006dec000		__PLK_TEXT_EXEC.__text	
LC 10: LC_SEGMENT_64         	 Mem: 0xfffffff0076f4000-0xfffffff0077e4000	__PRELINK_DATA
	Mem: 0xfffffff0076f4000-0xfffffff0077e4000		__PRELINK_DATA.__data	
LC 11: LC_SEGMENT_64         	 Mem: 0xfffffff006dec000-0xfffffff007004000	__PLK_DATA_CONST
	Mem: 0xfffffff006dec000-0xfffffff007004000		__PLK_DATA_CONST.__data	
LC 12: LC_SEGMENT_64         	 Mem: 0xfffffff0077e4000-0xfffffff0077e4000	__PLK_LLVM_COV
	Mem: 0xfffffff0077e4000-0xfffffff0077e4000		__PLK_LLVM_COV.__llvm_covmap	
LC 13: LC_SEGMENT_64         	 Mem: 0xfffffff0077e4000-0xfffffff0077e4000	__PLK_LINKEDIT
	Mem: 0xfffffff0077e4000-0xfffffff0077e4000		__PLK_LINKEDIT.__data	

其中__PRELINK_INFO段是一个由所有kext的Info.plist组成的plist数据,其中还包含了一些诸如kext起始地址、大小等信息。

在iOS 10之前,所有kext都存放在__PRELINK_TEXT当中且连续。而到了iOS 10及之后,kext的每个段都被拆到了__PLK开头的段中,macho头信息存放在__PRELINK_TEXT中。

补丁工具

上面也说了iOS10之后内核缓存结构的变化,这一变化导致代码段和其它段的间隔会被改变,而代码段当中的一些寻址指令就需要修改了。我尝试着编写了这个工具来将我编写的kext拆分并加入到我编译的内核当中,同时将现有固件的kext也加入到内核中:

Kernel has __PRELINK_DATA!
Mach-o has 31100 symbols
Kernel has __PRELINK_DATA!
Mach-o has 4771 symbols
Found prelink info
Prelink info size: 204000
iOS kernelcache has 193 kexts
Will load 45 kexts from list
Loading : com.apple.kpi.bsd
Mach-o has 833 symbols
Loading : com.apple.kpi.libkern
Mach-o has 775 symbols
Loading : com.apple.kpi.mach
Mach-o has 68 symbols
Loading : com.apple.kpi.iokit
Mach-o has 1898 symbols
Loading : com.apple.kpi.private
Mach-o has 816 symbols
Loading : com.apple.kpi.unsupported
Mach-o has 249 symbols
Loading : com.apple.kpi.dsep
Mach-o has 24 symbols
Loading : com.apple.AppleFSCompression.AppleFSCompressionTypeZlib
....dep: com.apple.kpi.dsep, 0x55f039c5f950
....dep: com.apple.kpi.private, 0x55f039c50680
....dep: com.apple.kpi.iokit, 0x55f039c5ce30
....dep: com.apple.kpi.libkern, 0x55f039c4c810
....dep: com.apple.kpi.bsd, 0x55f039c42600
Mach-o has 0 symbols
Loading : com.apple.iokit.IOStorageFamily
....dep: com.apple.kpi.mach, 0x55f039c432c0
....dep: com.apple.kpi.private, 0x55f039c50680
....dep: com.apple.kpi.unsupported, 0x55f039c86380
....dep: com.apple.kpi.iokit, 0x55f039c5ce30
....dep: com.apple.kpi.libkern, 0x55f039c4c810
....dep: com.apple.kpi.bsd, 0x55f039c42600
Mach-o has 0 symbols
Loading : com.apple.iokit.IONetworkingFamily
....dep: com.apple.kpi.mach, 0x55f039c432c0
....dep: com.apple.kpi.private, 0x55f039c50680
....dep: com.apple.kpi.unsupported, 0x55f039c86380
....dep: com.apple.kpi.iokit, 0x55f039c5ce30
....dep: com.apple.kpi.libkern, 0x55f039c4c810
....dep: com.apple.kpi.bsd, 0x55f039c42600
Mach-o has 0 symbols
...................................................

使用教程

工具命令行参数如下:

kernel_patcher <目标内核> <iOS 内核> <符号列表> <kext路径> <输出文件名> [iOS 内核补丁列表]

  1. 从ipsw提取内核,并解压,准备编译好的内核,两个内核的版本最好一致。

  2. 新建一个文件夹来存放kext,如kexts,然后将kext放进文件夹中

  3. 在文件夹中创建一个kexts.txt,填写所需要的kext的名称(如BCM2837),或者是提取内核中的kext包名(如com.apple.iokit.IOStorageFamily),一行一个,需要注意kext的依赖也要填上。

  4. 需要编写一个symbols.txt,将kext所需要的符号写进去,格式为地址,符号名

  5. 然后根据命令行参数将上述文件填入,即可生成新的内核。

该工具还有很多需要改进的地方,在边写边改。

平台驱动

前面提到触发内核恐慌的原因是因为缺少平台驱动,最后我参考这个代码以及分析iOS固件内核的驱动(参考AppleT7000)编写了一个驱动。

将编译好的驱动使用工具生成补丁好内核,随后启动内核,可以发现内核停止在了AppleARMCPU的一个验证上:

*** bool AppleARMCPU::_validateConfiguration():
***    Missing 'interrupts' property for CPU 0
*** bool AppleARMCPU::_validateConfiguration():
***    Missing 'function-ipi_dispatch' property for CPU 0
panic(cpu 0 caller 0xfffffff006d9f4a4): "CPU configuration in device tree is invalid"@/BuildRoot/Library/Caches/com.apple.xbs/Sources/AppleARMPlatform/AppleARMPlatform-700.260.3/AppleARMCPU.cpp:103

我想先不管function-*属性,经过反编译内核可以发现验证的一处逻辑:

屏幕截图 2025-03-14 103633.png

我使用补丁功能将BL指令替换成了MOV X0, #1:

*0xFFFFFFF0061BC490:
+D2800020

另外,由于我没有提供function-*属性,所以还需要这些补丁:

#AppleARMCPU::startCPU
*0xFFFFFFF0061BD204:
+1400000B
#AppleARMCPU::start patch
*0xFFFFFFF0061BC584:
+1400004D
#AppleARMCPU::signalCPU
*0xFFFFFFF0061BD278:
+14000014

重新生成内核并启动,可以看到如下输出,内核开始等待根设备:

Waiting on <dict ID="0"><key>IOProviderClass</key><string ID="1">IOService</string><key>BSD Name</key><string ID="2">disk0s2</string></dict>

根文件系统

因为Linux上对APFS的支持并不是很完善,不好进行写入,所以我使用HFS+分区作为文件系统。

使用分区工具在disk.img的空余空间上创建新分区,设置为Apple HFS/HFS+类型,随后将其格式化:

sudo mkfs.hfsplus -v System /dev/loop0p2

挂载新分区:

sudo mount -t hfsplus /dev/loop0p2 rootmnt

系统文件我打算直接用固件里面现有的,我使用的是iPad Mini 4(iOS 12.4),下载固件后将IPSW文件解压:

unzip iPad_64bit_TouchID_12.4_16G77_Restore.ipsw

安装dmg2img以及linux-apfs-rw,找到解压文件中最大的那个dmg,使用dmg2img查看dmg分区:

dmg2img -l 048-78033-092.dmg

dmg2img v1.6.7 (c) vu1tur (to@vu1tur.eu.org)

048-78033-092.dmg --> (partition list)

partition 0: Protective Master Boot Record (MBR : 0)
partition 1: GPT Header (Primary GPT Header : 1)
partition 2: GPT Partition Data (Primary GPT Table : 2)
partition 3:  (Apple_Free : 3)
partition 4: EFI System Partition (C12A7328-F81F-11D2-BA4B-00A0C93EC93B : 4)
partition 5: disk image (Apple_APFS : 5)
partition 6:  (Apple_Free : 6)
partition 7: GPT Partition Data (Backup GPT Table : 7)
partition 8: GPT Header (Backup GPT Header : 8)

将系统分区导出为img:

dmg2img -p 5 048-78033-092.dmg out.img

然后使用losetup映射img:

sudo losetup /dev/loop1 -P out.img

挂载系统分区:

mkdir rootfs
sudo mount /dev/loop1 rootfs

最后将根分区内的所有文件同步到模拟的SD卡上:

sudo rsync -a Unpack/Firmware/rootfs/* rootmnt/

修改etc/fstab, 将根挂载改为/dev/disk0s2,并注释掉var挂载:

/dev/disk0s2 / hfs ro 0 1
# /dev/disk0s3 /private/var hfs rw,nosuid,nodev 0 2

暂时删掉所有的LaunchDaemons:

sudo rm rootmnt/System/Library/LaunchDaemons/*

SD卡

现在有了一个基本的根文件系统,但是内核并没有相对应的SD卡驱动。SD卡读写原理还算比较简单,于是我参考Linux内核编写了一个简单的驱动,但是目前这个驱动还不完善,而且代码东拼西凑地太糟糕了,我打算重新写一份||。

将驱动加载至内核中并启动,可以看到内核成功读取分区并加载launchd进程:

屏幕截图 2025-03-17 144220.png

因为读取非常慢,所以卡在了fsck这里:

屏幕截图 2025-03-17 144340.png

接下来

这篇文章就先写到这里吧,目前卡在了fsck读取文件这里,后面需要对文件系统进行一些修改来让其继续运行。我还需要研究一下SD卡驱动如何优化来让读取速度快一点点(比如DMA),以及研究一下显示方面的东西。后续可能考虑在实体开发板上跑一下试试。

感谢以下项目及文章提供的灵感