mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-10-01 05:30:15 +02:00
- 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
447 lines
13 KiB
Markdown
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
|
|
```
|
|
|