Exploiting the iOS 5 iBoot bug

Created on 5.11.18
Important note: this is the very first bug I’ve ever exploited and, therefore, my first write-up of such kind. If you notice a mistake of any kind, polite criticism is always appreciated

The bug

This bug was discovered and exploited by @p0sixninja many years ago. It’s settled in HFS+ driver and was fixed in iOS 6. It is triggered by setting hfs_header->catalogFile.extents[1].blockCount to a high value (e.g. 0x10000000). This makes iBoot try to read a huge chunk of data from HFS+ partition to heap (near the end of it):

HFSInitPartition: 0x4ff57e00
HFSReadBlock: offset 0x400 size 0x200 buffer 0x4ff30efc
returned: 0x200
HFSReadBlock: offset 0x100a000 size 0x100 buffer 0x4ff310fc
returned: 0x100
HFSReadBlock: offset 0x80a000 size 0x100 buffer 0x4ff311fc
returned: 0x100
HFSReadBlock: offset 0x1214000 size 0x2000 buffer 0x4ffaf180
returned: 0x2000
HFSReadBlock: offset 0x28e000 size 0xffd92000 buffer 0x4ffaf180

The exploit

iBoot memory is mirrored after every 0x40000000 bytes (1 GB) on devices with 1 GB RAM, every 0x20000000 (512 MB) on devices with 512 MB RAM and every 0x10000000 (256 MB) on devices with 256 MB RAM, don’t know about the others. That means, for example, if iBoot is based at 0x4FF00000 (like in case of S5L8920-S5L8922), we can also read/write to it at 0x5FF00000:

[l] lina md 0x5ff00000 0x20
0x5ff00000: 0e0000ea 18f09fe5 18f09fe5 18f09fe5
0x5ff00010: 18f09fe5 18f09fe5 18f09fe5 18f09fe5

The general idea of our exploit is based on this fact — make iBoot overwrite itself with data from a partition, which we fully control. Target IRQ handler address to get arbitrary code execution

The exploitation

Note: this guide comes with the free exploitation starter kit, which will help you much on this way. It contains parts of HFS+ for initial bug triggering and a source code for payloads
We will use n18ap on iOS 5.1.1 in most of examples in this article
Let’s start with simply triggering the bug. This is pretty easy:
  • Create a third partition, we don’t want to corrupt main root filesystem
  • Delete from NVRAM anything that you feel isn’t needed (we’ll talk about the reasons behind that later)
  • Restore iOS 5.1.1 root filesystem for n18ap to the third partition
  • Replace the value at offset 0x52C in restored HFS by a large number (0x10000000 should do fine), and a value at 0x528 by a low number (e.g. 0x10). Note that all values in HFS+ header are big-endian
  • Take a target iBoot, patch its boot-command to be upgrade
  • Add boot-commanb (this is not a typo) variable with upgrade value to NVRAM
  • Boot the iBoot
Shortly after the iBoot tries to mount the filesystem and load iBEC from it, you’ll get a panic:

panic: arm_exception_abort: ARM data abort abort in supervisor mode at 0x4ff172b8 due to translation error:
far 0x50000000 fsr 0x00000805
r0 0x50000000 0x600000d3 0x00000001 0x4ff69ee0
r4 0x50001180 0x50000040 0x00000003 0x4ff3716c
r8 0x4ff172b8 0x00000cfc 0x00000001 0x4ffff180 0x00000000
sp 0x4ff3715c lr 0x4ff1cd97 spsr 0x000000d3

Yes, UART-cable is highly recommended for exploiting this bug. It’s still possible to succeed without it, but much harder. I’ll give some advice in the end of the guide
Extract from @p0sixninja’s write-up about the panic:
The only logical explanation would be something being overwritten between our heap buffer and 0x60000000 [this is about s5l8930x] is causing it to crash once it gets to 0x60000000. Dumping a lot of memory any analyzing it shows there's really only 2 distinct structure between our heap buffer (it's the last buffer allocated in heap so we're not corrupting any other heap buffers). These 2 structures are the stack (but we already know there was no stack corruption based on our earlier analysis), and a special structure in ARM called TLB (Translation Look-aside Buffer). The TLB is a specialized cache that holds a table of physical addresses which are mapped to virtual addresses in the system memory. In iBEC this structure starts at address 0x5FFF8000. This would make since because reason for the crash from the report states "Domain Error". A little research in the ARM technical reference shows that this happens when an illegal virtual to physical page mapping is attempted to be looked up. The TLB is essentially one 32bit address containing representing each physical to virtual mapping on the system
How can we avoid corrupting the TLB? Just rewrite it with a copy from the HFS partition. You're probably wondering now — how can we put TLB at random place in HFS? Won't it ruin its' structure? And even if it won’t, how do we know where to put it? These questions share an answer - a wrapper for HFSReadBlock()

