This is a write-up of the wInd3x bootrom exploit affecting iPod Nanos from 3rd gen to 5th gen. A ready-made tool to use this exploit is available at github.com/freemyipod/wInd3x.

iPod Nano 5G in disk mode with the 'do not disconnect' icon replaced with a mac bomb subtitled 'wInd3x'.

Background

The year is 2020. The End Times are nigh. I discover a funny Australian man on YouTube by the name of Dankpods.

“Hey, iPods are kinda neat! I wanna run Doom on them.”, I said to myself.

I quickly realized that while a lot these old iPods were ‘liberated”http://q3k.org/”hacked’ (meaning: you could run your own software on them), a lot of them still weren’t. Meaning, at the time, there was no public way to get code execution on any device newer than the iPod Nano 4G.

I found this a bit surprising. Unacceptable, even. I imagined the security on these devices must have been thoroughly broken, similar to older iPhones (see: checkm8 vuln).

I mean, could I maybe just port some old iPhone exploits to the iPods? Aren’t they like some sort of proto-XNU or something? Even if not, there must be super hackable, right?

iPod Tech Stack

Well, not quite. Let’s review:

Operating system: instead of running iOS/XNU, the iPods run a custom embedded RTOS that doesn’t even have a well-documented name. It’s based around the RTXC microkernel, with an UI stack provided by ‘Pixo’, a company Apple acquired soon in the product lifecycle. The good thing is that this is not a security-oriented OS and it’s likely to be full of bugs. The bad new is that there’s basically no research into it, no known bugs, nothing.

Boot chain: Everything’s encrypted and signed (we’ll get into that later). The on-die BootROM (similar to the BootROM found in early iPhones and iPod touches) chains into a second stage bootloader in NOR/NAND, which in turn sets up drivers for devices and boots a final payload, be it the main OS or disk mode or diagnostics. Interestingly, the second stage bootloader (and some of the payloads, like the diag tool) are based around EFI firmware volumes / drivers. but that’s a topic for another time.

Hardware: Early devices used PortalPlayer SoCs. The devices I’m interested in (Nano 3G+) use Samsung S5L87xx SoCs, which are a close relative of the S5L89xx early iPhone SoCs. The S5L8720 is used both in the iPod Nano 4G and the iPod Touch 2G, but has a different BootROM. There exist no publicly available datasheets of any of these SoCs.

Boot Security Chain

The usual boot flow of the device is as follows:

    .---------.
    | BootROM |
    '---------'
         |
         | Verifies
         V
    .-------------------------.
    | Second stage bootloader |
    | (NAND/NOR)              |
    '-------------------------'
        |
        | Selects and Verifies
        |'-------.-------------.
        V        V             V
    .--------. .-----------. .--------.
    | OS     | | Disk Mode | | Diags  |
    | (NAND) | | (NAND)    | | (NAND) |
    '--------' '-----------' '--------'

The BootROM performs enough hardware bring up to be able to load a second stage from a plain sector of NAND/NOR (depending on the generation of the device), then the second stage bootloader brings up a bunch of other peripherals like the LCD/DMA/DRAM/VICs…, displays the apple logo, and allows the user to select the firmware to be loaded (main firmware, disk mode, or diagnostics) based on keypresses. The firmware is loaded from NAND, but this time going through a full-blown Flash Translation Layer.

Each one of the stages being loaded (except the BootROM itself) is wrapped in an Image1, which was also used in early iPhone devices. This format has the following security properties:

  1. The header (excluding its signature field) is hashed with SHA1 and the result is encrypted with a fused per-device-generation key. The result is compared against the signature field. Thus, the header is signed.
  2. Another field in the header determines whether the body is decrypted using the same key. This is true for all firmware payloads observed in the wild.
  3. Another field in the header determines whether the body is signed using an X509 PKI system, with the fingerprint hardcoded in the previous boot stage. This is true for all firmware payloads observed in the wild, and is mandatory on devices newer than the Nano 3G.

