Boot Newer iOS with QEMU Step by Step

I decided to update my QEMU fork to support newer iOS versions for security research and some CTFs. Just note down how I did it here.

First attempt

Build D22-QEMU and load the iOS 14 kernel and we got

1
2
3
4
5
6
7
8
9
10
11
12
13
14
./qemu-system-aarch64 -M d22-idevice,kernelcache='ios14/kcache.patched',devicetree='ios14/dtre.bin',ramdisk='ios14/rdsk.dmg',trustcache='ios14/trustcache',bootargs='debug=0x14e kextlog=0xffff cpus=1 rd=md0 serial=2',ram-size='1073741824' \
-d unimp,int -cpu max -serial mon:stdio -D ./qemu.log
iBoot version: D22-QEMU loader
Darwin Image4 Validator Version 3.1.0: Fri Oct 30 00:14:28 PDT 2020; root:AppleImage4-106.40.12~2113/AppleImage4/RELEASE_ARM64
panic(cpu 0 caller 0xfffffff0080af660): Kernel data abort. at pc 0xfffffff008024f64, lr 0xfffffff008024f54 (saved state: 0xffffffe80260b5f0)
x0: 0xffffffe4cddbcbf0 x1: 0xfffffff00932cbe8 x2: 0x0000000000000000 x3: 0xffffffe80260b960
x4: 0x0000000000000007 x5: 0x0000000000000073 x6: 0x829f5c9941fe80a8 x7: 0x0000000000000630
x8: 0x0000000000000000 x9: 0xfffffff00774ada8 x10: 0xfffffff00774adb8 x11: 0x0000000000000001
x12: 0x00000000004a0000 x13: 0x00000000ffdfffff x14: 0x0000000000000001 x15: 0x0003ffffff933789
x16: 0x0000000000004000 x17: 0xfffffff0073ac824 x18: 0xfffffff0080a1000 x19: 0xffffffe4cde8ba00
x20: 0xffffffe4cdf70680 x21: 0x0000000000000000 x22: 0xffffffe80260bab0 x23: 0x0000000000000009
x24: 0xfffffff0081bdee8 x25: 0xfffffff009300de8 x26: 0x0000000000000009 x27: 0x0000000000000009
x28: 0xfffffff009300de8 fp: 0xffffffe80260b9c0 lr: 0xfffffff008024f54 sp: 0xffffffe80260b940
pc: 0xfffffff008024f64 cpsr: 0x60400204 esr: 0x96000005 far: 0x0000000000000000

Well although it paniked, it’s a good start at least the serial port is still working. Then let’s check the pc address

