mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-10-21 06:51:48 +02:00
Add end-to-end GDB and Ghidra reverse-engineering appendix to Week 11a
This commit is contained in:
1 parent
28c55c60e1
commit
1f061ca503
2 files changed
+350
-2
No files matched your search
@@ -198,7 +198,7 @@ development machine. It is validated against the four official test vectors
|
||||
from RFC 4493 using the well-known test key:
|
||||
|
||||
```
|
||||
Key = 2b7e151628aed2a6abf7158809cf4f3c
|
||||
Key = 2b7e151628aed2a6abf7158809cf4f3c
|
||||
|
||||
Message = (empty)
|
||||
Tag = bb1d6929e95937287fa37d129b756746
|
||||
@@ -1686,4 +1686,352 @@ and what happens if it is not there?*
|
||||
schedule so an image signed for one board will not run on another.
|
||||
|
||||
**Now go flash your own CA.** You control the manifest, the key, and the
|
||||
gate that decides what runs on your hardware.
|
||||
gate that decides what runs on your hardware.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: End-to-End Reverse Engineering with GDB and Ghidra
|
||||
|
||||
Parts 14-16 teach the two tools one at a time. This appendix is the
|
||||
consolidated, start-to-finish version: a single adversary workflow that starts
|
||||
with nothing but the shipped artifacts and a board, and ends with a forged
|
||||
image the bootloader accepts. Every command is meant to be run in order. It is
|
||||
the "give me the whole hunt" version - keep a terminal and a Ghidra window
|
||||
open side by side.
|
||||
|
||||
For short paths throughout, set:
|
||||
|
||||
```
|
||||
$ export W=0x0026a_custom-bootloader
|
||||
$ export GDB=~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb
|
||||
$ export PCT=~/.pico-sdk/picotool/2.3.1/picotool/picotool
|
||||
```
|
||||
|
||||
### A.1 What the attacker starts with
|
||||
|
||||
Assume you were handed the release folder and a Debug Probe, with no source
|
||||
and no README. Each artifact leaks a specific thing:
|
||||
|
||||
| Artifact | What it gives you |
|
||||
| ------------------------- | ----------------------------------------------------- |
|
||||
| `0x0026a_bootloader.elf` | Full symbols for the gate: function names + addresses |
|
||||
| `0x0026a_bootloader.bin` | The same code with no symbols (the real-world case) |
|
||||
| `manifest.bin` | The on-flash spec: magic, base, size, and the tag |
|
||||
| `0x0026a_app.bin` | The signed payload; its `.rodata` hides the AES key |
|
||||
| `0x0026a_full.uf2` | The one-drag installer (bootloader + manifest + app) |
|
||||
|
||||
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)
|
||||
|
||||
Never open Ghidra first. Two minutes of `file`/`strings`/`xxd` frames the whole
|
||||
investigation:
|
||||
|
||||
```
|
||||
$ file $W/0x0026a_bootloader.elf $W/0x0026a_bootloader.bin $W/0x0026a_app.bin
|
||||
$ $PCT info -a $W/0x0026a_full.uf2
|
||||
$ arm-none-eabi-size $W/build/0x0026a_bootloader.elf $W/build/0x0026a_app.elf
|
||||
$ shasum -a 256 $W/0x0026a_app.bin
|
||||
$ strings -n 6 $W/0x0026a_app.bin | sort -u | head -40
|
||||
$ xxd -l 64 $W/manifest.bin
|
||||
```
|
||||
|
||||
What each answers:
|
||||
|
||||
- `file` says ARM little-endian, and `picotool info` names the family
|
||||
(`rp2350-arm-s`) and the target - so you know the ISA and the load address.
|
||||
- `arm-none-eabi-size` prints the ELF `text`/`data`/`bss` totals, and
|
||||
`wc -c` on `0x0026a_app.bin` reports 21656 bytes = `0x5498`, which is exactly
|
||||
`app_size` in the manifest. Two artifacts agree, so the flash map is
|
||||
confirmed without a single line of source.
|
||||
- `shasum` gives you a fingerprint to prove a re-signed image actually changed.
|
||||
- `strings` shows `app banner: %s`, `%s` printf formats, and NUL-split
|
||||
fragments. Note them as search anchors for Ghidra.
|
||||
- `xxd manifest.bin` ends the "what is the format" question immediately:
|
||||
|
||||
```
|
||||
00000000: 5746 4c42 0100 0000 0000 0210 9854 0000 WFLB.........T..
|
||||
00000010: ef04 1ec5 92f0 c567 27da 2651 d373 947b .......g'.&Q.s.{
|
||||
```
|
||||
|
||||
Decode it as five little-endian words plus a blob:
|
||||
|
||||
| Offset | Bytes | Meaning |
|
||||
| ------ | ---------------- | ---------------------------------------- |
|
||||
| 0x00 | `57 46 4c 42` | magic `WFLB` (`0x424C4657` little-endian)|
|
||||
| 0x04 | `01 00 00 00` | version 1 |
|
||||
| 0x08 | `00 00 02 10` | app base `0x10020000` |
|
||||
| 0x0C | `98 54 00 00` | app size `0x5498` (21656) |
|
||||
| 0x10 | 16 bytes | `ef04...947b`, the AES-CMAC tag |
|
||||
| 0x20 | `ff` x 4064 | erased sector padding |
|
||||
|
||||
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)
|
||||
|
||||
1. **File > New Project** (non-shared), then **File > Import File** and pick
|
||||
`$W/build/0x0026a_bootloader.elf`.
|
||||
2. Accept the detected language `ARM:LE:32:v8` (Cortex-M33). Click **Analyze**
|
||||
and take the defaults.
|
||||
3. In **Symbol Tree > Functions**, confirm the boot sequence by address:
|
||||
|
||||
| Function | Address | Reads as |
|
||||
| ------------------ | ------------ | ----------------------------------------------- |
|
||||
| `main` | `0x10000234` | five `BL`s, no logic of its own |
|
||||
| `boot_init` | `0x10000278` | `stdio_init_all`, a wait, one `puts` |
|
||||
| `manifest_read` | `0x100002c8` | `memcpy` of 32 bytes from `0x1001F000` |
|
||||
| `manifest_is_valid`| `0x100002fc` | two compares: magic and version |
|
||||
| `verify_manifest` | `0x100003b0` | the 16-byte key push + CMAC + tag compare |
|
||||
| `aes128_cmac` | `0x1000031c` | mbedtls `cipher_info_from_type` + `cipher_cmac` |
|
||||
| `cmac_tags_equal` | `0x10000348` | constant-time 16-byte compare |
|
||||
| `image_start` | `0x10000394` | VTOR / MSP / `bx` launch block |
|
||||
| `boot_halt` | `0x10000294` | the `BOOT FAILED` dead end |
|
||||
|
||||
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)
|
||||
|
||||
Inside `verify_manifest`, note the address pushed as the CMAC key. Follow it in
|
||||
the **Data** panel to `0x10006064`, or just ask the binary:
|
||||
|
||||
```
|
||||
$ arm-none-eabi-nm -n $W/build/0x0026a_bootloader.elf | grep fw_key
|
||||
10006064 r fw_key
|
||||
```
|
||||
|
||||
Rename the data (`L`) to `fw_key`, then read it as bytes in Ghidra - it is
|
||||
`49 72 6f 6e 4e 65 74 42 6f 6f 74 4b 65 79 21 21`, which is ASCII
|
||||
`IronNetBootKey!!`. That is the AES-128-CMAC key. There was no cryptography to
|
||||
break: the key was in `.rodata` the whole time.
|
||||
|
||||
Cross-check the finding without the board. The signer implements AES and CMAC
|
||||
in pure Python, so the four official RFC 4493 vectors are a self-test you can
|
||||
read and reproduce (the `Key =`/`Tag =` block in Part 3 is your checklist).
|
||||
If your recovered key reproduces the shipped tag, you have proven both the key
|
||||
and the algorithm:
|
||||
|
||||
```
|
||||
$ python3 $W/tools/sign_firmware.py $W/0x0026a_app.bin \
|
||||
--verify $W/manifest.bin \
|
||||
--key 49726f6e4e6574426f6f744b65792121
|
||||
stored : ef041ec592f0c56727da2651d373947b
|
||||
computed : ef041ec592f0c56727da2651d373947b
|
||||
result : MATCH
|
||||
```
|
||||
|
||||
### A.5 Ghidra on the application (the AES gate)
|
||||
|
||||
Repeat A.3 with `$W/build/0x0026a_app.elf`. The app is the richer target.
|
||||
|
||||
| Symbol | Address | Observations |
|
||||
| --------------------- | ------------ | ----------------------------------------------- |
|
||||
| `_reset_handler` | `0x1002015c` | first code; trusts the SP/VTOR the bootloader set|
|
||||
| `main` | `0x10020234` | thin orchestrator |
|
||||
| `ir_init` / `ir_getkey`| `0x1002027c` / `0x100202a4` | NEC receiver setup |
|
||||
| `led_handle_ir` | `0x100204d8` | flattened struct logic; compares `0xc`, `0x18`, `0x5e` |
|
||||
| `secret_print_banner` | `0x10020568` | four mbedtls calls; the banner decrypt |
|
||||
|
||||
Open `secret_print_banner`. The decompiler names the imported calls directly:
|
||||
|
||||
```
|
||||
mbedtls_aes_init(ctx)
|
||||
mbedtls_aes_setkey_dec(ctx, key, 128)
|
||||
mbedtls_aes_crypt_cbc(ctx, MBEDTLS_AES_DECRYPT, 32, iv, ct, ct)
|
||||
mbedtls_aes_free(ctx)
|
||||
```
|
||||
|
||||
The red data references give you every constant you need:
|
||||
|
||||
| Data | Address (VA) | Bytes | ASCII |
|
||||
| ----------------- | ------------ | ------------------------------------------------- | ----------------- |
|
||||
| banner format | `0x10024d68` | `61 70 70 20 62 61 6e 6e 65 72 3a 20 25 73 0a 00` | `app banner: %s\n`|
|
||||
| banner ciphertext | `0x10024d78` | `ba ec 4a 19 ... 7b b2 e7 6e` (32 bytes) | (binary) |
|
||||
| banner IV | `0x10024d9c` | `41 70 70 53...31 32 33 34 35` | `AppSecretIV12345`|
|
||||
| banner key | `0x10024e6c` | `49 72 6f 6e...21 21` | `IronNetBootKey!!`|
|
||||
|
||||
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
|
||||
|
||||
Ciphertext, IV, and key are now just three hex strings. OpenSSL finishes the
|
||||
job; you never needed the source:
|
||||
|
||||
```
|
||||
$ printf 'baec4a1925861bb92dfd9f13f79b3e80cf12b698db9165be8fd7c3477bb2e76e' \
|
||||
| xxd -r -p \
|
||||
| openssl enc -d -aes-128-cbc \
|
||||
-K 49726f6e4e6574426f6f744b65792121 \
|
||||
-iv 41707053656372657449563132333435 -nopad
|
||||
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
|
||||
|
||||
The recovery is only interesting if you can act on it. Because you own the key,
|
||||
you can sign anything:
|
||||
|
||||
1. Tamper the app (any byte, e.g. flip one in `0x0026a_app.bin`).
|
||||
2. Re-sign the tampered image, producing a new manifest, with the recovered
|
||||
key:
|
||||
|
||||
```
|
||||
$ python3 $W/tools/sign_firmware.py $W/0x0026a_app.bin \
|
||||
--out $W/manifest.bin \
|
||||
--key 49726f6e4e6574426f6f744b65792121 \
|
||||
--app-base 0x10020000
|
||||
$ python3 $W/tools/sign_firmware.py $W/0x0026a_app.bin \
|
||||
--verify $W/manifest.bin --key 49726f6e4e6574426f6f744b65792121
|
||||
```
|
||||
|
||||
3. Flash the tampered app at `0x10020000` and the new manifest at `0x1001F000`
|
||||
(Part 11). The bootloader computes the same CMAC over the tampered bytes,
|
||||
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
|
||||
|
||||
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`
|
||||
(confines GDB reads to valid flash/RAM so register previews cannot fault SWD).
|
||||
|
||||
Terminal 1:
|
||||
|
||||
```
|
||||
$ ./debug-server.sh
|
||||
```
|
||||
|
||||
Terminal 2:
|
||||
|
||||
```
|
||||
$ $GDB $W/build/0x0026a_bootloader.elf
|
||||
(gdb) target extended-remote :3333
|
||||
(gdb) source pico-gdb-memory.gdb
|
||||
(gdb) monitor halt
|
||||
```
|
||||
|
||||
Read the manifest straight out of flash and confirm the A.2 decode at runtime:
|
||||
|
||||
```
|
||||
(gdb) x/4wx 0x1001F000
|
||||
0x1001f000: 0x424c4657 0x00000001 0x10020000 0x00005498
|
||||
(gdb) x/16bx 0x1001F010
|
||||
0x1001f010: 0xef 0x04 0x1e 0xc5 0x92 0xf0 0xc5 0x67 ...
|
||||
```
|
||||
|
||||
Break on the gate, reset, and continue:
|
||||
|
||||
```
|
||||
(gdb) hbreak *0x100003b0
|
||||
(gdb) monitor reset halt
|
||||
(gdb) continue
|
||||
```
|
||||
|
||||
When it stops, dump the key argument the CMAC call will receive - the live
|
||||
version of A.4:
|
||||
|
||||
```
|
||||
(gdb) x/16bx 0x10006064
|
||||
0x10060064: 0x49 0x72 0x6f 0x6e 0x4e 0x65 0x74 0x42 ...
|
||||
```
|
||||
|
||||
`0x49 0x72 0x6f 0x6e ...` is `IronNetBootKey!!` again. Then watch the verdict:
|
||||
|
||||
```
|
||||
(gdb) finish
|
||||
(gdb) info registers r0
|
||||
```
|
||||
|
||||
`r0 == 1` means the tags matched and `boot_launch` is next. Set a breakpoint at
|
||||
the launch tail and single-step the handoff:
|
||||
|
||||
```
|
||||
(gdb) break *0x100003ae
|
||||
(gdb) continue
|
||||
(gdb) info registers r3
|
||||
(gdb) x/2wx 0x10020000
|
||||
(gdb) stepi
|
||||
```
|
||||
|
||||
`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
|
||||
|
||||
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):
|
||||
|
||||
```
|
||||
(gdb) file $W/build/0x0026a_app.elf
|
||||
(gdb) hbreak *0x10020568
|
||||
(gdb) monitor reset halt
|
||||
(gdb) continue
|
||||
```
|
||||
|
||||
You are now at `secret_print_banner`, before the banner exists in RAM. Disassemble
|
||||
and read the three constants the same way Ghidra showed them:
|
||||
|
||||
```
|
||||
(gdb) disassemble 0x10020568,+120
|
||||
(gdb) x/16bx 0x10024e6c # banner key -> IronNetBootKey!!
|
||||
(gdb) x/16bx 0x10024d9c # banner IV -> AppSecretIV12345
|
||||
(gdb) x/32bx 0x10024d78 # banner ciphertext
|
||||
```
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
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 =
|
||||
ARM:LE:32:v8** and **Image base = 0x10020000**. Now reconstruct the anchors you
|
||||
already know from the easy pass:
|
||||
|
||||
- **Strings first.** `Ctrl+Shift+S`, search `app banner: %s`. Its only xref is
|
||||
the second half of `secret_print_banner` - you have found the function blind.
|
||||
- **Then its neighbours.** The IV `AppSecretIV12345` sits 0x24 bytes later in
|
||||
`.rodata`; the key `IronNetBootKey!!` sits at `0x10024e6c`. Search the raw
|
||||
bytes (`Search > For Bytes`, `41 70 70 53 ...`) to jump straight to them.
|
||||
- **Then the calls.** The `BL` targets into the mbedtls block
|
||||
(`mbedtls_aes_setkey_dec` at `0x10024088`, `mbedtls_aes_crypt_cbc` at
|
||||
`0x10024994`) confirm the four-call shape even without names.
|
||||
- **Then the manifest magic.** In the bootloader `.bin`, search for
|
||||
`57 46 4c 42` (`WFLB`); its only compare is `manifest_is_valid`, which anchors
|
||||
the whole boot sequence the same way `app banner: %s` anchored the app.
|
||||
|
||||
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
|
||||
|
||||
| Task | Command / address |
|
||||
| ----------------------------- | ------------------------------------------------------------ |
|
||||
| Start GDB server | `./debug-server.sh` then `target extended-remote :3333` |
|
||||
| Confine memory reads | `source pico-gdb-memory.gdb` |
|
||||
| Stop / reset target | `monitor halt` / `monitor reset halt` |
|
||||
| Break on the gate | `hbreak *0x100003b0` (`verify_manifest`) |
|
||||
| Manifest in flash | `x/4wx 0x1001F000` |
|
||||
| CMAC key in flash | `x/16bx 0x10006064` -> `IronNetBootKey!!` |
|
||||
| App banner gate | `hbreak *0x10020568` (`secret_print_banner`) |
|
||||
| Banner key / IV / ciphertext | `0x10024e6c` / `0x10024d9c` / `0x10024d78` |
|
||||
| Re-sign an image | `python3 tools/sign_firmware.py <bin> --out manifest.bin \` |
|
||||
| | ` --key <hex> --app-base 0x10020000` |
|
||||
| Verify a manifest | `python3 tools/sign_firmware.py <bin> --verify manifest.bin` |
|
||||
|
||||
The lesson of the appendix is the lesson of the lab: the adversary never breaks
|
||||
AES. They read the manifest spec from a sector, lift the key from `.rodata`,
|
||||
and re-sign. Design your next boot chain so that the key is not in the image
|
||||
you are trusting it to protect.
|
||||
Binary file not shown.
Reference in new issue
Block a user