Files
Kevin Thomas 35eacd2c0e Course update: lessons, CTF 0x0011a_cb, and documentation
- 0x0011a_cb (Operation Dark Vector): nation-state CTF redesign with an
  AES-128-ECB sealed target and a plaintext launch origin; RP2350 firmware with
  bearing-driven servo, tri-color LEDs, GSV stats, and a realistic no-fix path
- docs: story-driven classified brief, GDB and Ghidra tutorials with deep
  step-throughs, regenerated artifacts and PDFs
- scripts: docstring standard, AES per-student randomizer, telemetry monitor
- week 3 to week 5 lessons: Ghidra patching tutorial, CMSIS-SVD hardware RE,
  double floating-point and GPIO architecture chapters, README structure
2026-09-27 14:18:56 -04:00

447 lines
13 KiB
Markdown

# GDB Hardware Debugging Tutorial: Reverse Engineering the Stripped RP2350 Image
## 0. Cold Open
The board is powered. The blade is turning. And none of it is true to you yet,
because you have not seen it with your own eyes.
Every claim you will make about this machine has to survive one test: did you
watch it happen? Not did the decompiler suggest it. Not did the datasheet imply
it. Did you stop the core, read the register, and see the number with your own
eyes.
The Debug Probe is the only honest witness in the room. It reaches through SWD
into the silicon and pulls out the truth at 5,000 kHz while the rest of the
world argues. It does not care what you believe. It does not care what Buddy
answered. It reports.
A register is not an opinion. A clock divider is not a narrative. `SM0_CLKDIV =
0x07A12000` is not a talking point. It is 1953.125, and 1953.125 is the reason
the sky is readable at all.
Anyone can generate an explanation. You are here to *verify* one, bit by bit,
on live silicon, and to sign your name to it.
**Think, then verify.**
---
## 1. Executive Summary
This tutorial reverse engineers the **stripped** `0x0011a_cb.bin` on live
silicon using a Raspberry Pi Debug Probe, OpenOCD, and GNU GDB. There are **no
symbols**, no `main`, no variable names, nothing. You set breakpoints by
**address**, read raw memory, and let the hardware tell you the truth.
Everything shown was captured from the real target and is reproducible.
```
+-----------------------------------------------------------------+
| GDB HARDWARE DEBUG SIGNAL CHAIN |
+-----------------------------------------------------------------+
| HOST macOS -> USB -> Debug Probe -> SWD -> RP2350B Cortex-M33 |
| OpenOCD :3333 <------ SWD + UART bridge ------> GP0/GP1 115200 |
+-----------------------------------------------------------------+
```
The philosophy: an offline model can guess what a stripped image does. It
cannot read the live registers, watch the parser, or verify a patch. **Think,
then verify.**
---
## 2. Prerequisites
| Component | Value |
|---|---|
| Target | Raspberry Pi Pico 2 (RP2350B) |
| Image | `0x0011a_cb.bin` (raw flash image, base `0x10000000`) |
| Probe | Raspberry Pi Debug Probe (CMSIS-DAP v2) |
| Architecture | `ARM:LE:32:Cortex` (ARMv8-M / Cortex-M33, Thumb-2) |
The `.bin` is the stripped image. `file offset = address, 0x10000000`.
### 2.1 Tool Paths Per Host (macOS, Linux, Windows)
The probe, OpenOCD, and GDB behave identically on every platform; only the paths
and the shell differ. Pick your host.
| Tool | macOS | Linux | Windows |
|---|---|---|---|
| OpenOCD | `~/.pico-sdk/openocd/0.12.0+dev/openocd` | `$HOME/.pico-sdk/openocd/0.12.0+dev/openocd` | `%USERPROFILE%\.pico-sdk\openocd\0.12.0+dev\openocd.exe` |
| OpenOCD scripts | `~/.pico-sdk/openocd/0.12.0+dev/scripts` | `$HOME/.pico-sdk/openocd/0.12.0+dev/scripts` | `%USERPROFILE%\.pico-sdk\openocd\0.12.0+dev\scripts` |
| GDB | `~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb` | `$HOME/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb` | `%USERPROFILE%\.pico-sdk\toolchain\14_2_Rel1\bin\arm-none-eabi-gdb.exe` |
| Serial port | `/dev/tty.usbmodem*` | `/dev/ttyACM*` | `COMx` (Device Manager) |
Install the tools if you do not have them:
- macOS: `brew install --cask gcc-arm-embedded` for the toolchain, then
`brew install open-ocd`, or run the Raspberry Pi `pico-setup` script, which
places everything under `~/.pico-sdk`.
- Linux: install `gcc-arm-none-eabi` and `openocd` from your package manager,
or run the Raspberry Pi `pico-setup` script under `~/.pico-sdk`.
- Windows: install the Raspberry Pi Pico VS Code extension or the official
Windows installer; both place the toolchain under `%USERPROFILE%\.pico-sdk`.
Use PowerShell for every command below.
Serial console to the debug UART, per host:
```bash
# macOS
screen /dev/tty.usbmodem* 115200
# Linux
screen /dev/ttyACM* 115200
```
```powershell
# Windows
putty -serial COMx -sercfg 115200,8,n,1
```
---
## 3. Flash the Stripped Image
Because there are no object headers, you flash the raw bytes at the flash base
explicitly.
macOS and Linux:
```bash
OCD=~/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=~/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg \
-c "adapter speed 5000" \
-c "program 0x0011a_cb.bin 0x10000000 verify reset exit"
```
Windows PowerShell:
```powershell
$OCD = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\openocd.exe"
$SCR = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\scripts"
& $OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg `
-c "adapter speed 5000" `
-c "program 0x0011a_cb.bin 0x10000000 verify reset exit"
```
Expected on every host:
```
** Programming Started **
** Programming Finished **
** Verified OK **
** Resetting Target **
```
---
## 4. Attach GDB With No Symbols
Start the OpenOCD GDB server in one terminal, then attach **without** an
executable in another.
macOS and Linux:
```bash
OCD=~/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=~/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000" &
GDB=~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb
$GDB -q
(gdb) target extended-remote localhost:3333
(gdb) monitor reset halt
```
Windows PowerShell:
```powershell
$OCD = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\openocd.exe"
$SCR = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\scripts"
Start-Process $OCD -ArgumentList @("-s","$SCR","-f","interface/cmsis-dap.cfg","-f","target/rp2350.cfg","-c","adapter speed 5000")
$GDB = "$env:USERPROFILE\.pico-sdk\toolchain\14_2_Rel1\bin\arm-none-eabi-gdb.exe"
& $GDB -q
(gdb) target extended-remote localhost:3333
(gdb) monitor reset halt
```
`bt` and `break main` are useless: there are no symbols. Every stop is an
address. This is the stripped-image reality.
---
## 5. Break at the Reset Handler's Destination
From static analysis (see the Ghidra tutorial) we know the code entry `main`
begins at `0x10000234`. Break there by address:
```gdb
(gdb) break *0x10000234
(gdb) continue
Thread 1 hit Breakpoint 1, 0x10000234 in ?? ()
(gdb) info registers r0 r1 r2 r3 sp lr pc xpsr
r0 0x0 0
r1 0x10000235 268436021
r2 0x80808080 -2139062144
r3 0xe000ed08 -536810232
sp 0x20082000
lr 0x1000018f
pc 0x10000234
xpsr 0x69000000
```
Backtrace shows raw addresses only:
```
(gdb) bt
#0 0x10000234 in ?? ()
```
---
## 6. The Decoy And The Encrypted Target (No Symbols Needed)
The firmware carries **two** kinds of coordinate, and they are not the same kind
of thing.
**The decoy (plaintext).** A plaintext pair sits at `0x1000A098` (lat) /
`0x1000A090` (lon): `+38.840280` / `-77.428890`. The beacon broadcasts it and a
byte scanner grabs it. It is the lie.
```
+-----------------------------------------------------------------+
| DECOY WAYPOINT (PLAINTEXT, THE LIE) |
+-----------------------------------------------------------------+
| 0x1000A098 CF BD 87 4B 8E 6B 43 40 double +38.840280 |
| 0x1000A090 36 E5 0A EF 72 5B 53 C0 double -77.428890 |
+-----------------------------------------------------------------+
```
**The target (AES-encrypted).** The real waypoint is never stored as a double:
```gdb
(gdb) x/16bx 0x10009D90
0x10009D90: 0x56 0x45 0x43 0x54 0x4f 0x52 0x31 0x31 # "VECTOR11"
0x10009D98: 0x41 0x45 0x53 0x4b 0x45 0x59 0x21 0x21 # "AESKEY!!"
(gdb) x/2gx 0x10009DA4
0x10009DA4: 0x7aea23c0c2ac4f20 0xcaef2311710f933a # ciphertext block
```
`init_navigation @ 0x10000C2C` decrypts each word with the key into RAM at
`TARGET_LAT 0x20000D00` / `TARGET_LON 0x20000D08`. Read the reconstructed truth
after boot:
```gdb
(gdb) x/2gx 0x20000d00
0x20000d00: 0x404370e368f08462 0xc0535cd1633482bf
```
Decode it: `+38.881940` / `-77.450280` (NRO HQ). The decoy says Centreville. The
target says Chantilly. Same firmware. One of them is a lie.
## 7. Prove the Peripherals From Registers
Break at `send_telemetry @ 0x10000C7C` (called once per acquisition tick) so
the initialisers have already run:
```gdb
(gdb) break *0x10000c7c
(gdb) continue
Thread 1 hit Breakpoint 2, 0x10000c7c in ?? ()
(gdb) x/2xw 0x50200000
0x50200000: 0x00000001 0x0f010e01 # PIO0_CTRL PIO0_FSTAT
(gdb) x/6xw 0x502000c8
0x502000c8: 0x07a12000 0x0701fc00 # SM0_CLKDIV SM0_EXECCTRL
0x502000d0: 0x800c0000 0x00000018 # SM0_SHIFTCTRL SM0_ADDR
0x502000d8: 0x00002020 0x00038000 # SM0_INSTR SM0_PINCTRL
```
```gdb
(gdb) x/4xw 0x40078024
0x40078024: 0x000003d0 0x00000024 0x00000070 0x00000301 # UART1 LoRa
(gdb) x/4xw 0x40070024
0x40070024: 0x00000051 0x00000018 0x00000070 0x00000301 # UART0 debug
```
```
+-----------------------------------------------------------------+
| PIO0 SM0 CLOCK DIVIDER (RP2350 @ 150 MHz, 8 cycles/bit) |
+-----------------------------------------------------------------+
| SM0_CLKDIV = 0x07A12000 = 1953 + 32/256 = 1953.125 |
| f_sm = 150,000,000 / 1953.125 = 76,800 Hz = 9600 x 8 |
+-----------------------------------------------------------------+
```
UART baud uses `IBRD` and a 6-bit `FBRD`:
$$baud = \frac{f_{clk}}{16 \times (IBRD + FBRD/64)}$$
UART1: $976 + 36/64 = 976.5625 \Rightarrow 150{,}000{,}000 / 15625 = 9600$.
UART0: $81 + 24/64 = 81.375 \Rightarrow 150{,}000{,}000 / 1302 \approx 115200$.
---
## 8. Watch the NMEA Parser Prove Itself
Static analysis identifies the parser's static index at `0x200010F4` (the
`.bss` symbol `idx.1`) and the 96-byte NMEA buffer at `0x20001044` (`buf.0`).
Watch the index:
```gdb
(gdb) watch *(int*)0x200010f4
(gdb) continue
Hardware watchpoint 3: *(int*)0x200010f4
Old value = 0
New value = 1
0x10000458 in ?? ()
(gdb) bt
#0 0x10000458 in ?? ()
#1 0x1000027c in ?? ()
(gdb) x/32cb 0x20001044
0x20001044: 36 '$' 71 'G' 80 'P' 71 'G' 76 'L' 76 'L' ...
# => "$GPGLL,,,,,,220653.00,V,N*4A"
```
At reset the buffer and index are both zero; these bytes appear only after a
sentence is parsed off the wire.
The `V` says there is **no fix yet**, not a parser bug, not a UART fault.
Only the wire can tell you that.
---
## 9. The Actuators: `release_payload @ 0x10000BFC`
The Ghidra analysis (companion tutorial) shows `release_payload` drives GP16,
GP17 and GP18 high through the RP2350 GPIO coprocessor interface. It is reached
by a `b.w` tail call from `navigate_to_target` at `0x10000D18`, so there is no
return frame; break at its entry and step the writes:
```gdb
(gdb) break *0x10000bfc
(gdb) continue
Thread 1 hit Breakpoint 4, 0x10000bfc in ?? ()
(gdb) x/9i $pc
0x10000bfc: push {r3, lr}
0x10000bfe: movs r2, #16
0x10000c00: mov.w r3, #1
0x10000c04: mcrr 0, 4, r2, r3, cr0 # GPIO16 = 1
0x10000c08: movs r2, #17
0x10000c0a: mcrr 0, 4, r2, r3, cr0 # GPIO17 = 1
0x10000c0e: movs r2, #18
0x10000c10: mcrr 0, 4, r2, r3, cr0 # GPIO18 = 1
```
After the three writes, the SIO register reads `0x00070000` (bits 16, 17, 18).
The RP2350 exposes GPIO through the coprocessor (`mcrr p0, #4, ...`), not a
plain store, a fact only the bench reveals.
---
## 10. Reproducibility Checklist
1. Flash `0x0011a_cb.bin` at `0x10000000`; `Verified OK`.
2. Attach GDB with **no** symbol file.
3. `break *0x10000234`; PC lands exactly there.
4. `x/4xw 0x20000d00` -> `68f08462 404370e3 633482bf c0535cd1`.
5. `x/6xw 0x502000c8` -> `SM0_CLKDIV = 0x07A12000`.
6. `watch *(int*)0x200010f4` trips only when NMEA arrives.
7. `x/9i 0x10000bfc` shows the `mcrr` GPIO coprocessor writes.
If a single value differs, you are not on the image you think you are. That
check is the job an offline model cannot do.
---
## 11. Why Buddy Fails Here
Even a model trained on ARM cannot read `SM0_CLKDIV`, watch `idx.0`, decode
*your* randomized literal, or verify a patch by flashing it. It generates
plausible text; the bench generates truth. **Think, then verify.**
---
## Appendix A. Deep GDB Step-Through
### A.1 Start the server and attach
macOS:
```bash
OCD=~/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=~/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000"
```
Linux:
```bash
OCD=$HOME/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=$HOME/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000"
```
Windows PowerShell:
```powershell
$OCD = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\openocd.exe"
$SCR = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\scripts"
& $OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000"
```
Then attach from GDB (identical on all hosts). There are no symbols, so break by
address, not by name:
```gdb
target extended-remote localhost:3333
monitor reset halt
break *0x10000234
continue
```
### A.2 Read the vector table and the key material
```gdb
x/2xw 0x10000000 # SP and reset handler
x/16bx 0x10009D90 # AES key
x/16bx 0x10009DA4 # ciphertext block
x/2gx 0x20000D00 # decrypted target after boot
```
### A.3 Break the AES routine
```gdb
break *0x10000E5C
continue
x/4i $pc
stepi
stepi
```
`aes128_ecb_decrypt_block` runs once, early, inside `init_navigation`.
### A.4 Watch the decoy go out
```gdb
break *0x10000C7C
continue
info registers r0 r1 r2 r3
```
`send_telemetry` is called with the decoy while the GNSS has no fix.
### A.5 The tool trap
On a stripped target a floating point cast can byte-swap. Trust the raw read:
```gdb
x/1gx 0x20000D00 # then decode it yourself
```