16GB iPod Nano 3G Upgrade - Tucker Osman

Op dat moment had ik geen ervaring met solderen of reverse engineering. Hoewel ik sinds mijn vijfde op een zeker niveau met software bezig was, had ik zeer weinig hardware-ervaring buiten het werken met Arduinos op een basis "Adafruit maker"-niveau. Ik besefte niet hoe groot de uitdaging zou zijn, vooral omdat ik alles vanaf nul moest leren. Maar nu het af is, kan ik met zekerheid zeggen dat het het waard was. Het duurde langer dan het had moeten duren, maar daarvoor vraag ik begrip.

Een introductie tot het probleem

Je hebt waarschijnlijk video's op YouTube gezien van mensen die de opslag van hun iPod Classic upgraden naar 1TB, 2TB of zelfs 4TB (waarbij het vaak faalt omdat het RAM-geheugen opraakt om zoveel nummers te beheren). Hoewel ik de harde schijf in de grote iPods niet direct "gebruikersservicebaar" zou noemen, is dat zeker het geval vergeleken met de NAND-chips in de iPod Nano, Shuffle en alles wat Apple tegenwoordig maakt (met uitzondering van AirTags). Naïef dacht ik dat het vervangen van de NAND-chip in een Nano eenvoudig zou zijn en dat het gewoon zou werken.

Ik koos de iPod Nano 3e generatie (hierna het 'n3g' genoemd) om twee redenen. Ten eerste is het de nieuwste revisie van de Nano die een NAND-chip heeft met "poten" (wat minder complex was dan BGA-solderen, hoewel het nog steeds boven mijn niveau lag). Ten tweede is het de Nano die het dichtst bij mijn hart ligt, omdat ik er een had tijdens mijn jeugd.

Het uit elkaar halen van elke Nano is een hel; het is bijna onmogelijk om dit te doen zonder het apparaat te vernielen. Maar als je erin slaagt om in de n3g te komen, zie je direct de NAND-chip. Het desolderen is eenvoudig genoeg met langzame hete lucht, maar het solderen is lastiger. De eerste paar iPods werden gedaan door mijn goede vriend Wesley in zijn garage.

"Dit is het," dacht ik dwaas terwijl ik de camera vasthield, wachtend op die gezegende 16GB in de sectie "About" van de iPod-interface. Maar nee. In plaats daarvan kregen we de gevreesde Red X.

Staande in de garage van Wesley dacht ik dat dit betekende: "deze chip is niet geformatteerd, waar is mijn besturingssysteem?". Dat leek logisch. Ik probeerde de iPod te herstellen, maar ontdekte dat iTunes hem niet eens kon zien. Wesley, die ervaring heeft met embedded electronics, legde uit dat er meestal een tabel in de firmware staat met acceptabele NAND-chips, en als de chip niet in die tabel staat, stopt het proces simpelweg. Ik ging naar huis, vastberaden om uit te zoeken wat er aan de hand was en via een mod te zorgen dat het ding zou werken. Hoe moeilijk kon het zijn? Ik hoefde alleen maar de tabel te vinden en deze aan te passen met de details van de nieuwe NAND-chip, toch?

De eerste ontdekkingen

Mijn eerste ontdekking bevestigde dat Wesley gelijk had. Ik vond een soortgelijke tabel in de Rockbox-poort voor de n2g:

struct nand_device_info_type
{
    uint32_t id;
    uint16_t blocks;
    uint16_t userblocks;
    uint16_t pagesperblock;
    uint8_t blocksizeexponent;
    uint8_t tunk1;
    uint8_t twp;
    uint8_t tunk2;
    uint8_t tunk3;
} __attribute__((packed));

static const struct nand_device_info_type nand_deviceinfotable[] =
{
    {0x1580F1EC, 1024, 968, 0x40, 6, 2, 1, 2, 1},
    {0x1580DAEC, 2048, 1936, 0x40, 6, 2, 1, 2, 1},
    {0x15C1DAEC, 2048, 1936, 0x40, 6, 2, 1, 2, 1},
    {0x1510DCEC, 4096, 3872, 0x40, 6, 2, 1, 2, 1},
    {0x95C1DCEC, 4096, 3872, 0x40, 6, 2, 1, 2, 1},
    ...en veel meer...
};

