mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-09-20 00:22:27 +02:00
312 lines
13 KiB
Markdown
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.
|