啰啰嗦嗦的简介
关于这个“项目”只是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下载:
xnu: https://github.com/apple-oss-distributions/xnu/archive/xnu-4903.270.47.tar.gz
AvailabilityVersions: https://github.com/apple-oss-distributions/AvailabilityVersions/archive/AvailabilityVersions-33.200.7.tar.gz
dtrace: https://github.com/apple-oss-distributions/dtrace/archive/dtrace-284.250.4.tar.gz
libdispatch: https://github.com/apple-oss-distributions/libdispatch/archive/libdispatch-1008.270.1.tar.gz
编译
该版本的内核编译命令主要参考自这篇文章,就是使用的版本不一样。因为会修改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可以看到内核成功地跑了起来,并触发了一个内核恐慌:

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 内核补丁列表]从ipsw提取内核,并解压,准备编译好的内核,两个内核的版本最好一致。
新建一个文件夹来存放kext,如kexts,然后将kext放进文件夹中
在文件夹中创建一个
kexts.txt,填写所需要的kext的名称(如BCM2837),或者是提取内核中的kext包名(如com.apple.iokit.IOStorageFamily),一行一个,需要注意kext的依赖也要填上。需要编写一个symbols.txt,将kext所需要的符号写进去,格式为
地址,符号名。然后根据命令行参数将上述文件填入,即可生成新的内核。
该工具还有很多需要改进的地方,在边写边改。
平台驱动
前面提到触发内核恐慌的原因是因为缺少平台驱动,最后我参考这个代码以及分析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-*属性,经过反编译内核可以发现验证的一处逻辑:

我使用补丁功能将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进程:

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

接下来
这篇文章就先写到这里吧,目前卡在了fsck读取文件这里,后面需要对文件系统进行一些修改来让其继续运行。我还需要研究一下SD卡驱动如何优化来让读取速度快一点点(比如DMA),以及研究一下显示方面的东西。后续可能考虑在实体开发板上跑一下试试。
感谢以下项目及文章提供的灵感
zhuowei - Almost booting an iOS kernel in QEMU
darwin-on-arm - GenericBooter
Aleph Research - Running iOS in QEMU to an interactive bash shell (1): tutorial
TrungNguyen1909 - qemu-t8030
cocoahuke - ioskextdump_ios10
apple - xnu