Deze tabel moest ergens vandaan komen, dus ik moest vinden waar de versie voor de n3g in de firmware stond. Het bleek dat een bijna identieke tabel op verschillende plaatsen in de n3g-firmware voorkomt. Na onderzoek en gesprekken in de Rockbox IRC-kanalen, erfde ik wat half-voltooide reverse engineering-werkzaamheden van ontwikkelaars die jaren eerder probeerden Rockbox naar de n3g te poorten. Dit omvatte de Rockbox-bootloader en leidde tot mijn eerste succes in het uitvoeren van code op de iPod.

De Rockbox-bootloader gebruikt het Pwnage 2.0 exploit om code uit te voeren. In essentie is dit een stack-overflow exploit die mikt op een bug in de ASN.1/DER-certificaat parsing-logica van vroege Apple S5L8xxx BootROMs. Omdat de volledige context van de certificaatketen op de stack wordt gealloceerd, en de opgeslagen link register (LR) zich op een bekende offset net na het einde van die structuur bevindt, kan een aanvaller een kwaadaardig laatste certificaat maken. De te grote signatureValue overschrijdt de buffer met 344-345 bytes, waardoor de opgeslagen LR wordt overschreven met een adres dat door de aanvaller wordt beheerd. De aanvaller kan daar willekeurige executable shellcode plaatsen, waardoor de uitvoering wordt omgeleid naar die payload. Hiermee wordt volledige, niet-ondertekende code-executie op BootROM-niveau bereikt. Zodra je code kunt uitvoeren, kun je code zien en dumpen.

De EFI-bootloader en patching

Het eerste deel van de firmware dat we moeten aanpassen, is wat ik de EFI-bootloader noem. Ik was geschokt toen ik dit zag; ik dacht altijd dat EFI (en UEFI) primair voor computer-bootloaders was. Blijkbaar was Apple volledig overgestapt op EFI in de Apple TV, Macs en andere apparaten. Het voelde echter zwaar voor zoiets als een iPod, en het maakte statische analyse lastiger.

De NAND-tabel in de EFI bevindt zich in de NAND-driver. Tijdens de initialisatie controleert de NAND-driver de ID's van de NAND-banken, initialiseert buffergeheugen, de VFL en de FTL, en opent deze vervolgens. Het proces liep vast bij stap 1, dus ik verving de ID en geometrie in de tabel voor de chip die ik had.

In het begin was mijn patching-methodologie erg frustrerend. De Rockbox-bootloader kon lezen en schrijven naar NOR-flash. Elke keer dat ik een patch wilde proberen, moest ik deze vreselijke stappen volgen:

  1. In een hex-editor de bytes wijzigen voor de patches. Opslaan.
  2. Met UEFITool de NAND-driver PE32-binary vervangen door de mijne. Opslaan.
  3. Het door een zelfgemaakt binair diff-algoritme halen.
  4. Dit compileren in de Rockbox-bootloader.
  5. De Rockbox-bootloader uploaden en uitvoeren, die de EFI vanuit NOR laadt, de diff-bitstream erop uitpakt en de gewijzigde EFI terugschrijft naar NOR.
  6. De bootloader start de NOR en ik kijk of mijn patch werkt.

Dit proces was verschrikkelijk handmatig. Om het nog ingewikkelder te maken, hield ik mijn patches bij in een Excel-spreadsheet.

Het wijzigen van de parameters in de NAND-tabel hielp niet. Ik kreeg nog steeds de gevreesde Red X. Dat beeld heet bdhw (Bad Hardware) in NOR. Ik wist niet waarom het nog steeds niet werkte en had een vorm van introspectie nodig. De EFI exporteert namelijk niets dat voor de gebruiker zichtbaar is.

Tools voor introspectie

Tool 1: Spinning Ik kon codepaden bisecten door een b . (infinite loop) toe te voegen aan één tak van een conditionele instructie om te zien of de iPod vastliep of niet.

Tool 2: Data exfiltratie via de Diagnostic Mode Dit is interessanter. Ik ontdekte dat de EFI een System Information Table bouwt en doorgeeft aan de binary die het laadt. Deze tabel bevat alles van SysCfg (serienummer, modelnummer) tot RAM-informatie en, cruciaal, de NAND LBA-count. Dit deel van de tabel wordt gemaakt in het entry-point van de NAND-driver na initialisatie. De initialisatiecode geeft de berekende NAND LBA-count terug. Ik vroeg me af of we iets anders via die route naar buiten konden smokkelen, vier bytes per keer.