Things look slightly different when you want to perform recovery on the device:

    .---------.
    | BootROM |
    '---------'
         |
         | Verifies
         V
    .-----------.
    | WTF       |
    | (USB DFU) |
    '-----------'
        |
        | Verifies
        V
    .--------------------.
    | Recovery Disk Mode |
    | (USB DFU)          |
    '--------------------'

As you can see, when doing recovery, we have a similar split into secondary bootloader stage and firmware, but instead of loading the next stage from local storage, we load it over USB, using the DFU protocol. The second stage bootloader is also called ‘WTF’, for unknown reasons (its main difference from the standard second stage bootloader is that it defaults to loading the next stage over USB DFU, too).

Now, you might already spot a problem here. We’re running a full-blown USB stack and DFU protocol implementation in early bootloader stages, including the bootrom. That’s Generally A Bad Idea, with many systems’ security falling to shoddy USB implementations running at the highest privilege levels. Put a pin in that.

Prior Art

Of course, I wasn’t the first person looking to liberate these devices.

First, there was iPodLinux, a port of uClinux (now Linux nommu) to old PortalPlayer-based iPods. These folks paved the way towards the initial research on the file formats involved in the iPod firmware. However, things got kinda stuck when iPods moved over to Samsung-based chips (with the release of the iPod Nano 2 and iPod ‘Classic’).

Then there was Linux4Nano, now Freemyipod, a collective of people looking to get code exec on the S5L-based devices. In collaboration with the brand new iPhone hacking scene, they managed to find and exploit a but called ‘Pwnage 2.0’, another bootrom bug, but in the X509 parsing stack. They then reverse-engineered enough of the devices to allow for a Rockbox port to happen for the iPod Nano 2, and the iPod classic. The Pwnage 2.0 bug worked all the way up to the iPod Nano 4G, then it got patched by Apple. Another bug, in the Notes application, also allowed for code exec up to the iPod Nano 4G, this time at the firmware level.

And now, me. To summarize, I had access to the following:

  1. Code execution on the Nano 2G (Samsung S5L8701), Nano 3G / iPod Classic (Samsung S5L8702), Nano 4G (Samsung S5L8720) thanks to the Pwnage 2.0 bug.
  2. BootROM dumps from the above devices
  3. Decrypted firmware for the above devices
  4. Some reverse engineering notes left behind by previous generations of hackers.

Everything from the Nano 5G up was basically fully sealed. No firmware dumps, no code execution. Nothing to really reverse engineer. I decided the best course of action would be to find a new vulnerability in the Nano 4G, and port it to later devices.

So I decided to find new bugs in the Nano 4G BootROM

Reverse-engineering the BootROM

The easy part was getting started: just take a binary dump, load it at address 0x2000_0000, and start disassembling at the first bytes.

Making sense of what was going on was a bit more complicated. The BootROM is quite small with basically no human-readable strings to use as guidance. Thankfully, I had some reverse-engineered register/memory area notes from the previous generations of hackers, so I could quickly find out that early memory pokes were things like configuring the PLL and clock tree. A few other ‘anchors’ was code to enable/disable clock gates, schedule AES decryption using the built-in AES peripheral, basically any MMIO register pokes was a good place to start documenting things.

Soon enough, I recovered most of the main functionality of the BootROM. Naturally, all the symbol/struct names below are my own, as the BootROM binary was just that, a binary stripped of all its original symbols.

void boot(void)

