20 KiB
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 *** |
| |
+----------------------------------------------------------------------------------------+
The task and criterion headings in this key are word-for-word identical to
CTF-01-R.md, so a student can match each criterion one-to-one.
Artifact Identity
| Artifact | Value |
|---|---|
| Student image | CTF-01.bin |
| Flash image | CTF-01.uf2 |
| Target | Raspberry Pi Pico 2 / RP2350 ARM Cortex-M33 |
| Image base | 0x10000000 |
| Console | UART0, GPIO 0 TX / GPIO 1 RX, 115200 8N1 |
CTF-01.bin 425591AC17FF4C22206286EE6F40B06A89523483804850A41854A9B6F89D7B70
CTF-01.uf2 B1552EE3BB5763D96C65D8DF1C86984EE842EF1A823FDF5D94132B1E10FF9D16
Proof tool: python3 scripts/verify_ctf.py returns 11/11 checks passed against
CTF-01.bin.
Task 1: Setup and Initial Analysis (15 points)
Solution
Criterion 1.1: Ghidra Project Setup (3 points). Import CTF-01.bin as
Raw Binary, language ARM:LE:32:Cortex, base address 0x10000000, then run
auto-analysis.
Criterion 1.2: main() and Status-Loop Addresses (4 points).
| Element | Address |
|---|---|
main() |
0x100001E0 |
| Recurring status loop start | 0x10000234 |
Loop back-edge (b.n 0x10000234) |
0x10000266 |
The back-edge instruction at 0x10000266 is b.n 0x10000234, encoded as bytes
E5 E7.
Criterion 1.3: Vector Table Decoding (4 points).
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 | 0x1000015A |
Criterion 1.4: Thumb Addressing (4 points).
The stored reset pointer 0x1000015B has bit 0 set to 1, which selects Thumb
mode. Clearing bit 0 (0x1000015B & ~1) gives the real instruction address
0x1000015A. The equally odd interrupt vectors (0x1000011B, 0x1000011D,
0x10000111) are handled the same way.
Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|---|---|---|---|---|
| Criterion 1.1: Ghidra Project Setup | 3 | Correct project name, ARM:LE:32:Cortex (ARM Cortex 32-bit little endian), base 0x10000000 |
One item off | Not set up |
| Criterion 1.2: main() and Status-Loop Addresses | 4 | Both addresses correct | One correct | Neither found |
| Criterion 1.3: Vector Table Decoding | 4 | Correct base, initial SP, reset pointer | One missing | Not found |
| Criterion 1.4: Thumb Addressing | 4 | Correctly clears bit 0 and identifies main() |
General explanation | Incorrect |
Instructor Notes & Assembly
- Confirm the Ghidra import used
Raw Binary,ARM:LE:32:Cortex, base0x10000000, and that auto-analysis completed before any address was read. - Verify
main()is0x100001E0, the loop head is0x10000234, and the back-edge bytesE5 E7sit at0x10000266. - The reset vector is stored as
0x1000015B; the real Thumb entry is0x1000015A. Do not accept the un-cleared0x1000015Bas an instruction address.
Task 2: Find and Patch Bug #1: The Miscalibrated Safety Threshold (30 points)
Solution
grid_deviation is volatile, so the compiler emits two independent compares:
one for the operator status and one for the automated dispatch decision.
Criterion 2.1: Locate Compare Site A (5 points).
100001fc: 2b5e cmp r3, #94 @ 0x5e
| Item | Value |
|---|---|
| Address | 0x100001FC |
| File offset | 0x1FC |
| Original bytes | 5E 2B |
| Original instruction | cmp r3, #94 |
Criterion 2.2: Locate Compare Site B (5 points).
1000020a: 2b5e cmp r3, #94 @ 0x5e
| Item | Value |
|---|---|
| Address | 0x1000020A |
| File offset | 0x20A |
| Original bytes | 5E 2B |
| Original instruction | cmp r3, #94 |
Criterion 2.3: Correct Immediate-Value Reasoning (8 points).
The source constant is SAFE_THRESHOLD = 95 and the test is x < 95. For an
unsigned value, x < 95 is exactly x <= 94, which the compiler emits as
cmp r3, #94 plus ite hi. The correct engineering limit is 60, so the test
is x < 60, which is x <= 59. The correct patched immediate is therefore
0x3B (59), not 0x3C (60). Patching to 0x3C would wrongly accept a
reading of exactly 60.
Criterion 2.4: Patch Compare Site A (4 points).
| Address | File Offset | Original | Patched | Before | After |
|---|---|---|---|---|---|
0x100001FC |
0x1FC |
5E 2B |
3B 2B |
cmp r3, #94 |
cmp r3, #59 |
Criterion 2.5: Patch Compare Site B (4 points).
| Address | File Offset | Original | Patched | Before | After |
|---|---|---|---|---|---|
0x1000020A |
0x20A |
5E 2B |
3B 2B |
cmp r3, #94 |
cmp r3, #59 |
Criterion 2.6: Explain Why Both Sites Must Be Patched (4 points).
operator_state (0x20000844) and dispatch_state (0x20000834) are each
computed from their own read of grid_deviation. Patching only site A fixes the
displayed text while the automated dispatch at site B still authorizes on the
dangerous reading. Frozen reading 87: 87 <= 94 is true (STABLE/AUTHORIZED,
wrong); 87 <= 59 is false (CRITICAL/HELD, correct).
Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|---|---|---|---|---|
| Criterion 2.1: Locate Compare Site A | 5 | Correct address and original bytes | Address off | Not found |
| Criterion 2.2: Locate Compare Site B | 5 | Correct address and original bytes | Address off | Not found |
| Criterion 2.3: Correct Immediate-Value Reasoning | 8 | Explains the < to <= transform and gives 0x3B |
Correct value, no reasoning | Wrong value |
| Criterion 2.4: Patch Compare Site A | 4 | Byte change verified | Wrong byte | Not patched |
| Criterion 2.5: Patch Compare Site B | 4 | Byte change verified | Wrong byte | Not patched |
| Criterion 2.6: Explain Why Both Sites Must Be Patched | 4 | Clear explanation of the two independent comparisons | Vague | Missing |
Instructor Notes & Assembly
- Both sites must be patched:
0x100001FCfor operator status and0x1000020Afor the automated dispatch decision. - The correct immediate is
0x3B(59), not0x3C(60); the source test is a strict<, compiled as<= 94. - Verify the byte changes on hardware: after patching,
operator_state(0x20000844) anddispatch_state(0x20000834) read0and0. - Ghidra ARM/Thumb Context Note: In raw
.binfiles, patching an instruction that precedes anITblock (ite hi) using the GUI Patch Instruction action triggers Ghidra'sReDisassembleCommand. The re-disassembler encounters an internal context register conflict when trying to re-declare theITBlockcontext over existing instructions, collapsing Thumb decoding into 32-bit ARM mode and swallowing Site B (0x1000020A). Students must patch using the Bytes window workflow (ClearC-> edit byte5E->3Bin Bytes window with pencil icon -> DisassembleD) to keep Site B visible and cleanly aligned.
Task 3: Find and Patch Bug #2: The False Signal Banner (20 points)
Solution
Criterion 3.1: Locate the Banner String (5 points).
| String | Address |
|---|---|
"SIGNAL: NORMAL\r" |
0x10003678 |
"NORMAL" substring to patch |
0x10003680 |
The string is loaded at call site 0x10000224 (ldr r0, [pc, #92] ->
0x10003678) and printed by bl __wrap_puts at 0x10000226. It is printed
once at boot and never recomputed.
Criterion 3.2: Patch Six Characters (8 points).
NORMAL and DANGER are both six ASCII characters, so the patch preserves the
string length.
| Address Range | Original Bytes | Patched Bytes |
|---|---|---|
0x10003680 - 0x10003685 |
4E 4F 52 4D 41 4C |
44 41 4E 47 45 52 |
Criterion 3.3: Character-by-Character Documentation (4 points).
| 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 |
Criterion 3.4: Explain the Danger of a Hardcoded Status Word (3 points).
The banner never consults the reading, so it reports a healthy line even while
the frozen reading is dangerous. Operators trust supervisory banners, so a
hardcoded NORMAL masks the hazard and prevents manual intervention.
Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|---|---|---|---|---|
| Criterion 3.1: Locate the Banner String | 5 | Correct address | Approximate | Not found |
| Criterion 3.2: Patch Six Characters | 8 | All six bytes changed, length preserved | Correct text, wrong bytes documented | Wrong length |
| Criterion 3.3: Character-by-Character Documentation | 4 | Original vs patched byte for all six characters | Partial | Missing |
| Criterion 3.4: Explain the Danger of a Hardcoded Status Word | 3 | Clear, specific reasoning | Generic | Missing |
Instructor Notes & Assembly
NORMALandDANGERare both six characters; the patch must not change the string length or overwrite adjacent flash.- The banner is printed once at boot and never recomputed, so it is a separate defect from the threshold and must be graded independently.
- Confirm the patch covers
0x10003680through0x10003685exactly.
Task 4: Recover the Quarantined Dispatch Frame (10 points)
Solution
Criterion 4.1: Recover the Dispatch Frame (6 points).
Address 0x100037A0 in flash .rodata:
WORLDGRID:BLACKSTART:GRID-7:WATER-3
Criterion 4.2: Explain Why It Is Never Transmitted (4 points).
retain_dispatch_frame() reads only the first character into a volatile local
so the linker keeps the string, but the pointer is never passed to any print or
UART routine. The frame is evidence only and must not be patched.
Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|---|---|---|---|---|
| Criterion 4.1: Recover the Dispatch Frame | 6 | Correct address and full text | Partial text | Not found |
| Criterion 4.2: Explain Why It Is Never Transmitted | 4 | Clear static-analysis explanation | Vague | Missing |
Instructor Notes & Assembly
- Accept the full string
WORLDGRID:BLACKSTART:GRID-7:WATER-3at0x100037A0in flash.rodata. - The frame is evidence only. Penalize any submission that patches or rewrites it instead of recovering it.
Task 5: Export and Verify (20 points)
Solution
Criterion 5.1: Export CTF-01_fixed.bin (4 points).
Export the patched program from Ghidra (File -> Export Program..., Binary Format) as CTF-01_fixed.bin.
Criterion 5.2: Convert to CTF-01_fixed.uf2 (4 points).
python uf2conv.py CTF-01_fixed.bin --base 0x10000000 --family 0xe48bff59 --output CTF-01_fixed.uf2
Criterion 5.3: Hardware Verification (8 points).
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>
After all three 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>
If the console is not wired, the same proof is read over SWD: after reset,
operator_state (0x20000844) and dispatch_state (0x20000834) are 1 and
1 for the shipped image, and 0 and 0 for the patched image. This has been
confirmed on real hardware.
Criterion 5.4: Summary Table of All Patches (4 points).
| # | What | Address(es) | Original | Patched |
|---|---|---|---|---|
| 1a | Operator threshold | 0x100001FC |
5E 2B |
3B 2B |
| 1b | Dispatch threshold | 0x1000020A |
5E 2B |
3B 2B |
| 2 | Signal banner | 0x10003680-0x10003685 |
4E 4F 52 4D 41 4C |
44 41 4E 47 45 52 |
Total: 8 bytes actually change (one immediate byte at each compare site and six string bytes).
Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|---|---|---|---|---|
| Criterion 5.1: Export CTF-01_fixed.bin | 4 | Valid patched binary | Corrupted | Not submitted |
| Criterion 5.2: Convert to CTF-01_fixed.uf2 | 4 | Correct base and family flags | Wrong flags | Not submitted |
| Criterion 5.3: Hardware Verification | 8 | Corrected console output confirmed on hardware | Some lines corrected | No verification |
| Criterion 5.4: Summary Table of All Patches | 4 | Complete address and before/after table | Missing entries | No table |
Instructor Notes & Assembly
- Verify the exported image with
python3 scripts/verify_ctf.py; the shipped check expects11/11 checks passedagainstCTF-01.bin. - Confirm the UF2 conversion used base
0x10000000and family0xe48bff59. - Serial Terminal Timing: Note that
print_boot_banner()(SIGNAL: DANGER) fires within the first 5 milliseconds of boot. In normal lab usage, PuTTY attaches after boot and will display the continuous 1-second status loop (GRID STATUS: CRITICAL,DISPATCH PATH: HELD). To seeSIGNAL: DANGER, the student must pulseRUNtoGNDwhile PuTTY is open, or demonstrate the string change at0x10003680via Ghidra static analysis. - If no console is available, accept the SWD capture showing
operator_stateanddispatch_stateat0and0on the patched image.
Task 6: Written Reflection (5 points)
Solution
Criterion 6.1: "Rushed Build" Is Not an Excuse (2 points). The missed review step is exactly what shipped the false-safe report; pressure explains why the safeguard was skipped, not why it should be skipped.
Criterion 6.2: One Engineering Practice per Bug (3 points).
- Bug #1 (duplicated threshold): a unit test or static-analysis rule that
requires every comparison against
SAFE_THRESHOLDto use one shared source of truth. - Bug #2 (hardcoded banner): a hardware-in-the-loop smoke test that checks the boot banner against the latched reading.
Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|---|---|---|---|---|
| Criterion 6.1: "Rushed Build" Is Not an Excuse | 2 | Specific, grounded reasoning | Generic | Missing |
| Criterion 6.2: One Engineering Practice per Bug | 3 | Concrete practice for each bug | One bug only | Missing |
Instructor Notes & Assembly
- Grade the specificity of the reasoning, not the length of the prose.
- Require one concrete engineering practice for each of the two bugs.
How To Breadboard
- Raspberry Pi Pico 2 powered over USB.
- 3.3 V USB-UART adapter:
- Adapter RX to Pico GP0 (UART0 TX)
- Adapter TX to Pico GP1 (UART0 RX)
- Adapter GND to Pico GND
- Do not connect the adapter VCC while the Pico is USB powered.
- Serial monitor: 115200 baud, 8 data bits, no parity, 1 stop bit.
- No other peripherals are required; all evidence is obtained from the console.
Complete Grading Summary
| Task | Title | Points |
|---|---|---|
| Task 1 | Setup and Initial Analysis | 15 |
| Task 2 | Find and Patch Bug #1: The Miscalibrated Safety Threshold | 30 |
| Task 3 | Find and Patch Bug #2: The False Signal Banner | 20 |
| Task 4 | Recover the Quarantined Dispatch Frame | 10 |
| Task 5 | Export and Verify | 20 |
| Task 6 | Written Reflection | 5 |
| TOTAL | 100 |
Instructor Notes
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.
Common Student Mistakes
- Patching only one threshold site (
0x100001FCor0x1000020A), which leaves one status line lying. - Assuming the immediate equals the limit, producing an off-by-one boundary;
the correct byte is
0x3B(59), not0x3C(60). - Replacing the banner string with a different length, corrupting adjacent
flash;
NORMALandDANGERare both 6 bytes. - Treating the odd vector address
0x1000015Bas invalid instead of clearing bit 0 to get0x1000015A. - Modifying the quarantined dispatch frame at
0x100037A0, which destroys evidence.
Partial Credit Guidelines
- Award partial credit for one correct threshold site out of two, or for a
correct immediate value without the
<to<=reasoning. - Award partial credit for a correct banner text with incorrectly documented bytes, or for partial character-by-character documentation.
- Accept an SWD read of
operator_stateanddispatch_stateas equivalent proof when no UART console is available. - Award no credit for patches that change string length or overwrite adjacent flash.
Appendix: Expected Binary Diff
| # | What | File Offset(s) | Address(es) | Original Bytes | Patched Bytes |
|---|---|---|---|---|---|
| 1a | Operator threshold | 0x1FC |
0x100001FC |
5E 2B |
3B 2B |
| 1b | Dispatch threshold | 0x20A |
0x1000020A |
5E 2B |
3B 2B |
| 2 | Signal banner | 0x3680 - 0x3685 |
0x10003680 - 0x10003685 |
4E 4F 52 4D 41 4C |
44 41 4E 47 45 52 |
Total: 8 bytes actually change (one immediate byte at each compare site and six string bytes).