Door de NAND ID in de NAND LBA te plaatsen, verscheen deze in de NAND_SPEC test van de diagnostic mode. Hiermee kon ik bewijzen dat de iPod daadwerkelijk succesvol kon communiceren met de NAND-chip. Door Tool 1 en Tool 2 te combineren, kon ik vinden waarom de NAND-driver op dat punt faalde bij de initialisatie.

Waarom de driver faalde

Wanneer de iPod een gloednieuwe NAND-chip tegenkomt, probeert hij deze te "production formatten". Dit houdt in dat de VFL- en FTL-structuren worden opgezet. Dit deel faalde. Ik ontdekte dat het proces eindigde in een mislukte memcmp binnen een functie die de allereerste VFL-structuren schrijft. De driver wist de block, schreef de structuur (een driver-signatuur) en las deze terug om te verifiëren of het op de NAND was beland. De mislukte memcmp zorgde ervoor dat het systeem een paar keer opnieuw probeerde voordat het opgaf en resulteerde in bdhw.

Door data te smokkelen via NAND_SPEC ontdekte ik waarom: de verificatie-read gaf alleen maar FF terug, alsof de chip helemaal niet geschreven was.

De FMISS-laag

Hier ging ik een enorme rabbit hole in om te begrijpen hoe de NAND-peripheral werkt. In plaats van directe reads en writes naar een NAND-peripheral met adressen en commando's, zag ik iets dat een binary blob en parameters doorgaf en zei "ga".

Dit is een coprocessor die bespoke bytecode (of microcode) uitvoert om de interactie met NAND uit te besteden. Het heeft zijn eigen ISA (Instruction Set Architecture). Het uitzoeken hiervan was een van de meest bevredigende delen van het project.

Mijn uitgangspunt was een gelekt datasheet van de S5L8700X. Ik zag dat commando's zoals de READ ID (0x90) nooit in de ARM-code voorkwamen, dus ze moesten in de bytecode zitten. Door het Read ID-programma en het Read Page-programma te bestuderen, ontdekte ik dat elke instructie 64 bits is en georganiseerd is in een vreemde endianness.

Ik schreef een klein C-programma om de instructies te printen (een soort objdump voor deze bytecode). Uiteindelijk schreef ik een assembler en disassembler voor de bytecode en documenteerde ik ongeveer 90% van de instructieset. Samen met een andere ontwikkelaar (q3k) implementeerden we de FMISS-emulatie in QEMU.

Het goede nieuws is dat dit niets te maken had met mijn probleem. Hoewel het begrijpen van de FMISS-laag indirect nuttig was, had ik het project waarschijnlijk ook zonder deze stap kunnen voltooien.

Hardware-analyse en de WP#-pin

Nu bleef er maar één weg over: wat gebeurt er daadwerkelijk op de NAND-bus? Ik gebruikte een data recovery spider board en een DSLogic U3Pro32 om de signalen te analyseren. Wat ik vond was teleurstellend: de NAND weigerde simpelweg de page programs. Het leek alsof de chip write-protected was.

Ik controleerde de chip en ontdekte het ongelofelijke: de WP# leg (Write Protect) op de NAND was niet verbonden met het contactpunt; hij zat los als een melktand. Nadat ik deze had vastgesoldeerd, kwam ik verder, maar niet veel verder. De chip kon schrijven en verifiëren tot ongeveer pagina zes.

SLC vs. MLC NAND

De eerste zes pagina's werden perfect geschreven, maar vanaf pagina zes was de data nutteloos en vol ruis. Ik ontdekte dat dit een kenmerk is van MLC (Multi Level Cell) NAND.

In Single Level Cell (SLC) NAND houdt elke cel één bit. In MLC worden twee bits in één cel gepropt door vier voltageniveaus te definiëren. Hierdoor krimpt de marge voor een correcte uitlezing. De eerste bit is makkelijk te bepalen, maar de tweede bit is veel preciezer. Op deze chip delen pagina 0 en 6 cellen. De "makkelijke" eerste bit gaat naar pagina 0 (die is schoon), en de "moeilijke" tweede bit gaat naar pagina 6 (die is ruisgevoelig).

Ik had twee keuzes: de foutcorrectie (ECC) van de iPod verbeteren, of een dure SLC-chip kopen. Ik koos voor de SLC-chip. De EFI kon de chip nu succesvol formatteren en we bereikten de disk mode!

De uitdagingen van Disk Mode

Disk Mode is in feite een gestripte versie van het volledige besturingssysteem, beide gebaseerd op een real-time OS genaamd RTXC.

