FiiO X1 NAND map: fonts, plaintext and a stranger's music

422 of the 1,024 blocks on the flash chip were holding data. My map said 128.

The chip belongs to a FiiO X1, a $99 portable music player whose Ingenic JZ4760B MIPS chip runs FiiO's firmware on the µC/OS-II real-time kernel, and the map was mine: the first pass over a raw 128MB dump of its NAND flash, taken over USB with Rockbox's hwstub debug stub in Part 6. The goal is the firmware code itself, sys.bin, which I hadn't found. That map marked everything past block 215 as erased userdata. I had not opened those blocks. I had looked at where they sat and written down what I expected.

That earns this series its third rule. Rule three: a map drawn before every block is read is a forecast with a legend.

A classifier script across all 1,024 blocks found two populated regions where the map said "empty", plus a metadata block at the very end: font data with its tables and a boot animation, and a media database with file paths from the music library of the player's previous owner. The fonts turned out to be files from the firmware update image. The database is not, and it outlived the firmware updates since.

Finding the firmware in its own home#

The first question was where sys.bin lives. I had the binary from unpacking x1.fw, the firmware update image: 4,309,388 bytes of µC/OS-II, XOR-encrypted with the prime-number key from Part 5. The raw dump labels nothing. Page 0 of block 64, where Part 6 put the firmware, does not start with code, and blocks 64-163 turn out to be boot resources.

The search: take the first 8 bytes of sys_decrypted.bin, XOR-encrypt them with the prime key, and scan every page for the result. The first instruction of sys.bin is lui $ra, 0x8000 (0x3C1F8000), so the encrypted form is known.

It matched at block 164, page 17: absolute NAND page 10513.

From there, the extraction follows the data through the following blocks, skips every 5th block (an erased bad-block reserve), XOR-decrypts with the 172-byte key, and compares the result against sys_decrypted.bin from x1.fw:

Extracted: 4309388 bytes
Matching: 4309388/4309388 (100%)
PERFECT MATCH!

Every byte. The firmware sits on NAND at a fixed physical location, "encrypted" with a key that has been in the Rockbox source tree since 2015.

The PN randomizer that isn't used#

Part 6 assumed that blocks 16 and up were scrambled by the Ingenic PN (pseudo-noise) randomizer, a hardware XOR with an LFSR sequence that the NAND controller can apply on writes to break up long runs of identical bits. Rockbox's nand.lua even toggles the PNC register when it reads the bootloader. So the plan was to de-randomize the raw dump before anything would make sense.

Block 69, first 16 bytes:

Block 69: 89504e470d0a1a0a0000000d49484452  PNG!

A PNG file header, in the clear.

Block 16, the start of the SPL, is the harder test. De-randomizing it with every seed I tried produced bytes that matched nothing. The raw block, untouched, matched the SPL dump from Part 3 on all 64 pages. The PN hardware exists, Rockbox knows where its switch is, and nothing on this chip uses it.

The whole 128MB NAND is plaintext. The XOR prime key on sys.bin is the only protection on the chip.

1,609 PNGs and a Photoshop UUID#

Blocks 64-163, the 10MB in front of sys.bin, are the boot resource partition: icons, backgrounds, progress bars. binwalk counts 1,609 PNG images in it, most of the UI the X1 draws, stored as individual files on raw flash.

Block 69's first image is 320×217 pixels. Its XMP metadata:

xmp:CreatorTool="Adobe Photoshop CC (Windows)"
xmpMM:OriginalDocumentID="xmp.did:6ea6121f-ee38-894f-8a24-7b4d63d4d6e2"

Someone at FiiO, or a contractor, exported these from Photoshop on Windows (block 69 alone names both CC and CS5), and that document UUID ships inside this firmware. The PNGs range from icons 13×7 pixels across to 320×240 full-screen backgrounds. Also present: a 320×240×32bpp BMP boot logo, byte-for-byte identical to logo.bmp from x1.fw.

All unencrypted. A custom theme is a matter of replacing PNGs and repacking the firmware image.

Fonts that passed for MIPS#

Blocks 414-703 are the second populated region, in territory the Part 6 map called empty. The classifier's first pass:

=== FTL Region (414-703) Content Types ===
  MIPS_CODE   : 197 blocks
  BINARY      :  18 blocks
  PNG         :   5 blocks

197 blocks of MIPS code is about 25MB. sys.bin is 4.1MB. Six firmware images' worth of code, in a region nothing should be using. I have been doing this long enough to know that a result like that is a bug report about my own script.

It was font data.