1
2
3
4
5
6
7
8
9
10
11
12
13
__text:FFFFFFF008024F64 loc_FFFFFFF008024F64                    ; CODE XREF: sub_FFFFFFF008024D88+26C↓j
__TEXT_EXEC:__text:FFFFFFF008024F64 ; sub_FFFFFFF008024D88+278↓j ...
__TEXT_EXEC:__text:FFFFFFF008024F64 LDR X8, [X21] ; Load from Memory
__TEXT_EXEC:__text:FFFFFFF008024F68 LDR X8, [X8,#0x138] ; Load from Memory
__TEXT_EXEC:__text:FFFFFFF008024F6C MOV X0, X21 ; Rd = Op2
__TEXT_EXEC:__text:FFFFFFF008024F70 MOV X1, X20 ; Rd = Op2
__TEXT_EXEC:__text:FFFFFFF008024F74 BLR X8 ; Branch and Link Register
__TEXT_EXEC:__text:FFFFFFF008024F78 MOV X20, X0 ; Rd = Op2
__TEXT_EXEC:__text:FFFFFFF008024F7C CBZ X0, loc_FFFFFFF008024F90 ; Compare and Branch on Zero
__TEXT_EXEC:__text:FFFFFFF008024F80 LDR X8, [X20] ; Load from Memory
__TEXT_EXEC:__text:FFFFFFF008024F84 LDR X8, [X8,#0x20] ; Load from Memory
__TEXT_EXEC:__text:FFFFFFF008024F88 MOV X0, X20 ; Rd = Op2
__TEXT_EXEC:__text:FFFFFFF008024F8C BLR X8 ; Branch and Link Register

After some reversing, I found that that it’s nvram variable loading routine. But we didn’t load any nvram variable! In iOS 14, nvram data can be loaded to devicetree by iBoot

1
2
3
4
5
result = sub_FFFFFFF007FCE6F8("/chosen", qword_FFFFFFF009301760, 0LL, 0LL, 0LL);
if ( result )
{
v3 = result;
v4 = (*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)result + 328LL))(result, "nvram-proxy-data");

I am just too lazy to reverse the nvram stuff, but since it’s loaded to devicetree, we can dump it from a real device! Now let’s try to find the memory address of devicetree. Luckily, we have

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct xnu_boot_args {
uint16_t Revision; /* Revision of boot_args structure */
uint16_t Version; /* Version of boot_args structure */
uint64_t virtBase; /* Virtual base of memory */
uint64_t physBase; /* Physical base of memory */
uint64_t memSize; /* Size of memory */
uint64_t topOfKernelData; /* Highest physical address used in kernel data area */
struct XNU_Boot_Video Video; /* Video Information */
uint32_t machineType; /* Machine Type */
void *deviceTreeP; /* Base of flattened device tree */
uint32_t deviceTreeLength; /* Length of flattened tree */
char CommandLine[256]; /* Passed in command line */
uint64_t bootFlags; /* Additional flags specified by the bootloader */
uint64_t memSizeActual; /* Actual size of memory */
} boot_args;

Exactly what we need! And

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void
PE_init_platform(boolean_t vm_initialized, void *args)
{
DTEntry entry;
unsigned int size;
void * const *prop;
boot_args *boot_args_ptr = (boot_args *) args;

if (PE_state.initialized == FALSE) {
page_protection_type = ml_page_protection_type();
PE_state.initialized = TRUE;
PE_state.bootArgs = boot_args_ptr;
PE_state.deviceTreeHead = boot_args_ptr->deviceTreeP;
PE_state.deviceTreeSize = boot_args_ptr->deviceTreeLength;
PE_state.video.v_baseAddr = boot_args_ptr->Video.v_baseAddr;
PE_state.video.v_rowBytes = boot_args_ptr->Video.v_rowBytes;
PE_state.video.v_width = boot_args_ptr->Video.v_width;
PE_state.video.v_height = boot_args_ptr->Video.v_height;
// ...
}
// ...
}

With Def1nit3lyN0tAJa1lbr3akTool we have kernel memory reading and writing primitives. Now we can dump the devicetree from a real iPhone X!

Def1nit3lyN0tAJa1lbr3akTool has libkrw installed, so we can use it with Python ctypes to access kernel memory

1
2
3
4
5
6
from ctypes import *
libkrw = CDLL('/var/jb/usr/lib/libkrw.0.dylib')
kread = libkrw.kread
kread.argtypes = [c_uint64, c_void_p, c_uint64]
data = c_uint64(0)
kread(0x4141414141414141, byref(data), 8)

then dump the devicetree

1
2
3
4
5
6
7
8
9
10
11
iPhone:~ root# hexdump -C -n 128 ./dtdump
00000000 15 00 00 00 11 00 00 00 72 65 67 75 6c 61 74 6f |........regulato|
00000010 72 79 2d 6d 6f 64 65 6c 2d 6e 75 6d 62 65 72 00 |ry-model-number.|
00000020 00 00 00 00 00 00 00 00 20 00 00 00 41 31 39 30 |........ ...A190|
00000030 35 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |5...............|
00000040 00 00 00 00 00 00 00 00 00 00 00 00 23 61 64 64 |............#add|
00000050 72 65 73 73 2d 63 65 6c 6c 73 00 00 00 00 00 00 |ress-cells......|
00000060 00 00 00 00 00 00 00 00 00 00 00 00 04 00 00 00 |................|
00000070 02 00 00 00 41 41 50 4c 2c 70 68 61 6e 64 6c 65 |....AAPL,phandle|
00000080
iPhone:~ root#

Trust me

We need to load trustcache, simply download it from ipsw and load it. Then add the address and size to devicetree and the kernel should be happy enough.

Trapped in the loop

Now kernel stuck at

1
2
3
4
5
6
7
8
9
10
11
AppleSSE::start called
AppleSSE::start returning, result = 1
AppleSEPKeyStore:321:0: starting (BUILT: Oct 30 2020 00:31:23)
AppleSEPKeyStore:545:0: _sep_enabled = 1
AppleCredentialManager: start: called, instance = <ptr>.
ACMRM: _publishIOResource: AppleUSBRestrictedModeTimeout = 259200.
AppleCredentialManager: start: initializing power management, instance = <ptr>.
AppleCredentialManager: start: started, instance = <ptr>.
AppleCredentialManager: start: returning, result = true, instance = <ptr>.
AppleInterruptController::start: Num Shared Timestamps == 16
virtual bool AppleARMLightEmUp::start(IOService *): starting...

Trace it block by block, we found that it’s stuck at

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__text:FFFFFFF007FDB7F4                 SUB             SP, SP, #0x50 ; Rd = Op1 - Op2
__text:FFFFFFF007FDB7F8 STP X24, X23, [SP,#0x40+var_30] ; Store Pair
__text:FFFFFFF007FDB7FC STP X22, X21, [SP,#0x40+var_20] ; Store Pair
__text:FFFFFFF007FDB800 STP X20, X19, [SP,#0x40+var_10] ; Store Pair
__text:FFFFFFF007FDB804 STP X29, X30, [SP,#0x40+var_s0] ; Store Pair
__text:FFFFFFF007FDB808 ADD X29, SP, #0x40 ; Rd = Op1 + Op2
__text:FFFFFFF007FDB80C STR XZR, [SP,#0x40+var_38] ; Store to Memory
__text:FFFFFFF007FDB810 CBZ X0, loc_FFFFFFF007FDB884 ; Compare and Branch on Zero
__text:FFFFFFF007FDB814 MOV X19, X2 ; Rd = Op2
__text:FFFFFFF007FDB818 MOV X21, X1 ; Rd = Op2
__text:FFFFFFF007FDB81C MOV X20, X0 ; Rd = Op2
__text:FFFFFFF007FDB820 STR XZR, [SP,#0x40+var_38] ; Store to Memory
__text:FFFFFFF007FDB824 ADRP X22, #qword_FFFFFFF009352CA0@PAGE ; Address of Page
__text:FFFFFFF007FDB828 LDR X0, [X22,#qword_FFFFFFF009352CA0@PAGEOFF] ; Load from Memory
__text:FFFFFFF007FDB82C BL sub_FFFFFFF007FC6200 ; Branch with Link
__text:FFFFFFF007FDB830 CBZ X19, loc_FFFFFFF007FDB844 ; Compare and Branch on Zero
__text:FFFFFFF007FDB834 ADR X8, sub_FFFFFFF007FDB9C4 ; Load address
__text:FFFFFFF007FDB838 NOP ; No Operation
__text:FFFFFFF007FDB83C ADD X9, SP, #0x40+var_38 ; Rd = Op1 + Op2
__text:FFFFFFF007FDB840 STP X8, X9, [X19,#0x10] ; Store Pair
__text:FFFFFFF007FDB844

the function is called by

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 sub_FFFFFFF008330408()
{
OSDictionary *v0; // x20
__int64 v1; // x19
__int64 v2; // x0

v0 = IOService::resourceMatching("IORTC", 0LL);
v1 = sub_FFFFFFF00834FFC4(v0, 0x6FC23AC00uLL, 0LL);
v2 = (v0->vtable->OSObject.release_1)(v0);
if ( v1 )
v2 = (*(*v1 + 40LL))(v1);
return sub_FFFFFFF007D5A4D4(v2);
}


We don’t have that yet, so simply patch it to return after entering the function.

Shell we dance?

Now we can boot userland and launch bash but we can not input anything.

1
2
3
4
Thu Jan  1 00:00:00 1970 localhost com.apple.xpc.launchd[1] (com.apple.xpc.launchd.domain.system) <Error>: Failed to bootstrap path: path = /AppleInternal/Library/LaunchDaemons, error = 2: No such file or directory
Thu Jan 1 00:00:00 1970 localhost com.apple.xpc.launchd[1] (com.apple.xpc.launchd.domain.system) <Notice>: exiting bootstrap mode
Thu Jan 1 00:00:00 1970 localhost com.apple.xpc.launchd[1] (com.apple.xpc.launchd.domain.system) <Notice>: exiting ondemand mode
bash-5.0#

after some debugging we can find that the FIQ handler was never called. That means there might be something wrong with our timer. Comparing the kernel of iOS 13 and iOS 14, I notice that

1
2
3
4
5
6
7
8
__text:FFFFFFF007D05C00 loc_FFFFFFF007D05C00                    ; CODE XREF: sub_FFFFFFF007D05AB4+130↑j
__text:FFFFFFF007D05C00 MRS X9, #0, c14, c1, #0
__text:FFFFFFF007D05C04 MOV W10, #0xD
__text:FFFFFFF007D05C08 BFI W10, W8, #4, #0x1C
__text:FFFFFFF007D05C0C ORR X8, X9, X10
__text:FFFFFFF007D05C10 MSR #0, c14, c1, #0, X8
__text:FFFFFFF007D05C14 MOV W8, #1
__text:FFFFFFF007D05C18 MSR #3, c14, c2, #1, X8

iOS 13 enables the physical timer while in iOS 14

1
2
3
4
5
__text:FFFFFFF007B6708C                 STP             X9, XZR, [X8,#0x58] ; Store Pair
__text:FFFFFFF007B67090 MOV W8, #1 ; Rd = Op2
__text:FFFFFFF007B67094 MSR #3, c14, c3, #1, X8 ; Transfer Register to PSR
__text:FFFFFFF007B67098 MOV W8, #2 ; Rd = Op2
__text:FFFFFFF007B6709C MSR #3, c14, c2, #1, X8 ; Transfer Register to PSR

It enables the virtual timer, thus

1
qdev_connect_gpio_out(cpudev, GTIMER_VIRT, qdev_get_gpio_in(cpudev, ARM_CPU_FIQ));

that is all we need! And we can unpatch the IORTC hack. Now we have an interactive shell!

1
2
3
4
5
6
7
8
9
bash-5.0# uname -a
Darwin localhost 20.1.0 Darwin Kernel Version 20.1.0: Fri Oct 30 00:34:17 PDT 2020; root:xnu-7195.42.3~1/RELEASE_ARM64_T8015 iPhone10,3 arm64 D22AP Darwin
bash-5.0# sw_vers
ProductName: iPhone OS
ProductVersion: 14.2
BuildVersion: 18B92
bash-5.0# id
uid=0(root) gid=0(wheel) groups=0(wheel)
bash-5.0#

Conclusion

Ah, time to add iOS 16 support.