De iPod communiceert via SCSI over USB. Het SCSI "READ CAPACITY" commando geeft het aantal blokken en de grootte van de blokken terug. In Disk Mode wordt de capaciteit berekend door de logische blokgrootte te delen door de NAND-paginasize. Bij alle chips die Apple in de n3g gebruikte, was dit een logisch geheel getal. Echter, met de nieuwe NAND van 8192 bytes (terwijl de sector 4096 bytes is), werd de berekening 4096 / 8192 = 0. Dit leidde tot een data-abort, een panic en een boot loop.

Ik patchte dit zodat de ratio op 8192 werd berekend, waardoor de crash stopte. Maar het apparaat monteerde nog steeds niets. In dmesg zag ik: Unsupported sector size 8192.

Rehosting en Emulatie

Om sneller te kunnen analyseren, benaderde ik het probleem via rehosting:

  1. Full Emulation: Ik begon met de Unicorn Engine en stapte later over naar een QEMU-fork. Ik implementeerde de I2C-peripheral, de hardware JPEG-decoder en de FMISS-instructieset. Uiteindelijk kon ik RetailOS booten en via USB/IP virtueel aan mijn machine koppelen.
  2. User Space Emulation: Ik draaide de NAND-driver in Linux Userland op een Raspberry Pi 3b+. Door de driver uit de EFI-bootloader in het geheugen te laden en functies te patchen, kon ik de driver initialiseren en FTL_Read uitvoeren zonder echte hardware. Dit bewees dat het project mogelijk was.

Een Nieuwe Benadering: De Translator