The wrapper

We have to know from where, to where and how many bytes iBoot reads from a partition with a filesystem. For that purpose we’ll create a tiny wrapper for HFSReadBlock()
HFSReadBlock() is a very simple function, which just extracts the address of blockdev_read_hook() from the block device descriptor and passes its' own args to it:
The function is very easy to locate, it's used to read the HFS+ header in the very beginning of the HFSInitPartition() flow:
Our aim is to make a function with the same functionality plus printing args passed to it. Luckily for you, I already wrote one and it’s available in the starter kit
Pass your offsets - printf is obviously offset of printf()+1 (because we’re in Thumb) and BASEADDR is where the payload is going to be located
Good place to land it is around build banner’s strings. Put a branch (not with link! This is very important) to our wrapper payload right at the top of HFSReadBlock(), so the payload will be executed every time:
Apply patches and boot the iBoot. Now on serial you’ll see this:

HFSInitPartition: 0x4ff57e00
HFSReadBlock: offset 0x400 size 0x200 buffer 0x4ff30efc
returned: 0x200
HFSReadBlock: offset 0x100a000 size 0x100 buffer 0x4ff310fc
returned: 0x100
HFSReadBlock: offset 0x80a000 size 0x100 buffer 0x4ff311fc
returned: 0x100
HFSReadBlock: offset 0x1214000 size 0x2000 buffer 0x4ffaf180
returned: 0x2000
HFSReadBlock: offset 0x28e000 size 0xffd92000 buffer 0x4ffaf180
panic: arm_exception_abort: ARM data abort abort in supervisor mode at 0x4ff172b8 due to translation error:
far 0x50000000 fsr 0x00000805
r0 0x50000000 0x600000d3 0x00000001 0x4ff69ee0
r4 0x50001180 0x50000040 0x00000003 0x4ff3716c
r8 0x4ff172b8 0x00000cfc 0x00000001 0x4ffff180 0x00000000
sp 0x4ff3715c lr 0x4ff1cd97 spsr 0x000000d3

For now we’re only interested in the last call:

HFSReadBlock: offset 0x28e000 size 0xffd92000 buffer 0x4ffaf180

Now you know the precise location of a buffer iBoot tries to read the filesystem to

The TLB

Dump the stock TLB. Its location is always iBoot base address plus 0xF8000, so it’s 0x4FFF8000 in my case. 0x4000 (16 KB) of size. Once you've dumped it, calculate where it should be within the filesystem:
0x4FFF8000 - 0x4FFAF180 = 0x48E80 between heap address and TLB
0x48E80 + 0x28E000 = 0x2D6E80 exact position in our HFS+ partition
An address you’ll get most likely won’t be divided by block size without a remainder. And all operations with disk nodes on pre-PPN devices must be blocksize-aligned. Calculate on which block it has to be and with what padding on both sides:
0x2D6E80 / 0x2000 = 0x16B (360) block
0x2D6E80 - 0x16B * 0x2000 = 0xE80 padding on top
Wrap it in a block with calculated paddings consisted of zeroes and write to the filesystem

dd if=/block_363 of=/dev/rdisk0s1s3 bs=8192 seek=363

Boot the iBoot. Panic should no longer happen:

HFSInitPartition: 0x4ff57e00
HFSReadBlock: offset 0x400 size 0x200 buffer 0x4ff30efc
returned: 0x200
HFSReadBlock: offset 0x100a000 size 0x100 buffer 0x4ff310fc
returned: 0x100
HFSReadBlock: offset 0x80a000 size 0x100 buffer 0x4ff311fc
returned: 0x100
HFSReadBlock: offset 0x1214000 size 0x2000 buffer 0x4ffaf180
returned: 0x2000
HFSReadBlock: offset 0x28e000 size 0xffd92000 buffer 0x4ffaf180

But now it just hangs

The hang