{
  undefined4 spino;
  char *cnCA;
  DFUBoot dfuBoot;

  State *state = g_State;
  g_State->vtable = &StateVTable;
                    /* Unknown part of CHIPINFO, bits [3:0]. */
  CHIPINFO chipinfo = read_volatile_4(CHIPID_INFO);
  uint chipinfo_unk = (uint)((int)chipinfo << 0x1c) >> 0x1e;
  state->chipinfo_unk = chipinfo_unk;
  if (chipinfo_unk == 1) {
    cnCA = "/CN=Apple Secure Boot Certification Authority";
  }
  else {
                    /* This is in prod certs. */
    cnCA = "/CN=Apple iPod Certification Authority";
  }
  state->cnS5L8720SecureBoot = "/CN=S5L8720 Secure Boot";
  state->cnCertificationAuthority = cnCA;
  gstatus_set(0,0);
  int disconnected = boot_otg_try_connect_dfu();
  if (disconnected != 0) goto dfu;
  gpio_configure_input(3,5,0);
  gpio_configure_input(3,6,0);
  gpio_configure_input(3,7,0);
  int gpio5 = gpio_read(3,5);
  int gpio6 = gpio_read(3,6);
  int gpio7 = gpio_read(3,7);
  switch(gpio7 | gpio5 << 2 | gpio6 << 1) {
  case 0:
  case 2:
    spino = 0;
    break;
  case 1:
  case 3:
    spino = 1;
    break;
  case 4:
  case 5:
  case 6:
    boot_nand();
  default:
    goto dfu;
  }
  boot_spi(spino);
dfu:
  gpio_configure_unk(0xc,3,1);
  gpio_set_bit(0xc,3,1);
  (*g_State->vtable->DFUBootDFUBoot)(&dfuBoot);
  /* setup dfuBoot... */
  (*g_State->vtable->DFUBootSetup)(&dfuBoot);
  (*g_State->vtable->DFUBootSetupUSB)();
  (*g_State->vtable->DFUBootRun)();
  return;
}

You can see how the BootROM decides, based on some GPIO straps (and likely GPIO comms from some PMIC or the clickwheel) to boot over NAND, NOR(SPI) or over USB.

Inside each boot method handler, there would be some calls to verify the integrity of the loaded IMG1. For example, in boot_nand:

undefined4 boot_nand(void)

{
  clkgen_enable_gate(5);
  clkgen_enable_gate(9);
  nand_power_up_maybe();
  nand_reset(0);
  nand_read_maybe(0,0,(int)&hdr);
  bool bVar1 = (*g_State->vtable->verify_img_header)(&hdr,AES_KEY_TYPE_GLOBAL);
  if (((bVar1 != false) && (hdr.field2_0x7 == ASYMMETRIC || hdr.field2_0x7 == ASYMMETRIC_ENCRYPTED))
     && (hdr.bodySize int iVar2 = (*g_State->vtable->verify_decrypt_image)(&hdr,&g_IMG_Payload,2);
    if (iVar2 != 0) {
      offset = g_State->img_header_jump_offset;
      gstatus_set(3,0);
      prepare_and_jump((int)&g_IMG_Payload + offset);
    }
  }
  return 0;
}

You can see a NAND page (?) read, followed by a call to verify_img_header which ensures the header passes the AES(SHA(header), fused_key) == header.sign check. After that, we load a bunch of more NAND pages of the body of the IMG1, and finally we call verify_decrypt_image on the entirety of the loaded image. That in turn performs a signature checking of the body, this time using X509. The loaded payload is expected to have a valid certificate chain appended to its end.

That X509 codepath above is where ‘Pwnage 2.0’ lives. It’s an extremely dumb vulnerability in a quite clever ASN.1/DER parser. See the linked Wiki article for more details. Needless to say, I did look around the X509 parsing code for more bugs, but I didn’t really find anything. Some of the cert chain logic is quite hairy though, so maybe someone else will have more luck :).

There’s plenty more code in the BootROM, and this was mostly just an example of how such a codebase can look like when you spend dozens of hours pouring over it. This still isn’t perfect, but it’s enough to actually look for bugs.

The bug: USB wIndex == magic

Let’s get on with it. What’s the bug I ended up finding? An extremely trivial vulnerability in the USB stack, of course. It was so trivial in fact that I missed it when I was doing a first pass over the codebase.

Deep inside a forest of callbacks, ‘ops’ structures and magical register pokes to a Synopsys USB OTG peripheral, we find a function which actually parses a received SETUP packet:

void USB::HandlePendingSetup(void)