Het probleem met de sectorgrootte was dat 4096 bytes de limiet is van wat Linux (en de meeste OS'en) kan afhandelen. Een sector van 8192 bytes zou de iPod "Linux-only" maken.

De oplossing was een translator. Het logische blok hoeft immers niet gelijk te zijn aan de fysieke pagina. Een pagina van 8192 bytes is precies twee blokken van 4096 bytes. Ik plaatste een vertaallaag op het punt waar verzoeken de chip bereiken:

  • Boven de translator: Alles spreekt in 4 KiB logische blokken.
  • De translator: Vertoont een 2:1 vertaling.
  • Onder de translator: De NAND-stack spreekt in 8 KiB fysieke pagina's.

Logische bloknummers worden gedeeld door twee. Bij schrijven is een read-modify-write nodig, omdat je geen halve pagina kunt programmeren. Je haalt de hele 8KiB pagina op, vervangt jouw halve deel en schrijft het geheel terug.

Patching van Disk Mode

Ik implementeerde de translator in Disk Mode omdat dit makkelijker was (het heeft logging en is minder complex dan RetailOS). Ik moest drie dingen aanpassen:

  1. Blokgrootte rapporteren: De functie moet onvoorwaardelijk 4096 teruggeven in plaats van de hardwarematige 8192.
  2. Capaciteitsberekening: De deling door nul (ratio 0) werd vervangen door een shift-operatie: het aantal logische blokken is het aantal pagina's maal twee.
  3. Aantal blokken rapporteren: Ik berekende de capaciteit direct vanuit de FTL-geometrie: gebruikerspagina-count × 2 minus een reserve van 256 MiB aan de bovenkant van de chip.

Ik plaatste deze code in een ongebruikt stuk van de binary. Daarnaast vergrootte ik de memory allocation pool van Disk Mode, omdat een 8KiB pagina meer RAM vereist voor tracking.

Chipstructuur en Erasing Planes

Een ander probleem was de organisatie van de chip. De firmware denkt dat de NAND is georganiseerd in blokken van 256 pagina's. De chip heeft echter blokken van 128 pagina's, verdeeld over twee "planes".

De write-path programmeerde beide planes, maar de erase-path wistte alleen plane 0. Dit betekende dat bij het recyclen van een blok, de tweede helft nooit werd gewist, wat leidde tot fouten. De oplossing was simpel: twee keer wissen (één keer voor elke plane).

De "Poison" Bug (ECC)

De driver leest pagina's in chunks en berekent een checksum. Bij een paginagrootte van 8KiB (twee keer wat de ECC-engine verwacht) gaf de engine bij een perfect lege pagina een verdict van "uncorrectable". De driver dacht dat de data vernietigd was, waardoor goede media als "bad" werden gemarkeerd.

De oplossing was om het per-chunk status-verdict te negeren als de pagina simpelweg leeg was (herkenbaar aan de eerste acht bytes).

De boot-volgorde van de iPod

Om alles te patchen, moest ik begrijpen hoe de iPod start:

  • Stage 0 (BootROM): Ingebakken in de chip. Controleert signatures en start de EFI.
  • Stage 1 (WTF): Een tijdelijke fase in RAM om NOR-inhoud te ontvangen.
  • Stage 2 (Recovery): Uploaden van de recovery image (tijdelijke EFI, Disk Mode).
  • Stage 3 (Firmware): Disk Mode ontvangt de firmware-container (rsrc, osos, aupd).
  • Stage 4 (AUPD): De updater flasht de permanente EFI naar NOR.
  • Stage 5 (RetailOS): De iPod boot volledig in het besturingssysteem.

Dit betekent dat ik elke binary die met NAND werkt (EFI, Disk Mode, AUPD, RetailOS) apart moest patchen.

Patching de EFI en RetailOS

De EFI

De EFI-driver is gecompileerd naar THUMB, dus de patches moesten worden herschreven. Tijdens het debuggen liep ik tegen een "frozen logo" aan. In QEMU werkte alles, maar op echte hardware niet. De oorzaak was een fout in QEMU: ik gebruikte een nop encoding (0xbf00) die THUMB-2 is. Een echte ARMv5 CPU (die alleen THUMB-1 ondersteunt) ziet dit als een illegale instructie en hangt op.

Ook ontdekte ik een bug bij de "bad block scan". De EFI scande alleen even-nummers blokken. Als een oneven blok bad was, merkte de EFI dit niet op, wat later tot een crash leidde. Ik heb de scan uitgebreid naar vier pagina's per superblock.

RetailOS (osos)

Bij RetailOS liep ik tegen een data-abort aan. De oorzaak was een conflict in de memory pool. RetailOS deelt één allocator tussen de NAND-stack en de user interface. Omdat ik de pool-grootte blind had gekopieerd van Disk Mode, raakte het geheugen op zodra de UI fonts (glyphs) begon te laden.

Ik heb de pool correct geschaald op basis van de werkelijke behoeften (1.5 MiB voor de FTL en de rest voor de UI) en guards toegevoegd aan de bitmap-allocator om crashes bij null-pointers te voorkomen.

Het resultaat

Na zes jaar is het gelukt. Settings → About meldt 15GB (de reserve doet zijn werk). Het bestandssysteem overleeft een reboot, het menu wordt getekend en de muziek speelt af.

Laatste problemen: Voeding en Partities

Er waren nog twee stabiliteitsproblemen:

  1. Voeding: De batterij was gedegradeerd. USB alleen kan niet genoeg stroom leveren voor de NAND-programmeerbursts, wat leidde tot corruptie. Een externe voeding op de batterij-leads loste dit op.
  2. Partities: Ik gebruikte gparted om de disk te formatteren, maar dit overschreef de firmware-volume (die op LBA 63 begint). De muziek-sync van iTunes overschreef effectief de boot-image. De oplossing is om de iPod zelf de partitietabel te laten schrijven via de interne repartition-commando's, zodat de data-partitie pas start na het firmware-volume (LBA 32831).

Conclusie

De iPod is nu volledig functioneel met 16GB aan opslag. Ik heb alle Harry Potter-films en een enorme hoeveelheid muziek erop kunnen zetten. Dit was een langdurig project dat me veel heeft geleerd over reverse engineering, emulatie en hardware.

De code voor dit project is beschikbaar op GitHub: lemonjesus/iPod-n3g-16gb.

***

Openbaarmaking AI-gebruik

Ik geloof in transparantie over het gebruik van AI. In de laatste maanden van dit project heb ik LLM's (waaronder Claude) gebruikt voor:

  • Het begrijpen van patronen in firmware en binaries.
  • Het ontdekken van de "ECC Scrubbing Patches".
  • Let op: Elke regel code is door mijzelf gelezen en goedgekeurd. De tekst van dit artikel en het script van de bijbehorende video zijn 100% door mij geschreven.

Dankwoord

Mijn dank gaat uit naar:

  • q3k voor wInd3x en de EFI-modificatie-engine.
  • slackware voor hulp bij het software-onderzoek en het beheer van freemyipod.org.
  • benedikt93 en Cástor Muñoz voor het grondwerk bij het reverse engineeren van NAND en de Rockbox-bootloader voor de n3g.
  • De community op Discord voor hun bijdragen aan de iPod Nano-modding scene.