diff --git a/WEEK11/WEEK11a.md b/WEEK11/WEEK11a.md index 7ec49f2..0653dfc 100644 --- a/WEEK11/WEEK11a.md +++ b/WEEK11/WEEK11a.md @@ -1219,6 +1219,15 @@ You should see the miniature version of Part 7.1: Note `main` does almost nothing itself - it is a sequence of calls, exactly the design we built. +Before accepting `0x10000234` as "the start", note that it is not the start of +the image at all. The image begins at `0x10000000` with the Cortex-M vector +table, and `main` sits `0x234` bytes in because the vector table, the default +ISRs, the SDK binary-info header, the RP2350 IMAGE_DEF block, the reset handler, +and crt0 all run before it. Appendix A.2 derives that layout from first +principles and proves it on the board. It is also why you will see `0x10000234` +for the bootloader and `0x10020234` for the app: the same `0x234` offset, with +the base shifted by `0x20000`. + ### Step 18: Stop inside the crypto (the payoff breakpoint) The interesting code is `verify_manifest` at `0x100003b0` and the mbedtls @@ -1364,6 +1373,12 @@ our C - including the constant table it pulled from `.rodata`. Open the is the key. Rename it by selecting and pressing `L` (e.g. `fw_key`), then annotate `main`'s call graph with the whole boot sequence. +Do not read the bare addresses in that table as arbitrary. `main` at +`0x10000234` is `0x234` bytes into the image because the vector table and the +SDK startup preamble occupy everything ahead of it, and the app's `main` is the +same offset at `0x10020234`. Appendix A.2 derives this from first principles and +proves it with a live GDB session against the board. + ### Step 24: Recover the "spec" from the image - List exports/imports: the binary imports `cdns_usb_table`, stdio routines, @@ -1395,6 +1410,12 @@ strings. Open `main`. Because everything was inlined and optimized, Ghidra shows a clean calling skeleton - the same one GDB showed in Part 14 Step 21. +Compare the addresses with the bootloader's from Step 23 and you will see the +relocation for yourself: `_reset_handler` moved from `0x1000015c` to +`0x1002015c`, and `main` from `0x10000234` to `0x10020234`. Every symbol is the +bootloader's plus `0x20000`; only the base changed. Appendix A.2 explains and +proves why both images put `main` `0x234` bytes in. + ### Step 26: The "struct is a lie" lesson, revisited Open `led_handle_ir` at `0x100204d8`. Ghidra's decompiler will *not* show you @@ -1467,10 +1488,18 @@ kind. Everything you should have collected so far, in one table. Builds are deterministic for the checked-in sources, so your numbers should match. +Read every address as `base + offset`. The bootloader is based at `0x10000000` +and the app at `0x10020000`, so `main` is `0x10000234` in one and `0x10020234` +in the other - the same `0x234` offset, relocated. Appendix A.2 derives that +offset from the vector table and the SDK startup code and proves it on the +board. + ### Bootloader (0x0026a_bootloader.elf) | Role | Symbol | Address | | ----------------------- | ------------------- | ------------ | +| Reset vector (vector[1])| `_reset_handler` | `0x1000015c` | +| Initial SP (vector[0]) | (data) | `0x20082000` | | Boot sequence | `main` | `0x10000234` | | Console + 2 s pause | `boot_init` | `0x10000278` | | Failure idle loop | `boot_halt` | `0x10000294` | @@ -1724,7 +1753,178 @@ The trust boundary is a single comparison: a freshly computed AES-128-CMAC of the app bytes versus the 16-byte `tag` in the manifest. Everything below is about recovering the inputs to that comparison, then supplying your own. -### A.2 Static reconnaissance (before any disassembler) +### A.2 The image layout: from the vector table to main + +Read a Pico image at the byte level and know, with nothing assumed, why `main` +is at `0x234` and why the relocated app's `main` is at `0x10020234`. Every +number below is confirmed on the live board in A.2.5. + +#### A.2.1 The rule: an address is a base plus an offset + +An image is linked at a base address: `0x10000000` for the bootloader, +`0x10020000` for the app. The linker places byte 0 of the program at the base, +so every address is `base + offset`: + +``` +0x10000234 = 0x10000000 + 0x234 +``` + +`0x234` is hexadecimal (564 decimal). It is not a whole address and it is not +"the 234th byte"; it is the distance from the start of the image. + +#### A.2.2 The first bytes are a vector table, not main + +The Cortex-M33 does not reset into `main`. The hardware requires a vector table +at the image base and reads two words from it to start: word 0 is the initial +stack pointer, word 1 is the reset handler. The RP2350 has 16 system vectors +plus 52 IRQs, so the table is 68 words = 272 bytes = `0x110`. Live dump from +this board: + +``` +(gdb) x/16wx 0x10000000 +0x10000000 <__vectors>: 0x20082000 0x1000015d 0x1000011b 0x1000011d +0x10000010 <__vectors+16>: 0x10000111 0x10000111 0x10000111 0x10000111 +``` + +| Word | Value | Meaning | +| ---- | ------------ | ----------------------------------------------------------- | +| 0 | `0x20082000` | initial stack pointer (top of RP2350 SRAM) | +| 1 | `0x1000015d` | reset handler = `_reset_handler` (`0x1000015c`) + Thumb bit | +| 2 | `0x1000011b` | NMI | +| 3 | `0x1000011d` | HardFault | +| 4 | `0x10000111` | MemManage | +| ... | ... | remaining system vectors, then IRQ0..IRQ51 | + +Word 1 ends in `...5d`, not `...5c`, because Cortex-M code is Thumb code: the +hardware uses that low bit and branches to `0x1000015c`. GDB resolves it: + +``` +(gdb) info symbol 0x1000015d +_reset_handler + 1 in section .text +``` + +Two entry numbers exist and are not the same: +- the reset vector (table word 1) = `_reset_handler` at `0x1000015c`; +- the ELF entry point (`readelf -h`) = `0x1000014d` = `_entry_point` at + `0x1000014c`, an SDK trampoline that reloads SP and branches to + `_reset_handler`. + +Both converge on `_reset_handler`. Neither is `main`. + +#### A.2.3 The rest of the preamble and the arithmetic + +After the table, this fixed preamble runs before `main`: + +| Address | Symbol | Purpose | +| ------------ | ---------------------------- | ------------------------------------------- | +| `0x10000110` | `__default_isrs_start` | weak ISR defaults (`isr_irq0`..`isr_irq51`) | +| `0x10000124` | `binary_info_header` | name/build date shown by `picotool info` | +| `0x10000138` | `embedded_block` (IMAGE_DEF) | RP2350 metadata block the ROM reads | +| `0x1000014c` | `_entry_point` | trampoline | +| `0x10000150` | `_enter_vtable_in_r0` | `msr MSP` then `bx` to the reset handler | +| `0x1000015c` | `_reset_handler` | reset entry | +| `0x10000164` | `hold_non_core0_in_bootrom` | parks core 1 | +| `0x10000180` | `bss_fill_loop` / `data_cpy` | zero `.bss`, copy `.data` | +| `0x100001e4` | `_init` | runtime / constructors | +| `0x10000234` | `main` | your code | + +The sizes add up exactly: + +``` +vector table 0x110 (68 words) +default ISRs 0x014 +binary_info_header 0x014 +embedded IMAGE_DEF 0x014 + ----- +fixed preamble 0x14c ends at 0x1000014c +reset handler + crt0 0x0e8 fills 0x1000014c .. 0x10000234 + ----- +main 0x234 +``` + +So `main` is at `0x10000000 + 0x234`: 564 bytes in, because the hardware tables +and the C runtime occupy the first 564 bytes. This is the SDK layout for every +image; it is not specific to a bootloader. + +The live disassembly of the trampoline and reset handler shows the handoff: + +``` +(gdb) disassemble 0x1000014c,+56 +0x1000014c <_entry_point+0>: mov.w r0, #0 +0x10000150 <_enter_vtable_in_r0+0>: ldr r1, [pc, #120] +... +0x10000154 <_enter_vtable_in_r0+4>: ldmia r0!, {r1, r2} +0x10000156 <_enter_vtable_in_r0+6>: msr MSP, r1 +0x1000015a <_enter_vtable_in_r0+10>: bx r2 +0x1000015c <_reset_handler+0>: mov.w r0, #3489660928 +... +0x10000172 : bl 0x1000019a +... +0x10000180 : stmia r1!, {r0} +``` + +`_enter_vtable_in_r0` loads the initial stack pointer into MSP (`msr MSP, r1`) +and branches to the reset handler (`bx r2`); `_reset_handler` then parks core 1, +copies `.data`, zeroes `.bss`, runs `_init`, and calls `main`. + +#### A.2.4 Relocation: the same layout, base shifted + +The app is the same image linked at `0x10020000`, so every startup symbol shifts +by `0x20000`: + +| Symbol | Bootloader | App | +| ---------------- | ------------ | ------------ | +| `__VECTOR_TABLE` | `0x10000000` | `0x10020000` | +| `__vectors_end` | `0x10000110` | `0x10020110` | +| `_entry_point` | `0x1000014c` | `0x1002014c` | +| `_reset_handler` | `0x1000015c` | `0x1002015c` | +| `main` | `0x10000234` | `0x10020234` | + +`main` is at offset `0x234` in both. Relocation changed the base only, which is +why the bootloader can jump to an app linked at a different address. + +#### A.2.5 Reproduce and verify + +With the probe attached and `./debug-server.sh` running, this command proves +every claim above. The output is verbatim from this lab's board: + +``` +$ ~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb -batch \ + -ex "file 0x0026a_custom-bootloader/build/0x0026a_bootloader.elf" \ + -ex "target extended-remote :3333" \ + -ex "source pico-gdb-memory.gdb" \ + -ex "monitor halt" \ + -ex "x/16wx 0x10000000" \ + -ex "info symbol *(unsigned int*)0x10000004" \ + -ex "info address main" \ + -ex "x/4wx 0x10020000" + +0x10000000 <__vectors>: 0x20082000 0x1000015d 0x1000011b 0x1000011d +0x10000010 <__vectors+16>: 0x10000111 0x10000111 0x10000111 0x10000111 +... +_reset_handler + 1 in section .text +Symbol "main" is a function at address 0x10000234. +0x10020000: 0x20082000 0x1002015d 0x1002011b 0x1002011d +``` + +What each line proves: +- `0x10000000` = `0x20082000` then `0x1000015d`: the image begins with + `[initial SP][reset handler]`. +- `info symbol` resolves the reset word to `_reset_handler`. +- `info address main` = `0x10000234`, 564 bytes in. +- `0x10020000` repeats the same shape shifted by `0x20000`; the app's `main` is + `0x10020234`. +- Attaching mid-run showed the PC at `0x100202cc`, inside the app region: the + bootloader had already handed off. + +#### A.2.6 Two gotchas + +- `0x234` is hex; `0x234` = 564 decimal. +- No boot2 blob: `__boot2_start__ == __boot2_end__` in both images, so the image + begins at the vector table (the RP2350 boot ROM handles flash setup). + + +### A.3 Static reconnaissance (before any disassembler) Never open Ghidra first. Two minutes of `file`/`strings`/`xxd` frames the whole investigation: @@ -1770,7 +1970,7 @@ Decode it as five little-endian words plus a blob: You have the manifest spec before opening a decompiler. `binwalk -E` (if installed) confirms a low-entropy, uncompressed image - none of it is packed. -### A.3 Ghidra on the bootloader (the easy path) +### A.4 Ghidra on the bootloader (the easy path) 1. **File > New Project** (non-shared), then **File > Import File** and pick `$W/build/0x0026a_bootloader.elf`. @@ -1794,7 +1994,7 @@ Double-click `verify_manifest`. The decompiler will show a stack copy of the manifest and a call that hands `fw_key` to `aes128_cmac`, then a 16-byte compare with the tag. That is the entire security model on one screen. -### A.4 Recovering the key from the image (static) +### A.5 Recovering the key from the image (static) Inside `verify_manifest`, note the address pushed as the CMAC key. Follow it in the **Data** panel to `0x10006064`, or just ask the binary: @@ -1824,9 +2024,9 @@ computed : ef041ec592f0c56727da2651d373947b result : MATCH ``` -### A.5 Ghidra on the application (the AES gate) +### A.6 Ghidra on the application (the AES gate) -Repeat A.3 with `$W/build/0x0026a_app.elf`. The app is the richer target. +Repeat A.4 with `$W/build/0x0026a_app.elf`. The app is the richer target. | Symbol | Address | Observations | | --------------------- | ------------ | ----------------------------------------------- | @@ -1857,7 +2057,7 @@ The red data references give you every constant you need: Note the last row: the AES key is identical to the CMAC key. One compromise, the whole chain falls - the classic "one key, two jobs" smell. -### A.6 Decrypt on the host +### A.7 Decrypt on the host Ciphertext, IV, and key are now just three hex strings. OpenSSL finishes the job; you never needed the source: @@ -1874,7 +2074,7 @@ SECRET: IR LINK SECURED ONLINE Breaking the application took: import, click, copy three hex constants, run one command. AES was never attacked - the key schedule was simply lying in `.rodata`. -### A.7 Forging an image the bootloader accepts +### A.8 Forging an image the bootloader accepts The recovery is only interesting if you can act on it. Because you own the key, you can sign anything: @@ -1897,7 +2097,7 @@ $ python3 $W/tools/sign_firmware.py $W/0x0026a_app.bin \ the tag matches, and the rogue image runs. That is the whole "secure boot" defeated with a key you read out of the image it was protecting. -### A.8 Live GDB: watching the boot gate +### A.9 Live GDB: watching the boot gate Now do the same hunt at runtime. The repo ships `debug-server.sh` (owns the Probe, leaves the target running on `127.0.0.1:3333`) and `pico-gdb-memory.gdb` @@ -1918,7 +2118,7 @@ $ $GDB $W/build/0x0026a_bootloader.elf (gdb) monitor halt ``` -Read the manifest straight out of flash and confirm the A.2 decode at runtime: +Read the manifest straight out of flash and confirm the A.3 decode at runtime: ``` (gdb) x/4wx 0x1001F000 @@ -1936,7 +2136,7 @@ Break on the gate, reset, and continue: ``` When it stops, dump the key argument the CMAC call will receive - the live -version of A.4: +version of A.5: ``` (gdb) x/16bx 0x10006064 @@ -1964,7 +2164,7 @@ the launch tail and single-step the handoff: `r3` equals `vectors[1]`, and after `stepi` the PC is in the app's `_reset_handler` with SP already set to `0x20082000`. The gate is behind you. -### A.9 Live GDB: the application's AES +### A.10 Live GDB: the application's AES Reattach to the app ELF (the core is already running it, so no reset is strictly needed, but a reset proves the chain end to end): @@ -1987,12 +2187,12 @@ and read the three constants the same way Ghidra showed them: ``` Single-step the four mbedtls calls and confirm the argument registers line up -with A.5 (`r0 = ctx`, `r3 = iv`, and the ciphertext pointer on the stack). By +with A.6 (`r0 = ctx`, `r3 = iv`, and the ciphertext pointer on the stack). By the time `secret_print_banner` returns, the plaintext `SECRET: IR LINK SECURED ONLINE` has been printed - the same bytes OpenSSL produced on the host, now observed live on the silicon. -### A.10 The stripped binary (no symbols, the real case) +### A.11 The stripped binary (no symbols, the real case) Everything above leaned on symbols. A shipped image usually has none. Delete the app from the Ghidra project and import `$W/0x0026a_app.bin` with **Language = @@ -2015,7 +2215,7 @@ Renaming data (`L`), applying types, and setting a couple of function signatures rebuilds the module structure from the raw blob - no debugger, no source, no strings beyond a printf format. -### A.11 Cheat sheet +### A.12 Cheat sheet | Task | Command / address | | ----------------------------- | ------------------------------------------------------------ | diff --git a/WEEK11/WEEK11a.pdf b/WEEK11/WEEK11a.pdf index af687c1..f4fbbb1 100644 Binary files a/WEEK11/WEEK11a.pdf and b/WEEK11/WEEK11a.pdf differ