The bug was in the threshold. The classifier read the first 4 KB of each block and called it MIPS_CODE when more than 10 of those 1,024 words had the bit pattern of a lui instruction, which fixes only the top six bits. Six fixed bits match about one word in 64 of any dense binary data, so a random 4 KB sample scores about 16 without trying. The non-erased blocks in this region score a median of 17. The heuristic had been counting bytes and calling them code.

The actual contents:

FileSizeBlocks
msyh.ttf (Microsoft YaHei)~21MB~200
Korean.ttf~4.3MB~33
Thai.ttf~30KB1

Microsoft YaHei, a Chinese font so large it spans about 200 NAND blocks. The X1 ships Chinese, Korean and Thai text as raw TrueType files on flash. Next to the fonts: Unicode conversion tables (iso_8859_2_unicode_tbl.bin and friends), 40 boot animation JPEG frames, and litegui theme resources.

All plaintext. The fonts alone are about six times the size of the firmware.

A stranger's music#

Blocks 786-994 are the third region, with 63 blocks of structured records. The records are file paths: a:\, which is how the firmware names the SD card, then a folder, then a track. Thousands of them.

The player's media database, and most of it is not mine. I bought this X1 second-hand, so the bulk of the library it indexes is the previous owner's: about two thirds of the paths sit in one flat folder, and most of the rest in folders named after genres. The same database also lists one album folder from my own SD card, so the player had been taking notes on me too.

Counting distinct path strings in those 63 blocks gives about 8,900 ending in .mp3 and about 4,300 in .flac, plus one .ape. That counts distinct strings in raw flash, not tracks: the 63 blocks hold about 48,800 path strings in all, most of them two to five times over, and each distinct one is counted once. Roughly two MP3 paths for every FLAC.

Someone cared about this device enough to fill it. Then it changed hands, and every firmware flash since has rewritten the firmware region and left the database where it was.

The music library survived the reflash. I can't hear a note of it. I can count it by extension, by folder and by bit depth, which is the most intimate thing a hex dump has ever let me do.

The complete map#

Block 0:         IPL (8KB plaintext)
Blocks 1-15:     Erased
Blocks 16-21:    SPL/X-Boot (768KB plaintext)
Blocks 22-63:    Erased
Blocks 64-163:   Boot resources — 1609 PNGs, BMP, INI, XML (10MB plaintext)
Block 164 pg17:  sys.bin starts here (4.1MB, XOR encrypted)
Blocks 164-214:  sys.bin + remaining resources (skip every 5th block)
Blocks 215-413:  Erased
Blocks 414-703:  Fonts (25MB), unicode tables, boot animation (all plaintext)
Blocks 704-785:  Erased
Blocks 786-994:  Media database — mostly the previous owner's library (plaintext)
Blocks 995-1022: Erased
Block 1023:      FTL metadata

Three things stand out.

Security is cosmetic. The PN randomizer is off. The only encryption is a 172-byte XOR key on sys.bin. Boot resources, fonts and user data sit in plaintext. Injecting firmware means writing to block 164, page 17, with the right XOR applied. Custom firmware is a Python script away.

The firmware region is simpler than feared. No remapping I could see in the reserves: every 5th block is a pre-allocated bad-block reserve, and all of them sit erased. The FTL keeps metadata in block 1023. For a partition table that addresses 460MB of virtual space on 128MB of flash, the firmware layout is straightforward.

Firmware updates don't erase everything. The update rewrites the firmware region, and the fonts, Unicode tables and animation frames in 414-703 are files from the same update image. The user data in 786-994 is not, and it survives. A second-hand X1 carries its previous owner's library in flash, readable by anyone with a USB cable and seven hours of patience.

What comes next#

This was supposed to be the hard part, finding the firmware needle in a 128MB haystack. It took one pattern search, one hit, one script, one perfect match. The fonts were harder to identify than the firmware, which is the kind of sentence rule three exists to prevent.

The next step is the firmware itself: 4.1MB of µC/OS-II compiled for MIPS32, running the audio pipeline. The Ghidra work has started. One gap is still on this map's legend, though: the first 44KB of block 64, which matched nothing. That one gets its own part.

The NAND gave up a boot chain in plaintext, a designer's document UUID, a 21-megabyte Chinese font impersonating executable code, forty frames of boot animation, and a stranger's music library, still on the chip, waiting for nobody. I wrote "erased" over all of it, once. The map is redrawn now, from bytes this time.

Filed under: reverse-engineering, mips, embedded, fiio, hardware

Sources#