Now, nothing interrupts it from reading. At some point you might get weird distortions on your device’s display. This means it reached framebuffer and overwrote it with data from the filesystem
Obviously, we don’t want to wait until it reads enough data to hit mirrored iBoot. There must be a way to speed up this process

The accelerator

When create new partition, LwVM (or something else?) marks all its blocks as empty. (Partition isn’t a filesystem!) The real reason for this may differ, but the fact is that iBoot skips all zero marked blocks and reads only those that matter
Dump all the blocks iBoot reads from the FS, delete the third partition, save, recreate it, write all the blocks back at their original positions plus TLB
Boot the iBoot, see how it quickly returns from reading 500+ MB and then starts an infinite loop

The TLB hack

Let’s take a look at TLB now. Imagine it’s an array of 32-bit integers. At both TLB[0x0] and TLB[iBoot baseaddr/0x100000] you can see the same record. Put it to TLB[mirrored iBoot baseaddr/0x100000]
By this we’re kinda directly mapping iBoot physical address (0x4FF00000) to the mirrored iBoot virtual address (0x5FF00000). The real explanation is probably different, but the fact is that it increases success rate of the exploit

The overwritten iBoot

Calculate where you have to put iBoot image (part of it) in the exploit partition to overwrite it on mirrored address
In my case iBoot base is at 0x4FF00000, so I need to put part of iBoot image in a place in the partition that blockdev_read_hook() will overwrite memory at 0x5FF00000 by it:
0x5FF00000 - 0x4FFAF180 = 0xFF50E80 between heap address and mirrored iBoot
0xFF50E80 + 0x28E000 = 0x101DEE80 exact position in our HFS+
0x101DEE80 / 0x2000 = 0x80EF (33007) block
0x101DEE80 - 0x80EF * 0x2000 = 0xE80 padding on top
In the near future our aim will be overwriting IRQ handler vector, for now we’ll touch reset vector instead. Replace 0E0000EA with FFFFFFFF. If overwriting will be done fine, a task_yield() panic will be immediately called. That way we’ll know how reliably the mirrored iBoot is overwritten
Important: the more bytes of iBoot you overwrite, the more reliable the exploit. It's highly recommended to write about 0x16000 bytes of iBoot to get 100% success rate
Boot the patched iBoot 10 or so times. Every attempt you should get such a panic:

panic: task_yield: reset vector overwritten while executing task 'main' (0xffffffff)

The payload

If it panics reliably, we can now make it to jump to our payload. The idea is very typical for ARM exploitation - overwrite address of the IRQ vector. In iBoot image for overwriting overwrite value at 0x38 to a pointer to our payload plus 0x1, since our payload is in Thumb (mostly)
The payload calls the real IRQ handler to the handle previous interrupt. Then it restores original address of the IRQ handler, then it prints cool strings and finally jumps to a new fresh iBoot of your choice with whatever patches you would like
In the payload replace offsets and compile with GNU ARM Toolchain. JUMPADDR is an address you'll put a new iBoot to jump to. Where to put it in the partition is your choice, I usually use the very first block iBoot reads in the last reading

The new iBoot

Just take a decrypted unpacked iBoot, apply all necessary patches. Choose a position to put the new iBoot very wisely not to overwrite some other block you write
Boot the iBoot. If everything's alright, you'll see something like here:

HFSInitPartition: 0x4ff57e00
Hello darkness, my old friend
jumping into image at 0x4ffb0180
image 0x4ff3db00: bdev 0x4ff3ea80 type SCAB offset 0x600
image 0x4ff3db80: bdev 0x4ff3ea80 type ibot offset 0x1200 len 0x2f1f8
image 0x4ff3dc00: bdev 0x4ff3ea80 type dtre offset 0x31200 len 0x9338

It should jump to your new bootloader
Now reboot to iOS, remove boot-commanb variable from NVRAM and replace it with boot-command variable. Reboot

The warnings

This is a heap-based exploit. Be careful with NVRAM variables and partitions. Some additional variables or just one additional partition and heap buffers' addresses will shift, as result the exploit will die

The recommendations

Here are the recommendations I promised for those who have no UART-cable: you can try to create a wrapper which will put information about HFS+ readings into NVRAM. Just don't forget to clear it before every next attempt. I personally have never tried such method, since I already have an UART-cable. I highly recommend you get one too

The credits

  • @p0sixninja - for discovering the bug and initial idea of the exploit
  • @JonathanSeals - for a lot of wording corrections in this write-up