mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-10-02 14:10:25 +02:00
Course update: lessons, CTF 0x0011a_cb, and documentation
- 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
This commit is contained in:
1 parent
5201ee4b6b
commit
35eacd2c0e
162 files changed
+125658
-232
No files matched your search
@@ -0,0 +1,4 @@
|
||||
build
|
||||
!.vscode/*
|
||||
!build-ctf/0x0001b_ctf.bin
|
||||
!CTF-01.bin
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Pico",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**",
|
||||
"${userHome}/.pico-sdk/sdk/2.3.1/**"
|
||||
],
|
||||
"forcedInclude": [
|
||||
"${workspaceFolder}/build/generated/pico_base/pico/config_autogen.h",
|
||||
"${userHome}/.pico-sdk/sdk/2.3.1/src/common/pico_base_headers/include/pico.h"
|
||||
],
|
||||
"defines": [],
|
||||
"compilerPath": "${userHome}/.pico-sdk/toolchain/15_2_Rel1/bin/arm-none-eabi-gcc.exe",
|
||||
"compileCommands": "${workspaceFolder}/build/compile_commands.json",
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "c++14",
|
||||
"intelliSenseMode": "linux-gcc-arm"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"name": "Pico",
|
||||
"compilers": {
|
||||
"C": "${command:raspberry-pi-pico.getCompilerPath}",
|
||||
"CXX": "${command:raspberry-pi-pico.getCxxCompilerPath}"
|
||||
},
|
||||
"environmentVariables": {
|
||||
"PATH": "${command:raspberry-pi-pico.getEnvPath};${env:PATH}"
|
||||
},
|
||||
"cmakeSettings": {
|
||||
"Python3_EXECUTABLE": "${command:raspberry-pi-pico.getPythonPath}"
|
||||
}
|
||||
}
|
||||
]
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"marus25.cortex-debug",
|
||||
"ms-vscode.cpptools",
|
||||
"ms-vscode.cpptools-extension-pack",
|
||||
"ms-vscode.vscode-serial-monitor",
|
||||
"raspberry-pi.raspberry-pi-pico"
|
||||
]
|
||||
}
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Pico Debug (Cortex-Debug)",
|
||||
"cwd": "${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
|
||||
"executable": "${command:raspberry-pi-pico.launchTargetPath}",
|
||||
"request": "launch",
|
||||
"type": "cortex-debug",
|
||||
"servertype": "openocd",
|
||||
"serverpath": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
|
||||
"gdbPath": "${command:raspberry-pi-pico.getGDBPath}",
|
||||
"debuggerArgs": ["-ex", "set debug-file-directory /debug"],
|
||||
"device": "${command:raspberry-pi-pico.getChipUppercase}",
|
||||
"configFiles": [
|
||||
"interface/cmsis-dap.cfg",
|
||||
"target/${command:raspberry-pi-pico.getTarget}.cfg"
|
||||
],
|
||||
"svdFile": "${userHome}/.pico-sdk/sdk/2.3.1/src/${command:raspberry-pi-pico.getChip}/hardware_regs/${command:raspberry-pi-pico.getChipUppercase}.svd",
|
||||
"runToEntryPoint": "main",
|
||||
// Fix for no_flash binaries, where monitor reset halt doesn't do what is expected
|
||||
// Also works fine for flash binaries
|
||||
"overrideLaunchCommands": [
|
||||
"monitor reset init",
|
||||
"load \"${command:raspberry-pi-pico.launchTargetPath}\""
|
||||
],
|
||||
"openOCDLaunchCommands": [
|
||||
"adapter speed 5000"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Pico Debug (Cortex-Debug with external OpenOCD)",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"executable": "${command:raspberry-pi-pico.launchTargetPath}",
|
||||
"request": "launch",
|
||||
"type": "cortex-debug",
|
||||
"servertype": "external",
|
||||
"gdbTarget": "localhost:3333",
|
||||
"gdbPath": "${command:raspberry-pi-pico.getGDBPath}",
|
||||
"debuggerArgs": ["-ex", "set debug-file-directory /debug"],
|
||||
"device": "${command:raspberry-pi-pico.getChipUppercase}",
|
||||
"svdFile": "${userHome}/.pico-sdk/sdk/2.3.1/src/${command:raspberry-pi-pico.getChip}/hardware_regs/${command:raspberry-pi-pico.getChipUppercase}.svd",
|
||||
"runToEntryPoint": "main",
|
||||
// Fix for no_flash binaries, where monitor reset halt doesn't do what is expected
|
||||
// Also works fine for flash binaries
|
||||
"overrideLaunchCommands": [
|
||||
"monitor reset init",
|
||||
"load \"${command:raspberry-pi-pico.launchTargetPath}\""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"cmake.showSystemKits": false,
|
||||
"cmake.options.statusBarVisibility": "hidden",
|
||||
"cmake.options.advanced": {
|
||||
"build": {
|
||||
"statusBarVisibility": "hidden"
|
||||
},
|
||||
"launch": {
|
||||
"statusBarVisibility": "hidden"
|
||||
},
|
||||
"debug": {
|
||||
"statusBarVisibility": "hidden"
|
||||
},
|
||||
"variant": {
|
||||
"statusBarVisibility": "hidden"
|
||||
},
|
||||
"buildTarget": {
|
||||
"statusBarVisibility": "hidden"
|
||||
}
|
||||
},
|
||||
"cmake.configureOnEdit": false,
|
||||
"cmake.automaticReconfigure": false,
|
||||
"cmake.configureOnOpen": false,
|
||||
"cmake.generator": "Ninja",
|
||||
"cmake.cmakePath": "${userHome}/.pico-sdk/cmake/v4.3.4/bin/cmake",
|
||||
"C_Cpp.debugShortcut": false,
|
||||
"terminal.integrated.env.windows": {
|
||||
"PICO_SDK_PATH": "${env:USERPROFILE}/.pico-sdk/sdk/2.3.1",
|
||||
"PICO_TOOLCHAIN_PATH": "${env:USERPROFILE}/.pico-sdk/toolchain/15_2_Rel1",
|
||||
"Path": "${env:USERPROFILE}/.pico-sdk/toolchain/15_2_Rel1/bin;${env:USERPROFILE}/.pico-sdk/picotool/2.3.1/picotool;${env:USERPROFILE}/.pico-sdk/cmake/v4.3.4/bin;${env:USERPROFILE}/.pico-sdk/ninja/v1.13.2;${env:PATH}"
|
||||
},
|
||||
"terminal.integrated.env.osx": {
|
||||
"PICO_SDK_PATH": "${env:HOME}/.pico-sdk/sdk/2.3.1",
|
||||
"PICO_TOOLCHAIN_PATH": "${env:HOME}/.pico-sdk/toolchain/15_2_Rel1",
|
||||
"PATH": "${env:HOME}/.pico-sdk/toolchain/15_2_Rel1/bin:${env:HOME}/.pico-sdk/picotool/2.3.1/picotool:${env:HOME}/.pico-sdk/cmake/v4.3.4/bin:${env:HOME}/.pico-sdk/ninja/v1.13.2:${env:PATH}"
|
||||
},
|
||||
"terminal.integrated.env.linux": {
|
||||
"PICO_SDK_PATH": "${env:HOME}/.pico-sdk/sdk/2.3.1",
|
||||
"PICO_TOOLCHAIN_PATH": "${env:HOME}/.pico-sdk/toolchain/15_2_Rel1",
|
||||
"PATH": "${env:HOME}/.pico-sdk/toolchain/15_2_Rel1/bin:${env:HOME}/.pico-sdk/picotool/2.3.1/picotool:${env:HOME}/.pico-sdk/cmake/v4.3.4/bin:${env:HOME}/.pico-sdk/ninja/v1.13.2:${env:PATH}"
|
||||
},
|
||||
"raspberry-pi-pico.cmakeAutoConfigure": true,
|
||||
"raspberry-pi-pico.useCmakeTools": false,
|
||||
"raspberry-pi-pico.cmakePath": "${HOME}/.pico-sdk/cmake/v4.3.4/bin/cmake",
|
||||
"raspberry-pi-pico.ninjaPath": "${HOME}/.pico-sdk/ninja/v1.13.2/ninja"
|
||||
}
|
||||
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Compile Project",
|
||||
"type": "process",
|
||||
"isBuildCommand": true,
|
||||
"command": "${userHome}/.pico-sdk/ninja/v1.13.2/ninja",
|
||||
"args": ["-C", "${workspaceFolder}/build"],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated"
|
||||
},
|
||||
"problemMatcher": "$gcc",
|
||||
"windows": {
|
||||
"command": "${env:USERPROFILE}/.pico-sdk/ninja/v1.13.2/ninja.exe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run Project",
|
||||
"type": "process",
|
||||
"command": "${env:HOME}/.pico-sdk/picotool/2.3.1/picotool/picotool",
|
||||
"args": [
|
||||
"load",
|
||||
"${command:raspberry-pi-pico.launchTargetPath}",
|
||||
"-fx"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated"
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"windows": {
|
||||
"command": "${env:USERPROFILE}/.pico-sdk/picotool/2.3.1/picotool/picotool.exe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Flash",
|
||||
"type": "process",
|
||||
"command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
|
||||
"args": [
|
||||
"-s",
|
||||
"${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
|
||||
"-f",
|
||||
"interface/cmsis-dap.cfg",
|
||||
"-f",
|
||||
"target/${command:raspberry-pi-pico.getTarget}.cfg",
|
||||
"-c",
|
||||
"adapter speed 5000; program \"${command:raspberry-pi-pico.launchTargetPath}\" verify reset exit"
|
||||
],
|
||||
"problemMatcher": [],
|
||||
"windows": {
|
||||
"command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Rescue Reset",
|
||||
"type": "process",
|
||||
"command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
|
||||
"args": [
|
||||
"-s",
|
||||
"${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
|
||||
"-f",
|
||||
"interface/cmsis-dap.cfg",
|
||||
"-f",
|
||||
"target/${command:raspberry-pi-pico.getChip}-rescue.cfg",
|
||||
"-c",
|
||||
"adapter speed 5000; reset halt; exit"
|
||||
],
|
||||
"problemMatcher": [],
|
||||
"windows": {
|
||||
"command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "RISC-V Reset (RP2350)",
|
||||
"type": "process",
|
||||
"command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
|
||||
"args": [
|
||||
"-s",
|
||||
"${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
|
||||
"-c",
|
||||
"set USE_CORE { rv0 rv1 cm0 cm1 }",
|
||||
"-f",
|
||||
"interface/cmsis-dap.cfg",
|
||||
"-f",
|
||||
"target/rp2350.cfg",
|
||||
"-c",
|
||||
"adapter speed 5000; init;",
|
||||
"-c",
|
||||
"write_memory 0x40120158 8 { 0x3 }; echo [format \"Info : ARCHSEL 0x%02x\" [read_memory 0x40120158 8 1]];",
|
||||
"-c",
|
||||
"reset halt; targets rp2350.rv0; echo [format \"Info : ARCHSEL_STATUS 0x%02x\" [read_memory 0x4012015C 8 1]]; exit"
|
||||
],
|
||||
"problemMatcher": [],
|
||||
"windows": {
|
||||
"command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2026 Kevin Thomas
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# Author: Kevin Thomas
|
||||
# Email: kevin@mytechnotalent.com
|
||||
# GitHub: https://github.com/mytechnotalent
|
||||
# File: CMakeLists.txt
|
||||
# Desc: Configures the RP2350 Pico SDK project for the Operation Black Start
|
||||
# CTF relay firmware.
|
||||
# Created: 2026
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Initialise pico_sdk from installed location
|
||||
# (note this can come from environment, CMake cache etc)
|
||||
|
||||
# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work ==
|
||||
if(WIN32)
|
||||
set(USERHOME $ENV{USERPROFILE})
|
||||
else()
|
||||
set(USERHOME $ENV{HOME})
|
||||
endif()
|
||||
set(sdkVersion 2.3.1)
|
||||
set(toolchainVersion 15_2_Rel1)
|
||||
set(picotoolVersion 2.3.1)
|
||||
set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake)
|
||||
if (EXISTS ${picoVscode})
|
||||
include(${picoVscode})
|
||||
endif()
|
||||
# ====================================================================================
|
||||
set(PICO_BOARD pico2 CACHE STRING "Board type")
|
||||
|
||||
# Pull in Raspberry Pi Pico SDK (must be before project)
|
||||
include(pico_sdk_import.cmake)
|
||||
|
||||
project(0x0001b_ctf C CXX ASM)
|
||||
|
||||
# Initialise the Raspberry Pi Pico SDK
|
||||
pico_sdk_init()
|
||||
|
||||
# Add executable with modular sources in src/
|
||||
add_executable(0x0001b_ctf
|
||||
src/main.c
|
||||
src/grid.c
|
||||
src/console.c
|
||||
)
|
||||
|
||||
pico_set_program_name(0x0001b_ctf "0x0001b_ctf")
|
||||
pico_set_program_version(0x0001b_ctf "0.1")
|
||||
|
||||
# Modify the below lines to enable/disable output over UART/USB
|
||||
pico_enable_stdio_uart(0x0001b_ctf 1)
|
||||
pico_enable_stdio_usb(0x0001b_ctf 0)
|
||||
target_compile_definitions(0x0001b_ctf PRIVATE
|
||||
PICO_DEFAULT_UART_BAUD_RATE=115200
|
||||
)
|
||||
|
||||
# Add the standard library to the build
|
||||
target_link_libraries(0x0001b_ctf
|
||||
pico_stdlib
|
||||
)
|
||||
|
||||
# Add the standard include files to the build
|
||||
target_include_directories(0x0001b_ctf PRIVATE
|
||||
${CMAKE_CURRENT_LIST_DIR}/include
|
||||
)
|
||||
|
||||
pico_add_extra_outputs(0x0001b_ctf)
|
||||
@@ -0,0 +1,442 @@
|
||||
# Operation Black Start - Student Instructions
|
||||
|
||||
**⚠ WORLDGRID EMERGENCY INCIDENT ⚠**
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------------------------+
|
||||
| |
|
||||
| ██████╗ ██╗ █████╗ ██████╗██╗ ██╗███████╗████████╗ █████╗ ██████╗ ████████╗ |
|
||||
| ██╔══██╗██║ ██╔══██╗██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝ |
|
||||
| ██████╔╝██║ ███████║██║ █████╔╝ ███████╗ ██║ ███████║██████╔╝ ██║ |
|
||||
| ██╔══██╗██║ ██╔══██║██║ ██╔═██╗ ╚════██║ ██║ ██╔══██║██╔══██╗ ██║ |
|
||||
| ██████╔╝███████╗██║ ██║╚██████╗██║ ██╗███████╗ ██║ ██║ ██║██║ ██║ ██║ |
|
||||
| ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝██║ ██║╚══════╝ ╚═╝ ╚═╝ ╚═╝██║ ██║ ██║ |
|
||||
| |
|
||||
| |
|
||||
| O P E R A T I O N B L A C K S T A R T |
|
||||
| |
|
||||
| *** PRIORITY RED *** |
|
||||
| |
|
||||
+----------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
WorldGrid Compact's emergency firmware build for its GRID-7 relay fleet
|
||||
shipped a miscompiled safety threshold and a hardcoded false status string,
|
||||
so deployed relays report a false-safe "STABLE" status while the frozen
|
||||
frequency deviation reading of 0.87 Hz is nearly 50% beyond the 0.60 Hz
|
||||
engineering limit. The source was overwritten by the next build and cannot
|
||||
be recovered, so the only surviving evidence is the exact miscompiled
|
||||
training image. Students reverse engineer `CTF-01.bin` with Ghidra, locate
|
||||
and patch both defects directly in the binary, export a corrected image,
|
||||
flash it to a Pico 2, and prove the corrected behavior with GDB and a UART
|
||||
console.
|
||||
|
||||
---
|
||||
|
||||
## Scenario Briefing
|
||||
|
||||
### Background
|
||||
|
||||
**WorldGrid Compact** is the emergency interconnection standard shared by
|
||||
three allied national grid operators. When any member's primary SCADA
|
||||
network goes dark, a fleet of small embedded relay nodes - call sign
|
||||
**GRID-7** - is the only thing standing between an orderly recovery and an
|
||||
uncontrolled cascade. Each relay node watches the last known grid frequency
|
||||
deviation, decides whether conditions are safe, and either **holds** the
|
||||
automatic black-start dispatch or **authorizes** it.
|
||||
|
||||
At 03:11 UTC, a coordinated cyberattack severed the primary SCADA uplink
|
||||
across the GRID-7 corridor and the WATER-3 aqueduct pumping stations that
|
||||
depend on it. With the network coordination center offline and three
|
||||
continents' worth of hospitals, rail systems, and water treatment plants
|
||||
running on backup power, WorldGrid's engineering team did the only thing
|
||||
they could: they rushed an emergency firmware build for the relay fleet and
|
||||
pushed it within **eleven minutes** of the attack being detected.
|
||||
|
||||
### The Disaster
|
||||
|
||||
The engineer who built that emergency image, **Dr. Elias Renner**, has not
|
||||
slept in thirty-one hours. He compiled the fix, ran a five-second bench
|
||||
test, and shipped it - because the alternative was leaving the relay fleet
|
||||
completely blind. It appears to work. The relay boots. It prints a status
|
||||
report. It reports **GRID STATUS: STABLE** and **DISPATCH PATH:
|
||||
AUTHORIZED**.
|
||||
|
||||
There is a problem: the frozen frequency reading latched at the moment
|
||||
communications were cut shows a deviation of **0.87 Hz** - nearly *50%
|
||||
beyond* WorldGrid's hard engineering limit of **0.60 Hz**. A deviation this
|
||||
large, if trusted, means the grid is nowhere near stable enough for an
|
||||
automatic black-start dispatch. If the fleet authorizes dispatch on a false
|
||||
"STABLE" reading, cascading generator trips will follow within minutes,
|
||||
and GRID-7 and WATER-3 will go dark for the second time - this time with no
|
||||
backup plan.
|
||||
|
||||
**Dr. Renner's rushed build has a bug. Multiple relay nodes are already
|
||||
reporting the same false-safe status. Nobody has found where in the
|
||||
compiled firmware the error lives, because the source code used for that
|
||||
emergency compile was overwritten by the next build fifteen minutes later
|
||||
and cannot be recovered.**
|
||||
|
||||
### The Only Surviving Evidence
|
||||
|
||||
One relay node - the training/verification unit - still holds the exact
|
||||
miscompiled image that shipped to the fleet. This binary, and this binary
|
||||
alone, is the only remaining copy of the emergency build. There is no
|
||||
source code. There is no build log. There is only the compiled image, a
|
||||
UART cable, and whatever a skilled embedded reverse engineer can prove by
|
||||
reading machine code.
|
||||
|
||||
### The Human Stakes
|
||||
|
||||
| Consequence if the false "STABLE" reading is trusted | Scale |
|
||||
|---|---|
|
||||
| Hospitals on generator backup past their fuel reserve | 214 facilities |
|
||||
| Water treatment and pumping stations losing pressure | 3 aqueduct systems |
|
||||
| Rail corridors stranded mid-route | 6 national rail networks |
|
||||
| Estimated population affected by cascading failure | 40+ million people |
|
||||
|
||||
**The options are:**
|
||||
|
||||
1. ❌ **Trust the fleet's reported status** - dispatch fires on a false
|
||||
reading, cascading failure follows within the hour.
|
||||
2. ❌ **Shut the entire relay fleet down** - buys time, but leaves 40
|
||||
million people with no automated recovery path at all.
|
||||
3. **REVERSE ENGINEER THE EMERGENCY BUILD** - find the exact
|
||||
miscompiled bytes, patch them, verify the corrected image on real
|
||||
hardware, and hand the fix to the field team so the *rest of the fleet*
|
||||
can be safely repatched before the next attempt.
|
||||
|
||||
### THE SHORTAGE
|
||||
|
||||
For years, the world treated embedded systems as invisible infrastructure.
|
||||
The engineers who could read a vector table, decode a Thumb branch, or
|
||||
patch a miscompiled constant directly in a stripped binary were never
|
||||
numerous enough. Tonight almost all of them are already in the field
|
||||
chasing other failures. **You are the reserve team.**
|
||||
|
||||
You were called in because you can do something Dr. Renner's exhausted
|
||||
team cannot do right now: read what the processor is actually doing, with
|
||||
no source code, no time for a rewrite, and no room for a guess.
|
||||
|
||||
> **⏰ TIME PRESSURE:** The field team is standing by to push your verified
|
||||
> patch to the rest of the GRID-7 fleet. Every relay node still reporting
|
||||
> a false "STABLE" status is one dispatch cycle away from disaster.
|
||||
|
||||
> **AUTHORIZED LAB ONLY:** This challenge uses a supplied Pico 2 training
|
||||
> relay and its exact miscompiled firmware image. Do not connect this
|
||||
> exercise to a public network, an operational grid, a water utility, or
|
||||
> any device you do not own or have explicit written authorization to test.
|
||||
|
||||
---
|
||||
|
||||
## Learning Objectives
|
||||
|
||||
- Decode the RP2350 / ARM Cortex-M33 vector table and identify the reset
|
||||
handler and initial stack pointer.
|
||||
- Trace the bootrom-to-reset handoff and translate Thumb reset-vector
|
||||
addresses into real function entry points.
|
||||
- Locate a miscompiled boundary comparison and reason about the correct
|
||||
immediate value the compiler should have encoded.
|
||||
- Patch compare instructions and a status string directly in a raw binary
|
||||
with Ghidra.
|
||||
- Recover a hidden quarantined dispatch frame from the compiled image.
|
||||
- Export and UF2-convert a corrected image, then verify the corrected
|
||||
behavior on real hardware with GDB and a UART console.
|
||||
|
||||
---
|
||||
|
||||
## What This Project Tests
|
||||
|
||||
| Week | Concepts Tested |
|
||||
|------|-----------------|
|
||||
| 1 | RP2350 architecture, ARM Cortex-M33 registers, stack, flash/RAM, Thumb assembly, Ghidra static analysis |
|
||||
| 2 | GDB connection, breakpoints, disassembly, register and memory inspection, UART observation |
|
||||
| 3 | Bootrom handoff, vector table, reset handler, startup code, XIP, Thumb-bit addressing |
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Understanding the System
|
||||
|
||||
### GRID-7 Relay Hardware
|
||||
|
||||
| Component | Connection | Purpose |
|
||||
|-----------|------------|---------|
|
||||
| Raspberry Pi Pico 2 | RP2350 | Runs the miscompiled emergency firmware |
|
||||
| UART TX | GPIO 0 | Relay telemetry output |
|
||||
| UART RX | GPIO 1 | Reserved (no command parser is implemented) |
|
||||
| SWD debug interface | Supplied probe | Authorized GDB inspection |
|
||||
|
||||
No LED, relay output, sensor, display, or other peripheral is part of this
|
||||
CTF. Every graded finding lives in flash (`.rodata`/`.text`) or SRAM, and is
|
||||
reachable with only the Weeks 1-3 toolset: Ghidra, GDB, and a UART monitor.
|
||||
|
||||
### UART Configuration
|
||||
|
||||
- Baud: `115200`
|
||||
- Data: `8 bits`
|
||||
- Parity: `none`
|
||||
- Stop: `1`
|
||||
- Logic: `3.3 V`
|
||||
|
||||
### Normal (Intended) Behavior
|
||||
|
||||
The relay should latch the frozen deviation reading, compare it against the
|
||||
**real** WorldGrid safety limit of **60** (0.60 Hz, encoded as an integer
|
||||
`x100`), and report honestly:
|
||||
|
||||
```
|
||||
+-----------------------------------------------------------------+
|
||||
| Intended Relay Behavior |
|
||||
| |
|
||||
| 1. Boot and initialize UART |
|
||||
| 2. Print the boot identity and a signal-quality banner |
|
||||
| 3. Compare the frozen 87 (0.87 Hz) reading against the 60 |
|
||||
| (0.60 Hz) safety limit |
|
||||
| 4. 87 exceeds 60, so the grid is NOT stable |
|
||||
| 5. Report GRID STATUS: CRITICAL and DISPATCH PATH: HELD |
|
||||
| 6. Repeat the report once per second until conditions change |
|
||||
+-----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
### Observed (Buggy) Behavior - What You Will See When You First Flash `CTF-01.uf2`
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
> **Terminal Timing Note:** The first four lines (`GLOBAL EMBEDDED...` through
|
||||
> `SIGNAL: NORMAL`) represent the **initial boot banner**, emitted once during
|
||||
> startup. If your serial terminal (PuTTY) connects after the board has
|
||||
> booted, you will observe the continuous 1-second status stream (`GRID
|
||||
> STATUS...` and `DISPATCH PATH...`). To view the boot banner in your terminal,
|
||||
> reset the Pico (pulse `RUN` to `GND`) while PuTTY is actively connected.
|
||||
|
||||
This is exactly what Dr. Renner's team is seeing on the deployed fleet. It
|
||||
is wrong, and it is wrong in **two independent ways** inside the compiled
|
||||
binary. Do not assume the first readable sentence is the full truth -
|
||||
treat every printed line as evidence to be checked against the machine
|
||||
code, not as a fact on its own.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: The Firmware
|
||||
|
||||
You do not have the source code. It was overwritten fifteen minutes after
|
||||
the emergency build shipped. You have only the compiled image. Your job is
|
||||
to reverse engineer it with Ghidra, locate the defects, and patch the
|
||||
binary directly - exactly the way Dr. Renner's field team will need to
|
||||
patch the rest of the deployed fleet.
|
||||
|
||||
### What The Firmware Does
|
||||
|
||||
1. Initializes UART0 and stdio.
|
||||
2. Reads a frozen grid-frequency-deviation reading that was latched in
|
||||
memory before communications were severed.
|
||||
3. Compares that reading against a compiled-in safety threshold - **twice**,
|
||||
once for each independent status line it reports.
|
||||
4. Prints a boot banner containing an unconditional signal-quality line.
|
||||
5. Enters an infinite loop printing the grid classification and dispatch
|
||||
decision once per second.
|
||||
|
||||
### Bug Summary - What You Are Graded On
|
||||
|
||||
| Bug # | Category | Severity | Description | Hint |
|
||||
|-------|----------|----------|--------------|------|
|
||||
| **Bug #1** | Miscompiled safety constant | **CRITICAL** | The safety threshold used to classify the frozen reading was compiled far too permissive. It is used **twice** - once for the operator-facing status and once for the automated dispatch decision - and **both** copies must be corrected. | The real WorldGrid safety limit is 60 (0.60 Hz). Search for the wrong immediate value used in the comparison. |
|
||||
| **Bug #2** | Hardcoded string literal | **HIGH** | The boot banner unconditionally prints a signal-quality word that does not reflect the actual reading, regardless of what the relay later reports. | The correct word describes the true state of a 0.87 Hz deviation against a 0.60 Hz limit - not "NORMAL". |
|
||||
|
||||
**Important:** The replacement text for Bug #2 **must be the same length**
|
||||
as the original - patching a shorter or longer string will corrupt
|
||||
adjacent flash data.
|
||||
|
||||
### A Third Finding - Not a Bug, a Recovery Task
|
||||
|
||||
Somewhere in this image is the **quarantined black-start authorization
|
||||
frame** - the exact frame the relay is supposed to transmit to the
|
||||
regional dispatcher once a human operator confirms it is safe to proceed.
|
||||
It is never printed by the firmware. Recovering it (without patching
|
||||
anything) is required evidence for your final report.
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Your Assignment
|
||||
|
||||
Whenever a task asks you to **Document** or **answer**, write your answers
|
||||
in a single file named `CTF-01-Answers.md`.
|
||||
|
||||
### Task 1: Setup and Initial Analysis
|
||||
|
||||
1. Create a new Ghidra project named `Black_Start_Investigation`.
|
||||
2. Import `CTF-01.bin`.
|
||||
3. Configure the language as **ARM Cortex 32-bit, little endian**.
|
||||
4. Set the base address to `0x10000000`.
|
||||
5. Run auto-analysis.
|
||||
|
||||
**Document:**
|
||||
- A screenshot of the Ghidra **Import Results** or **Program Information**
|
||||
window showing the project name, processor settings, and base address.
|
||||
- The address of `main()`.
|
||||
- The address of the recurring status loop (the branch target that repeats
|
||||
once per second).
|
||||
- The vector-table base, the initial stack pointer, and the reset-handler
|
||||
pointer as stored (note its Thumb bit) versus the actual instruction
|
||||
address.
|
||||
|
||||
### Task 2: Find and Patch Bug #1 - The Miscalibrated Safety Threshold
|
||||
|
||||
1. Find **both** locations where the frozen reading is compared against
|
||||
the miscompiled safety constant.
|
||||
2. Document the exact address, the original instruction, and the original
|
||||
immediate value at each location.
|
||||
3. Determine the correct immediate value. **Caution:** the compiler may
|
||||
not have encoded the raw threshold you expect - a strict "less than"
|
||||
comparison against an unsigned value is often optimized into a
|
||||
"less-or-equal" comparison against one less than the threshold. Show
|
||||
your reasoning.
|
||||
4. Patch **both** locations in Ghidra using the **Bytes Window** workflow:
|
||||
> **Critical ARM Thumb-2 Patching Note:** In ARM Cortex-M, compare instructions that directly precede conditional execution blocks (`ite hi`) must **not** be patched using the right-click *Patch Instruction* dialog. Ghidra's automatic re-disassembler encounters an internal context conflict with the subsequent `ite hi` instruction, which collapses Thumb decoding and swallows Compare Site B (`0x1000020A`).
|
||||
>
|
||||
> To patch cleanly without breaking downstream disassembly, use the **Bytes Window**:
|
||||
> 1. Ensure the Bytes window is open (**Window** -> **Bytes: CTF-01.bin**).
|
||||
> 2. In the Bytes window toolbar, click the **pencil icon** (**Toggle Edit Mode**).
|
||||
> 3. In the Listing window, click on address `0x100001FC` (Compare Site A) and press **`C`** (**Clear Code Bytes**). The instruction temporarily clears into raw bytes (`5E 2B`).
|
||||
> 4. In the Bytes window, locate offset `100001fc`, click on `5E`, and change it to **`3B`**.
|
||||
> 5. Click back in the Listing window on address `0x100001FC` and press **`D`** (**Disassemble**). The instruction immediately disassembles cleanly as `cmp r3, #0x3b`.
|
||||
> 6. Notice that Compare Site B at `0x1000020A` remains completely intact and visible! Repeat the exact same steps at `0x1000020A`: click `0x1000020A` in the Listing, press **`C`**, change `5E` to **`3B`** in the Bytes window, click back in the Listing, and press **`D`**.
|
||||
|
||||
**Questions to answer:**
|
||||
- Why must both locations be patched? What happens if you only patch one?
|
||||
- Why is a false "STABLE" classification on an 0.87 Hz reading dangerous
|
||||
for an automated black-start dispatch?
|
||||
|
||||
### Task 3: Find and Patch Bug #2 - The False Signal Banner
|
||||
|
||||
1. Find the boot-banner string that unconditionally reports the wrong
|
||||
signal quality.
|
||||
2. Document its address and the exact bytes that must change.
|
||||
3. Patch the string, preserving its exact length.
|
||||
|
||||
**Questions to answer:**
|
||||
- Document the original vs. patched bytes, character by character.
|
||||
- Why is a hardcoded, unconditional status word more dangerous than one
|
||||
that is at least computed from a (miscalibrated) reading?
|
||||
|
||||
### Task 4: Recover the Quarantined Dispatch Frame
|
||||
|
||||
1. Use Ghidra's Defined Strings (or a raw string search) to locate the
|
||||
hidden black-start authorization frame.
|
||||
2. Document its address and explain why it is never transmitted by the
|
||||
current firmware.
|
||||
3. Do **not** attempt to patch this value - it is evidence, not a bug.
|
||||
|
||||
### Task 5: Export and Verify
|
||||
|
||||
1. Export your patched binary as `CTF-01_fixed.bin`.
|
||||
2. Convert it to UF2 format for the RP2350:
|
||||
```bash
|
||||
python uf2conv.py CTF-01_fixed.bin --base 0x10000000 --family 0xe48bff59 --output CTF-01_fixed.uf2
|
||||
```
|
||||
3. Flash `CTF-01_fixed.uf2` to your Pico 2 and capture the corrected UART
|
||||
output.
|
||||
4. Confirm that the corrected image now reports **GRID STATUS: CRITICAL**,
|
||||
**DISPATCH PATH: HELD**, and the corrected signal-quality word - an
|
||||
honest, safe report instead of a false "all clear."
|
||||
5. Build a summary table of every patch: address, original bytes, patched
|
||||
bytes, and a one-line description.
|
||||
|
||||
### Task 6: Written Reflection (short answers, 150 words or less each)
|
||||
|
||||
1. Why is "the build was rushed under emergency pressure" not an
|
||||
acceptable excuse for shipping a firmware defect that could trigger a
|
||||
cascading grid failure?
|
||||
2. Name one concrete engineering practice (code review, static analysis,
|
||||
hardware-in-the-loop test, etc.) that would have caught **each** of the
|
||||
two graded bugs before this image ever reached the fleet.
|
||||
|
||||
---
|
||||
|
||||
## How To Breadboard
|
||||
|
||||
- Pico 2 **GPIO 0 / UART TX** -> USB-UART adapter **RX**
|
||||
- Pico 2 **GPIO 1 / UART RX** -> USB-UART adapter **TX**
|
||||
- Pico 2 **GND** -> USB-UART adapter **GND**
|
||||
- Use **3.3 V logic only**. Never connect a 5 V line to a Pico GPIO.
|
||||
- Connect the supplied SWD probe according to its documented pinout.
|
||||
|
||||
The supplied image is `CTF-01.bin` (for Ghidra analysis) and `CTF-01.uf2`
|
||||
(for flashing). If your instructor supplies different filenames, record the
|
||||
actual filenames in your report.
|
||||
|
||||
---
|
||||
|
||||
## Memory Map Reference
|
||||
|
||||
| Region | Address | Purpose |
|
||||
|--------|---------|---------|
|
||||
| Bootrom | `0x00000000` | Immutable boot code |
|
||||
| Flash/XIP | `0x10000000` | Vector table, code, constants, strings |
|
||||
| SRAM | `0x20000000` | Stack and writable state |
|
||||
|
||||
---
|
||||
|
||||
## Submission Format
|
||||
|
||||
Submit a folder containing:
|
||||
|
||||
- `CTF-01-Answers.md`;
|
||||
- screenshots or terminal transcripts;
|
||||
- `CTF-01_fixed.bin` and `CTF-01_fixed.uf2`;
|
||||
- the original image hash.
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
You complete the challenge when you can prove all of the following:
|
||||
|
||||
- You can explain how the RP2350 reaches the relay's code from reset.
|
||||
- You can locate and patch both copies of the miscalibrated threshold.
|
||||
- You can locate and patch the false signal-quality string without
|
||||
corrupting adjacent data.
|
||||
- You can export, convert, and flash a corrected image.
|
||||
- You can prove on real hardware that the corrected image reports the
|
||||
true, dangerous state instead of the false "all clear."
|
||||
- You can recover the quarantined dispatch frame as evidence.
|
||||
|
||||
---
|
||||
|
||||
## Academic Integrity
|
||||
|
||||
By submitting this CTF work, you certify that:
|
||||
|
||||
1. You used only the supplied training relay, image, and lab interface.
|
||||
2. You did not connect the challenge to a public network, an operational
|
||||
grid, a water utility, or any third-party device.
|
||||
3. You understand that embedded reverse engineering and binary patching
|
||||
require explicit authorization in any real-world context.
|
||||
4. You will report any discovered weakness responsibly to the course
|
||||
instructor.
|
||||
|
||||
The world is short on people who can do this work. Treat that
|
||||
responsibility seriously: verify before you patch, patch before you trust,
|
||||
and never confuse a clean-looking status line with a safe system.
|
||||
|
||||
---
|
||||
|
||||
## Reference Material
|
||||
|
||||
- ARM Cortex-M33 Technical Reference Manual
|
||||
- RP2350 datasheet
|
||||
- GDB documentation
|
||||
- Ghidra documentation: [https://ghidra-sre.org/](https://ghidra-sre.org/)
|
||||
Binary file not shown.
@@ -0,0 +1,237 @@
|
||||
# Operation Black Start - Requirements & Grading Criteria
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------------------------+
|
||||
| |
|
||||
| ██████╗ ██╗ █████╗ ██████╗██╗ ██╗███████╗████████╗ █████╗ ██████╗ ████████╗ |
|
||||
| ██╔══██╗██║ ██╔══██╗██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝ |
|
||||
| ██████╔╝██║ ███████║██║ █████╔╝ ███████╗ ██║ ███████║██████╔╝ ██║ |
|
||||
| ██╔══██╗██║ ██╔══██║██║ ██╔═██╗ ╚════██║ ██║ ██╔══██║██╔══██╗ ██║ |
|
||||
| ██████╔╝███████╗██║ ██║╚██████╗██║ ██╗███████╗ ██║ ██║ ██║██║ ██║ ██║ |
|
||||
| ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝██║ ██║╚══════╝ ╚═╝ ╚═╝ ╚═╝██║ ██║ ██║ |
|
||||
| |
|
||||
| |
|
||||
| O P E R A T I O N B L A C K S T A R T |
|
||||
| |
|
||||
| REQUIREMENTS & GRADING CRITERIA |
|
||||
| |
|
||||
+----------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
|
||||
Students are the reverse-engineering reserve team called in after WorldGrid
|
||||
Compact's emergency firmware build shipped a miscompiled safety threshold and
|
||||
a false status string to its GRID-7 relay fleet. Students reverse engineer
|
||||
`CTF-01.bin` with Ghidra, locate two real defects, patch them in the binary,
|
||||
export a corrected image, flash it to real hardware, and prove the corrected
|
||||
behavior with the debugger and the console.
|
||||
|
||||
The challenge is separate from all FINAL projects and contains no FINAL-project
|
||||
answer, constant, address, bug, or patch.
|
||||
|
||||
---
|
||||
|
||||
## Learning Objectives
|
||||
|
||||
- Decode an ARM Cortex-M33 vector table and identify the reset handler and
|
||||
initial stack pointer.
|
||||
- Translate Thumb reset-vector addresses into real function entry points.
|
||||
- Locate a miscompiled boundary comparison and reason about its immediate
|
||||
value.
|
||||
- Patch compare instructions and a status string in a raw binary with Ghidra.
|
||||
- Export and UF2-convert a corrected image, then verify it on real hardware.
|
||||
- Capture derived state with GDB and read UART console output.
|
||||
|
||||
Students must use only Weeks 1-3 concepts: ARM registers and stack behavior,
|
||||
UART output, GDB, Ghidra static analysis and binary patching, vector tables,
|
||||
reset startup, XIP, and Thumb addressing.
|
||||
|
||||
---
|
||||
|
||||
## Deliverables Checklist
|
||||
|
||||
| # | Deliverable | Format | Criterion |
|
||||
|---|-------------|--------|-----------|
|
||||
| 1 | Ghidra project screenshot (project name, processor, base address) | PNG/JPG | 1.1 |
|
||||
| 2 | `main()` and status-loop addresses | Inside `CTF-01-Answers.md` | 1.2 |
|
||||
| 3 | Vector table base, initial SP, reset pointer | Inside `CTF-01-Answers.md` | 1.3 |
|
||||
| 4 | Thumb bit explanation | Inside `CTF-01-Answers.md` | 1.4 |
|
||||
| 5 | Bug #1 evidence and patches (both compare sites) | Inside `CTF-01-Answers.md` | 2.1-2.6 |
|
||||
| 6 | Bug #2 evidence and patch (six characters) | Inside `CTF-01-Answers.md` | 3.1-3.4 |
|
||||
| 7 | Recovered dispatch frame and address | Inside `CTF-01-Answers.md` | 4.1-4.2 |
|
||||
| 8 | `CTF-01_fixed.bin` | BIN file | 5.1 |
|
||||
| 9 | `CTF-01_fixed.uf2` | UF2 file | 5.2 |
|
||||
| 10 | Corrected console transcript | Inside `CTF-01-Answers.md` | 5.3 |
|
||||
| 11 | Summary table of all patches | Inside `CTF-01-Answers.md` | 5.4 |
|
||||
| 12 | Written reflection | Inside `CTF-01-Answers.md` | 6.1-6.2 |
|
||||
|
||||
---
|
||||
|
||||
## Required Tools and Equipment
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| Raspberry Pi Pico 2 | Isolated target |
|
||||
| 3.3 V USB-UART adapter | UART capture on GPIO 0 (TX) / GPIO 1 (RX) |
|
||||
| Serial monitor | Observe output |
|
||||
| Ghidra | Static analysis and binary patching |
|
||||
| Python (`uf2conv.py`) | UF2 conversion |
|
||||
| `CTF-01.bin` and `CTF-01.uf2` | Supplied artifacts |
|
||||
|
||||
UART settings: **115200 baud, 8 data bits, no parity, 1 stop bit**.
|
||||
|
||||
---
|
||||
|
||||
## Artifact Identity
|
||||
|
||||
The instructor-issued artifact hashes are:
|
||||
|
||||
```text
|
||||
CTF-01.bin 6FD296F7A85F243FB26BF6BFFCBEAB26815FD8915101A81F72069063A5635E5A
|
||||
CTF-01.uf2 980F04369C23AD32A063DFE18DE5AF08DF3830138FC7E7898B1F011B4F5E1D9D
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Grading Rubric - Detailed Breakdown
|
||||
|
||||
### Task 1: Setup and Initial Analysis (15 points)
|
||||
|
||||
| Criterion | Points | Full credit | Partial credit | No credit |
|
||||
|-----------|--------|-------------|----------------|-----------|
|
||||
| Criterion 1.1: Ghidra Project Setup | 3 | Correct project name, `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 |
|
||||
|
||||
### Task 2: Find and Patch Bug #1: The Miscalibrated Safety Threshold (30 points)
|
||||
|
||||
| Criterion | Points | Full credit | 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 |
|
||||
|
||||
### Task 3: Find and Patch Bug #2: The False Signal Banner (20 points)
|
||||
|
||||
| Criterion | Points | Full credit | 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 |
|
||||
|
||||
### Task 4: Recover the Quarantined Dispatch Frame (10 points)
|
||||
|
||||
| Criterion | Points | Full credit | 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 |
|
||||
|
||||
### Task 5: Export and Verify (20 points)
|
||||
|
||||
| Criterion | Points | Full credit | 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 (CRITICAL/HELD in 1s stream; DANGER at boot/Ghidra) | Some lines corrected | No verification |
|
||||
| Criterion 5.4: Summary Table of All Patches | 4 | Complete address and before/after table | Missing entries | No table |
|
||||
|
||||
### Task 6: Written Reflection (5 points)
|
||||
|
||||
| Criterion | Points | Full credit | 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 |
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
| Pitfall | Consequence | Avoidance |
|
||||
|---------|-------------|-----------|
|
||||
| Patching only one threshold site | One status line still lies | Patch both `0x100001FC` and `0x1000020A` |
|
||||
| Assuming the immediate equals the limit | Off-by-one, wrong boundary | Use `0x3B` (59), not `0x3C` (60) |
|
||||
| Using Patch Instruction before IT block | Re-disassembler context conflict swallows Site B | In Listing press `C` -> edit byte in Bytes window (pencil) -> press `D` |
|
||||
| Missing boot banner in serial terminal | PuTTY misses one-time 5ms boot banner | Pulse RUN to GND while connected to capture |
|
||||
| Replacing a string with a different length | Corrupts adjacent flash | `NORMAL` and `DANGER` are both 6 bytes |
|
||||
| Treating an odd vector address as invalid | Thumb analysis fails | Clear bit 0 |
|
||||
| Modifying the quarantined dispatch frame | Destroys evidence | Recover it, do not patch it |
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## Memory Map Reference
|
||||
|
||||
| Region | Address | Purpose |
|
||||
|--------|---------|---------|
|
||||
| Bootrom | `0x00000000` | Immutable boot code |
|
||||
| Flash/XIP | `0x10000000` | Vector table, code, constants, strings |
|
||||
| SRAM | `0x20000000` | Stack and writable state |
|
||||
|
||||
---
|
||||
|
||||
## Deadline & Submission
|
||||
|
||||
- Create a folder containing the Ghidra screenshot, `CTF-01_fixed.bin`, and
|
||||
`CTF-01_fixed.uf2`.
|
||||
- Write all written answers in a single file named `CTF-01-Answers.md` inside that
|
||||
folder.
|
||||
- ZIP the folder as `lastname-firstname-CTF-01.zip`.
|
||||
- Submit the ZIP before the posted deadline; late submissions lose 10 percent
|
||||
per day.
|
||||
|
||||
---
|
||||
|
||||
## Grade Scale
|
||||
|
||||
| Grade | Percentage | Points |
|
||||
|-------|------------|--------|
|
||||
| A+ | 97-100% | 97-100 |
|
||||
| A | 93-96% | 93-96 |
|
||||
| A- | 90-92% | 90-92 |
|
||||
| B+ | 87-89% | 87-89 |
|
||||
| B | 84-86% | 84-86 |
|
||||
| B- | 80-83% | 80-83 |
|
||||
| C | 70-79% | 70-79 |
|
||||
| F | 0-69% | 0-69 |
|
||||
|
||||
---
|
||||
|
||||
## Academic Integrity
|
||||
|
||||
Use only the supplied Pico 2 and firmware. Do not connect the exercise to an
|
||||
operational grid, water plant, public network, military system, or third-party
|
||||
device. This is a controlled, isolated educational exercise. All analysis and
|
||||
patches must be your own work; sharing binaries, addresses, or answers is a
|
||||
violation of the academic integrity policy.
|
||||
|
||||
---
|
||||
|
||||
## Reference Material
|
||||
|
||||
| Topic | Reference |
|
||||
|-------|-----------|
|
||||
| ARM Cortex-M33 registers and stack | Week 1 |
|
||||
| UART output and console capture | Week 2 |
|
||||
| Vector tables, reset startup, and XIP | Week 2 |
|
||||
| Ghidra static analysis and binary patching | Week 3 |
|
||||
| Thumb addressing | Week 3 |
|
||||
Binary file not shown.
@@ -0,0 +1,467 @@
|
||||
# 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 |
|
||||
|
||||
```text
|
||||
CTF-01.bin 6FD296F7A85F243FB26BF6BFFCBEAB26815FD8915101A81F72069063A5635E5A
|
||||
CTF-01.uf2 980F04369C23AD32A063DFE18DE5AF08DF3830138FC7E7898B1F011B4F5E1D9D
|
||||
```
|
||||
|
||||
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`:
|
||||
|
||||
```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 | `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 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`, base
|
||||
`0x10000000`, and that auto-analysis completed before any address was read.
|
||||
- Verify `main()` is `0x100001E0`, the loop head is `0x10000234`, and the
|
||||
back-edge bytes `E5 E7` sit at `0x10000266`.
|
||||
- The reset vector is stored as `0x1000015B`; the real Thumb entry is
|
||||
`0x1000015A`. Do not accept the un-cleared `0x1000015B` as 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).**
|
||||
|
||||
```text
|
||||
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).**
|
||||
|
||||
```text
|
||||
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: `0x100001FC` for operator status and
|
||||
`0x1000020A` for the automated dispatch decision.
|
||||
- The correct immediate is `0x3B` (59), not `0x3C` (60); the source test is a
|
||||
strict `<`, compiled as `<= 94`.
|
||||
- Verify the byte changes on hardware: after patching, `operator_state`
|
||||
(`0x20000844`) and `dispatch_state` (`0x20000834`) read `0` and `0`.
|
||||
- **Ghidra ARM/Thumb Context Note:** In raw `.bin` files, patching an instruction
|
||||
that precedes an `IT` block (`ite hi`) using the GUI *Patch Instruction* action
|
||||
triggers Ghidra's `ReDisassembleCommand`. The re-disassembler encounters an
|
||||
internal context register conflict when trying to re-declare the `ITBlock`
|
||||
context over existing instructions, collapsing Thumb decoding into 32-bit ARM
|
||||
mode and swallowing Site B (`0x1000020A`). Students must patch using the Bytes
|
||||
window workflow (Clear `C` -> edit byte `5E` -> `3B` in Bytes window with pencil
|
||||
icon -> Disassemble `D`) 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
|
||||
|
||||
- `NORMAL` and `DANGER` are 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 `0x10003680` through `0x10003685` exactly.
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Recover the Quarantined Dispatch Frame (10 points)
|
||||
|
||||
### Solution
|
||||
|
||||
**Criterion 4.1: Recover the Dispatch Frame (6 points).**
|
||||
Address `0x100037A0` in flash `.rodata`:
|
||||
|
||||
```text
|
||||
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-3` at `0x100037A0`
|
||||
in 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).**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
After all three 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>
|
||||
```
|
||||
|
||||
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 expects `11/11 checks passed` against `CTF-01.bin`.
|
||||
- Confirm the UF2 conversion used base `0x10000000` and family `0xe48bff59`.
|
||||
- **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 see `SIGNAL: DANGER`,
|
||||
the student must pulse `RUN` to `GND` while PuTTY is open, or demonstrate
|
||||
the string change at `0x10003680` via Ghidra static analysis.
|
||||
- If no console is available, accept the SWD capture showing `operator_state`
|
||||
and `dispatch_state` at `0` and `0` on 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_THRESHOLD` to 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 (`0x100001FC` or `0x1000020A`), which leaves
|
||||
one status line lying.
|
||||
- Assuming the immediate equals the limit, producing an off-by-one boundary;
|
||||
the correct byte is `0x3B` (59), not `0x3C` (60).
|
||||
- Replacing the banner string with a different length, corrupting adjacent
|
||||
flash; `NORMAL` and `DANGER` are both 6 bytes.
|
||||
- Treating the odd vector address `0x1000015B` as invalid instead of clearing
|
||||
bit 0 to get `0x1000015A`.
|
||||
- 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_state` and `dispatch_state` as 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).
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,56 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2026 Kevin Thomas
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// Author: Kevin Thomas
|
||||
// Email: kevin@mytechnotalent.com
|
||||
// GitHub: https://github.com/mytechnotalent
|
||||
// File: console.h
|
||||
// Desc: Declares the UART console banner and status reporting interface
|
||||
// for Operation Black Start.
|
||||
// Created: 2026
|
||||
|
||||
#ifndef CONSOLE_H
|
||||
#define CONSOLE_H
|
||||
|
||||
/**
|
||||
* @brief Print the response controller's boot identity and unconditional signal line.
|
||||
*
|
||||
* Emits the boot banner, operation window, serial configuration banner,
|
||||
* unconditional signal quality line, and the interactive command prompt.
|
||||
*
|
||||
* @param None.
|
||||
* @return None.
|
||||
*/
|
||||
void print_boot_banner(void);
|
||||
|
||||
/**
|
||||
* @brief Print the recurring grid classification and dispatch authorization report.
|
||||
*
|
||||
* Evaluates the current operator state and dispatch state flags and transmits
|
||||
* the formatted status report over UART0 with the quarantined frame notification.
|
||||
*
|
||||
* @param None.
|
||||
* @return None.
|
||||
*/
|
||||
void print_status(void);
|
||||
|
||||
#endif // CONSOLE_H
|
||||
@@ -0,0 +1,73 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2026 Kevin Thomas
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// Author: Kevin Thomas
|
||||
// Email: kevin@mytechnotalent.com
|
||||
// GitHub: https://github.com/mytechnotalent
|
||||
// File: grid.h
|
||||
// Desc: Declares the grid telemetry and safety evaluation interface for
|
||||
// Operation Black Start.
|
||||
// Created: 2026
|
||||
|
||||
#ifndef GRID_H
|
||||
#define GRID_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Safety margin (Hz deviation x100) below which grid frequency is tolerable.
|
||||
// BUG: emergency build miscompiled this far too permissive; correct value is 60.
|
||||
#define SAFE_THRESHOLD 95
|
||||
|
||||
// Frozen grid frequency deviation reading latched when comms were severed.
|
||||
extern volatile uint32_t grid_deviation;
|
||||
|
||||
// Operator-facing classification of the frozen reading (drives GRID STATUS).
|
||||
extern volatile uint32_t operator_state;
|
||||
|
||||
// Automated dispatch authorization decision (drives DISPATCH PATH).
|
||||
extern volatile uint32_t dispatch_state;
|
||||
|
||||
/**
|
||||
* @brief Anchor the hidden dispatch frame in flash without transmitting it.
|
||||
*
|
||||
* Reads the first character of the quarantined black-start authorization
|
||||
* frame into a volatile local marker to prevent the compiler from optimizing
|
||||
* out the string literal from .rodata flash storage.
|
||||
*
|
||||
* @param None.
|
||||
* @return None.
|
||||
*/
|
||||
void retain_dispatch_frame(void);
|
||||
|
||||
/**
|
||||
* @brief Classify the frozen telemetry reading against the compiled threshold.
|
||||
*
|
||||
* Evaluates the frozen grid deviation reading against SAFE_THRESHOLD twice,
|
||||
* once for the operator-facing status line and once for the automated
|
||||
* dispatch decision, mirroring the duplicated immediate comparison site.
|
||||
*
|
||||
* @param None.
|
||||
* @return None.
|
||||
*/
|
||||
void evaluate_grid(void);
|
||||
|
||||
#endif // GRID_H
|
||||
@@ -0,0 +1,121 @@
|
||||
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
|
||||
|
||||
# This can be dropped into an external project to help locate this SDK
|
||||
# It should be include()ed prior to project()
|
||||
|
||||
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
||||
# following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
||||
# disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
|
||||
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
|
||||
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
|
||||
endif ()
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
|
||||
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
|
||||
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
|
||||
endif ()
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
|
||||
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
|
||||
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
|
||||
endif ()
|
||||
|
||||
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
|
||||
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
|
||||
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
|
||||
endif ()
|
||||
|
||||
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
|
||||
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
|
||||
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
|
||||
endif()
|
||||
|
||||
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
|
||||
set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
|
||||
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
|
||||
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
|
||||
|
||||
if (NOT PICO_SDK_PATH)
|
||||
if (PICO_SDK_FETCH_FROM_GIT)
|
||||
include(FetchContent)
|
||||
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
|
||||
if (PICO_SDK_FETCH_FROM_GIT_PATH)
|
||||
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
|
||||
endif ()
|
||||
FetchContent_Declare(
|
||||
pico_sdk
|
||||
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||
)
|
||||
|
||||
if (NOT pico_sdk)
|
||||
message("Downloading Raspberry Pi Pico SDK")
|
||||
# GIT_SUBMODULES_RECURSE was added in 3.17
|
||||
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
|
||||
FetchContent_Populate(
|
||||
pico_sdk
|
||||
QUIET
|
||||
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||
GIT_SUBMODULES_RECURSE FALSE
|
||||
|
||||
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
|
||||
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
|
||||
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
|
||||
)
|
||||
else ()
|
||||
FetchContent_Populate(
|
||||
pico_sdk
|
||||
QUIET
|
||||
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
|
||||
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
|
||||
|
||||
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
|
||||
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
|
||||
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
|
||||
)
|
||||
endif ()
|
||||
|
||||
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
|
||||
endif ()
|
||||
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
|
||||
else ()
|
||||
message(FATAL_ERROR
|
||||
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
|
||||
)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
|
||||
if (NOT EXISTS ${PICO_SDK_PATH})
|
||||
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
|
||||
endif ()
|
||||
|
||||
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
|
||||
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
|
||||
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
|
||||
endif ()
|
||||
|
||||
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
|
||||
|
||||
include(${PICO_SDK_INIT_CMAKE_FILE})
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify every technical claim of Operation Black Start against CTF-01.bin.
|
||||
|
||||
Exits 0 only when every address, byte, and hash in CTF-R.md and CTF-S.md
|
||||
matches the shipped image.
|
||||
"""
|
||||
import hashlib
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
BASE = 0x10000000
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
BIN = ROOT / "CTF-01.bin"
|
||||
UF2 = ROOT / "CTF-01.uf2"
|
||||
|
||||
EXPECTED_BIN_SHA = "6fd296f7a85f243fb26bf6bffcbeab26815fd8915101a81f72069063a5635e5a"
|
||||
EXPECTED_UF2_SHA = "980f04369c23ad32a063dfe18de5af08df3830138fc7e7898b1f011b4f5e1d9d"
|
||||
|
||||
CMP_A = 0x100001FC
|
||||
CMP_B = 0x1000020A
|
||||
STRING_SIGNAL = 0x10003678
|
||||
STRING_FRAME = 0x100037A0
|
||||
LOOP_BACK = 0x10000266
|
||||
|
||||
RESULTS = []
|
||||
|
||||
|
||||
def check(label, ok, detail=""):
|
||||
"""Record one verification result.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
Human-readable check name.
|
||||
ok : bool
|
||||
Whether the check passed.
|
||||
detail : str
|
||||
Extra context printed with the result.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
RESULTS.append(ok)
|
||||
print(f"[{'PASS' if ok else 'FAIL'}] {label} {detail}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all verification checks.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Zero when every check passes, else one.
|
||||
"""
|
||||
data = BIN.read_bytes()
|
||||
check("CTF-01.bin SHA-256", hashlib.sha256(data).hexdigest() == EXPECTED_BIN_SHA)
|
||||
check("CTF-01.uf2 SHA-256",
|
||||
hashlib.sha256(UF2.read_bytes()).hexdigest() == EXPECTED_UF2_SHA)
|
||||
check("vector table", data[0:32].hex() ==
|
||||
"002008205b0100101b0100101d01001011010010110100101101001011010010")
|
||||
check("initial SP", struct.unpack("<I", data[0:4])[0] == 0x20082000)
|
||||
check("reset vector", struct.unpack("<I", data[4:8])[0] == 0x1000015B)
|
||||
check("compare site A", data[CMP_A - BASE:CMP_A - BASE + 2]
|
||||
== bytes.fromhex("5E2B"), "(cmp r3,#94)")
|
||||
check("compare site B", data[CMP_B - BASE:CMP_B - BASE + 2]
|
||||
== bytes.fromhex("5E2B"), "(cmp r3,#94)")
|
||||
check("loop back-edge", data[LOOP_BACK - BASE:LOOP_BACK - BASE + 2]
|
||||
== bytes.fromhex("E5E7"), "(b.n 0x10000234)")
|
||||
check("SIGNAL banner", data[STRING_SIGNAL - BASE:STRING_SIGNAL - BASE + 15]
|
||||
== b"SIGNAL: NORMAL\r")
|
||||
check("dispatch frame", data[STRING_FRAME - BASE:STRING_FRAME - BASE + 36]
|
||||
== b"WORLDGRID:BLACKSTART:GRID-7:WATER-3\x00")
|
||||
check("grid_deviation init", data[0x3D94:0x3D98] == struct.pack("<I", 87))
|
||||
total = sum(RESULTS)
|
||||
print(f"\n{total}/{len(RESULTS)} checks passed")
|
||||
return 0 if total == len(RESULTS) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,50 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2026 Kevin Thomas
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// Author: Kevin Thomas
|
||||
// Email: kevin@mytechnotalent.com
|
||||
// GitHub: https://github.com/mytechnotalent
|
||||
// File: console.c
|
||||
// Desc: Implements the UART console banner and status reporting logic for
|
||||
// Operation Black Start.
|
||||
// Created: 2026
|
||||
|
||||
#include "console.h"
|
||||
#include "grid.h"
|
||||
#include <stdio.h>
|
||||
|
||||
void print_boot_banner(void)
|
||||
{
|
||||
printf("GLOBAL EMBEDDED RESPONSE NETWORK\r\n");
|
||||
printf("BLACK START WINDOW: 27 MINUTES\r\n");
|
||||
printf("UART0 115200 8N1 | AUTHORIZED LAB CONSOLE\r\n");
|
||||
printf("SIGNAL: NORMAL\r\n");
|
||||
printf("RESPONSE> ");
|
||||
}
|
||||
|
||||
void print_status(void)
|
||||
{
|
||||
printf("GRID STATUS: %s\r\n", operator_state ? "STABLE" : "CRITICAL");
|
||||
printf("DISPATCH PATH: %s\r\n", dispatch_state ? "AUTHORIZED" : "HELD");
|
||||
printf("LAST FRAME: QUARANTINED\r\n");
|
||||
printf("RESPONSE> ");
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2026 Kevin Thomas
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// Author: Kevin Thomas
|
||||
// Email: kevin@mytechnotalent.com
|
||||
// GitHub: https://github.com/mytechnotalent
|
||||
// File: grid.c
|
||||
// Desc: Implements the grid telemetry and safety evaluation logic for
|
||||
// Operation Black Start.
|
||||
// Created: 2026
|
||||
|
||||
#include "grid.h"
|
||||
#include <stdint.h>
|
||||
|
||||
// Frozen grid frequency deviation reading latched when comms were severed.
|
||||
volatile uint32_t grid_deviation = 87;
|
||||
|
||||
// Operator-facing classification of the frozen reading (drives GRID STATUS).
|
||||
volatile uint32_t operator_state = 0;
|
||||
|
||||
// Automated dispatch authorization decision (drives DISPATCH PATH).
|
||||
volatile uint32_t dispatch_state = 0;
|
||||
|
||||
// Quarantined black-start authorization frame, retained in flash, never sent.
|
||||
static volatile const char dispatch_frame[] =
|
||||
"WORLDGRID:BLACKSTART:GRID-7:WATER-3";
|
||||
|
||||
void retain_dispatch_frame(void)
|
||||
{
|
||||
volatile char frame_marker = dispatch_frame[0];
|
||||
(void)frame_marker;
|
||||
}
|
||||
|
||||
void evaluate_grid(void)
|
||||
{
|
||||
operator_state = (grid_deviation < SAFE_THRESHOLD) ? 1 : 0;
|
||||
dispatch_state = (grid_deviation < SAFE_THRESHOLD) ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2026 Kevin Thomas
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// Author: Kevin Thomas
|
||||
// Email: kevin@mytechnotalent.com
|
||||
// GitHub: https://github.com/mytechnotalent
|
||||
// File: main.c
|
||||
// Desc: Main entry point for the Operation Black Start CTF relay firmware.
|
||||
// Created: 2026
|
||||
|
||||
#include "console.h"
|
||||
#include "grid.h"
|
||||
#include "pico/stdlib.h"
|
||||
|
||||
/**
|
||||
* @brief Run the recurring UART evidence stream for the isolated CTF image.
|
||||
*
|
||||
* Initializes the standard I/O subsystems, anchors the quarantined dispatch
|
||||
* frame in flash memory, evaluates the frozen telemetry against the compiled
|
||||
* safety threshold, prints the startup banner, and repeatedly emits the
|
||||
* system status report every second.
|
||||
*
|
||||
* @param None.
|
||||
* @return int Standard exit code (never reached in normal firmware execution).
|
||||
*/
|
||||
int main(void)
|
||||
{
|
||||
stdio_init_all();
|
||||
retain_dispatch_frame();
|
||||
evaluate_grid();
|
||||
print_boot_banner();
|
||||
while (true) {
|
||||
print_status();
|
||||
sleep_ms(1000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import struct
|
||||
import subprocess
|
||||
import re
|
||||
import os
|
||||
import os.path
|
||||
import argparse
|
||||
import json
|
||||
from time import sleep
|
||||
|
||||
|
||||
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
|
||||
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
|
||||
UF2_MAGIC_END = 0x0AB16F30 # Ditto
|
||||
|
||||
INFO_FILE = "/INFO_UF2.TXT"
|
||||
|
||||
appstartaddr = 0x2000
|
||||
familyid = 0x0
|
||||
|
||||
|
||||
def is_uf2(buf):
|
||||
w = struct.unpack("<II", buf[0:8])
|
||||
return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1
|
||||
|
||||
def is_hex(buf):
|
||||
try:
|
||||
w = buf[0:30].decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
if w[0] == ':' and re.match(rb"^[:0-9a-fA-F\r\n]+$", buf):
|
||||
return True
|
||||
return False
|
||||
|
||||
def convert_from_uf2(buf):
|
||||
global appstartaddr
|
||||
global familyid
|
||||
numblocks = len(buf) // 512
|
||||
curraddr = None
|
||||
currfamilyid = None
|
||||
families_found = {}
|
||||
prev_flag = None
|
||||
all_flags_same = True
|
||||
outp = []
|
||||
for blockno in range(numblocks):
|
||||
ptr = blockno * 512
|
||||
block = buf[ptr:ptr + 512]
|
||||
hd = struct.unpack(b"<IIIIIIII", block[0:32])
|
||||
if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
|
||||
print("Skipping block at " + ptr + "; bad magic")
|
||||
continue
|
||||
if hd[2] & 1:
|
||||
# NO-flash flag set; skip block
|
||||
continue
|
||||
datalen = hd[4]
|
||||
if datalen > 476:
|
||||
assert False, "Invalid UF2 data size at " + ptr
|
||||
newaddr = hd[3]
|
||||
if (hd[2] & 0x2000) and (currfamilyid == None):
|
||||
currfamilyid = hd[7]
|
||||
if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid):
|
||||
currfamilyid = hd[7]
|
||||
curraddr = newaddr
|
||||
if familyid == 0x0 or familyid == hd[7]:
|
||||
appstartaddr = newaddr
|
||||
padding = newaddr - curraddr
|
||||
if padding < 0:
|
||||
assert False, "Block out of order at " + ptr
|
||||
if padding > 10*1024*1024:
|
||||
assert False, "More than 10M of padding needed at " + ptr
|
||||
if padding % 4 != 0:
|
||||
assert False, "Non-word padding size at " + ptr
|
||||
while padding > 0:
|
||||
padding -= 4
|
||||
outp.append(b"\x00\x00\x00\x00")
|
||||
if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]):
|
||||
outp.append(block[32 : 32 + datalen])
|
||||
curraddr = newaddr + datalen
|
||||
if hd[2] & 0x2000:
|
||||
if hd[7] in families_found.keys():
|
||||
if families_found[hd[7]] > newaddr:
|
||||
families_found[hd[7]] = newaddr
|
||||
else:
|
||||
families_found[hd[7]] = newaddr
|
||||
if prev_flag == None:
|
||||
prev_flag = hd[2]
|
||||
if prev_flag != hd[2]:
|
||||
all_flags_same = False
|
||||
if blockno == (numblocks - 1):
|
||||
print("--- UF2 File Header Info ---")
|
||||
families = load_families()
|
||||
for family_hex in families_found.keys():
|
||||
family_short_name = ""
|
||||
for name, value in families.items():
|
||||
if value == family_hex:
|
||||
family_short_name = name
|
||||
print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex))
|
||||
print("Target Address is 0x{:08x}".format(families_found[family_hex]))
|
||||
if all_flags_same:
|
||||
print("All block flag values consistent, 0x{:04x}".format(hd[2]))
|
||||
else:
|
||||
print("Flags were not all the same")
|
||||
print("----------------------------")
|
||||
if len(families_found) > 1 and familyid == 0x0:
|
||||
outp = []
|
||||
appstartaddr = 0x0
|
||||
return b"".join(outp)
|
||||
|
||||
def convert_to_carray(file_content):
|
||||
outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
|
||||
outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
|
||||
for i in range(len(file_content)):
|
||||
if i % 16 == 0:
|
||||
outp += "\n"
|
||||
outp += "0x%02x, " % file_content[i]
|
||||
outp += "\n};\n"
|
||||
return bytes(outp, "utf-8")
|
||||
|
||||
def convert_to_uf2(file_content):
|
||||
global familyid
|
||||
datapadding = b""
|
||||
while len(datapadding) < 512 - 256 - 32 - 4:
|
||||
datapadding += b"\x00\x00\x00\x00"
|
||||
numblocks = (len(file_content) + 255) // 256
|
||||
outp = []
|
||||
for blockno in range(numblocks):
|
||||
ptr = 256 * blockno
|
||||
chunk = file_content[ptr:ptr + 256]
|
||||
flags = 0x0
|
||||
if familyid:
|
||||
flags |= 0x2000
|
||||
hd = struct.pack(b"<IIIIIIII",
|
||||
UF2_MAGIC_START0, UF2_MAGIC_START1,
|
||||
flags, ptr + appstartaddr, 256, blockno, numblocks, familyid)
|
||||
while len(chunk) < 256:
|
||||
chunk += b"\x00"
|
||||
block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
|
||||
assert len(block) == 512
|
||||
outp.append(block)
|
||||
return b"".join(outp)
|
||||
|
||||
class Block:
|
||||
def __init__(self, addr, default_data=0xFF):
|
||||
self.addr = addr
|
||||
self.bytes = bytearray([default_data] * 256)
|
||||
|
||||
def encode(self, blockno, numblocks):
|
||||
global familyid
|
||||
flags = 0x0
|
||||
if familyid:
|
||||
flags |= 0x2000
|
||||
hd = struct.pack("<IIIIIIII",
|
||||
UF2_MAGIC_START0, UF2_MAGIC_START1,
|
||||
flags, self.addr, 256, blockno, numblocks, familyid)
|
||||
hd += self.bytes[0:256]
|
||||
while len(hd) < 512 - 4:
|
||||
hd += b"\x00"
|
||||
hd += struct.pack("<I", UF2_MAGIC_END)
|
||||
return hd
|
||||
|
||||
def convert_from_hex_to_uf2(buf):
|
||||
global appstartaddr
|
||||
appstartaddr = None
|
||||
upper = 0
|
||||
currblock = None
|
||||
blocks = []
|
||||
for line in buf.split('\n'):
|
||||
if line[0] != ":":
|
||||
continue
|
||||
i = 1
|
||||
rec = []
|
||||
while i < len(line) - 1:
|
||||
rec.append(int(line[i:i+2], 16))
|
||||
i += 2
|
||||
tp = rec[3]
|
||||
if tp == 4:
|
||||
upper = ((rec[4] << 8) | rec[5]) << 16
|
||||
elif tp == 2:
|
||||
upper = ((rec[4] << 8) | rec[5]) << 4
|
||||
elif tp == 1:
|
||||
break
|
||||
elif tp == 0:
|
||||
addr = upper + ((rec[1] << 8) | rec[2])
|
||||
if appstartaddr == None:
|
||||
appstartaddr = addr
|
||||
i = 4
|
||||
while i < len(rec) - 1:
|
||||
if not currblock or currblock.addr & ~0xff != addr & ~0xff:
|
||||
currblock = Block(addr & ~0xff)
|
||||
blocks.append(currblock)
|
||||
currblock.bytes[addr & 0xff] = rec[i]
|
||||
addr += 1
|
||||
i += 1
|
||||
numblocks = len(blocks)
|
||||
resfile = b""
|
||||
for i in range(0, numblocks):
|
||||
resfile += blocks[i].encode(i, numblocks)
|
||||
return resfile
|
||||
|
||||
def to_str(b):
|
||||
return b.decode("utf-8")
|
||||
|
||||
def get_drives():
|
||||
drives = []
|
||||
if sys.platform == "win32":
|
||||
r = subprocess.check_output([
|
||||
"powershell",
|
||||
"-Command",
|
||||
'(Get-WmiObject Win32_LogicalDisk -Filter "VolumeName=\'RPI-RP2\'").DeviceID'
|
||||
])
|
||||
drive = to_str(r).strip()
|
||||
if drive:
|
||||
drives.append(drive)
|
||||
else:
|
||||
searchpaths = ["/mnt", "/media"]
|
||||
if sys.platform == "darwin":
|
||||
searchpaths = ["/Volumes"]
|
||||
elif sys.platform == "linux":
|
||||
searchpaths += ["/media/" + os.environ["USER"], "/run/media/" + os.environ["USER"]]
|
||||
if "SUDO_USER" in os.environ.keys():
|
||||
searchpaths += ["/media/" + os.environ["SUDO_USER"]]
|
||||
searchpaths += ["/run/media/" + os.environ["SUDO_USER"]]
|
||||
|
||||
for rootpath in searchpaths:
|
||||
if os.path.isdir(rootpath):
|
||||
for d in os.listdir(rootpath):
|
||||
if os.path.isdir(os.path.join(rootpath, d)):
|
||||
drives.append(os.path.join(rootpath, d))
|
||||
|
||||
|
||||
def has_info(d):
|
||||
try:
|
||||
return os.path.isfile(d + INFO_FILE)
|
||||
except:
|
||||
return False
|
||||
|
||||
return list(filter(has_info, drives))
|
||||
|
||||
|
||||
def board_id(path):
|
||||
with open(path + INFO_FILE, mode='r') as file:
|
||||
file_content = file.read()
|
||||
return re.search(r"Board-ID: ([^\r\n]*)", file_content).group(1)
|
||||
|
||||
|
||||
def list_drives():
|
||||
for d in get_drives():
|
||||
print(d, board_id(d))
|
||||
|
||||
|
||||
def write_file(name, buf):
|
||||
with open(name, "wb") as f:
|
||||
f.write(buf)
|
||||
print("Wrote %d bytes to %s" % (len(buf), name))
|
||||
|
||||
|
||||
def load_families():
|
||||
# The expectation is that the `uf2families.json` file is in the same
|
||||
# directory as this script. Make a path that works using `__file__`
|
||||
# which contains the full path to this script.
|
||||
filename = "uf2families.json"
|
||||
pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
|
||||
with open(pathname) as f:
|
||||
raw_families = json.load(f)
|
||||
|
||||
families = {}
|
||||
for family in raw_families:
|
||||
families[family["short_name"]] = int(family["id"], 0)
|
||||
|
||||
return families
|
||||
|
||||
|
||||
def main():
|
||||
global appstartaddr, familyid
|
||||
def error(msg):
|
||||
print(msg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.')
|
||||
parser.add_argument('input', metavar='INPUT', type=str, nargs='?',
|
||||
help='input file (HEX, BIN or UF2)')
|
||||
parser.add_argument('-b', '--base', dest='base', type=str,
|
||||
default="0x2000",
|
||||
help='set base address of application for BIN format (default: 0x2000)')
|
||||
parser.add_argument('-f', '--family', dest='family', type=str,
|
||||
default="0x0",
|
||||
help='specify familyID - number or name (default: 0x0)')
|
||||
parser.add_argument('-o', '--output', metavar="FILE", dest='output', type=str,
|
||||
help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible')
|
||||
parser.add_argument('-d', '--device', dest="device_path",
|
||||
help='select a device path to flash')
|
||||
parser.add_argument('-l', '--list', action='store_true',
|
||||
help='list connected devices')
|
||||
parser.add_argument('-c', '--convert', action='store_true',
|
||||
help='do not flash, just convert')
|
||||
parser.add_argument('-D', '--deploy', action='store_true',
|
||||
help='just flash, do not convert')
|
||||
parser.add_argument('-w', '--wait', action='store_true',
|
||||
help='wait for device to flash')
|
||||
parser.add_argument('-C', '--carray', action='store_true',
|
||||
help='convert binary file to a C array, not UF2')
|
||||
parser.add_argument('-i', '--info', action='store_true',
|
||||
help='display header information from UF2, do not convert')
|
||||
args = parser.parse_args()
|
||||
appstartaddr = int(args.base, 0)
|
||||
|
||||
families = load_families()
|
||||
|
||||
if args.family.upper() in families:
|
||||
familyid = families[args.family.upper()]
|
||||
else:
|
||||
try:
|
||||
familyid = int(args.family, 0)
|
||||
except ValueError:
|
||||
error("Family ID needs to be a number or one of: " + ", ".join(families.keys()))
|
||||
|
||||
if args.list:
|
||||
list_drives()
|
||||
else:
|
||||
if not args.input:
|
||||
error("Need input file")
|
||||
with open(args.input, mode='rb') as f:
|
||||
inpbuf = f.read()
|
||||
from_uf2 = is_uf2(inpbuf)
|
||||
ext = "uf2"
|
||||
if args.deploy:
|
||||
outbuf = inpbuf
|
||||
elif from_uf2 and not args.info:
|
||||
outbuf = convert_from_uf2(inpbuf)
|
||||
ext = "bin"
|
||||
elif from_uf2 and args.info:
|
||||
outbuf = ""
|
||||
convert_from_uf2(inpbuf)
|
||||
elif is_hex(inpbuf):
|
||||
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
|
||||
elif args.carray:
|
||||
outbuf = convert_to_carray(inpbuf)
|
||||
ext = "h"
|
||||
else:
|
||||
outbuf = convert_to_uf2(inpbuf)
|
||||
if not args.deploy and not args.info:
|
||||
print("Converted to %s, output size: %d, start address: 0x%x" %
|
||||
(ext, len(outbuf), appstartaddr))
|
||||
if args.convert or ext != "uf2":
|
||||
if args.output == None:
|
||||
args.output = "flash." + ext
|
||||
if args.output:
|
||||
write_file(args.output, outbuf)
|
||||
if ext == "uf2" and not args.convert and not args.info:
|
||||
drives = get_drives()
|
||||
if len(drives) == 0:
|
||||
if args.wait:
|
||||
print("Waiting for drive to deploy...")
|
||||
while len(drives) == 0:
|
||||
sleep(0.1)
|
||||
drives = get_drives()
|
||||
elif not args.output:
|
||||
error("No drive to deploy.")
|
||||
for d in drives:
|
||||
print("Flashing %s (%s)" % (d, board_id(d)))
|
||||
write_file(d + "/NEW.UF2", outbuf)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{
|
||||
"short_name": "RP2040",
|
||||
"id": "0xe48bff56",
|
||||
"description": "Raspberry Pi RP2040"
|
||||
},
|
||||
{
|
||||
"short_name": "RP2350-ARM-S",
|
||||
"id": "0xe48bff59",
|
||||
"description": "Raspberry Pi RP2350, ARM, Secure"
|
||||
},
|
||||
{
|
||||
"short_name": "RP2350-ARM-NS",
|
||||
"id": "0xe48bff5a",
|
||||
"description": "Raspberry Pi RP2350, ARM, Non-Secure"
|
||||
},
|
||||
{
|
||||
"short_name": "RP2350-RISCV",
|
||||
"id": "0xe48bff5b",
|
||||
"description": "Raspberry Pi RP2350, RISC-V"
|
||||
}
|
||||
]
|
||||
Reference in new issue
Block a user