Logo xia0o0o0o

Boot Newer iOS with QEMU Step by Step

March 9, 2024
8 min read
Table of Contents

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

./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

__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

  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

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

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

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

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

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

__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

__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.

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

__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

__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

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!

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.