{
  usb_device_request *req;
  USBHandler *fptr;
  uint index;
  byte bmRequestType;
  USBState state;

  if (g_State->ep0state != SETUP) {
    return;
  }
  req = (usb_device_request *)g_State->ep0_dma;
  state = g_State->usbState;
                    /* INIT or CONFIGURED */
  if (state 

The important part is that g_state->ep0_dma is the memory buffer configured to be populated with a USB packet received on Endpoint 0. That is, these are directly controlled by the attacker. The code assumes this is USB SETUP packet, xtracts the bmRequestType field from it and dispatches on it:

  bmRequestType = req->bmRequestType;
  if ((bmRequestType & 0x60) == 0) {
    /* Type == Standard */
    EP0OutSetupStandard(req);
    PrepareRecvBuf();
    return;
  }
  if ((bmRequestType & 0x60) == 0x20) {
    /* Type == Class */
    [...]
  } else if ((bmRequestType & 0x60) == 0x40) {
    /* Type == Vendor */
    [...]
  } else {
    goto LAB_20004e18;
  }

Pretty normal stuff. Check if it's a Standard request, and if so, handle that by a call to EP0OutSetupStandard. If it's a Class or Vendor request type, handle that too. Otherwise, stall and fail. Let's zoom into that Class handler:

    index = (uint)req->wIndex[0];
    if (state usbHandlers[index].handlerClass;
      goto joined_r0x20004d9c;
    }
    if ((bmRequestType & 3) == 1) {
      if (index == 0) {
        fptr = (USBHandler *)g_State->usbHandlers[0].handlerClass;
        goto joined_r0x20004d9c;
      }
    }
    [...]

There it is. The bug's right there. Can you see it?

That's right. If (bmRequestType & 0x3) == 0, the user-controlled 'index' (populated from the lower byte of the wIndex field of the SETUP packet) is used as an index into g_State->usbHandlers without any boundary checks. Then, the result of that index is executed as a function (not shown here). Oops. The same buggy codepath exists in the Vendor handler, too.

And indeed, if we send a crafted USB SETUP packet to the BootROM in DFU mode with bmRequestType set to 0x20 (passing the two checks above) and wIndex set to 0xff00, we crash the BootROM on the Nano 4G and... Nano 5G! That's a new device on which we never had code exec before! But let's first try to exploit this on the Nano 4G, as we have the BootROM dump for that.

Exploiting the Nano 4G

At this point we need to familiarize ourselves a bit more with the State structure and its usbHandlers member.

typedef struct {
    uint stuff[0x15];
    USBInterfaceHandlers usbHandlers[1];
    // other things afterwards...
} State;

typedef struct {
    USBInterfaceDescriptor *interfaceDescriptor;
    void *onSetConfiguration;
    void *unk;
    void *onSynchFrame;
    void *handlerClass;
    void *handlerVendor;
} USBInterfaceHandlers;

So effectively, what we can do, is treat some of the fields after usbHandlers in State as a code pointer to jump to. Doing some math, these are offsets 0x64 + (0x1c * n) and 0x68 + (0x1c * n) for N in 0..255.

I'll spare you the details, but what I found was the following: if you set wIndex to 3 and send a Class request, we'll treat the uint at offset 184 in the State structure as a code pointer and execute it. And in that offset in State, there's a counter I called ep0_txbuf_offs.

The BootROM DFU can not only receive an image to run on the device, but also send what it currently has in its buffer. The implementation of it is actually broken, but if you first request N bytes of the image, then time out the read from the host side, ep0_txbuf_offs will contain N-0x40 (ie. the amount of data left to send, minus the first packet size).

There's some size limits in place, but by scheduling a firmware send from the device, and then sending bmRequest=0x20 and wIndex=3, we can get the device to execute at any address from 0x000 to 0x600. If you know your ARM devices, you'll also know that these low addresses must contain an interrupt vector for the CPU. That usually means most devices will mirror their current execution medium (in this case the BootROM) into these low addresses, and that's also the case here.

So, we can execute addresses 0x000 to 0x600 from the BootROM... what now? Well, under address 0x3b0 we find the following:

        200003b0 30 ff 2f e1     blx        r0

This nice little gadget is effectively a trampoline that will continue executing from whatever address is in r0, the first register of the ARM CPU. This register is also used, in the standard ARM ABI, to hold the first argument passsed to a function. And luckily, in USB::HandlePendingSetup, the corrupt fptr is actually called with an argument: ep0_dma.

Well that's fun! By scheduling a read of 0x3b0+0x40, then performing the wInd3x bug, we'll start executing the USB SETUP packet as ARM code!

This means it's time for me to introduce to you to my favourite part of the exploit chain: a polyglot ARM shellcode and USB packet:

    0x20 0xfe 0xff 0xea 0x03 0x00 0x00 0x00

When parsed as a USB SETUP packet, it has a bmRequsetType of 0x20, a bRequest of 0xfe, a wValue of 0xffea, and a wIndex of 3. This means it triggers the wInd3x bug above.

When parsed as ARM code executing from 0x2202e300 (where ep0_dma lives), it's:

$ rasm2 -a arm -b 32 -o 0x2202e300 -D "20feffea03000000"
0x2202e300   4                 20feffea  b 0x2202db88
0x2202e304   4                 03000000  andeq r0, r0, r3

Incidentally, 0x2202db88 is 136 bytes into the DFU buffer, which is pretty much 'unlimited' in size and very easily controlled. We now have stable code exec!

With this, we can start sending the device some 'shell'code. On of the earliest things I wrote was proof of concept 'send-memory-region-over-USB'. In a future article, we'll discuss more practical payloads, but for now let's focus on the reason we're even here:

Exploiting the Nano 5G

We're now in The Cool Zone. We experimentally confirmed the Nano 5G BootROM is vulnerable to wInd3x due to a crash, but we have absolutely no BootROM dumps to actually craft our exploit as easily as for the Nano 4G. Just trying the same payload doesn't work either (as expected, the offsets are probably all different due to a recompilation of the BootROM).

First I wanted to check if the first part of the exploit, jumping into the 0x000..0x600 memory by controlling ep0_dma still worked. I was able to come up with a simple 'oracle' test: if ep0_dma is 0, then we should jump to address 0x000 which is the reset vector, which should basically restart DFU mode. However, if I set it to 4, then it should jump to 0x004 which is the invalid instruction handler, which I know the Nano 4G BootROM implemented as an infinite loop. That worked, so I knew I could control that part of the execution.

Now, my goal was to find a working 'blx r0' gadget somewhere in the first 0x600 bytes. So I bruteforced that, and kept notes:

    # 374: stuck
    # 378: returns
    # 37c: restart 

One address popped out as having a different behaviour: 0x37c. Everything else either crashed the device (and caused it to be stuck in an infloop) or had no effect (likely hitting a block of code ending with a mov pc, lr). I could now work on my USB/ARM polyglot payload. To my surprise, it worked out of the box: depending on whether I populated the DFU buffer with 'loop: b loop' or 'bl #0x0' instructions, I got different behaviour.

Now, I could execute code, but I had really no way to get any result out from it, as I didn't know the address of the USB send buffer to inject data into. Well, almost no way. I could after all, either get the device stuck (by doing an infloop), or restart it (by jumping to 0). That means I could leak, one bit at a time, some data. So I wrote some janky ARM payloads that would calculate a bit from somwhere (eg. looking for some memory pattern, then leaking the Nth bit of the found address) that allowed me to very slowly leak some facts about the BootROM, like where the buffers lived.

... but in the end, it turns out the buffer addresses for the Nano 5G were exactly the same as for the Nano 4G. So I could pretty much just re-use my old code as was, the only difference being 0x37c instead of 0x3b0 as the 'blx r0' trampoline address :).

What have we learned?

Don't put a C implementation of a USB stack in your BootROM, lol.

What's next?

Well, the exploit is pretty stable. I've build up some more tooling on top of the bug (subject to a future article here) that allows me to boot customized firmware over USB.

I've also made some progress on porting Linux and U-Boot to the Nano 5G:

U-Boot 2023.01-rc4-q3k-00055-g7de7e65add (Jan 01 1980 - 00:00:00 +0000)

CPU: Samsung/Apple S5L8730
Model: Apple iPod Nano 5G
DRAM:  64 MiB
Core:  5 devices, 5 uclasses, devicetree: separate
MMC:
Loading Environment from nowhere... OK
In:    serial@3cc00000
Out:   serial@3cc00000
Err:   serial@3cc00000
Net:   No ethernet found.
=> dfu 0 ram 0
s5l87xx_lcd_init: detected LCD type 38f7 (2)
s5l87xx_otgphy: turning on
s5l87xx: ungating usb-otg
s5l87xx: ungating usb2-phy
#DOWNLOAD ... OK
Ctrl+C to exit ...
s5l87xx_otgphy: turning off
=> bootm
## Booting kernel from Legacy Image at 08000000 ...
   Image Name:
   Image Type:   ARM Linux Multi-File Image (uncompressed)
   Data Size:    7189312 Bytes = 6.9 MiB
   Load Address: 08000000
   Entry Point:  08000000
   Contents:
      Image 0: 5722624 Bytes = 5.5 MiB
      Image 1: 1465230 Bytes = 1.4 MiB
      Image 2: 1440 Bytes = 1.4 KiB
   Verifying Checksum ... OK
## Loading init Ramdisk from multi component Legacy Image at 08000000 ...
## Flattened Device Tree from multi component Image at 08000000
   Booting using the fdt at 0x086dade0
Working FDT set to 86dade0
   Loading Multi-File Image
WARNING: legacy format multi component image overwritten
   Loading Ramdisk to 0ae1c000, end 0af81b8e ... OK
   Loading Device Tree to 0ae18000, end 0ae1b59f ... OK
Working FDT set to ae18000

Starting kernel ...

[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 6.2.0-rc3-00024-ge2e3252e9e4e (q3k@mimeomia) (arm-none-eabi-gcc (GNU Arm Embedded Toolchain 10.3-2021.10) 10.3.1 20210824 (release), GNU ld (GNU Arm Embedded Toolchain 10.3-2021.10) 2.36.1.20210621) #15 Thu Jan 12 23:37:12 CET 2023
[    0.000000] CPU: ARMv6-compatible processor [410fb764] revision 4 (ARMv7), cr=00c5387d
[    0.000000] CPU: PIPT / VIPT nonaliasing data cache, VIPT nonaliasing instruction cache
[    0.000000] OF: fdt: Machine model: Apple iPod Nano 5G
[...]
[    1.520000] Freeing unused kernel image (initmem) memory: 1024K
[    1.530000] Run /init as init process
Starting syslogd: OK
Starting klogd: OK
Running sysctl: OK
Saving random seed: [    2.160000] random: dd: uninitialized urandom read (32 bytes read)
OK
Starting network: OK

Welcome to Buildroot
buildroot login:
#

There's still some things to figure out though for a Fully Liberated iPod Experience:

  1. Untethering code execution on the Nano 5G: currently we always have to run our own code over USB.
  2. Getting code execution on the Nano 6G and 7G, for example by finding a bug in the firmware. They are not susceptible to wInd3x.
  3. Reverse-engineering more peripherals for the Nano 5G, and finishing the Linux port.
  4. Writing a Good implementation of the Whimory FTL used in the iPod Nano 3G+.
  5. A Rockbox port for the Nano 3G, 4G and 5G.

If you're curious in seeing regular updates (or even want to help!), join us on the #freemyipod channel on Libera.chat, or on #freemyipod:hackerspace.pl on Matrix. You can also check out the (mostly not dead) Freemyipod Wiki, or take a look at the wInd3x tool itself.

And as a prize for making it all the way through this drivel, I'll leave you with a final curiosity: wInd3x also affects the iPhone 3G (and probably the original iPhone), but I wasn't yet able to chain together an exploit for them. Wanna be a part of the Useless Old Device 0dayz Scene? 🙂

Copyright 2023 Serge Bazanski. This work is licensed under a Creative Commons Attribution 4.0 International License.

Back to q3k.org.

Read More