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

13 KiB

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
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)

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:

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

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

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

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

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.