boot1
boot1 is the second stage of the Starlet boot chain. boot0 loads it into SRAM, decrypts and verifies it and jumps to 0xFFF00000. boot1 then finds boot2 in NAND, checks its signature chain, decrypts and hashes it into MEM1 and jumps to it.
boot1 is a 94 KiB (0x17800 byte) image. It starts executing through the 0xFFF00000 alias of SRAM but its pointers to its own data and to some of its own functions use the 0x0D400000 alias. Both aliases refer to the same memory. The memory map shows the mappings.
Entry
boot1 starts in ARM state at 0xFFF00000. The entry code prepares the caches and the stack, calls the loader and then jumps to whatever address the loader returns:
void start(void)
{
invalidate_icache(); // MCR p15, 0, r0, c7, c5, 0
invalidate_dcache(); // MCR p15, 0, r0, c7, c6, 0
cp15_control |= 1 << 12; // enable the instruction cache
memset32(bss_start, bss_end, 0, 4); // the range is empty in boot1
sp = 0x0D40EFFC;
void *entry = load_boot2(); // Thumb code
cp15_control &= ~(1 << 12); // disable the instruction cache
goto *entry; // MOV PC, R1, stays in ARM state
}
Loader
load_boot2 does all the work. It writes progress code 0x11 to bits 23:16 of 0x0D8000E0 (the same GPIO field that boot0 uses for its error codes). It reads the byte at 0x0D800217 and writes 0x47 to HW_SRNPROT if the upper four bits of that byte are zero or 0x07 otherwise.
After that, boot1 makes up to two attempts to load boot2. The first attempt searches NAND blocks 1 to 7 in ascending order. If it fails, boot1 waits for the NAND, AES and SHA-1 engines to finish and tries again, this time searching from block 7 down to block 1. If the second attempt also fails, boot1 shows the error code forever.
void *load_boot2(void)
{
int error;
debug_out(0x11);
HW_SRNPROT = (read8(0x0D800217) >> 4) == 0 ? 0x47 : 0x07;
for (int attempt = 1; attempt <= 2; attempt++) {
int first = attempt == 1 ? 1 : 7;
int step = attempt == 1 ? +1 : -1;
if (attempt == 2)
wait_until_idle(NAND, AES, SHA1);
read_block_map(&map);
error = load(first, step, &map, &content);
if (error == 0) {
write32(content + 0x0C, 1);
debug_out(0x22);
return content + read32(content);
}
}
for (;;) {
debug_out(error);
delay(1000000);
debug_out(~error);
delay(1000000);
}
}
debug_out(code) writes the low byte of code to bits 23:16 of 0x0D8000E0.
Finding boot2
Block map
Before each attempt, boot1 looks for a block map. It reads the last page (page 63) of every block from 1 to 7. Each of these pages can hold three copies of a 76 byte map, at offsets 0x00, 0x4C and 0x98:
| Offset | Size | Contents |
|---|---|---|
0x00 |
8 | Magic, an 8 byte constant stored in boot1 |
0x08 |
4 | Generation number, big-endian |
0x0C |
64 | One byte per block number, 1 means skip block |
A page only provides a map if at least two of its three copies are identical over all 76 bytes. boot1 uses copy 0 when its magic matches and it is identical to copy 1 or copy 2. Otherwise it uses copy 1 when its magic matches and it is identical to copy 2. If neither applies, the page provides no map. Out of all accepted copies in blocks 1 to 7, the one with the highest generation wins.
Choosing a block
If the winning generation is greater than 1, boot1 trusts the map and skips every block whose byte is 1. If there is no usable map, it checks the blocks directly instead: it reads pages 0 and 1 of the block and skips the block if the first spare byte of either page is not 0xFF.
The first block that is not skipped, in the direction of the current attempt, holds the start of boot2. If no such block exists between 1 and 7, the attempt fails with error -4.
Reading pages
boot1 reads a page by programming the NAND engine with these writes:
0x0D010008=0,0x0D01000C= page number,0x0D010000=0x9F000000.- Wait until bit 31 of
0x0D010000reads as 0. 0x0D010010= data buffer address,0x0D010014= spare buffer address,0x0D010000=0x8030B840.
The function returns right away. boot1 waits for bit 31 of 0x0D010000 to clear before it uses the page and corrects every header and content page with ecc_correct.
boot2 layout
Page 0 of the boot2 block starts with a 32 byte header. All values are big-endian:
| Offset | Contents |
|---|---|
0x00 |
Header size, must be 0x20 |
0x04 |
Offset of the encrypted content from the start of the block |
0x08 |
Size of the certificate chain |
0x0C |
Size of the ticket |
0x10 |
Size of the TMD |
The content offset must not be larger than 0x20000 and must be a multiple of 64. Otherwise boot1 fails with error -5.
boot1 reads pages 0 up to content_offset / 0x800 of the block into MEM1 at 0x00028000. The certificate chain follows the header, the ticket follows the certificate chain and the TMD follows the ticket:
0x00028000 header (0x20 bytes)
+ 0x20 certificate chain
+ 0x20 + certs ticket
+ 0x20 + certs + ticket TMD
Signature checks
boot1 checks five signed blobs. Each check copies the blob to MEM1 at 0x000AC000, compares the 64 byte issuer field with the expected issuer string, hashes everything from the issuer field to the end of the blob with SHA-1 and checks an RSA signature over that hash:
int verify_signed(const u8 *blob, const char *issuer, const u8 *modulus,
const u8 *exponent, int kind, u32 size)
{
u8 *copy = (u8 *)0x000AC000;
u32 body = kind == 1 ? 0x240 : 0x140; // kind 1: RSA-4096, kinds 2-4: RSA-2048
u32 key_len = kind == 1 ? 0x200 : 0x100;
memcpy(copy, blob, size);
if (memcmp(copy + body, issuer, 64) != 0)
return -8;
u8 hash[20] = SHA1(copy + body, size - body);
u8 m[key_len] = RSA_Public(copy + 4, modulus, exponent); // m = sig^e mod n, big-endian
if (strncmp(m + key_len - 20, hash, 20) != 0)
return -8;
return 0;
}
The signature starts at offset 4 of the blob. The 32 bit value at offset 0 is not looked at. The public exponent is stored as 4 big-endian bytes. boot1 drops any trailing zero bytes before using it.
The check has two vulns:
- Only the last 20 bytes of
mare compared with the hash. The padding in front of them is ignored. - The comparison uses
strncmp, which stops at the first position where both strings contain a zero byte. If the first byte of the hash is0x00and the matching byte ofmis also0x00, the check passes no matter what the remaining 19 bytes are.
The checks run in this order. Offsets are relative to the start of the certificate chain:
| Blob | Size | Expected issuer | Key |
|---|---|---|---|
Certificate at 0x000 |
0x400 |
Root |
Root key stored in boot1 (RSA-4096) |
Certificate at 0x400 |
0x300 |
Root- + name of the first certificate |
Modulus at 0x2C8, exponent at 0x3C8 |
| TMD | TMD size | Root- + first name + - + second name |
Modulus at 0x5C8, exponent at 0x6C8 |
Certificate at 0x700 |
0x300 |
Root- + name of the first certificate |
Modulus at 0x2C8, exponent at 0x3C8 |
| Ticket | 0x2A4 |
Root- + first name + - + third name |
Modulus at 0x8C8, exponent at 0x9C8 |
The certificate names are stored at 0x284, 0x584 and 0x884. Before a name is used, boot1 checks its first two characters: they must be CA, CP and XS respectively or the attempt fails with error -9.
Decryption and hashing
Once all signatures pass, boot1 decrypts the content key, checks the TMD and loads the content:
- It decrypts the 16 bytes at offset
0x1BFof the ticket with AES-128-CBC. The key is a constant stored in boot1. The IV is the 8 bytes at offset0x18Cof the TMD followed by 8 zero bytes. This decryption happens in place at MEM10x000B0000and boot1 clears that buffer afterwards. - It checks that the two big-endian words at offsets
0x18Cand0x190of the TMD are both1. Otherwise the attempt fails with error-11. - It checks that the byte at offset
0x18Bof the TMD is at least version byte 0 of the SEEPROM version record. If it is smaller or if no valid record can be read, the attempt fails with error-10. - It takes the content size from the word at offset
0x1F0of the TMD and the expected SHA-1 hash from offset0x1F4and builds the content IV from the 2 bytes at offset0x1E8followed by 14 zero bytes.
The content then streams from NAND into MEM1. The page that contains the content offset is loaded to 0x00028000, so the content itself starts at 0x00028000 + (content_offset & 0x7FF). Later pages follow in order. When boot1 reaches the end of a block, it continues with the next block in the direction of the current attempt, skipping blocks the same way as before. The content size is rounded up to a multiple of 16 bytes and the spare buffers are placed right behind the content. If round_up(0x00028800 + (content_offset & 0x7FF) + padded_size, 128) + 256 is larger than 0x7FFFF or if boot1 runs out of blocks, the attempt fails with error -6.
The pages go through msht like a instruciton pipeline: while the NAND engine reads one page, the previous page is corrected and decrypted in place and the page before that is hashed. All of the content is decrypted as one AES-128-CBC stream with the decrypted content key. The first AES command written to 0x0D020000 is 0x98000000 | (length / 16 - 1) and every following one is 0x98001000 | (length / 16 - 1). The SHA-1 hash covers exactly the content size (not the padding to 16 bytes) and uses standard SHA-1 padding. If the hash does not match the TMD, the attempt fails with error -7.
SHA-1
The SHA-1 engine only processes whole 64 byte blocks, so boot1 adds the padding itself. To finish a hash, it builds the last one or two blocks in MEM1 at 0x000A8000: the leftover bytes, a 0x80 byte, zeros and the message length in bits. Only the low 32 bits of the length are written. The upper 32 bits are always zero.
ECC correction
ecc_correct checks a 2048 byte page in four 512 byte chunks. For each chunk it compares the ECC word stored in the spare area at 0x30 + 4 * i with the word the NAND engine calculated at 0x40 + 4 * i:
int ecc_correct(u8 *data, u8 *spare)
{
for (int i = 0; i < 4; i++) {
u32 stored = read32(spare + 0x30 + 4 * i);
u32 computed = read32(spare + 0x40 + 4 * i);
if (stored == computed)
continue;
u32 diff = bswap32(stored) ^ bswap32(computed);
if ((diff & (diff - 1)) == 0)
continue; // only one bit differs: the error is in the ECC itself
if (((diff & 0xFFF) ^ (diff >> 16)) != 0xFFF)
return -1; // the data cannot be corrected
u32 byte = (diff >> 19) & 0x1FF;
u32 bit = (diff >> 8) & 7;
data[i * 512 + byte] ^= 1 << bit;
}
return 0;
}
Engine handshake
Before every AES or SHA-1 command and after every NAND read or AES command, boot1 runs a handshake that its error message calls ahbMemFlush. It writes a mask to the 16 bit register at 0x0D8B4228 and then reads the 16 bit register at 0x0D8B422A until it returns the same mask, calling udelay(1) between reads. When the register matches or after 1,000,000 failed reads, boot1 writes 0 back to 0x0D8B4228.
| When | Mask |
|---|---|
| Before a SHA-1 or AES command | 1 |
| After a NAND read | 8 |
| After an AES command | 2 |
If the register never matches, boot1 prints ERROR: ahbMemFlush(0x%x) did not ack! and carries on. The message goes out through ARM semihosting, never had to deal with this so take it with a grain of salt pls: a Thumb SVC 0xAB with R0 = 4 (SYS_WRITE0) and R1 pointing at the text. boot1 sends the text in pieces of up to 15 characters.
udelay
udelay(n) busy-waits on the counter at 0x0D800010. It converts n to a number of counter ticks by adding shifted copies of n:
u32 ticks = scale
? n + (n >> 1) + (n >> 2) + (n >> 3) + (n >> 6) + (n >> 8) + (n >> 9) + (n >> 10) + (n >> 12)
: n + (n >> 2) + (n >> 6);
if (ticks < 2)
ticks = 2;
scale is a word stored in boot1 at 0x0D404314 and its value is 1. The wait handles the counter wrapping around.
SEEPROM
boot1 reads the SEEPROM, a small serial EEPROM, by toggling GPIO lines in software:
| Register | Bit | Use |
|---|---|---|
0x0D8000E0 |
10 | Chip select, high during a transfer |
0x0D8000E0 |
11 | Clock |
0x0D8000E0 |
12 | Data sent to the SEEPROM |
0x0D8000E8 |
13 | Data received from the SEEPROM |
For every bit, boot1 sets the clock low and chip select high, puts the next bit on the data line, calls delay(125), sets the clock high and calls delay(125) again. Incoming bits are read at the end of that sequence, while the clock is still high. Bits are sent and received most significant bit first.
To read a 16 bit word, boot1 sends the 11 bits 0x600 + word_address, receives 16 bits and then ends the transfer by clearing chip select and toggling the clock two more times.
SEEPROM version record
The SEEPROM holds two copies of a 10 byte version record, at byte offsets 0x48 and 0x52. All values are big-endian:
| Offset | Size | Contents |
|---|---|---|
0x00 |
1 | Version byte 0, the lowest allowed value for TMD byte 0x18B |
0x04 |
4 | Counter |
0x08 |
2 | Checksum: the sum of the four 16 bit words at 0x00-0x07, truncated to 16 bits |
A copy is valid when its checksum matches. Among the valid copies, boot1 uses the one with the highest counter. If neither copy is valid, the version check fails.
Errors
When both attempts fail, the GPIO debug field alternates between the low byte of the error code and its complement.
| Code | Shown | Meaning |
|---|---|---|
-1 |
0xFF / 0x00 |
A page has an error that ECC cannot correct |
-4 |
0xFC / 0x03 |
No usable block between 1 and 7 |
-5 |
0xFB / 0x04 |
The header is invalid |
-6 |
0xFA / 0x05 |
The content is too large or boot1 ran out of blocks |
-7 |
0xF9 / 0x06 |
The content hash does not match the TMD |
-8 |
0xF8 / 0x07 |
An issuer does not match or a signature check failed |
-9 |
0xF7 / 0x08 |
A certificate name does not start with CA, CP or XS |
-10 |
0xF6 / 0x09 |
The TMD version is too old or the SEEPROM record is invalid |
-11 |
0xF5 / 0x0A |
The TMD title ID is not 00000001-00000001 |