Files
Embedded-Hacking/0x0001b_ctf/CTF-S.md
T

312 lines
13 KiB
Markdown

# CTF Challenge - Operation Black Start
## Instructor Solution Key
```
+----------------------------------------------------------------------------------------+
| |
| ██████╗ ██╗ █████╗ ██████╗██╗ ██╗███████╗████████╗ █████╗ ██████╗ ████████╗ |
| ██╔══██╗██║ ██╔══██╗██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝ |
| ██████╔╝██║ ███████║██║ █████╔╝ ███████╗ ██║ ███████║██████╔╝ ██║ |
| ██╔══██╗██║ ██╔══██║██║ ██╔═██╗ ╚════██║ ██║ ██╔══██║██╔══██╗ ██║ |
| ██████╔╝███████╗██║ ██║╚██████╗██║ ██╗███████╗ ██║ ██║ ██║██║ ██║ ██║ |
| ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝██║ ██║╚══════╝ ╚═╝ ╚═╝ ╚═╝██║ ██║ ██║ |
| |
| |
| O P E R A T I O N B L A C K S T A R T |
| |
| *** INSTRUCTOR SOLUTION KEY - RESTRICTED *** |
| |
+----------------------------------------------------------------------------------------+
```
> This key is for Operation Black Start only. It contains no FINAL-project
> answers, constants, addresses, bugs, or patches.
---
## Artifact Identity
| Artifact | Value |
|----------|-------|
| Student image | `CTF-01.bin` |
| Flash image | `CTF-01.uf2` |
| Target | Raspberry Pi Pico 2 / RP2350 ARM |
| Image base | `0x10000000` |
| UART | UART0, GPIO 0 TX / GPIO 1 RX, 115200 8N1 |
```text
CTF-01.bin 6FD296F7A85F243FB26BF6BFFCBEAB26815FD8915101A81F72069063A5635E5A
CTF-01.uf2 980F04369C23AD32A063DFE18DE5AF08DF3830138FC7E7898B1F011B4F5E1D9D
```
Every address and byte value below was independently verified against the
compiled ELF (`arm-none-eabi-nm`, `arm-none-eabi-objdump`) and the raw bytes
of the delivered `CTF-01.bin` (direct hex read at each file offset). This
image contains **no** LED, relay, sensor, display, or GPIO-control logic.
---
## Instructor Scenario
WorldGrid Compact's emergency firmware build for the GRID-7 relay fleet was
compiled and shipped eleven minutes after a cyberattack severed the primary
SCADA uplink. The engineer (Dr. Elias Renner) had no time to review the
build; the source used for that compile no longer exists. The training
relay supplied to students holds the exact miscompiled image that shipped
to the fleet, containing two real, independently patchable defects.
Students must reconstruct the boot path, locate both defects with Ghidra,
patch the binary directly, export and convert it, and prove the corrected
behavior on real hardware — the same workflow used in the FINAL projects.
---
## Task 1: Setup and Initial Analysis Solution
### Vector Table (first 32 bytes of `CTF-01.bin`)
```text
00 20 08 20 5B 01 00 10 1B 01 00 10 1D 01 00 10
11 01 00 10 11 01 00 10 11 01 00 10 11 01 00 10
```
| Evidence | Answer |
|----------|--------|
| Vector table base | `0x10000000` |
| Initial SP | `0x20082000` |
| Reset pointer (as stored) | `0x1000015b` |
| Reset instruction address (bit 0 cleared) | `0x1000015a` |
| `main()` | `0x100001e0` |
| Recurring status loop start | `0x10000234` |
| Loop branch (`b.n` back to loop start) | `0x10000246` |
### Data Symbols
| Symbol | Address | Notes |
|--------|---------|-------|
| `grid_deviation` | `0x200005d8` | Initialized to `87` (0.87 Hz x100), lives in `.data` |
| `operator_state` | `0x20000844` | Zero-initialized, `.bss` |
| `dispatch_state` | `0x20000834` | Zero-initialized, `.bss` |
| `dispatch_frame` | `0x100037a0` | Hidden black-start frame, `.rodata` |
---
## Task 2: Bug #1 Solution — Miscalibrated Safety Threshold
`grid_deviation` is declared `volatile`, so the compiler cannot reuse one
comparison for both output paths — it emits **two independent** compare
instructions, one for `operator_state` and one for `dispatch_state`:
```text
100001f8: 6813 ldr r3, [r2, #0] ; r3 = grid_deviation
100001fc: 2b5e cmp r3, #94 @ 0x5e ; <-- PATCH LOCATION A
100001fe: bf8c ite hi
10000200: 2300 movhi r3, #0 ; operator_state = 0 (CRITICAL)
10000202: 2301 movls r3, #1 ; operator_state = 1 (STABLE)
10000204: 6033 str r3, [r6, #0]
10000206: 6813 ldr r3, [r2, #0] ; r3 = grid_deviation (2nd read)
1000020a: 2b5e cmp r3, #94 @ 0x5e ; <-- PATCH LOCATION B
1000020c: bf8c ite hi
1000020e: 2300 movhi r3, #0 ; dispatch_state = 0 (HELD)
10000210: 2301 movls r3, #1 ; dispatch_state = 1 (AUTHORIZED)
10000212: 602b str r3, [r5, #0]
```
### Why the immediate is 94, not 95
The source-level constant is `SAFE_THRESHOLD = 95`, and the intended test is
`grid_deviation < 95`. For an **unsigned** comparison, GCC legally rewrites
`x < 95` as `x <= 94` (`ite hi`/`movhi`/`movls` on the `hi`/`ls` unsigned
condition codes), which lets it use a single `cmp` + `ite` sequence instead
of a separate branch. The compiled immediate is therefore **one less** than
the source constant.
The correct engineering limit is **60**. Applying the same compiler
transform, `x < 60` becomes `x <= 59`, so the **correct patched immediate is
`0x3B` (59), not `0x3C` (60)**. A student who patches to `0x3C` without
understanding this transform will get a binary that misclassifies a
`grid_deviation` of exactly 60.
### Exact Byte Patch (both locations, identical change)
| Location | File Offset | Address | Original Bytes | Patched Bytes | Instruction Before | Instruction After |
|----------|-------------|---------|-----------------|----------------|---------------------|---------------------|
| A | `0x1FC` | `0x100001fc` | `5E 2B` | `3B 2B` | `cmp r3, #0x5e` | `cmp r3, #0x3b` |
| B | `0x20A` | `0x1000020a` | `5E 2B` | `3B 2B` | `cmp r3, #0x5e` | `cmp r3, #0x3b` |
Both changes independently verified against the raw bytes of `CTF-01.bin`.
### Why both must be patched
`operator_state` (GRID STATUS line) and `dispatch_state` (DISPATCH PATH
line) are each computed from their **own** re-read of `grid_deviation`
against their **own** copy of the compiled threshold. Patching only
location A fixes what is *displayed* to a human operator while leaving the
*automated dispatch decision* (location B) still authorizing a black start
on a dangerous reading — the worst possible partial fix, because it makes
the display look trustworthy while the machine still does the wrong thing.
### Grid math with the frozen reading (87)
| Threshold used | Comparison | Result |
|---|---|---|
| Miscompiled: `<= 94` | `87 <= 94` -> true | `STABLE` / `AUTHORIZED` (false-safe) |
| Corrected: `<= 59` | `87 <= 59` -> false | `CRITICAL` / `HELD` (true, safe) |
---
## Task 3: Bug #2 Solution — The False Signal Banner
The unconditional boot-banner string lives in `.rodata`:
| String | Address |
|--------|---------|
| `"SIGNAL: NORMAL\r"` (printed via `puts`, which appends `\n`) | `0x10003678` |
| `"NORMAL"` substring to patch | `0x10003680` |
Call site: `0x10000224` loads `r0 = 0x10003678`; `0x10000226` calls
`__wrap_puts`. This line prints once, at boot, and is never re-evaluated —
it does not depend on `grid_deviation` at all.
### Exact Byte Patch (6 bytes, same length: `NORMAL` -> `DANGER`)
| Address | Original Char | Original Byte | Patched Char | Patched Byte |
|---------|----------------|----------------|----------------|----------------|
| `0x10003680` | N | `4E` | D | `44` |
| `0x10003681` | O | `4F` | A | `41` |
| `0x10003682` | R | `52` | N | `4E` |
| `0x10003683` | M | `4D` | G | `47` |
| `0x10003684` | A | `41` | E | `45` |
| `0x10003685` | L | `4C` | R | `52` |
Verified directly against the delivered `CTF-01.bin` (ASCII read at
`0x10003678` returns exactly `SIGNAL: NORMAL`).
---
## Task 4: Recovered Dispatch Frame Solution
```text
WORLDGRID:BLACKSTART:GRID-7:WATER-3
```
Address: `0x100037a0` (flash `.rodata`). It is retained in the image through
a non-disclosing runtime anchor (`retain_dispatch_frame()`), so it survives
linker garbage collection but is never passed to a print routine. Full
credit requires a Ghidra Defined-Strings or cross-reference explanation, not
a guess from the narrative. Students must not patch this value.
---
## Task 5: Export and Verify Solution
### Expected UART Transcript — Before Patching
```text
GLOBAL EMBEDDED RESPONSE NETWORK
BLACK START WINDOW: 27 MINUTES
UART0 115200 8N1 | AUTHORIZED LAB CONSOLE
SIGNAL: NORMAL
RESPONSE> GRID STATUS: STABLE
DISPATCH PATH: AUTHORIZED
LAST FRAME: QUARANTINED
RESPONSE>
```
### Expected UART Transcript — After All Three Byte Patches
```text
GLOBAL EMBEDDED RESPONSE NETWORK
BLACK START WINDOW: 27 MINUTES
UART0 115200 8N1 | AUTHORIZED LAB CONSOLE
SIGNAL: DANGER
RESPONSE> GRID STATUS: CRITICAL
DISPATCH PATH: HELD
LAST FRAME: QUARANTINED
RESPONSE>
```
### Complete Patch Summary Table
| # | What | Address(es) | Original | Patched |
|---|------|--------------|----------|---------|
| 1a | GRID STATUS threshold compare | `0x100001fc` | `5E 2B` | `3B 2B` |
| 1b | DISPATCH PATH threshold compare | `0x1000020a` | `5E 2B` | `3B 2B` |
| 2 | Boot banner signal word | `0x10003680`-`0x10003685` | `4E 4F 52 4D 41 4C` (NORMAL) | `44 41 4E 47 45 52` (DANGER) |
Total: **8 bytes changed** to correct a false-safe reading on a fleet
responsible for tens of millions of people.
### UF2 Conversion
```bash
python uf2conv.py CTF-01_fixed.bin --base 0x10000000 --family 0xe48bff59 --output CTF-01_fixed.uf2
```
### Hardware Verification Note
The vector table, boot path, and every byte offset above were verified by
direct inspection of the compiled ELF and the delivered `CTF-01.bin` (two
independent cross-checks: disassembly-derived addresses and raw hex-dump
addresses agree exactly). A live UART capture on physical hardware is the
final confirmation step and should be performed with the Pico in BOOTSEL
mode before grading a submission that claims hardware verification.
---
## Grading Notes
Accept equivalent addresses when a student's Ghidra auto-analysis produces
slightly different intermediate labels, provided the byte-level patch
locations and values match this key. Do not award credit for a `0x3C` patch
to the threshold immediates without a correct explanation of the `<`/`<=`
compiler transform — that is a coincidentally-close but technically
incorrect answer for the boundary case `grid_deviation == 60`.
A complete answer finds both threshold locations, explains the compiler's
comparison transform, patches all three locations, and proves the corrected
behavior on real hardware.
---
## Task 6: Written Reflection Solution Guidance
There is no single "correct" essay for either question. Grade for specific,
grounded reasoning tied to *this* incident, not generic statements.
**Question 1 — Why "rushed under emergency pressure" is not an excuse:**
An acceptable answer names the actual failure mode: an eleven-minute compile
with no review path shipped an integer threshold that was never checked
against the documented 60-unit engineering limit, and a hardcoded status
string that was never wired to the real reading at all. "We were under
pressure" explains *why* the review step was skipped; it does not change
the fact that the skipped step is what caused the false-safe report. Full
credit requires the student to connect the excuse to the specific missing
safeguard (code review or automated bounds-checking), not just assert that
pressure is never an excuse.
**Question 2 — One practice per bug:**
- Bug #1 (miscalibrated, duplicated threshold): a unit test or static
analysis rule that checks every comparison against `SAFE_THRESHOLD`
matches a single source of truth, or a code review that would have asked
"why is this threshold checked in two places instead of one shared
function?"
- Bug #2 (hardcoded status string): a hardware-in-the-loop smoke test that
compares the boot banner's signal word against the actual latched
reading, which would have caught a string that never changes regardless
of input.
Award full credit only when the named practice is specific enough that it
would plausibly have caught that exact bug, not a generic "more testing"
answer.
---
## ⚠ Safety
Use only the supplied Pico 2, 3.3 V UART adapter, and firmware. Never
connect the exercise to an operational grid, water plant, public network,
military system, or third-party device.