Introduction to iBoot
The Kernel is not the first piece of code to run on a device - far from it. There is a great deal of setup and initialisation that is required before the kernel can be loaded; this is done by a piece of software called a "bootloader".
On Apple devices the bootloader is given the name iBoot. This actually encompasses the ROM (Read-Only Memory), first and second stage boot loaders. All three of these are used for different levels of device initialisation.
Starting with the ROM, or more commonly known as the SecureROM. This is a very small piece of software that is burned into the Application Processor of Apple devices during their manufacturing, therefore it cannot be modified. This has it's benefits and drawbacks, most notably that if a security vulnerability is found there is no way to fix it (see Checkra1n). The purpose of the SecureROM is very early device initialisation, such as enabling cpu cores, wakeup, choosing a boot device, then verifying if it's valid and signed by Apple.
The SecureROM cannot load the kernel itself for a number of reasons. Firstly, the different stages of boot loaders can be executed at different "Exception Levels" (discussed later). Secondly, including too much functionality in the SecureROM exposes it to more vulnerabilities. Third, it is not feasible to run the kernel directly from boot because the kernel does so much and has such a wide scope. Issues in the kernel could cause the device to be unusable as there is no previous stage to recover it, e.g. restoring the device.
Now, this explanation is more specific to iOS devices. macOS works in a similar way - especially with the upcoming ARM-based Mac's.
The next stage that is loaded after the SecureROM depends on the device. Apple devices with the A9 Application Processor and older first load the LLB, or Low-Level Bootloader. The LLB generally will cary out low-level SoC initialisation, console buffer setup, resuming the device if necessary, finding and subsequently loading an iBoot image. Failing that, LLB will fall back to DFU mode - which is out of the scope of this article.
For devices with the A10 and newer AP, the LLB is not used. Instead, the SecureROM directly loads the iBoot image.
iBoot is where we can really start getting into how things work. As we have already established, A9 and older devices will use LLB to load iBoot, and newer devices load iBoot directly from the SecureROM. This means more platform initialisation must be done at the SecureROM level. For reference, we are reversing an iBoot for the t8010 Application Processor.
Relocation Loop
First things first, when SecureROM jumps to iBoot we land in area of code referred to as the "Relocation Loop" which is responsible for relocating the iBoot image to a specified virtual address, providing it's not already there. When disassembling an iBoot image with IDA, rebasing the image to 0x870000000
will present one with the Relocation Loop. Let's have a look:
#
# iBoot Relocation Loop.
#
0x870000000 _start:
0x870000000 ADRP X0, #_start ;
0x870000004 ADD X0, X0, #_start ; x0 = 0x870000000 = current page address
0x870000008 LDR X1, =0x1800B0000 ; x0 = 0x1800B0000 = iBoot Load address
0x87000000C BL _platform_start ; _platform_start(x0, x1) function.
0x870000010 CMP X1, X0 ; check if we are running at the desired address.
0x870000014 B.EQ _continue_start ; jmp to _continue_start
;
0x870000018 MOV X30, X1 ; set lr to iBoot load address.
0x87000001C LDR X2, =0x18010DD80 ; x2 = iboot end-of-img
0x870000020 LDR X3, =0x1800B0000 ; x3 = iboot start-of-img
0x870000024 SUB X2, X2, X3 ; x2 = x2 - x3 = iboot-size
...
0x870000028 _copy_loop: ; copy loop
0x870000028 LDP X3, X4, [X0], #0x10 ;
0x87000002C STP X3, X4, [X1], #0x10 ;
0x870000030 SUBS X2, X2, #0x10 ;
0x870000034 B.NE loc_870000028 ;
0x870000038 RET ; returns to loading address, LR.
Although I've written notes beside instructions, let's discuss this more clearly. When SecureROM loads iBoot it will land at the address 0x870000000
, I have named this _start
, which is the virtual address iBoot was loaded to by SecureROM. The idea here is to immediately load that address into x0
, and then load the desired address of iBoot into x1
. The aim is to then compare the two to check whether we are already running at the desired address. However, before that, a call to _platforn_start
is made - I'll discuss this in a moment.
Now we make that compare between x0
and x1
. Notice the B.EQ
instruction, this means that if the two register are equal, we would jump to _continue_start
- this branch call would not return. Otherwise, iBoot will assume that we are not currently running from the desired address and therefore must fix that. The desired load address is copied into x30
, otherwise known as lr
or the Link Register, this register is used as the return-point when a ret
call is made. iBoot will calculate it's own size by loading the end-of-image address into x2
, and the start-of-image address into x3
, then subtracting x2
from x3
leaving the size of iBoot in x2
. We'll continue into the _copy_loop
where it will loop round until the correct values are placed, then returns.
_platform_start
Now, back to that _platform_start
call. This function is very low level and handles modifying control bits of Apple-specific System Registers. It's difficult to read and understand what it's doing, so we won't stay on the topic for too long. _platform_start
must preserve registers x0
and x1
as they have been assigned important values in _start
.
#
# _platform_start function called by _start for low-level system reg config.
#
0x870013C18 _platform_start:
// Enable the L2 cache load/store prefetcher.
0x870013C18 MRS X2, #0, c15, c5, #0 ; read HID5, s3_0_c15_c5_0
0x870013C1C AND x2, x2, #0xFFFFCFFFFFFFFFFF ;
0x870013C20 MSR #0, c15, c5, #0, X2 ; write HID5
0x870013C24 ISB
0x870013C28 MRS X2, #0, c15, c5, #0 ; read HID5
0x870013C2C AND X2, X2, #0xFFFFFFFFFFFF3FFF ;
0x870013C30 ORR X2, X2, #0x8000 ;
0x870013C34 MSR #0, c15, c5, #0, X2 ; write HID5
0x870013C38 MRS X2, #0, c15, c5, #0 ; read HID5
0x870013C3C ORR X2, X2, #0x200000000000000 ;
0x870013C40 MSR #0, c15, c5, #0, X2 ; write HID5
0x870013C44 MRS X2, #0, c15, c11, #0 ; read HID11, s3_0_c15_c11_0
0x870013C48 ORR X2, X2, #0x800000
0x870013C4C MSR #0, c15, c11, #0, X2 ; write HID11
0x870013C50 MRS X2, #0, c15, c1, #0 ; read HID1, s3_0_c15_c1_0
0x870013C54 ORR X2, X2, #0x100000000000
0x870013C58 MSR #0, c15, c1, #0, X2 ; write HID1
0x870013C5C CMP X1, X0
0x870013C60 B.EQ locret_870013C7C
0x870013C64 MRS X2, #3, c15, c7, #0 ; read L2_CRAMCONFIG, s3_3_c15_c7_0
0x870013C68 ORR X2, X2, #2
0x870013C6C MSR #3, c15, c7, #0, X2 ; write L2_CRAMCONFIG
0x870013C70
0x870013C70 loc_870013C70
0x870013C70 MRS X2, #3, c15, c7, #0 ; read L2_CRAMCONFIG
0x870013C74 AND X2, X2, #0x8000000000000000
0x870013C78 CBZ X2, loc_870013C70
0x870013C7C
0x870013C7C locret_870013C7C
0x870013C7C RET
Here we have _platform_start
. I'll take this as a chance to explain these rather odd looking instruction sequences. Take the following instruction, mrs x2, #0, x15, c5, #0
. This looks rather daunting, but from the outset we can simplify it to mrs x2, s3_0_c15_c5_0
. These are what we call "System Registers" of which we can have Generic AArch64 registers, and Apple system registers. @bazad has a neat listing all all known Apple and AArch64 System Registers so we can grasp a rough idea of what these odd-looking operations do.
Now looking back at our code listing, take a look at the system registers we are working with. You'll notice the pattern #0, c15, c5, #0
repeats three times, remember this can be translated to s3_0_c15_c5_0
. So there are three operations being performed on the s3_0_c15_c5_0
system register, each time with it being written back before the next is performed. So what is s3_0_c15_c5_0
? Looking it up in Bazad's listing, we find it's called HID5
with it's use unknown. However, due to... an undisclosed source... we know that the first sequence is to "Enable the L2 cache load/store prefetcher".
The only system register we know the name and use of is s3_3_c15_c7_0
, named L2_CRAMCONFIG
and described as being the "LSU Error Status" by Bazad.
After Relocation.
After the relocation code returns, the following is executed. This code will disable exceptions so the execution cannot be interrupted, setup the exception table base address, clear 16K of memory at a set base address, clear 96K of memory (for an unknown reason) at another given base address, setting the current and EL0 stack pointers, zero out additional space, and then clear both aligned and unaligned space.
#
# See kpwn/iOSRE for more information on the relocation code. Used as a reference
# but with small changes due to different iBoot versions.
#
0x87000003C ; ---------------- Relocation code above ----------------
0x87000003C
0x87000003C loc_87000003C:
0x87000003C ; Masking exceptions
0x87000003C MSR #6, #0xF ; MSR DAIFSET, #0xF
0x870000040
0x870000040 ; Setup exception tables base address
0x870000040 ADRP X10, #unk_870000800@PAGE
0x870000044 ADD X10, X10, #unk_870000800@PAGEOFF
0x870000048 MSR #0, c12, c0, #0, X10 ; Store x10 into VBAR_EL1, or Vector Base Address Register (EL1).
0x87000004C
0x87000004C ; Clear 16K of memory
0x87000004C LDR X10, =0x180098000 ; base stack address
0x870000050 LDR X11, =0x4000 ; 16K
0x870000054 ADD X11, X11, X10 ; x11 = base_addr + 16K bytes
0x870000058 MOV X12, #0 ; x12 = 0
0x87000005C
0x87000005C _loop__clear_16K:
0x87000005C STP X12, X12, [X10], #0x10
0x870000060 ; x10 == base addr, x11 == size (16K).
0x870000060 CMP X10, X11 ; if (x10 != x11)
0x870000064 B.NE _loop__clear_16K ; _loop__clear_16K()
0x870000068
0x870000068 ; Unsure here? Appears to be clearing 96K at 0x180080000
0x870000068 LDR X10, =0x180080000 ; loads 0x180080000 into x10
0x87000006C LDR X11, =0x18000 ; 96K
0x870000070 ADD X11, X11, X10 ; x11 == end addr, x10 == base
0x870000074 MOV X12, #0 ; x12 = 0
0x870000078
0x870000078 _loop__clear_96K:
0x870000078 STP X12, X12, [X10], #0x10
0x870000078 ; Same as _loop__clear_16K, just with 96K.
0x87000007C CMP X10, X11 ; if (x10 != x11)
0x870000080 B.NE _loop__clear_96K ; _loop__clear_96K()
0x870000084
0x870000084 ; Set the stack pointer
0x870000084 LDR X10, =0x180099000
0x870000088 MOV SP, X10 ; SP = 0x180099000
0x87000008C
0x87000008C ; set EL0 SP to x10
0x87000008C LDR X10, =0x18009A000 ; x10 = 0x18009A000
0x870000090 MSR #5, #0 ; select EL0 SP
0x870000094 MOV SP, X10 ; SP_EL0 = x10
0x870000098
0x870000098 ; zero additional space
0x870000098 LDR X10, =0x18010DD80 ; _iboot_end
0x87000009C LDR X11, =0x180121A88 ; fb_mbe
0x8700000A0 MOV X12, #0xF
0x8700000A4 BIC X12, X11, X12 ; x11 aligned to 16
0x8700000A8 MOV X13, #0
0x8700000AC
0x8700000AC ; clear more space aligned 16
0x8700000AC _loop__clear_more__aligned:
0x8700000AC STP X13, X13, [X10], #0x10
0x8700000B0 CMP X10, X12
0x8700000B4 B.NE _loop__clear_more__aligned
0x8700000B8 CMP X11, X12
0x8700000BC B.EQ loc_8700000CC
0x8700000C0
0x8700000C0 ; clear the rest of the space, unaligned
0x8700000C0 _loop__clear_more__unaligned:
0x8700000C0 STR W13, [X10], #4
0x8700000C4 CMP X10, X11
0x8700000C8 B.LT _loop__clear_more__unaligned
...
0x8700000F4 ADRP X30, #unk_87000266C@PAGE ; unk_87000266C point to _main_generic
0x8700000F8 ADD X30, X30, #unk_87000266C@PAGEOFF ;
0x8700000FC RET
0x8700000FC ; ---------------------------------------------------------------------------
The location for iBoot to return to is now stored in x30
, being the address of unk_87000266C
. This is actually an undefined section of code that IDA could not identify. When jumping to it, there is a chunk of instructions with their addresses highlighted red. By selecting all of the highlighted code, we can create a function, this function being _main_generic
.
void _main_generic()
{
...
v18 = _task_create("main", _main_task_, 1, 0x1C00);
_task_start(v18);
_task_exit(0);
}
Now, _main_generic
does some uninteresting platform hardware initialisation, but it also creates and starts the "main" task which is where the majority of iBoot's functionality regarding setup and booting lies.
Summary
In this post we have covered the very first code to run as part of iBoot. The "Relocation Loop", as we've already discussed, ensures that iBoot is running at the correct address, and will move it if required. The format of System Registers, how they are accessed and written to, and the _platform_start
function whose responsibility it is to configure particular system registers, such as L2_CRAMCONFIG
, or s3_3_c15_c7_0
. And finally the code to run after the Relocation Loop which sets up areas of memory, the stack and eventually runs the rest of iBoot.
In the next post we shall cover the early iBoot initialisation, Tasking System, how tasks are created and how they are started.
Thank you for reading, as always you can reach me by email at me@h3adsh0tzz.com, and on Twitter @h3adsh0tzz.