mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-10-20 14:37:26 +02:00
Add single-drag combined UF2 (bootloader + manifest + app), verified on hardware
This commit is contained in:
1 parent
cf6f1f2c33
commit
2c8ee0a70e
7 files changed
+185
-24
No files matched your search
Binary file not shown.
@@ -81,8 +81,9 @@ pico_enable_stdio_usb(0x0026a_app 0)
|
||||
pico_set_linker_script(0x0026a_app
|
||||
${CMAKE_CURRENT_LIST_DIR}/app/memmap_app.ld)
|
||||
|
||||
# The application starts at 0x10020000, so a UF2 (which BOOTSEL always writes to
|
||||
# 0x10000000) would be wrong. Emit the raw artifacts we flash with OpenOCD.
|
||||
# The application starts at 0x10020000. A standalone app UF2 would flash only
|
||||
# this one region, so the lab ships a single combined UF2 instead (built by
|
||||
# tools/make_full_uf2.py). Emit the raw artifacts we sign and flash with OpenOCD.
|
||||
pico_add_dis_output(0x0026a_app)
|
||||
pico_add_hex_output(0x0026a_app)
|
||||
pico_add_bin_output(0x0026a_app)
|
||||
|
||||
@@ -24,13 +24,14 @@ The CMSIS-RM9 flash-safe range starts at `0x10000000` on the RP2350, so adding 2
|
||||
|
||||
## Artifacts
|
||||
|
||||
| File | Description |
|
||||
| ---------------------------- | ---------------------------------------- |
|
||||
| `0x0026a_bootloader.bin` | Bootloader image (flash at `0x10000000`) |
|
||||
| `0x0026a_bootloader.uf2` | Bootloader for the BOOTSEL drag-and-drop |
|
||||
| `0x0026a_app.bin` | Signed app image (flash at `0x10020000`) |
|
||||
| `manifest.bin` | Manifest (flash at `0x1001F000`) |
|
||||
| `WEEK11a.md` / `WEEK11a.pdf` | Full lab notebook |
|
||||
| File | Description |
|
||||
| ---------------------------- | ------------------------------------------------- |
|
||||
| `0x0026a_full.uf2` | **All three images** in one BOOTSEL drag |
|
||||
| `0x0026a_bootloader.bin` | Bootloader image (flash at `0x10000000`) |
|
||||
| `0x0026a_bootloader.uf2` | Bootloader-only UF2 for the BOOTSEL drag-and-drop |
|
||||
| `0x0026a_app.bin` | Signed app image (flash at `0x10020000`) |
|
||||
| `manifest.bin` | Manifest (flash at `0x1001F000`) |
|
||||
| `WEEK11a.md` / `WEEK11a.pdf` | Full lab notebook |
|
||||
|
||||
## Build
|
||||
|
||||
@@ -38,12 +39,20 @@ The CMSIS-RM9 flash-safe range starts at `0x10000000` on the RP2350, so adding 2
|
||||
cmake -B build -DPICO_SDK_PATH=~/.pico-sdk/sdk/2.2.0
|
||||
cmake --build build
|
||||
python3 tools/sign_firmware.py build/0x0026a_app.bin --out manifest.bin --key "IronNetBootKey!!" --app-base 0x10020000
|
||||
PICOTOOL=~/.pico-sdk/picotool/2.3.1/picotool/picotool python3 tools/make_full_uf2.py
|
||||
```
|
||||
|
||||
This produces `0x0026a_bootloader.bin/.uf2` and `0x0026a_app.bin`; the signer writes `manifest.bin`.
|
||||
This produces `0x0026a_bootloader.bin/.uf2` and `0x0026a_app.bin`; the signer
|
||||
writes `manifest.bin`; `make_full_uf2.py` packs all three into
|
||||
`0x0026a_full.uf2`.
|
||||
|
||||
## Flash
|
||||
|
||||
Easiest - copy `0x0026a_full.uf2` to the `RP2350` BOOTSEL drive; one drag writes
|
||||
the whole chain (bootloader + manifest + app).
|
||||
|
||||
Or with OpenOCD:
|
||||
|
||||
```sh
|
||||
openocd -f interface/picoprobe.cfg -f target/rp2350.cfg -c \
|
||||
"program 0x0026a_bootloader.bin 0x10000000 verify; \
|
||||
|
||||
@@ -519,15 +519,31 @@ $ ~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-nm -n build/0x0026a_app.elf
|
||||
...
|
||||
```
|
||||
|
||||
### 6.9 Why the Application Has No .uf2
|
||||
### 6.9 One UF2 for the Whole Chain
|
||||
|
||||
A UF2 file writes to the address encoded in each of its blocks, and the SDK's
|
||||
UF2 tooling always targets `0x10000000`. If a user holds BOOTSEL and drags in
|
||||
`0x0026a_app.uf2`, the app would be written at the wrong base and the
|
||||
bootloader would jump into garbage (or nothing). So the app is deliberately
|
||||
built **without** a UF2 and with explicit `.bin`/`.hex`/`.dis`/`.map` outputs
|
||||
instead. The **bootloader** still ships a UF2 (it belongs at `0x10000000`), and
|
||||
the manifest + app are written to their own addresses with OpenOCD.
|
||||
No single image is the whole story. The device needs the bootloader at
|
||||
`0x10000000`, the manifest at `0x1001F000`, and the app at `0x10020000`. A
|
||||
standalone `0x0026a_app.uf2` would flash only the app region, leaving the other
|
||||
two to be written separately, so the app is deliberately built with explicit
|
||||
`.bin`/`.hex`/`.dis`/`.map` outputs instead of its own UF2.
|
||||
|
||||
To bring a bare board up in one move the lab ships a single combined UF2,
|
||||
`0x0026a_full.uf2`, that carries all three images. It is built by laying the
|
||||
images out flat - offset 0 mapped to `0x10000000`, the reserved gaps filled
|
||||
with `0xFF` - and handing the result to picotool as one image:
|
||||
|
||||
```
|
||||
$ PICOTOOL=~/.pico-sdk/picotool/2.3.1/picotool/picotool \
|
||||
python3 tools/make_full_uf2.py
|
||||
```
|
||||
|
||||
picotool numbers the blocks, tags them with the `rp2350-arm-s` family, and
|
||||
injects the RP2350-E10 "absolute block" (family `0xE48BFF57` at `0x10FFFF00`)
|
||||
that tells the boot ROM to write every block to its **absolute** address rather
|
||||
than into a partition. A single BOOTSEL drag then restores the whole chain.
|
||||
Part 11 uses it. Note the contrast with the naive file layout: a UF2 is not
|
||||
restricted to `0x10000000`, but a multi-region UF2 is only correct once the
|
||||
blocks and the E10 absolute block are laid out the way picotool produces them.
|
||||
|
||||
---
|
||||
|
||||
@@ -974,11 +990,27 @@ the manifest, then the app. If you flash the app before the manifest, the
|
||||
bootloader happily boots it anyway (the manifest is already in flash from a
|
||||
previous lab run) - order only matters on a bare board.
|
||||
|
||||
### The whole chain in one drag (recommended)
|
||||
|
||||
On a bare board skip straight to one file: hold BOOTSEL, connect the Pico 2,
|
||||
and copy `0x0026a_full.uf2` to the `RP2350` drive. That single UF2 writes all
|
||||
three images at once - no OpenOCD, no second drag. Section 6.9 explains how it
|
||||
is built; the hardware-verified behavior is:
|
||||
|
||||
- BOOTSEL accepts it and unmounts the drive;
|
||||
- the boot ROM writes bootloader, manifest, and app to their absolute
|
||||
addresses;
|
||||
- on reset the bootloader verifies the manifest and jumps to the app.
|
||||
|
||||
The steps below are the three-image equivalent, which is what you use while
|
||||
iterating on a single image or when you prefer OpenOCD over the button.
|
||||
|
||||
### A. The bootloader: two options
|
||||
|
||||
**Option 1 - BOOTSEL (no extra tools):** hold the BOOTSEL button, connect the
|
||||
Pico 2, copy `0x0026a_bootloader.uf2` to the drive. BOOTSEL always writes at
|
||||
`0x10000000`, which is correct for the bootloader.
|
||||
Pico 2, copy `0x0026a_bootloader.uf2` to the drive, which lands the bootloader
|
||||
at `0x10000000` (or drag `0x0026a_full.uf2` to write the whole chain in one
|
||||
shot).
|
||||
|
||||
**Option 2 - OpenOCD:**
|
||||
|
||||
@@ -1107,9 +1139,10 @@ the bootloader refuses.
|
||||
|
||||
### Step 13: Confirm it still boots after restoring the good image
|
||||
|
||||
Re-flash `0x0026a_app.bin` (fresh, matching the manifest) and reset. The
|
||||
banner returns. The bootloader is doing real enforcement, not just complaining
|
||||
once.
|
||||
Re-flash `0x0026a_app.bin` (fresh, matching the manifest) and reset - or just
|
||||
drag `0x0026a_full.uf2` onto the BOOTSEL drive again to restore the whole
|
||||
chain in one move. The banner returns. The bootloader is doing real
|
||||
enforcement, not just complaining once.
|
||||
|
||||
Stop and think about the security statement this demonstrated:
|
||||
|
||||
|
||||
Binary file not shown.
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a single BOOTSEL-draggable UF2 holding the whole secure-boot chain.
|
||||
|
||||
The 0x0026a chain is three separate images at three flash addresses:
|
||||
|
||||
bootloader 0x10000000 (owns the system, verifies the next image)
|
||||
manifest 0x1001F000 (header + AES-128-CMAC tag)
|
||||
app 0x10020000 (the signed application)
|
||||
|
||||
The SDK emits a UF2 only for the bootloader, so a bare board would need three
|
||||
separate flashes. This tool packs all three into ONE UF2 so a single BOOTSEL
|
||||
drag-and-drop brings the whole device up.
|
||||
|
||||
How it works:
|
||||
|
||||
1. Lay the three images out in a flat buffer whose offset 0 maps to
|
||||
0x10000000, filling the reserved gap between them with 0xFF (erased
|
||||
flash).
|
||||
2. Hand that flat image to picotool as a normal image at 0x10000000.
|
||||
3. picotool numbers the blocks, tags them with the rp2350-arm-s family and
|
||||
injects the RP2350-E10 absolute block that makes the boot ROM write each
|
||||
block to its absolute address.
|
||||
|
||||
The result is exactly the shape the boot ROM accepts over the BOOTSEL mass
|
||||
storage interface, and it has been verified on hardware: the ROM accepts the
|
||||
drag, the bootloader verifies the manifest, and the app runs.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
BOOTLOADER_BASE = 0x10000000
|
||||
MANIFEST_BASE = 0x1001F000
|
||||
APP_BASE = 0x10020000
|
||||
FAMILY = "rp2350-arm-s"
|
||||
PLATFORM = "rp2350"
|
||||
|
||||
|
||||
def build_flat(bootloader, manifest, app):
|
||||
"""Lay the three images out flat, offset 0 mapped to 0x10000000."""
|
||||
if APP_BASE < MANIFEST_BASE < BOOTLOADER_BASE:
|
||||
raise SystemExit("address map is not ordered")
|
||||
size = (APP_BASE - BOOTLOADER_BASE) + len(app)
|
||||
image = bytearray(b"\xFF" * size)
|
||||
image[0:len(bootloader)] = bootloader
|
||||
m_off = MANIFEST_BASE - BOOTLOADER_BASE
|
||||
image[m_off:m_off + len(manifest)] = manifest
|
||||
a_off = APP_BASE - BOOTLOADER_BASE
|
||||
image[a_off:a_off + len(app)] = app
|
||||
return bytes(image)
|
||||
|
||||
|
||||
def convert(flat, out, picotool):
|
||||
"""Run picotool to turn the flat image into an absolute-address UF2."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as tmp:
|
||||
tmp.write(flat)
|
||||
flat_path = tmp.name
|
||||
try:
|
||||
cmd = [
|
||||
picotool, "uf2", "convert", flat_path, out,
|
||||
"-o", hex(BOOTLOADER_BASE),
|
||||
"--family", FAMILY,
|
||||
"--platform", PLATFORM,
|
||||
"--abs-block",
|
||||
]
|
||||
subprocess.run(cmd, check=True)
|
||||
finally:
|
||||
os.unlink(flat_path)
|
||||
|
||||
|
||||
def verify(out):
|
||||
"""Report the block count and the families present in the UF2."""
|
||||
data = open(out, "rb").read()
|
||||
blocks = [data[i:i + 512] for i in range(0, len(data), 512)]
|
||||
families = set()
|
||||
payload = 0
|
||||
for blk in blocks:
|
||||
if len(blk) != 512 or struct.unpack_from("<I", blk, 0)[0] != 0x0A324655:
|
||||
continue
|
||||
families.add(struct.unpack_from("<I", blk, 28)[0])
|
||||
if not (struct.unpack_from("<I", blk, 8)[0] & 0x8000):
|
||||
payload += 1
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
print("wrote %s" % out)
|
||||
print(" blocks : %d (%d payload)" % (len(blocks), payload))
|
||||
print(" families : %s" % ", ".join(hex(f) for f in sorted(families)))
|
||||
print(" sha256 : %s" % digest)
|
||||
|
||||
|
||||
def main():
|
||||
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--dir", default=here, help="folder holding the images")
|
||||
ap.add_argument("--bootloader", default="0x0026a_bootloader.bin")
|
||||
ap.add_argument("--manifest", default="manifest.bin")
|
||||
ap.add_argument("--app", default="0x0026a_app.bin")
|
||||
ap.add_argument("--out", default="0x0026a_full.uf2")
|
||||
ap.add_argument("--picotool", default=os.environ.get("PICOTOOL", "picotool"),
|
||||
help="picotool binary (env PICOTOOL)")
|
||||
args = ap.parse_args()
|
||||
|
||||
bootloader = open(os.path.join(args.dir, args.bootloader), "rb").read()
|
||||
manifest = open(os.path.join(args.dir, args.manifest), "rb").read()
|
||||
app = open(os.path.join(args.dir, args.app), "rb").read()
|
||||
|
||||
flat = build_flat(bootloader, manifest, app)
|
||||
out = os.path.join(args.dir, args.out)
|
||||
convert(flat, out, args.picotool)
|
||||
verify(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -581,7 +581,7 @@ Structures and Functions in Embedded Systems: Debugging and Hacking w/ IR Remote
|
||||
|
||||
### 0x0026a Bootloader BIN [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/0x0026a_bootloader.bin)
|
||||
|
||||
### 0x0026a Bootloader UF2 [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/0x0026a_bootloader.uf2)
|
||||
### 0x0026a Full Secure Boot UF2 (bootloader + manifest + app) [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/0x0026a_full.uf2)
|
||||
|
||||
### 0x0026a Signed Application BIN [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/0x0026a_custom-bootloader/0x0026a_app.bin)
|
||||
|
||||
|
||||
Reference in new issue
Block a user