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.