FiiO X1 factory.x1: unsigned code execution from the SD card
Contents
Put a file named factory.x1 on the SD card of a FiiO X1, a portable music player built on an Ingenic JZ4760B MIPS chip, boot it down the recovery path (the button-combination boot that also installs firmware updates from the card), and its bootloader jumps into that file without asking for a signature. I read that in a decompiled bootloader after four parts of getting in the hard way: the chip's boot ROM over USB, a RAM (DDR) capture from live registers, a NAND flash dump with the hwstub debug stub from Rockbox (the open-source player firmware project), and a decompiled IPL, the first-stage loader that sets up RAM. The stage after the IPL had been willing all along.
The SPL, the second-stage program loader, is X-Boot — Ingenic's second-stage bootloader. It runs after the IPL initializes DDR. Its job: a little more hardware setup, check the SD card for firmware update files, boot sys.bin, the main firmware, from NAND.
At offset 0x114CC, on the SPL's recovery path (the one that otherwise tries an x1.fw update), the decompile from Ghidra, the NSA's open-source reverse-engineering tool, reads like this, cut at the jump:
iVar3 = func_0x00616f34(&stack0x00000018,0x80619e38,1,0);
if (iVar3 == 0) {
iVar3 = func_0x00616b50(in_stack_00000018,0,2);
if (iVar3 == 0) {
iVar3 = func_0x00615e60(in_stack_00000018,&stack0x00000014);
if (((iVar3 == 0) && (iVar3 = func_0x00616b50(in_stack_00000018,0,0), iVar3 == 0)) &&
(iVar3 = func_0x006165d4(&SUB_80001000,in_stack_00000014,&stack0x00000014,in_stack_00000018
), iVar3 == 0)) {
func_0x00615ef4(in_stack_00000018);
func_0x00600038();
if (0 < (int)in_stack_00000014) {
uVar5 = 0;
uVar4 = 0;
do {
if (uVar4 + _DAT_806b9880 <= uVar5) {
uVar4 = uVar4 + _DAT_806b9880;
}
iVar3 = uVar5 - uVar4;
pbVar7 = &SUB_80001000 + uVar5;
uVar5 = uVar5 + 1;
*pbVar7 = *pbVar7 ^ *(byte *)(iVar3 * 4 + _DAT_806b9884);
} while ((int)uVar5 < (int)in_stack_00000014);
}
func_0x00600860(0x80619e44);
func_0x00600920(&SUB_80001000);
func_0x00600fc0();
(*(code *)&SUB_80001000)();
0x80619e38 is the string factory.x1. The func_0x0061… calls are the SPL's FAT routines: open the file, seek to the end, read the size, seek back, read the whole thing to 0x80001000, close. The loop XORs every byte with a key table whose length and address sit at 0x806b9880 and 0x806b9884. func_0x00600fc0 flushes the caches. Then it calls 0x80001000.
No signature check. No hash verification. No secure boot. If factory.x1 is on the card when X-Boot takes that path, X-Boot descrambles it and jumps in. That's my unit's X-Boot; Rockbox's own tooling builds payloads under the same file name, so it isn't a one-unit quirk.
Award number three in this series, and the easiest one yet to judge: Widest Open Door in a Shipping Product, to the factory.x1 check in X-Boot at offset 0x114CC.
The factory.x1 key: XOR with 172 primes#
The "decryption" is XOR with a 344-byte key: the lower 8 bits of the first 172 prime numbers, repeated twice. Primes above 255 get truncated — prime 257 becomes 0x01, prime 1021 becomes 0xFD. Rockbox's packtools.cpp carries the table:
/* This table is generated programmatically: it contains two copies of a 172-bytes
* table. This table contains the lower 8-bit of the first 172 prime numbers:
* 2, 3, 5, 7, ..., 251(=0xfb), 257(=0x101 -> 0x01), ... */
const unsigned JZ4760_XOR_KEY_SIZE = 344;
uint8_t jz4760_xor_key[JZ4760_XOR_KEY_SIZE] =
{
0x02, 0x03, 0x05, 0x07, 0x0b, 0x0d, 0x11, 0x13,
That file is copyright 2015, so by now the key has been sitting in Rockbox's public tree for years, comment included.
Applying the scramble:
packtools --scramble -i custom.bin -o factory.x1
This is rot13 with a math degree.
What's running when factory.x1 gets control#
By the time factory.x1 executes, the IPL and SPL have done their work:
| Item | State |
|---|---|
| DDR | Initialized and calibrated by the IPL |
| CPU clock | 384MHz when the IPL hands over |
| UART1 | 57600 baud, PD28 TX / PD26 RX |
| NAND | NEMC enabled, BCH-8 controller clocked |
| SD/MMC | Initialized (SPL just used it to read your file) |
| Stack | Valid, deep in SDRAM |
| Audio DAC | Not initialized |
Part 2's script decoded 288MHz from the PLL register the stock firmware left behind; by the programming manual's divider table that value is 144MHz. The IPL programs PLL0 with 0x40090120, which works out to 384MHz from the 12 MHz crystal, and prints Freq: followed by 384,000,000, in hex, before it jumps to the SPL.
Initialized RAM. Real peripherals. A working stack. A 384MHz MIPS32 core. On paper this is a complete embedded development environment, and you get there with a FAT-formatted SD card and one command.
The contrast with boot ROM code execution#
Compare to what I had in Part 1: I-cache execution, no stack, no sw to peripherals, 32 registers and nothing else. I spent hours proving that battery voltage could be read with sb (store byte) instructions, in MIPS opcodes I built byte by byte in Python. That was the hard way in, forced by the boot ROM constraint that SDRAM doesn't exist until someone initializes it.
factory.x1 is the easy way in. It was always there, and the whole time I was hand-assembling nops, a FAT-formatted SD card was offering me a stack.
The reason to spend Parts 1 through 3 doing it the hard way: by Part 3 I had read that factory.x1 existed, but not what it actually did or what state it left the hardware in. That required understanding the boot sequence end-to-end. The IPL decompilation in Part 4 confirmed DDR is fully initialized before the SPL runs — which meant factory.x1 gets a real environment, not a stripped-down one.
The irony is structural. Rockbox's hwstub build, the one I fought through Part 3's version archaeology, already writes its output as factory.x1: the last step of its Makefile is packtools --scramble. The door's name was in a build script I ran. What the door did was in the SPL, and dumping the SPL took the hwstub that same build produced. That adventure started because the I-cache couldn't read NAND. The entire journey through Parts 1-4 was necessary to understand a mechanism that makes most of Parts 1-4 unnecessary for the next person. I'd like to file a complaint about this, and the only party I can file it with is me.
Building a factory.x1 payload#
Linker script — load address matches where X-Boot drops the payload:
OUTPUT_FORMAT("elf32-tradlittlemips")
OUTPUT_ARCH(mips)
ENTRY(_start)
SECTIONS {
. = 0x80001000;
.text : { *(.text) }
.rodata : { *(.rodata) }
.data : { *(.data) }
.bss : { *(.bss) }
}
Build and scramble:
mips64-linux-gnu-gcc -EL -mips32 -mabi=32 -fno-pic -mno-abicalls \
-nostdlib -T linker.ld -o custom.elf custom.c
mips64-linux-gnu-objcopy -O binary custom.elf custom.bin
packtools --scramble -i custom.bin -o factory.x1
Copy factory.x1 to the SD card root, boot the X1 down the recovery path, and X-Boot should run it. Should: as of this post I had built payloads this way but not yet run one from a card.
UART1 at 57600 baud is the output channel, once something is wired to it. Pin PD28 is TX. The IPL already initialized it, and its Serial ok Time: banner goes out before anything your code sends.
Where hwstub fits#
The hwstub build from Part 3 produces a factory.x1 payload; that is what Rockbox's Makefile for the JZ4760B stub targets. In Part 3 I didn't use that file. I uploaded the plain hwstub.bin over USB with usbboot --stage1, and the Lua NAND dump scripts ran in hwstub_shell, which talks to the USB memory server that hwstub exposes.
The NAND dump from Part 3 came from that USB-loaded hwstub. The IPL analysis in Part 4 came from that dump, and so did the SPL dump behind this post. The understanding of factory.x1 comes from the SPL. The loop closes, just not through the SD card.
Who put the door there: factory test mode#
The name says what factory.x1 is for: production testing. Presumably the factory floor needed to run diagnostics on assembled X1 units before shipping. Requiring physical UART connections or JTAG probes at scale is slow and expensive. An SD card with a known-format payload is fast, cheap, and needs no custom jigs beyond whatever mounts the SD card.
The XOR key is security by obscurity — it keeps random files from accidentally executing and keeps the format from being immediately obvious. It doesn't keep anyone out who actually reads the firmware. It is a lock in the sense that a curtain is a lock.
It lives in the SPL, waiting for whoever reads that far.
What this means for the Rockbox port#
The gap that stalled the Rockbox port since 2015 was the DDR timing parameters. With those parameters now sourced from the decompiled IPL rather than Part 2's live capture, the ingenic-boot stage1 can be rebuilt with correct values. From here:
- Build ingenic-boot stage1 with confirmed DDR parameters
- Upload via USB boot, verify DDR initialization succeeds
- Build hwstub as
factory.x1, confirm full NAND access - Port Rockbox bootloader to replace X-Boot
- Run Rockbox on the X1
None of these is finished. Part 3's stage1 builds all failed with the old parameters, and the hwstub build already emits a factory.x1 that has not run from a card yet. The map is drawn; the walking hasn't started.
Years after it shipped, the FiiO X1 is better understood than the day it left the factory.
The end of the first arc#
This closes the series' first arc, the boot chain: from "USB boot mode exists, what can I do with it" to NAND access over USB, every DDR init parameter read out of the IPL, and a code-execution path through the SD card that needs no hardware modification, at least on paper.
The next posts are about what you do with that access. There's a UART to wire up, a NAND to fully map, and a Rockbox target to bring up.
The door was always open. I just needed to know where to look.
So one more award, and the only one I'm giving myself: Longest Route to an Unlocked Door. Five parts, a boot ROM, a deadlock, a 64-bit compiler and a decompiled IPL, to arrive at a file name a Rockbox Makefile had already printed at me in Part 3. Knowing what I know now, I'd still have had to take it: the only map of what was behind the door was the route.
Filed under: reverse-engineering, mips, embedded, fiio, hardware
Read next#
- FiiO X1 boot ROM: running code on a JZ4760B with no RAM — the hard way in that this part makes unnecessary, for contrast.
- Dumping all 128 MB of the FiiO X1's flash — the next part: two programs written to dump the whole NAND, and which one actually finished the job.
Sources#
- Rockbox FiiO X1 port thread — factory.x1 mechanism discussion, hwstub usage, community RE coordination
- Rockbox packtools.cpp (XOR key) —
jz4760_xor_keydefinition,--scramble/--descrambleimplementation - Rockbox jz4760_tools — packtools build, scramble utilities
- Rockbox hwstub source — USB memory server; the
jz4760b/Makefilebuilds its output asfactory.x1 - ingenic-boot stage1 (JZ4760B) — DDR init reference implementation, PLL encoding
- Ingenic JZ4760 Datasheet (PDF) — UART1 pins PD26/PD28
- 4PDA FiiO X1 custom firmware thread — Russian-language FiiO X1 mod scene (custom themes, modified firmware)