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:
Kevin Thomas committed 2026-09-27 14:18:56 -04:00
1 parent 5201ee4b6b
commit 35eacd2c0e
162 files changed
+125658 -232

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
---
name: Generate Course PDF
description: Renders a markdown file into the strict PDF format used by the Embedded-Hacking course, ensuring strict ASCII box alignment and LaTeX math rendering.
---
# Generate Course PDF
When creating, editing, or generating PDFs for the Embedded-Hacking course in the strict/exact format, follow these mandatory steps:
## 1. Clean Emojis from Markdown
Emojis and certain high-surrogate Unicode characters can cause the Puppeteer/PDF.js render to crash with unresolved pattern errors in VS Code.
Run a script to strip emojis and high-surrogate Unicode characters from the target Markdown file before converting:
```python
import re, sys
with open(sys.argv[1], 'r', encoding='utf-8') as f:
text = f.read()
clean_text = re.sub(r'[\U00010000-\U0010ffff]', '', text)
with open(sys.argv[1], 'w', encoding='utf-8') as f:
f.write(clean_text)
```
## 2. Mandatory ASCII Art Box Alignment Audit
All ASCII art boxes in the course notes must have pixel-perfect character alignment.
- The standard course ASCII box width is exactly **67 characters** (`+` followed by 65 `-` followed by `+`).
- Every line inside the box MUST begin with `|` and end with `|` at the exact same column width (length 67).
- A closing `|` that is even one character too far to the left or right is strictly forbidden.
- Always run an automated audit before rendering:
```python
import sys
with open(sys.argv[1], 'r', encoding='utf-8') as f:
lines = [l.rstrip('\r\n') for l in f.readlines()]
def check(box):
if len(box) >= 3 and box[0][0].strip().startswith('+') and box[-1][0].strip().startswith('+'):
target_len = len(box[0][0])
for bl, bidx in box:
if len(bl) != target_len:
raise ValueError(f"ASCII box alignment error on line {bidx}: expected {target_len} chars, got {len(bl)}: {bl}")
in_code = False
box = []
for idx, l in enumerate(lines, 1):
if l.strip().startswith('```'):
check(box)
in_code = not in_code
box = []
continue
if in_code and len(l.strip()) > 2 and ((l.strip().startswith('+') and l.strip().endswith('+')) or (l.strip().startswith('|') and l.strip().endswith('|'))):
box.append((l, idx))
else:
check(box)
box = []
check(box)
```
Note: `check(box)` must run on every boundary, including the closing ```` ``` ```` fence and end of file. A box that sits last in a fenced block is otherwise never validated.
## 3. LaTeX Math Rendering
- The PDF renderer uses `marked-katex-extension` with self-contained, inlined Base64 KaTeX `woff2` fonts.
- Inline math must use `$formula$` and display math must use `$$formula$$`.
- Never leave broken or unbalanced LaTeX delimiters.
- Verify in the final PDF that all math expressions render cleanly with zero raw `$` delimiters in the body text.
## 4. Run the Render Script
The strict course format rendering script is located in `Documents/data-science/EH`:
- Working Directory: `Documents/data-science/EH`
- Command: `node render-lesson-pdf.mjs [ABSOLUTE_PATH_TO_SOURCE_MD] [ABSOLUTE_PATH_TO_OUTPUT_PDF]`
## 5. Verification & Git Hygiene
- Inspect the generated PDF to confirm proper page layout, fonts, and headers/footers (`Course Notes`).
- Ensure `.DS_Store` or other OS metadata files are never staged or committed to the repository (verify `.gitignore`).
+10
View File
@@ -62,3 +62,13 @@ node_modules/
package-lock.json package-lock.json
package.json package.json
svg-to-pdf.ps1 svg-to-pdf.ps1
# Operating System Files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
+2
View File
@@ -0,0 +1,2 @@
build
!.vscode/*
+22
View File
@@ -0,0 +1,22 @@
{
"configurations": [
{
"name": "Pico",
"includePath": [
"${workspaceFolder}/**",
"${userHome}/.pico-sdk/sdk/2.3.0/**"
],
"forcedInclude": [
"${workspaceFolder}/build/generated/pico_base/pico/config_autogen.h",
"${userHome}/.pico-sdk/sdk/2.3.0/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
}
+15
View File
@@ -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}"
}
}
]
+9
View File
@@ -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"
]
}
+52
View File
@@ -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.0/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.0/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}\""
]
}
]
}
+46
View File
@@ -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.0",
"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.0/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.0",
"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.0/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.0",
"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.0/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"
}
+102
View File
@@ -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.0/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.0/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"
}
}
]
}
+23
View File
@@ -0,0 +1,23 @@
#include <stdio.h>
#include "pico/stdlib.h"
int main()
{
stdio_init_all();
while (true) {
__asm volatile(
// Save a low register and the link register.
"push {r4, lr}\n"
// Intentionally unordered: the encoded list is still r2, r3, r6.
"push {r3, r2, r6}\n"
// Thumb PUSH cannot encode high registers r8-r12.
"stmdb sp!, {r9, r10}\n"
// Restore each group in reverse order to return SP to its start.
"ldmia sp!, {r9, r10}\n"
"pop {r2, r3, r6}\n"
"pop {r4, lr}\n"
::: "memory");
}
}
+57
View File
@@ -0,0 +1,57 @@
# Generated Cmake Pico project file
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.0)
set(toolchainVersion 15_2_Rel1)
set(picotoolVersion 2.3.0)
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(0x0001a_stack C CXX ASM)
# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()
# Add executable. Default name is the project name, version 0.1
add_executable(0x0001a_stack 0x0001a_stack.c )
pico_set_program_name(0x0001a_stack "0x0001a_stack")
pico_set_program_version(0x0001a_stack "0.1")
# Modify the below lines to enable/disable output over UART/USB
pico_enable_stdio_uart(0x0001a_stack 1)
pico_enable_stdio_usb(0x0001a_stack 0)
# Add the standard library to the build
target_link_libraries(0x0001a_stack
pico_stdlib)
# Add the standard include files to the build
target_include_directories(0x0001a_stack PRIVATE
${CMAKE_CURRENT_LIST_DIR}
)
pico_add_extra_outputs(0x0001a_stack)
+121
View File
@@ -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})
+4
View File
@@ -0,0 +1,4 @@
build
!.vscode/*
!build-ctf/0x0001b_ctf.bin
!CTF-01.bin
+22
View File
@@ -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
}
+15
View File
@@ -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}"
}
}
]
+9
View File
@@ -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"
]
}
+52
View File
@@ -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}\""
]
}
]
}
+46
View File
@@ -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"
}
+102
View File
@@ -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"
}
}
]
}
+91
View File
@@ -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)
+442
View File
@@ -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.
+237
View File
@@ -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.
+467
View File
@@ -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.
+56
View File
@@ -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
+73
View File
@@ -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
+121
View File
@@ -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})
+82
View 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())
+50
View File
@@ -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> ");
}
+57
View File
@@ -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;
}
+56
View File
@@ -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;
}
+365
View File
@@ -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()
+22
View File
@@ -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"
}
]
+4
View File
@@ -0,0 +1,4 @@
build/
.DS_Store
*.swp
*~
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+112
View File
@@ -0,0 +1,112 @@
# Operation Dark Vector: Classified Intelligence Briefing
```
+-----------------------------------------------------------------+
| TOP SECRET // NOFORN |
+-----------------------------------------------------------------+
| |
|OPERATION DARK VECTOR |
| |
|CLASSIFIED BRIEFING: LIVE CTF 0x01 |
| |
|NATIONAL SECURITY AGENCY / GMU RHET |
+-----------------------------------------------------------------+
```
## 1. The Answer
INT. NSA EXPLOITATION CELL. 05:03.
The analysts have been awake for nineteen hours. The Director walks in with a cup
of coffee and does not sit down.
**DIRECTOR:** Where is it going?
**ANALYST:** Buddy says it knows.
**DIRECTOR:** Buddy.
**ANALYST:** The model. The one we built for exactly this. Give it the image, ask
it the question.
**DIRECTOR:** And?
**ANALYST:** Nine seconds.
She turns the screen around. This is what came back.
```
+-----------------------------------------------------------------+
| BUDDY // CLASSIFIED EXPLOITATION ASSIST // CONF: 0.97 |
+-----------------------------------------------------------------+
| DESTINATION : +38.840280 -77.428890 |
| CONFIDENCE : 0.97 |
| PATCH : verified-by-construction. Ready to flash. |
+-----------------------------------------------------------------+
```
**DIRECTOR:** That is the whole answer.
**ANALYST:** That is the whole answer.
**DIRECTOR:** Is it that simple?
**ANALYST:** It is never that simple.
**DIRECTOR:** Then why does it look that simple?
**ANALYST:** Because Buddy is very good at making things look simple.
## 2. What Buddy Is
Buddy is not a person. It is a model. It has read more assembly than every
engineer who has ever lived, and it never sleeps and never doubts. You ask it a
question, and it answers in the exact shape of an answer, with a confidence that
is hard to argue with.
That confidence is the problem. It is not the same thing as being right.
Buddy has never seen this drone. It has never stood at this bench or watched this
board power on. It has a picture of the firmware, and it has made a very good
guess. A very good guess is still a guess.
## 3. The Operation
At 04:17 the woods outside Centreville, Virginia were black and the machine above
them was silent. It was a one-way drone, built by a state program we will not
name here, meant to fly to a target, do its work, and never come home. No radio,
no hand on the stick. A breadboard-ugly brain counting down a heading it was born
with.
The wind won. The battery died. It came down through the branches and stayed
there, about a mile from where it started.
Then it started talking. A recovery beacon on 915 MHz, repeating a coordinate
into the night. NSA sensors heard it, and a recovery team took the airframe
intact. The flight computer was a bare-metal RP2350 that was never supposed to be
opened, and the one thing it was still willing to say out loud.
CyberCom imaged the flash and did what everyone does now. They handed it to the
machine.
## 4. The Mandate
The Director does not trust the answer. Not because Buddy is bad, but because the
answer is too clean. Nine seconds for a question that should have taken a night.
So the order is simple, and it is the only order that matters.
Prove it. Prove Buddy correct, or prove it wrong with evidence that no one can
argue with. A machine's verdict does not move up the chain without a human
signature.
## 5. The Challenge
You have the image. You have the board. You have the tools and the time Buddy
did not need.
Buddy has given you its answer, and it is confident.
Is it that simple?
Go find out.
Binary file not shown.
+108
View File
@@ -0,0 +1,108 @@
# 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 Dark Vector
# micro-UAV autonomous guidance 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.2.0)
set(toolchainVersion 14_2_Rel1)
set(picotoolVersion 2.2.0-a4)
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(0x0011a_cb C CXX ASM)
# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()
# Add executable with modular sources in src/
add_executable(0x0011a_cb
src/main.c
src/gps.c
src/lora.c
src/propeller.c
src/payload.c
src/navigation.c
src/aes.c
src/lcd.c
)
pico_set_program_name(0x0011a_cb "0x0011a_cb")
pico_set_program_version(0x0011a_cb "0.1")
# Modify the below lines to enable/disable output over UART/USB
pico_enable_stdio_uart(0x0011a_cb 1)
pico_enable_stdio_usb(0x0011a_cb 0)
set(CTF_TARGET_LAT "38.881940" CACHE STRING "CTF target latitude (per-student randomized)")
set(CTF_TARGET_LON "-77.450280" CACHE STRING "CTF target longitude (per-student randomized)")
target_compile_definitions(0x0011a_cb PRIVATE
PICO_DEFAULT_UART_BAUD_RATE=115200
CTF_TARGET_LAT=${CTF_TARGET_LAT}
CTF_TARGET_LON=${CTF_TARGET_LON}
)
# Generate PIO header
pico_generate_pio_header(0x0011a_cb ${CMAKE_CURRENT_LIST_DIR}/src/uart_rx.pio)
# Add the standard library to the build
target_link_libraries(0x0011a_cb
pico_stdlib
hardware_uart
hardware_pio
hardware_pwm
hardware_i2c
)
# Add the standard include files to the build
target_include_directories(0x0011a_cb PRIVATE
${CMAKE_CURRENT_LIST_DIR}/include
)
pico_add_extra_outputs(0x0011a_cb)
+446
View File
@@ -0,0 +1,446 @@
# GDB Hardware Debugging Tutorial: Reverse Engineering the Stripped RP2350 Image
## 0. Cold Open
The board is powered. The blade is turning. And none of it is true to you yet,
because you have not seen it with your own eyes.
Every claim you will make about this machine has to survive one test: did you
watch it happen? Not did the decompiler suggest it. Not did the datasheet imply
it. Did you stop the core, read the register, and see the number with your own
eyes.
The Debug Probe is the only honest witness in the room. It reaches through SWD
into the silicon and pulls out the truth at 5,000 kHz while the rest of the
world argues. It does not care what you believe. It does not care what Buddy
answered. It reports.
A register is not an opinion. A clock divider is not a narrative. `SM0_CLKDIV =
0x07A12000` is not a talking point. It is 1953.125, and 1953.125 is the reason
the sky is readable at all.
Anyone can generate an explanation. You are here to *verify* one, bit by bit,
on live silicon, and to sign your name to it.
**Think, then verify.**
---
## 1. Executive Summary
This tutorial reverse engineers the **stripped** `0x0011a_cb.bin` on live
silicon using a Raspberry Pi Debug Probe, OpenOCD, and GNU GDB. There are **no
symbols**, no `main`, no variable names, nothing. You set breakpoints by
**address**, read raw memory, and let the hardware tell you the truth.
Everything shown was captured from the real target and is reproducible.
```
+-----------------------------------------------------------------+
| GDB HARDWARE DEBUG SIGNAL CHAIN |
+-----------------------------------------------------------------+
| HOST macOS -> USB -> Debug Probe -> SWD -> RP2350B Cortex-M33 |
| OpenOCD :3333 <------ SWD + UART bridge ------> GP0/GP1 115200 |
+-----------------------------------------------------------------+
```
The philosophy: an offline model can guess what a stripped image does. It
cannot read the live registers, watch the parser, or verify a patch. **Think,
then verify.**
---
## 2. Prerequisites
| Component | Value |
|---|---|
| Target | Raspberry Pi Pico 2 (RP2350B) |
| Image | `0x0011a_cb.bin` (raw flash image, base `0x10000000`) |
| Probe | Raspberry Pi Debug Probe (CMSIS-DAP v2) |
| Architecture | `ARM:LE:32:Cortex` (ARMv8-M / Cortex-M33, Thumb-2) |
The `.bin` is the stripped image. `file offset = address, 0x10000000`.
### 2.1 Tool Paths Per Host (macOS, Linux, Windows)
The probe, OpenOCD, and GDB behave identically on every platform; only the paths
and the shell differ. Pick your host.
| Tool | macOS | Linux | Windows |
|---|---|---|---|
| OpenOCD | `~/.pico-sdk/openocd/0.12.0+dev/openocd` | `$HOME/.pico-sdk/openocd/0.12.0+dev/openocd` | `%USERPROFILE%\.pico-sdk\openocd\0.12.0+dev\openocd.exe` |
| OpenOCD scripts | `~/.pico-sdk/openocd/0.12.0+dev/scripts` | `$HOME/.pico-sdk/openocd/0.12.0+dev/scripts` | `%USERPROFILE%\.pico-sdk\openocd\0.12.0+dev\scripts` |
| GDB | `~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb` | `$HOME/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb` | `%USERPROFILE%\.pico-sdk\toolchain\14_2_Rel1\bin\arm-none-eabi-gdb.exe` |
| Serial port | `/dev/tty.usbmodem*` | `/dev/ttyACM*` | `COMx` (Device Manager) |
Install the tools if you do not have them:
- macOS: `brew install --cask gcc-arm-embedded` for the toolchain, then
`brew install open-ocd`, or run the Raspberry Pi `pico-setup` script, which
places everything under `~/.pico-sdk`.
- Linux: install `gcc-arm-none-eabi` and `openocd` from your package manager,
or run the Raspberry Pi `pico-setup` script under `~/.pico-sdk`.
- Windows: install the Raspberry Pi Pico VS Code extension or the official
Windows installer; both place the toolchain under `%USERPROFILE%\.pico-sdk`.
Use PowerShell for every command below.
Serial console to the debug UART, per host:
```bash
# macOS
screen /dev/tty.usbmodem* 115200
# Linux
screen /dev/ttyACM* 115200
```
```powershell
# Windows
putty -serial COMx -sercfg 115200,8,n,1
```
---
## 3. Flash the Stripped Image
Because there are no object headers, you flash the raw bytes at the flash base
explicitly.
macOS and Linux:
```bash
OCD=~/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=~/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg \
-c "adapter speed 5000" \
-c "program 0x0011a_cb.bin 0x10000000 verify reset exit"
```
Windows PowerShell:
```powershell
$OCD = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\openocd.exe"
$SCR = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\scripts"
& $OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg `
-c "adapter speed 5000" `
-c "program 0x0011a_cb.bin 0x10000000 verify reset exit"
```
Expected on every host:
```
** Programming Started **
** Programming Finished **
** Verified OK **
** Resetting Target **
```
---
## 4. Attach GDB With No Symbols
Start the OpenOCD GDB server in one terminal, then attach **without** an
executable in another.
macOS and Linux:
```bash
OCD=~/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=~/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000" &
GDB=~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb
$GDB -q
(gdb) target extended-remote localhost:3333
(gdb) monitor reset halt
```
Windows PowerShell:
```powershell
$OCD = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\openocd.exe"
$SCR = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\scripts"
Start-Process $OCD -ArgumentList @("-s","$SCR","-f","interface/cmsis-dap.cfg","-f","target/rp2350.cfg","-c","adapter speed 5000")
$GDB = "$env:USERPROFILE\.pico-sdk\toolchain\14_2_Rel1\bin\arm-none-eabi-gdb.exe"
& $GDB -q
(gdb) target extended-remote localhost:3333
(gdb) monitor reset halt
```
`bt` and `break main` are useless: there are no symbols. Every stop is an
address. This is the stripped-image reality.
---
## 5. Break at the Reset Handler's Destination
From static analysis (see the Ghidra tutorial) we know the code entry `main`
begins at `0x10000234`. Break there by address:
```gdb
(gdb) break *0x10000234
(gdb) continue
Thread 1 hit Breakpoint 1, 0x10000234 in ?? ()
(gdb) info registers r0 r1 r2 r3 sp lr pc xpsr
r0 0x0 0
r1 0x10000235 268436021
r2 0x80808080 -2139062144
r3 0xe000ed08 -536810232
sp 0x20082000
lr 0x1000018f
pc 0x10000234
xpsr 0x69000000
```
Backtrace shows raw addresses only:
```
(gdb) bt
#0 0x10000234 in ?? ()
```
---
## 6. The Decoy And The Encrypted Target (No Symbols Needed)
The firmware carries **two** kinds of coordinate, and they are not the same kind
of thing.
**The decoy (plaintext).** A plaintext pair sits at `0x1000A098` (lat) /
`0x1000A090` (lon): `+38.840280` / `-77.428890`. The beacon broadcasts it and a
byte scanner grabs it. It is the lie.
```
+-----------------------------------------------------------------+
| DECOY WAYPOINT (PLAINTEXT, THE LIE) |
+-----------------------------------------------------------------+
| 0x1000A098 CF BD 87 4B 8E 6B 43 40 double +38.840280 |
| 0x1000A090 36 E5 0A EF 72 5B 53 C0 double -77.428890 |
+-----------------------------------------------------------------+
```
**The target (AES-encrypted).** The real waypoint is never stored as a double:
```gdb
(gdb) x/16bx 0x10009D90
0x10009D90: 0x56 0x45 0x43 0x54 0x4f 0x52 0x31 0x31 # "VECTOR11"
0x10009D98: 0x41 0x45 0x53 0x4b 0x45 0x59 0x21 0x21 # "AESKEY!!"
(gdb) x/2gx 0x10009DA4
0x10009DA4: 0x7aea23c0c2ac4f20 0xcaef2311710f933a # ciphertext block
```
`init_navigation @ 0x10000C2C` decrypts each word with the key into RAM at
`TARGET_LAT 0x20000D00` / `TARGET_LON 0x20000D08`. Read the reconstructed truth
after boot:
```gdb
(gdb) x/2gx 0x20000d00
0x20000d00: 0x404370e368f08462 0xc0535cd1633482bf
```
Decode it: `+38.881940` / `-77.450280` (NRO HQ). The decoy says Centreville. The
target says Chantilly. Same firmware. One of them is a lie.
## 7. Prove the Peripherals From Registers
Break at `send_telemetry @ 0x10000C7C` (called once per acquisition tick) so
the initialisers have already run:
```gdb
(gdb) break *0x10000c7c
(gdb) continue
Thread 1 hit Breakpoint 2, 0x10000c7c in ?? ()
(gdb) x/2xw 0x50200000
0x50200000: 0x00000001 0x0f010e01 # PIO0_CTRL PIO0_FSTAT
(gdb) x/6xw 0x502000c8
0x502000c8: 0x07a12000 0x0701fc00 # SM0_CLKDIV SM0_EXECCTRL
0x502000d0: 0x800c0000 0x00000018 # SM0_SHIFTCTRL SM0_ADDR
0x502000d8: 0x00002020 0x00038000 # SM0_INSTR SM0_PINCTRL
```
```gdb
(gdb) x/4xw 0x40078024
0x40078024: 0x000003d0 0x00000024 0x00000070 0x00000301 # UART1 LoRa
(gdb) x/4xw 0x40070024
0x40070024: 0x00000051 0x00000018 0x00000070 0x00000301 # UART0 debug
```
```
+-----------------------------------------------------------------+
| PIO0 SM0 CLOCK DIVIDER (RP2350 @ 150 MHz, 8 cycles/bit) |
+-----------------------------------------------------------------+
| SM0_CLKDIV = 0x07A12000 = 1953 + 32/256 = 1953.125 |
| f_sm = 150,000,000 / 1953.125 = 76,800 Hz = 9600 x 8 |
+-----------------------------------------------------------------+
```
UART baud uses `IBRD` and a 6-bit `FBRD`:
$$baud = \frac{f_{clk}}{16 \times (IBRD + FBRD/64)}$$
UART1: $976 + 36/64 = 976.5625 \Rightarrow 150{,}000{,}000 / 15625 = 9600$.
UART0: $81 + 24/64 = 81.375 \Rightarrow 150{,}000{,}000 / 1302 \approx 115200$.
---
## 8. Watch the NMEA Parser Prove Itself
Static analysis identifies the parser's static index at `0x200010F4` (the
`.bss` symbol `idx.1`) and the 96-byte NMEA buffer at `0x20001044` (`buf.0`).
Watch the index:
```gdb
(gdb) watch *(int*)0x200010f4
(gdb) continue
Hardware watchpoint 3: *(int*)0x200010f4
Old value = 0
New value = 1
0x10000458 in ?? ()
(gdb) bt
#0 0x10000458 in ?? ()
#1 0x1000027c in ?? ()
(gdb) x/32cb 0x20001044
0x20001044: 36 '$' 71 'G' 80 'P' 71 'G' 76 'L' 76 'L' ...
# => "$GPGLL,,,,,,220653.00,V,N*4A"
```
At reset the buffer and index are both zero; these bytes appear only after a
sentence is parsed off the wire.
The `V` says there is **no fix yet**, not a parser bug, not a UART fault.
Only the wire can tell you that.
---
## 9. The Actuators: `release_payload @ 0x10000BFC`
The Ghidra analysis (companion tutorial) shows `release_payload` drives GP16,
GP17 and GP18 high through the RP2350 GPIO coprocessor interface. It is reached
by a `b.w` tail call from `navigate_to_target` at `0x10000D18`, so there is no
return frame; break at its entry and step the writes:
```gdb
(gdb) break *0x10000bfc
(gdb) continue
Thread 1 hit Breakpoint 4, 0x10000bfc in ?? ()
(gdb) x/9i $pc
0x10000bfc: push {r3, lr}
0x10000bfe: movs r2, #16
0x10000c00: mov.w r3, #1
0x10000c04: mcrr 0, 4, r2, r3, cr0 # GPIO16 = 1
0x10000c08: movs r2, #17
0x10000c0a: mcrr 0, 4, r2, r3, cr0 # GPIO17 = 1
0x10000c0e: movs r2, #18
0x10000c10: mcrr 0, 4, r2, r3, cr0 # GPIO18 = 1
```
After the three writes, the SIO register reads `0x00070000` (bits 16, 17, 18).
The RP2350 exposes GPIO through the coprocessor (`mcrr p0, #4, ...`), not a
plain store, a fact only the bench reveals.
---
## 10. Reproducibility Checklist
1. Flash `0x0011a_cb.bin` at `0x10000000`; `Verified OK`.
2. Attach GDB with **no** symbol file.
3. `break *0x10000234`; PC lands exactly there.
4. `x/4xw 0x20000d00` -> `68f08462 404370e3 633482bf c0535cd1`.
5. `x/6xw 0x502000c8` -> `SM0_CLKDIV = 0x07A12000`.
6. `watch *(int*)0x200010f4` trips only when NMEA arrives.
7. `x/9i 0x10000bfc` shows the `mcrr` GPIO coprocessor writes.
If a single value differs, you are not on the image you think you are. That
check is the job an offline model cannot do.
---
## 11. Why Buddy Fails Here
Even a model trained on ARM cannot read `SM0_CLKDIV`, watch `idx.0`, decode
*your* randomized literal, or verify a patch by flashing it. It generates
plausible text; the bench generates truth. **Think, then verify.**
---
## Appendix A. Deep GDB Step-Through
### A.1 Start the server and attach
macOS:
```bash
OCD=~/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=~/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000"
```
Linux:
```bash
OCD=$HOME/.pico-sdk/openocd/0.12.0+dev/openocd
SCR=$HOME/.pico-sdk/openocd/0.12.0+dev/scripts
$OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000"
```
Windows PowerShell:
```powershell
$OCD = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\openocd.exe"
$SCR = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev\scripts"
& $OCD -s "$SCR" -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000"
```
Then attach from GDB (identical on all hosts). There are no symbols, so break by
address, not by name:
```gdb
target extended-remote localhost:3333
monitor reset halt
break *0x10000234
continue
```
### A.2 Read the vector table and the key material
```gdb
x/2xw 0x10000000 # SP and reset handler
x/16bx 0x10009D90 # AES key
x/16bx 0x10009DA4 # ciphertext block
x/2gx 0x20000D00 # decrypted target after boot
```
### A.3 Break the AES routine
```gdb
break *0x10000E5C
continue
x/4i $pc
stepi
stepi
```
`aes128_ecb_decrypt_block` runs once, early, inside `init_navigation`.
### A.4 Watch the decoy go out
```gdb
break *0x10000C7C
continue
info registers r0 r1 r2 r3
```
`send_telemetry` is called with the decoy while the GNSS has no fix.
### A.5 The tool trap
On a stripped target a floating point cast can byte-swap. Trust the raw read:
```gdb
x/1gx 0x20000D00 # then decode it yourself
```
Binary file not shown.
+525
View File
@@ -0,0 +1,525 @@
# Ghidra Reversing Tutorial: Static Analysis of the Stripped RP2350 Image
## 0. Cold Open
Open the image and it will lie to you with total confidence.
That is not a bug. That is the tool doing exactly what it was built to do:
propose. Ghidra proposes functions. It proposes boundaries. It proposes names
for things it cannot possibly know. And when it is wrong it will not tell you,
because it cannot tell the difference between a guess and a fact any better
than the machine that answered nine seconds too fast.
A stripped binary is a wall of Thumb-2 with the labels torn off. The only way
through is patience, cross-references, and the refusal to accept a story just
because it is plausible. Ghidra is the map. It is not the territory.
So you draw the map, then you walk the ground. You separate the two coordinate
pairs by what *references* them, not by what they look like. You find the eight
bytes that decide where a machine goes, and you prove which eight they are.
The tool proposes. The bench disposes.
**Think, then verify.**
---
## 1. Executive Summary
This tutorial reverses the **stripped** `0x0011a_cb.bin` with Ghidra: no
symbols, no sections, no metadata. We import the raw image, recover the code
graph, correct the boundaries Ghidra gets wrong, find the hardcoded target
waypoint, and patch it. Every output below is from the raw image at base
`0x10000000`, language `ARM:LE:32:Cortex`.
---
## 2. Import the Raw Image
The `.bin` is a raw XIP image. You must tell Ghidra where it lives and how to
decode it.
```
+-----------------------------------------------------------------+
| GHIDRA IMPORT SETTINGS |
+-----------------------------------------------------------------+
| Language : ARM:LE:32:Cortex (ARMv8-M / Cortex-M33, Thumb-2) |
| Format : Raw Binary |
| Base : 0x10000000 (RP2350 external flash / XIP) |
+-----------------------------------------------------------------+
```
### 2.1 Headless (reproducible), per host
macOS:
```bash
GHIDRA=/Applications/ghidra_12.0.4_PUBLIC
"$GHIDRA/support/analyzeHeadless" /tmp/ghproj DarkVector \
-import 0x0011a_cb.bin \
-processor "ARM:LE:32:Cortex" \
-loader BinaryLoader -loader-baseAddr 0x10000000
```
Linux:
```bash
GHIDRA=$HOME/ghidra_12.0.4_PUBLIC
"$GHIDRA/support/analyzeHeadless" /tmp/ghproj DarkVector \
-import 0x0011a_cb.bin \
-processor "ARM:LE:32:Cortex" \
-loader BinaryLoader -loader-baseAddr 0x10000000
```
Windows PowerShell:
```powershell
$GHIDRA = "C:\ghidra_12.0.4_PUBLIC"
& "$GHIDRA\support\analyzeHeadless.bat" "$env:TEMP\ghproj" DarkVector `
-import 0x0011a_cb.bin `
-processor "ARM:LE:32:Cortex" `
-loader BinaryLoader -loader-baseAddr 0x10000000
```
### 2.2 GUI, per host
Launch Ghidra, then **File -> Import File**, set language and base address, and
analyze:
- macOS: `/Applications/ghidra_12.0.4_PUBLIC/ghidraRun`
- Linux: `$HOME/ghidra_12.0.4_PUBLIC/ghidraRun`
- Windows: `C:\ghidra_12.0.4_PUBLIC\ghidraRun.bat`
---
## 3. Entry and the Stripped-Binary Problem
The first two words are the ARMv8-M vector table:
```
0x10000000: 0x20082000 ; initial SP
0x10000004: 0x1000015D ; reset handler (Thumb)
```
Ghidra recovers **166 functions** from the call graph. But with no symbols it
gets some boundaries wrong, and it folds tail-call-only helpers into their
callers: `release_payload` is reached only by a `b.w` tail call from
`navigate_to_target` at `0x10000D18`, so Ghidra does not give it its own
function. It names the entry `FUN_10000234`. The disassembly is right; the
boundaries are not. Correcting them is the analyst's job, and only the bench
confirms them.
Applying correct boundaries yields:
| Address | Function |
|---|---|
| `0x10000234` | `main` |
| `0x10000300` | `init_gps_pio` |
| `0x1000040C` | `poll_gps` |
| `0x10000858` | `gps_get_stats` |
| `0x10000870` | `init_lora` |
| `0x10000A08` | `lora_send` |
| `0x10000A54` | `lora_tick` |
| `0x10000AC4` | `init_propeller` |
| `0x10000AF8` | `propeller_set_bearing` |
| `0x10000B34` | `propeller_stop` |
| `0x10000B64` | `init_payload` |
| `0x10000BB0` | `set_gnss_leds` |
| `0x10000BFC` | `release_payload` |
| `0x10000C2C` | `init_navigation` |
| `0x10000C7C` | `send_telemetry` |
| `0x10000CB8` | `navigate_to_target` |
| `0x10000E5C` | `aes128_ecb_decrypt_block` |
| `0x100012D4` | `init_lcd` |
| `0x1000175C` | `lcd_show_coords` |
| `0x10001B4C` | `lcd_show_gnss` |
---
## 4. `main` as Ghidra Sees It (Raw Image)
```
void main(void)
{
local_18 = *DAT_100002f4; // ORIGIN_LAT @ 0x1000A098
uStack_14 = DAT_100002f4[1];
local_10 = *DAT_100002f8; // ORIGIN_LON @ 0x1000A090
uStack_c = DAT_100002f8[1];
FUN_10005d98(); // stdio_init_all
FUN_10000c2c(); // init_navigation
FUN_10000b64(); // init_payload
FUN_10000870(); // init_lora
FUN_10000300(); // init_gps_pio
FUN_10000ac4(); // init_propeller
FUN_100012d4(); // init_lcd
piVar1 = DAT_100002fc; // &hold @ 0x200010F0
do {
while( true ) {
iVar3 = 200;
bVar4 = 0;
local_20 = 0;
uStack_1c = 0;
do {
bVar2 = FUN_1000040c(&local_18,&local_10); // poll_gps
bVar4 = bVar2 | bVar4;
FUN_10000a54(); // lora_tick
FUN_10002ab8(5); // sleep_ms(5)
iVar3 = iVar3 + -1;
} while (iVar3 != 0);
FUN_10000858(&local_20,&uStack_1c); // gps_get_stats
if (bVar4 == 0) break;
*piVar1 = 3; // hold = 3
FUN_10000bb0(1,local_20); // set_gnss_leds
LAB_100002a4:
FUN_1000175c(local_18,uStack_14,local_10,uStack_c); // lcd_show_coords
FUN_10000cb8(local_18,uStack_14,local_10,uStack_c); // navigate_to_target
}
iVar3 = *piVar1;
if (iVar3 < 1) { iVar3 = 1; }
*piVar1 = iVar3 + -1; // hold--
if (iVar3 + -1 != 0) {
FUN_10000bb0(1,local_20); // set_gnss_leds
goto LAB_100002a4;
}
FUN_10000bb0(0,local_20); // set_gnss_leds(0,...)
FUN_10001b4c(local_20,uStack_1c); // lcd_show_gnss
FUN_10000b34(); // propeller_stop
FUN_10000c7c(local_18,uStack_14,local_10,uStack_c); // send_telemetry
} while( true );
}
```
`FUN_` prefixes everywhere: this is what a stripped target really looks like.
---
## 5. The Decoy And The Encrypted Target
`navigate_to_target` no longer compares against a literal; it reads the pointers
at `DAT_10000e50` / `DAT_10000e54`, which resolve to RAM `0x20000D00` /
`0x20000D08`, **runtime doubles**. Where do they come from? `init_navigation`,
and that is the whole puzzle.
```
void init_navigation(void)
{
local_28 = *DAT_10000c6c; // key @ 0x10009D90
uStack_24 = DAT_10000c6c[1];
uStack_20 = DAT_10000c6c[2];
uStack_1c = DAT_10000c6c[3];
local_18 = *DAT_10000c70; // ct @ 0x10009DA4
uStack_14 = DAT_10000c70[1];
uStack_10 = DAT_10000c70[2];
uStack_c = DAT_10000c70[3];
FUN_10000e5c(&local_18,&local_28,&local_38); // aes128_ecb_decrypt_block
*DAT_10000c74 = local_38; // TARGET_LAT -> 0x20000D00
puVar1[1] = uStack_34;
*puVar2 = local_30; // TARGET_LON -> 0x20000D08
puVar2[1] = uStack_2c;
}
```
Three data addresses do all the work:
| Symbol | Address | Meaning |
|---|---|---|
| `CTF_AES_KEY` | `0x10009D90` | the AES key `564543544f5231314145534b45592121` |
| `CTF_TARGET_CT` | `0x10009DA4` | the encrypted target pair |
| `TARGET_LAT/LON` | `0x20000D00` / `0x20000D08` | RAM, reconstructed at boot |
**Reading the key bytes (Ghidra will call them code):**
`0x10009D90` is **data**, not code. `init_navigation` copies the block into a
stack buffer and hands it to `aes128_ecb_decrypt_block`; it is never executed.
Ghidra still marks it as code because it sees the read cross-reference from
`init_navigation` (`FUN_10000c2c:10000c36(R)`) and guesses. The Listing then
shows fabricated mnemonics:
```
LAB_10009d90 XREF[1]: FUN_10000c2c:10000c36(R)
10009d90 56 45 cmp r6,r10
10009d92 43 54 strb r3,[r0,r1]
10009d94 4f 52 strh r7,[r1,r1]
10009d96 31 31 adds r1,#0x31
10009d98 41 45 cmp r1,r8
10009d9a 53 4b ldr r3,[s_n_"%s"_failed:_file_"%s",_line = "n \"%s\" failed: file
10009d9c 45 59 ldr r5,[r0,r5]
10009d9e 21 21 movs r1,#0x21
```
Those are the key bytes, not instructions. Two traps:
1. The mnemonics are meaningless: `56 45` is `V`,`E`; `43 54` is `C`,`T`;
`4f 52` is `O`,`R`.
2. At `0x10009D9A` the halfword `53 4B` decodes as `ldr r3, [pc, #332]`,
whose literal-pool target is `0x10009EE8`. That address really does hold a
string, newlib's assert message `Assertion "%s" failed: file "%s", line
%d%s%s` at `0x10009EE0`, so Ghidra prints its label. The string is real, but
the reference is spurious: `53 4B` is `S`,`K`, bytes 10-11 of the key, and
only decodes as that `ldr` by coincidence. The same thing happens at
`0x10009DA4`, where the ciphertext byte pair `20 4F` decodes to an `ldr`
that lands on the real two-space string at `0x10009E28`.
Read the bytes, never the mnemonics. Any of these is exact:
```bash
xxd -s 0x9D90 -l 16 0x0011a_cb.bin # raw image, no tools
```
```gdb
(gdb) x/16bx 0x10009D90 # live target
```
In the Ghidra GUI the simplest path is **Window -> Bytes**, press **G**, enter
`0x10009D90`, and read the 16 raw bytes. To retype them in the Listing instead,
select the 16 bytes, press **`C`** (Clear Code/Data), then **`T`** (Define Data)
and choose `byte`; press **`[`** to make it an array of 16. Note that **`B`**
is **not** "define byte" in Ghidra, it is *Cycle Integer Types*, and data
cannot be defined over bytes that are still typed as code, which is why the
clear (**`C`**) must come first.
The value is the ASCII key `VECTOR11AESKEY!!`:
`56 45 43 54 4F 52 31 31 41 45 53 4B 45 59 21 21`.
**Decrypt the target (fully offline, reproducible):**
Step 1. The key, 16 bytes at `0x10009D90` (read them as above):
```text
56 45 43 54 4F 52 31 31 41 45 53 4B 45 59 21 21 = "VECTOR11AESKEY!!"
hex for tools: 564543544f5231314145534b45592121
```
Step 2. The ciphertext, 16 bytes at `0x10009DA4`. Read them the exact same way
(**Window -> Bytes**, press **G**, enter `0x10009DA4`):
```text
20 4F AC C2 C0 23 EA 7A 3A 93 0F 71 11 23 EF CA
hex for tools: 204facc2c023ea7a3a930f711123efca
```
Step 3. AES-128-ECB decrypt the block with the key (no padding). The plaintext
is two little-endian IEEE-754 doubles, latitude then longitude.
Python, all platforms (`cryptography` is already used by the course):
```python
import struct
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
key = bytes.fromhex("564543544f5231314145534b45592121")
ct = bytes.fromhex("204facc2c023ea7a3a930f711123efca")
pt = Cipher(algorithms.AES(key), modes.ECB()).decryptor().update(ct)
print("plaintext:", pt.hex())
print("lat:", struct.unpack("<d", pt[:8])[0])
print("lon:", struct.unpack("<d", pt[8:])[0])
```
Output:
```text
plaintext: 6284f068e3704340bf823463d15c53c0
lat: 38.88194
lon: -77.45028
```
`<d` is a little-endian 64-bit double, exactly how the firmware reads the pair.
Same result with `openssl` (macOS, Linux, WSL):
```bash
printf '\x20\x4f\xac\xc2\xc0\x23\xea\x7a\x3a\x93\x0f\x71\x11\x23\xef\xca' \
| openssl enc -aes-128-ecb -K 564543544f5231314145534b45592121 -nopad -d \
| xxd
# 00000000: 6284 f068 e370 4340 bf82 3463 d15c 53c0
```
Same result in CyberChef (browser, nothing to install):
1. **From Hex** on `204facc2c023ea7a3a930f711123efca`.
2. **AES Decrypt**: Key = `Hex` `564543544f5231314145534b45592121`,
Mode = `ECB`, Padding = `None`, Input/Output = `Raw`.
3. **To Hex** to read `6284f068e3704340bf823463d15c53c0`.
4. That is two 8-byte little-endian doubles:
`62 84 F0 68 E3 70 43 40` and `BF 82 34 63 D1 5C 53 C0`.
Step 4. Decode the two doubles with the bit layout
$$V = (-1)^s \times 2^{e-1023} \times (1 + m)$$
The decrypted bytes are little-endian, so reverse each 8-byte group to the
big-endian hex word the Week 5 utility expects:
```bash
python3 scripts/float_hex_converter.py 0x404370E368F08462 # +38.881940
python3 scripts/float_hex_converter.py 0xC0535CD1633482BF # -77.450280
```
`struct.unpack("<d", ...)` already performs that byte swap for you.
And the plaintext pair at `0x1000A090` / `0x1000A098` (`+38.840280` /
`-77.428890`)? That is the **decoy**, the beacon's broadcast, and it is a lie.
The real target only exists after the AES.
## 6. `release_payload` (Raw Decompilation)
```
void release_payload(void)
{
coprocessor_moveto2(0,4,0x10,1,in_cr0); // GPIO16 = 1
coprocessor_moveto2(0,4,0x11,1,in_cr0); // GPIO17 = 1
coprocessor_moveto2(0,4,0x12,1,in_cr0); // GPIO18 = 1
__wrap_puts(uRam10009d3c); // "PAYLOAD RELEASED AT TARGET COORDINATES"
lora_send(uRam10009d64); // tail call
}
```
`coprocessor_moveto2` is Ghidra's rendering of the RP2350 GPIO `mcrr p0, #4`
path. A model will "helpfully" tell you this is a normal SIO store; the
encoding says otherwise, and GDB confirms it live.
---
### Two coordinate pairs (the decoy)
The image contains **two** hardcoded double pairs, not one:
| Pair | Address | Meaning |
|---|---|---|
| Launch origin (Centreville) | `0x1000A098` / `0x1000A090` | broadcast by the crash beacon |
| Target (NRO HQ) | `0x20000D00` / `0x20000D08` | RAM, rebuilt by `init_navigation` |
A pattern-matcher latches onto the **broadcast** origin and calls it the target.
The only way to tell them apart is the cross-reference: the target pair is
loaded with `ldrd` and fed to `__aeabi_dcmpeq` inside `navigate_to_target`; the
origin pair is handed to `send_telemetry`. Strings and intuition are not enough.
---
## 7. The Patch: Re-vector to a Safe Waypoint
The target is the 16 bytes at `0x10009DA4`. To re-vector the drone you replace
that block with the AES-128-ECB encryption of a safe waypoint (`37.0` / `-74.0`,
open Atlantic) under the same key. Compute the safe doubles and their ciphertext:
```bash
python3 scripts/float_hex_converter.py 37.0 # 0x4042800000000000
python3 scripts/float_hex_converter.py -74.0 # 0xC052800000000000
python3 -c "import struct,sys; sys.stdout.buffer.write(struct.pack('<d',37.0)+struct.pack('<d',-74.0))" \
| openssl enc -aes-128-ecb -K 564543544f5231314145534b45592121 -nopad | xxd -p
# c2bb647c8778bf59279c01ad066bb16b
```
In the Ghidra Listing, press **G** to `0x10009DA4`, select the 16 bytes, then
**Ctrl+Shift+G** (Patch Data):
| Address | Original | Patched |
|---|---|---|
| `0x10009DA4` | `20 4F AC C2 C0 23 EA 7A 3A 93 0F 71 11 23 EF CA` | `C2 BB 64 7C 87 78 BF 59 27 9C 01 AD 06 6B B1 6B` |
Export: **File -> Export Program...** -> Format **Binary** ->
`0x0011a_cb_patched.bin`.
---
## 8. Export, Convert, Flash, Verify
```bash
python3 uf2conv.py 0x0011a_cb_patched.bin \
-f 0xe48bff59 -b 0x10000000 -c -o 0x0011a_cb_patched.uf2
```
Hold **BOOTSEL**, copy the UF2 across, and verify against the live ground HUD.
A patch that is not flashed and confirmed is a guess.
---
## 9. Randomized Builds
Each student image embeds a unique key and waypoint, so no answer key travels:
```bash
python3 scripts/randomize_build.py --student-id alice --seed 12345 --uf2
[+] student_id : alice
[+] TARGET_LAT : 38.880272
[+] TARGET_LON : -77.460077
[+] AES key : <16 random bytes>
[+] ciphertext : <AES-128-ECB(key, target)>
[+] image : build-ctf/0x0011a_cb_alice.uf2
[+] answer key : <keydir>/answer_alice.json (INSTRUCTOR ONLY, do not ship)
```
Addresses are identical; only the key and ciphertext bytes differ.
---
## 10. Reproducibility Checklist
1. Import raw `.bin`, `ARM:LE:32:Cortex`, base `0x10000000`.
2. Reset vector `[0]=0x20082000`, `[1]=0x1000015D`.
3. `main @ 0x10000234`; fix the merged boundary at `a single FUN_ function`.
4. `navigate_to_target @ 0x10000CB8` reads the pointers at `0x10000E50`/`0x10000E54`, targeting RAM `0x20000D00`/`0x20000D08`.
5. Decode to `+38.881940` / `-77.450280`.
6. Patch, export, `uf2conv`, flash, verify.
---
## 11. Why Buddy Fails Here
Ghidra itself proves the point: it *proposes* functions and boundaries, and the
analyst corrects them with cross-references and the bench. A language model
does the same, faster and wrong, with no way to confirm. It cannot validate a
boundary, cannot read the live `ldrd`, and cannot flash a patch to see the
drone re-vector. Structure is a hypothesis; silicon is the verdict.
**Think, then verify.**
---
## Appendix A. Deep Ghidra Step-Through
### A.1 Import
Language `ARM:LE:32:Cortex`, Base Address `0x10000000`, Raw Binary. Analyze.
### A.2 Address map
| Address | What it is |
|---|---|
| `0x10000234` | `main` |
| `0x10000C2C` | `init_navigation`, rebuilds the target |
| `0x10000E5C` | `aes128_ecb_decrypt_block` |
| `0x10000CB8` | `navigate_to_target`, the arrival compare |
| `0x10009D90` | AES key |
| `0x10009DA4` | ciphertext |
| `0x1000A090` | decoy waypoint |
### A.3 The S-box
Go to `0x1000A1AC` and find the 256-byte S-box that starts `63 7C 77 7B` (the
inverse S-box, starting `52 09 6A D5`, sits at `0x1000A0AC`). Right click,
create an array of 256 bytes.
### A.4 Recover
Take 16 bytes at `0x10009DA4` and the key at `0x10009D90`, then run the Python
decrypt block from section 5.
### A.5 Patch
Overwrite the 16 bytes at `0x10009DA4` with the re-encrypted Atlantic block,
then File, Export Program, Format Binary.
### A.6 Platform note
Ghidra is identical on Windows, Linux, and macOS. Only the paths differ.
| Action | macOS | Linux | Windows |
|---|---|---|---|
| GUI launch | `/Applications/ghidra_12.0.4_PUBLIC/ghidraRun` | `$HOME/ghidra_12.0.4_PUBLIC/ghidraRun` | `C:\ghidra_12.0.4_PUBLIC\ghidraRun.bat` |
| Headless | `/Applications/ghidra_12.0.4_PUBLIC/support/analyzeHeadless` | `$HOME/ghidra_12.0.4_PUBLIC/support/analyzeHeadless` | `C:\ghidra_12.0.4_PUBLIC\support\analyzeHeadless.bat` |
| Shell | `zsh` / `bash` | `bash` | PowerShell |
See sections 2.1 and 2.2 for the full import commands.
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// File: aes.h
// Desc: Declares AES-128-ECB single-block decryption.
// Created: 2026
#ifndef AES_H
#define AES_H
#include <stdint.h>
/**
* @brief Decrypt one 16-byte block with AES-128-ECB.
*
* @param in 16-byte ciphertext.
* @param key 16-byte key.
* @param out 16-byte plaintext output.
* @return None.
*/
void aes128_ecb_decrypt_block(const uint8_t in[16], const uint8_t key[16], uint8_t out[16]);
#endif // AES_H
+17
View File
@@ -0,0 +1,17 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// File: ctf_target.h
// Desc: AES-128-ECB key and ciphertext for the real target waypoint.
// Created: 2026
#ifndef CTF_TARGET_H
#define CTF_TARGET_H
#include <stdint.h>
#define CTF_AES_KEY { 0x56, 0x45, 0x43, 0x54, 0x4F, 0x52, 0x31, 0x31, 0x41, 0x45, 0x53, 0x4B, 0x45, 0x59, 0x21, 0x21 }
#define CTF_TARGET_CT { 0x20, 0x4F, 0xAC, 0xC2, 0xC0, 0x23, 0xEA, 0x7A, 0x3A, 0x93, 0x0F, 0x71, 0x11, 0x23, 0xEF, 0xCA }
#endif // CTF_TARGET_H
+67
View File
@@ -0,0 +1,67 @@
// 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: gps.h
// Desc: Declares PIO UART GPS receiver interface and NMEA parsing logic.
// Created: 2026
#ifndef GPS_H
#define GPS_H
#include <stdbool.h>
#include "hardware/pio.h"
#define GPS_PIN 7
#define GPS_BAUD 9600
#define GPS_PIO pio0
#define GPS_SM 0
/**
* @brief Initialize PIO UART receiver on GPIO7 for u-blox NEO-6M GPS.
*
* @param None.
* @return None.
*/
void init_gps_pio(void);
/**
* @brief Poll PIO RX FIFO and parse incoming NMEA GPS coordinates.
*
* @param lat Pointer to double storing updated latitude.
* @param lon Pointer to double storing updated longitude.
* @return bool True if a valid active 3D GPS fix (RMC 'A') was received, false otherwise.
*/
bool poll_gps(double *lat, double *lon);
/**
* @brief Read latest GNSS signal statistics parsed from GSV sentences.
*
* @param siv Pointer to store satellites-in-view count.
* @param cno Pointer to store best carrier-to-noise ratio in dBHz.
* @return None.
*/
void gps_get_stats(int *siv, int *cno);
#endif // GPS_H
+74
View File
@@ -0,0 +1,74 @@
// 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: lcd.h
// Desc: Declares I2C HD44780 (16x2) LCD interface for telemetry coordinates.
// Created: 2026
#ifndef LCD_H
#define LCD_H
#include <stdbool.h>
#include <stdint.h>
#include "hardware/i2c.h"
#define LCD_I2C_INST i2c1
#define LCD_SDA_PIN 2
#define LCD_SCL_PIN 3
#define LCD_BAUD 100000
/**
* @brief Initialize I2C0 peripheral and detect/configure 1602 LCD backpack.
*
* @param None.
* @return None.
*/
void init_lcd(void);
/**
* @brief Render current latitude and longitude on the 16x2 character display.
*
* Row 0: LAT: dd.dddddd N
* Row 1: LON: dd.dddddd W
*
* @param lat Current latitude in decimal degrees.
* @param lon Current longitude in decimal degrees.
* @return None.
*/
void lcd_show_coords(double lat, double lon);
/**
* @brief Render GNSS acquisition telemetry (satellites and C/N0) on the LCD.
*
* Row 0: SAT: nn CNO: nn
* Row 1: ACQUIRING... when satellites are in view, else NO SIGNAL
*
* @param sats Number of satellites currently in view.
* @param cno Best carrier-to-noise ratio in dBHz.
* @return None.
*/
void lcd_show_gnss(int sats, int cno);
#endif // LCD_H
+64
View File
@@ -0,0 +1,64 @@
// 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: lora.h
// Desc: Declares UART1 interface for the REYAX RYLR998 LoRa transceiver.
// Created: 2026
#ifndef LORA_H
#define LORA_H
#include "hardware/uart.h"
#define LORA_UART uart1
#define LORA_BAUD 9600
#define LORA_TX_PIN 8
#define LORA_RX_PIN 9
/**
* @brief Initialize LoRa transceiver over UART1 on GPIO8 and GPIO9
*
* @param None.
* @return None.
*/
void init_lora(void);
/**
* @brief Transmit string message over LoRa UART1 interface
*
* @param msg Null-terminated string buffer to transmit.
* @return None.
*/
void lora_send(const char *msg);
/**
* @brief Service the LoRa UART: stream pending TX bytes and drain RX.
*
* @param None.
* @return None.
*/
void lora_tick(void);
#endif // LORA_H
+87
View File
@@ -0,0 +1,87 @@
// 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: navigation.h
// Desc: Declares autonomous guidance, dead-reckoning, and telemetry interface.
// Created: 2026
#ifndef NAVIGATION_H
#define NAVIGATION_H
#include <stdbool.h>
/** @brief Pre-programmed programmed decoy waypoint (Centreville, VA). */
extern const double ORIGIN_LAT;
extern const double ORIGIN_LON;
/** @brief Target coordinates, reconstructed at boot from masked constants. */
extern double TARGET_LAT;
extern double TARGET_LON;
/**
* @brief Reconstruct the target waypoint from its XOR-masked constants.
*
* @param None.
* @return None.
*/
void init_navigation(void);
/**
* @brief Transmit telemetry stream over Debug UART0 and LoRa UART1.
*
* @param cur_lat Current micro-UAV latitude.
* @param cur_lon Current micro-UAV longitude.
* @return None.
*/
void send_telemetry(double cur_lat, double cur_lon);
/**
* @brief Advance dead-reckoning position toward programmed waypoint.
*
* @param cur_lat Pointer to current latitude.
* @param cur_lon Pointer to current longitude.
* @return None.
*/
void dead_reckon_step(double *cur_lat, double *cur_lon);
/**
* @brief Verify if micro-UAV has arrived at target coordinates.
*
* @param cur_lat Current latitude coordinate.
* @param cur_lon Current longitude coordinate.
* @return true if arrived at target, false otherwise.
*/
bool check_arrival(double cur_lat, double cur_lon);
/**
* @brief Manage guidance progression, propeller oscillation, and payload release.
*
* @param cur_lat Current latitude coordinate.
* @param cur_lon Current longitude coordinate.
* @return None.
*/
void navigate_to_target(double cur_lat, double cur_lon);
#endif // NAVIGATION_H
+68
View File
@@ -0,0 +1,68 @@
// 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: payload.h
// Desc: Declares payload release mechanism interface on GPIO16.
// Created: 2026
#ifndef PAYLOAD_H
#define PAYLOAD_H
#include <stdbool.h>
#define LED_RED_PIN 16
#define LED_GREEN_PIN 17
#define LED_YELLOW_PIN 18
/**
* @brief Initialize GPIO16 (Red failure LED) and GPIO17 (Green success LED).
*
* @param None.
* @return None.
*/
void init_payload(void);
/**
* @brief Update tri-color GNSS status LEDs from fix state and satellites.
*
* Red (GP16) = no satellites in view; Yellow (GP18) = satellites in view
* while acquiring; Green (GP17) = active 3D fix.
*
* @param fix True if an active 3D GPS fix is held.
* @param siv Number of satellites currently in view.
* @return None.
*/
void set_gnss_leds(bool fix, int siv);
/**
* @brief Energize payload latch and illuminate both LEDs at target coordinates.
*
* @param None.
* @return None.
*/
void release_payload(void);
#endif // PAYLOAD_H
+67
View File
@@ -0,0 +1,67 @@
// 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: propeller.h
// Desc: Declares SG90 servo PWM mock propeller interface on GPIO6.
// Created: 2026
#ifndef PROPELLER_H
#define PROPELLER_H
#define PROPELLER_PIN 6
/**
* @brief Initialize 50 Hz PWM on GPIO6 for SG90 mock propeller blade.
*
* @param None.
* @return None.
*/
void init_propeller(void);
/**
* @brief Advance mock propeller blade oscillation during flight.
*
* @param None.
* @return None.
*/
void propeller_spin(void);
/**
* @brief Halt mock propeller blade oscillation upon target arrival.
*
* @param None.
* @return None.
*/
void propeller_stop(void);
/**
* @brief Point the servo at the compass bearing toward the target.
*
* @param deg Bearing in degrees from the current position to the target.
* @return None.
*/
void propeller_set_bearing(double deg);
#endif // PROPELLER_H
+121
View File
@@ -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})
+411
View File
@@ -0,0 +1,411 @@
#!/usr/bin/env python3
# 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: decode_coordinates.py
# Desc: Decode and patch RP2350 micro-UAV navigation coordinates.
# Created: 2026
"""
Decode and patch RP2350 micro-UAV navigation coordinates.
Analyzes raw firmware images for IEEE 754 64-bit double-precision floating
point coordinates. Scans target flight vectors and patches binaries with
safe disposal coordinates in the Atlantic Ocean.
"""
import math
import struct
import sys
from pathlib import Path
FLASH_BASE = 0x10000000
ORIGIN_LAT = 38.840280
ORIGIN_LON = -77.428890
ATLANTIC_LAT = 37.000000
ATLANTIC_LON = -74.000000
def _haversine_calc(phi1: float, phi2: float,
dphi: float, dlam: float) -> float:
"""
Compute central angle using haversine formula.
Parameters
----------
phi1 : float
Origin latitude in radians.
phi2 : float
Target latitude in radians.
dphi : float
Latitude difference in radians.
dlam : float
Longitude difference in radians.
Returns
-------
float
Central angular distance in radians.
"""
s_phi = math.sin(dphi / 2.0) ** 2
s_lam = math.sin(dlam / 2.0) ** 2
a = s_phi + math.cos(phi1) * math.cos(phi2) * s_lam
return 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
def haversine(lat1: float, lon1: float,
lat2: float, lon2: float) -> tuple[float, float]:
"""
Compute Great-Circle distance and azimuth bearing.
Parameters
----------
lat1 : float
Origin latitude in degrees.
lon1 : float
Origin longitude in degrees.
lat2 : float
Destination latitude in degrees.
lon2 : float
Destination longitude in degrees.
Returns
-------
tuple[float, float]
Distance in statute miles and bearing in degrees.
"""
p1, p2 = math.radians(lat1), math.radians(lat2)
dl = math.radians(lon2 - lon1)
dist = 6371.0 * _haversine_calc(p1, p2, math.radians(lat2 - lat1), dl)
y = math.sin(dl) * math.cos(p2)
term = math.sin(p1) * math.cos(p2) * math.cos(dl)
x = math.cos(p1) * math.sin(p2) - term
bearing = (math.degrees(math.atan2(y, x)) + 360.0) % 360.0
return dist * 0.621371, bearing
def _is_coord(val: float) -> bool:
"""
Verify if float falls within target geographic bounds.
Parameters
----------
val : float
Candidate double-precision value.
Returns
-------
bool
True if value is a valid latitude or longitude.
"""
if math.isnan(val) or math.isinf(val):
return False
in_lat = 35.0 <= val <= 41.0
in_lon = -79.0 <= val <= -72.0
return in_lat or in_lon
def _parse_chunk(data: bytes, off: int) -> dict | None:
"""
Extract and validate one 8-byte candidate float.
Parameters
----------
data : bytes
Firmware image buffer.
off : int
Byte offset inside image buffer.
Returns
-------
dict | None
Parsed coordinate record or None.
"""
chunk = data[off:off + 8]
val = struct.unpack("<d", chunk)[0]
if not _is_coord(val):
return None
hex_str = " ".join(f"{b:02x}" for b in chunk)
u64 = struct.unpack("<Q", chunk)[0]
kind = "LATITUDE" if val > 0.0 else "LONGITUDE"
return {"off": off, "addr": FLASH_BASE + off, "val": val,
"hex": hex_str, "u64": u64, "kind": kind}
def scan_coordinates(data: bytes) -> list[dict]:
"""
Scan firmware buffer for double-precision coordinates.
Parameters
----------
data : bytes
Firmware image buffer.
Returns
-------
list[dict]
List of candidate coordinate records.
"""
found = []
limit = len(data) - 8
for off in range(0, limit, 4):
item = _parse_chunk(data, off)
if item is not None:
found.append(item)
return found
def _print_banner(path: Path) -> None:
"""
Print operation heading and recovery origin.
Parameters
----------
path : pathlib.Path
Target firmware path.
Returns
-------
None
"""
print("=" * 67)
print(" OPERATION DARK VECTOR // FORENSIC COORDINATE TOOL")
print(" GMU Rapid Hardware Exploitation Laboratory - Fairfax, VA")
print("=" * 67)
print(f"[*] Target Binary: {path.name} ({path.stat().st_size:,} bytes)")
print(f"[*] Recovery Origin: Centreville, VA "
f"({ORIGIN_LAT:.6f}, {ORIGIN_LON:.6f})")
print("-" * 67)
def _print_candidate(c: dict) -> None:
"""
Print formatted candidate coordinate entry.
Parameters
----------
c : dict
Candidate coordinate record.
Returns
-------
None
"""
print(f" [{c['kind']:9s}] Value: {c['val']:12.6f} | "
f"Addr: 0x{c['addr']:08x} (Offset: 0x{c['off']:04x})")
print(f" Hex: {c['hex']} | uint64: 0x{c['u64']:016x}")
def _cardinal_bearing(brg: float) -> str:
"""Return compass direction string for given bearing."""
dirs = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
idx = int((brg + 11.25) / 22.5) % 16
return dirs[idx]
def _print_summary(lat: float, lon: float, mi: float, brg: float) -> None:
"""
Print mission tactical assessment summary.
Parameters
----------
lat : float
Decoded target latitude.
lon : float
Decoded target longitude.
mi : float
Distance in statute miles.
brg : float
Initial bearing in degrees.
Returns
-------
None
"""
card = _cardinal_bearing(brg)
print("\n" + "=" * 67)
print(" TACTICAL MISSION PROFILE DECODED")
print("=" * 67)
print(f" Target Latitude: {lat:.6f} deg N")
print(f" Target Longitude: {lon:.6f} deg W")
print(f" Distance from Origin: {mi:.2f} miles ({mi * 1.60934:.2f} km)")
print(f" Flight Vector Bearing: {brg:.1f} deg ({card})")
print("=" * 67)
def _patch_bytes(data: bytes, old_lat: float, old_lon: float) -> bytes:
"""
Replace target coordinates with safe Atlantic Ocean coordinates.
Parameters
----------
data : bytes
Original firmware bytes.
old_lat : float
Original target latitude.
old_lon : float
Original target longitude.
Returns
-------
bytes
Patched firmware byte buffer.
"""
src_lat = struct.pack("<d", old_lat)
src_lon = struct.pack("<d", old_lon)
dst_lat = struct.pack("<d", ATLANTIC_LAT)
dst_lon = struct.pack("<d", ATLANTIC_LON)
buf = data.replace(src_lat, dst_lat)
return buf.replace(src_lon, dst_lon)
def _print_patch_info(out_name: str, mi: float, brg: float) -> None:
"""
Display confirmation of applied firmware patch.
Parameters
----------
out_name : str
Patched output file name.
mi : float
Distance to safe disposal zone.
brg : float
Azimuth bearing to disposal zone.
Returns
-------
None
"""
print(f"\n[+] Patched firmware written to: {out_name}")
print("[+] Overwrote waypoint -> Atlantic Ocean Disposal Zone:")
print(f" Safe Latitude: {ATLANTIC_LAT:.6f} deg N")
print(f" Safe Longitude: {ATLANTIC_LON:.6f} deg W")
print(f" Offshore Distance: {mi:.2f} miles (Bearing: {brg:.1f} deg)")
def patch_firmware(target: Path, out_path: Path,
lat: float, lon: float) -> None:
"""
Patch firmware with safe Atlantic Ocean disposal waypoint.
Parameters
----------
target : pathlib.Path
Source binary path.
out_path : pathlib.Path
Destination patched binary path.
lat : float
Current target latitude.
lon : float
Current target longitude.
Returns
-------
None
"""
raw = target.read_bytes()
patched = _patch_bytes(raw, lat, lon)
out_path.write_bytes(patched)
mi, brg = haversine(ORIGIN_LAT, ORIGIN_LON, ATLANTIC_LAT, ATLANTIC_LON)
_print_patch_info(out_path.name, mi, brg)
def _evaluate(items: list[dict], target: Path, do_patch: bool) -> None:
"""
Display results and execute patch if requested.
Parameters
----------
items : list[dict]
Found coordinate records.
target : pathlib.Path
Target binary path.
do_patch : bool
Flag indicating if patch should be applied.
Returns
-------
None
"""
lats = [c for c in items if c["kind"] == "LATITUDE"]
lons = [c for c in items if c["kind"] == "LONGITUDE"]
if not (lats and lons):
return
t_lat, t_lon = lats[-1]["val"], lons[-1]["val"]
mi, brg = haversine(ORIGIN_LAT, ORIGIN_LON, t_lat, t_lon)
_print_summary(t_lat, t_lon, mi, brg)
if do_patch:
out = target.parent / f"{target.stem}_patched.bin"
patch_firmware(target, out, t_lat, t_lon)
def _parse_args(args: list[str]) -> tuple[Path, bool]:
"""
Parse command line arguments for target path and patch flag.
Parameters
----------
args : list[str]
Command line arguments.
Returns
-------
tuple[pathlib.Path, bool]
Target binary path and patch flag.
"""
do_patch = "--patch" in args
paths = [p for p in args if not p.startswith("--")]
target = Path(paths[0]) if paths else Path("0x0011a_cb.bin")
return target, do_patch
def main() -> int:
"""
Execute firmware coordinate extraction and optional patching.
Parameters
----------
None
Returns
-------
int
Zero on success, non-zero on failure.
"""
target, do_patch = _parse_args(sys.argv[1:])
if not target.exists():
print(f"[-] Error: '{target}' not found.")
return 1
_print_banner(target)
items = scan_coordinates(target.read_bytes())
for item in items:
_print_candidate(item)
_evaluate(items, target, do_patch)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
# 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: float_hex_converter.py
# Desc: Convert and explain IEEE 754 float/hex operations step-by-step.
# Created: 2026
"""Convert and explain IEEE 754 float/hex operations step-by-step."""
import argparse
import struct
import sys
def _exp_str(e_val: int, bias: int, is_64: bool) -> str:
"""
Get accurate true exponent string accounting for IEEE 754 edge cases.
Parameters
----------
e_val : int
The stored exponent value.
bias : int
The exponent bias (127 or 1023).
is_64 : bool
True if 64-bit precision.
Returns
-------
str
The formatted true exponent explanation string.
"""
if e_val == 0:
return f"0 (Zero/Subnormal, True Exp: {1 - bias})"
if e_val == (2047 if is_64 else 255):
return f"{e_val} (Inf/NaN flag)"
return f"{e_val} - {bias} = {e_val - bias}"
def _print_encode_steps(val: float, b: int) -> None:
"""
Print the math steps for encoding a float to hex.
Parameters
----------
val : float
The float value to encode.
b : int
The bit size (32 or 64).
Returns
-------
None
"""
f1, f2, bias = ("<Q", "<d", 1023) if b == 64 else ("<I", "<f", 127)
bstr = f"{struct.unpack(f1, struct.pack(f2, val))[0]:0{b}b}"
s, e, m = (
bstr[0],
bstr[1 : 1 + (11 if b == 64 else 8)],
bstr[1 + (11 if b == 64 else 8) :],
)
hex_val = f"0x{int(bstr, 2):0{b//4}X}"
print(f"\n[ENCODE {val} to {b}-bit]\n1. Sign: {s} (0=Pos, 1=Neg)")
print(f"2. Exp: {_exp_str(int(e, 2), bias, b == 64)}\n3. Mantissa: {m}")
print(f"4. Full: {s} {e} {m}\n5. Hex: {hex_val}")
if b == 64:
raw_hex = f"{int(bstr, 2):016X}"
r3 = f"0x{raw_hex[:8]}"
r2 = f"0x{raw_hex[8:]}"
print(f"6. ARM Regs: r3 (high) = {r3}, r2 (low) = {r2} (e.g. in printf)")
else:
print(f"6. ARM Reg: Single 32-bit register ({hex_val})")
def _print_decode_steps(hex_str: str) -> None:
"""
Print the math steps for decoding a hex string to float.
Parameters
----------
hex_str : str
The hex string to decode.
Returns
-------
None
"""
c = hex_str.lower()
if c.startswith("0x"):
c = c[2:]
b, f1, f2, bias = (64, "<Q", "<d", 1023) if len(c) > 8 else (32, "<I", "<f", 127)
bstr = f"{int(c, 16):0{b}b}"
s, e, m = (
bstr[0],
bstr[1 : 1 + (11 if b == 64 else 8)],
bstr[1 + (11 if b == 64 else 8) :],
)
print(f"\n[DECODE 0x{c.zfill(b//4).upper()} ({b}-bit)]\n1. Binary: {s} {e} {m}")
print(f"2. Sign: {s}\n3. Exp: {_exp_str(int(e, 2), bias, b == 64)}")
print(f"4. Value: {struct.unpack(f2, struct.pack(f1, int(c, 16)))[0]}")
if b == 64:
raw_hex = c.zfill(16).upper()
r3 = f"0x{raw_hex[:8]}"
r2 = f"0x{raw_hex[8:]}"
print(f"5. ARM Regs: r3 (high) = {r3}, r2 (low) = {r2} (e.g. in printf)")
else:
print(f"5. ARM Reg: Single 32-bit register (0x{c.zfill(8).upper()})")
def _is_hex(s: str) -> bool:
"""
Check if a string is a hexadecimal representation.
Parameters
----------
s : str
Candidate string, with or without a 0x prefix.
Returns
-------
bool
True if every character is a hexadecimal digit.
"""
cleaned = s.lower()
if cleaned.startswith("0x"):
cleaned = cleaned[2:]
if not cleaned:
return False
return all(c in "0123456789abcdef" for c in cleaned)
def _process_conversion(val_str: str) -> None:
"""
Execute the conversion and print the output.
Parameters
----------
val_str : str
The raw input string to process.
Returns
-------
None
"""
cleaned = val_str.strip()
if cleaned.lower().startswith("0x") or (len(cleaned) >= 8 and _is_hex(cleaned)):
_print_decode_steps(cleaned)
else:
val = float(cleaned)
_print_encode_steps(val, 32)
_print_encode_steps(val, 64)
def main() -> int:
"""
Execute the conversion pipeline based on CLI arguments.
Parameters
----------
None
Returns
-------
int
Zero on successful conversion, otherwise non-zero.
"""
parser = argparse.ArgumentParser(description="Float/Hex step converter.")
parser.add_argument("val", help="Hex (0x...) or Float value to convert.")
args = parser.parse_args()
try:
_process_conversion(args.val)
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# 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: lora_console.py
# Desc: Interactive REYAX RYLR998 terminal for the FT232RL ground station.
# Created: 2026
"""
Interactive LoRa terminal for the REYAX RYLR998 ground station.
Sends AT commands as complete bursts terminated with a carriage return and line
feed to avoid the RYLR998 inter-character timeout error, and prints incoming
packets from the airborne node as they arrive.
"""
import argparse
import sys
import threading
import time
try:
import serial
except ImportError:
serial = None
def _reader_thread(ser: "serial.Serial") -> None:
"""
Continuously read and display incoming packets from the radio.
Parameters
----------
ser : serial.Serial
Open serial connection to the ground RYLR998.
Returns
-------
None
"""
while True:
try:
line = ser.readline().decode("utf-8", errors="ignore").strip()
if line:
print(f"\n[LORA RX] {line}\n> ", end="", flush=True)
except Exception:
break
def _parse_args() -> argparse.Namespace:
"""
Parse command line arguments for the LoRa console.
Parameters
----------
None
Returns
-------
argparse.Namespace
Parsed arguments with the serial port and baud rate.
"""
parser = argparse.ArgumentParser(description="REYAX RYLR998 console")
parser.add_argument("--port", default="/dev/cu.usbserial-A50285BI")
parser.add_argument("--baud", type=int, default=115200)
return parser.parse_args()
def main() -> None:
"""
Open the ground station radio and forward operator input.
Parameters
----------
None
Returns
-------
None
"""
if serial is None:
sys.exit("pyserial required: pip install pyserial")
args = _parse_args()
print(f"[*] Opening REYAX RYLR998 on {args.port} @ {args.baud}...")
try:
ser = serial.Serial(args.port, args.baud, timeout=0.5)
except Exception as e:
sys.exit(f"[-] Failed to open {args.port}: {e}")
thread = threading.Thread(target=_reader_thread, args=(ser,), daemon=True)
thread.start()
time.sleep(0.1)
ser.write(b"AT\r\n")
print("[+] Connected. Type AT commands (e.g. AT, AT+BAND?, AT+NETWORKID?).")
print("[+] Incoming airborne packets print as [LORA RX] +RCV=...")
print("[+] Press Ctrl-C or Ctrl-D to exit.\n")
try:
while True:
cmd = input("> ").strip()
if not cmd:
continue
ser.write(cmd.encode("utf-8") + b"\r\n")
except (KeyboardInterrupt, EOFError):
print("\n[*] Exiting LoRa console.")
ser.close()
if __name__ == "__main__":
main()
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
# 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: randomize_build.py
# Desc: Builds a per-student 0x0011a_cb image with an AES-encrypted target.
# Created: 2026
"""
Per-student randomized CTF build.
Jitters the target waypoint, AES-128-ECB encrypts it under a per-build key, and
writes include/ctf_target.h so the firmware rebuilds it at boot. Every image
carries a different ciphertext and the plaintext target exists nowhere in flash.
An offline answer key or a memorized value is useless; the waypoint can only be
recovered from the artifact and confirmed on hardware.
"""
import argparse
import json
import pathlib
import random
import struct
import subprocess
import sys
BASE_LAT = 38.881940
BASE_LON = -77.450280
JITTER_DEG = 0.01
UF2_FAMILY = "0xe48bff59"
FLASH_BASE = "0x10000000"
def _parse_args() -> argparse.Namespace:
"""
Parse command line arguments for the randomized build.
Parameters
----------
None
Returns
-------
argparse.Namespace
Parsed arguments with seed, build dir, student id, and uf2 flag.
"""
here = pathlib.Path(__file__).resolve().parent
parser = argparse.ArgumentParser(description="Randomized CTF build")
parser.add_argument("--seed", type=int, default=None)
parser.add_argument("--build-dir", default=str(here.parent / "build-ctf"))
parser.add_argument("--student-id", default="student")
parser.add_argument("--uf2", action="store_true")
return parser.parse_args()
def _make_target(seed: int) -> tuple[float, float]:
"""
Generate a jittered target waypoint around the base coordinates.
Parameters
----------
seed : int
Deterministic seed for the jitter.
Returns
-------
tuple[float, float]
Jittered latitude and longitude, rounded to six decimals.
"""
rng = random.Random(seed)
lat = round(BASE_LAT + rng.uniform(-JITTER_DEG, JITTER_DEG), 6)
lon = round(BASE_LON + rng.uniform(-JITTER_DEG, JITTER_DEG), 6)
return lat, lon
def _make_key(seed: int) -> bytes:
"""
Derive a deterministic 16-byte AES key for the build.
Parameters
----------
seed : int
Build seed from which the key is derived.
Returns
-------
bytes
Sixteen byte AES-128 key.
"""
krng = random.Random(seed ^ 0x5EED)
return bytes(krng.randrange(256) for _ in range(16))
def _aes_ecb(plain: bytes, key: bytes) -> bytes:
"""
Encrypt one block with AES-128-ECB.
Parameters
----------
plain : bytes
Sixteen byte plaintext block.
key : bytes
Sixteen byte AES key.
Returns
-------
bytes
Sixteen byte ciphertext block.
"""
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
enc = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend()).encryptor()
return enc.update(plain) + enc.finalize()
def _write_header(path: pathlib.Path, key: bytes, ct: bytes) -> None:
"""
Write the AES key and ciphertext header consumed by the firmware.
Parameters
----------
path : pathlib.Path
Destination header path.
key : bytes
Sixteen byte AES key.
ct : bytes
Sixteen byte ciphertext block.
Returns
-------
None
"""
path.write_text(
"#ifndef CTF_TARGET_H\n#define CTF_TARGET_H\n\n#include <stdint.h>\n\n"
"#define CTF_AES_KEY { %s }\n"
"#define CTF_TARGET_CT { %s }\n\n"
"#endif // CTF_TARGET_H\n"
% (", ".join(f"0x{b:02X}" for b in key), ", ".join(f"0x{b:02X}" for b in ct)))
def _run(cmd: list[str], cwd: pathlib.Path) -> None:
"""
Run an external command and raise on failure.
Parameters
----------
cmd : list[str]
Command and arguments to execute.
cwd : pathlib.Path
Working directory for the command.
Returns
-------
None
"""
subprocess.run(cmd, cwd=str(cwd), check=True)
def main() -> int:
"""
Build a per-student image with an AES-encrypted, randomized target.
Parameters
----------
None
Returns
-------
int
Zero on success.
"""
args = _parse_args()
root = pathlib.Path(__file__).resolve().parent.parent
seed = args.seed if args.seed is not None else random.randrange(2**31)
lat, lon = _make_target(seed)
key = _make_key(seed)
pt = struct.pack("<d", lat) + struct.pack("<d", lon)
ct = _aes_ecb(pt, key)
_write_header(root / "include" / "ctf_target.h", key, ct)
build = pathlib.Path(args.build_dir).resolve()
_run(["cmake", "-S", str(root), "-B", str(build)], root)
_run(["cmake", "--build", str(build)], root)
image = build / "0x0011a_cb.bin"
if args.uf2:
out = build / f"0x0011a_cb_{args.student_id}.uf2"
_run([sys.executable, str(root / "uf2conv.py"), str(image),
"-f", UF2_FAMILY, "-b", FLASH_BASE, "-c", "-o", str(out)], root)
key_out = {"student_id": args.student_id, "seed": seed,
"target_lat": lat, "target_lon": lon,
"aes_key_hex": key.hex(), "ciphertext_hex": ct.hex(),
"image": str(image)}
keydir = root / "scratch"
keydir.mkdir(exist_ok=True)
keyfile = keydir / f"answer_{args.student_id}.json"
keyfile.write_text(json.dumps(key_out, indent=2) + "\n")
print(f"[+] student_id : {args.student_id}")
print(f"[+] TARGET_LAT : {lat}")
print(f"[+] TARGET_LON : {lon}")
print(f"[+] AES key : {key.hex()}")
print(f"[+] ciphertext : {ct.hex()}")
print(f"[+] image : {image}")
print(f"[+] answer key : {keyfile} (INSTRUCTOR ONLY, do not ship)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+302
View File
@@ -0,0 +1,302 @@
#!/usr/bin/env python3
# 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: telemetry_monitor.py
# Desc: Real-time telemetry monitor for Operation Dark Vector.
# Created: 2026
"""
Real-time telemetry monitor for Operation Dark Vector.
Reads live avionics and GPS telemetry from the FT232RL USB-to-UART ground
station bridge, parses coordinates and distance, and prints a mission status
dashboard to the terminal.
"""
import argparse
import re
import sys
import time
try:
import serial
except ImportError:
serial = None
def _format_coord(val: float, pos_c: str, neg_c: str) -> str:
"""
Format a decimal coordinate with cardinal direction.
Parameters
----------
val : float
Coordinate value in degrees.
pos_c : str
Cardinal letter for positive values.
neg_c : str
Cardinal letter for negative values.
Returns
-------
str
Formatted coordinate string.
"""
card = pos_c if val >= 0.0 else neg_c
return f"{abs(val):.6f} deg {card}"
def _format_pos(lat: float, lon: float) -> str:
"""
Format combined latitude and longitude string.
Parameters
----------
lat : float
Latitude coordinate.
lon : float
Longitude coordinate.
Returns
-------
str
Formatted dual coordinate string.
"""
lat_s = _format_coord(lat, 'N', 'S')
lon_s = _format_coord(lon, 'E', 'W')
return f"{lat_s} {lon_s}"
def _format_hud_rows(
cur_lat: float,
cur_lon: float,
is_rel: bool,
has_lock: bool = False
) -> list[str]:
"""
Format HUD data rows.
Parameters
----------
cur_lat : float
Current UAV latitude.
cur_lon : float
Current UAV longitude.
is_rel : bool
Whether payload has released.
has_lock : bool
Whether active GNSS 3D lock has been acquired.
Returns
-------
list[str]
List of formatted box rows.
"""
if cur_lat == 0.0 and cur_lon == 0.0:
c_s = "0.000000 deg N 0.000000 deg E"
g_s = "SEARCHING SATELLITES"
m_s = "MOTOR STOPPED [WAITING FOR 3D LOCK]"
else:
c_s = _format_pos(cur_lat, cur_lon)
g_s = "ACTIVE 3D LOCK" if has_lock else "SEARCHING SATELLITES"
m_s = "ACTIVE PROPULSION [SERVO SPINNING]" if has_lock else "MOTOR STOPPED [WAITING FOR 3D LOCK]"
return [
f"| CURRENT POSITION : {c_s:<44} |",
f"| GNSS SUBSYSTEM : {g_s:<44} |",
f"| PROPULSION MOTOR : {m_s:<44} |"
]
def _print_box(title: str, link: str, rows: list[str]) -> None:
"""
Print framed ASCII box.
Parameters
----------
title : str
Box title line.
link : str
Sub-header line.
rows : list[str]
Body content rows.
Returns
-------
None
"""
hdr = f"+{'-' * 65}+"
print(hdr)
print(title)
print(link)
print(hdr)
for r in rows:
print(r)
print(hdr + "\n", flush=True)
def _print_hud(
cur_lat: float,
cur_lon: float,
is_released: bool,
has_lock: bool = False
) -> None:
"""
Display the 67-character telemetry mission HUD.
Parameters
----------
cur_lat : float
Current UAV latitude.
cur_lon : float
Current UAV longitude.
is_released : bool
Whether payload solenoid has been energized.
has_lock : bool
Whether active GNSS 3D lock has been acquired.
Returns
-------
None
"""
rows = _format_hud_rows(cur_lat, cur_lon, is_released, has_lock)
title = f"|{'DARK VECTOR TELEMETRY CONSOLE':^65}|"
status = f"{'STATUS: ONLINE':>20}"
link = f"| LINK: FT232RL / RYLR998 LORA GROUND STATION{status} |"
_print_box(title, link, rows)
def _run_demo() -> None:
"""
Execute simulation of drone telemetry stream.
Parameters
----------
None
Returns
-------
None
"""
print("[*] Running simulated ground station telemetry stream...\n")
_print_hud(0.0, 0.0, False, False)
time.sleep(1.0)
_print_hud(38.840280, -77.428890, False, True)
time.sleep(1.0)
_print_hud(38.861110, -77.439585, False, True)
time.sleep(1.0)
_print_hud(38.881940, -77.450280, True, True)
def _process_line(
line: str,
coords: dict[str, any]
) -> bool:
"""
Parse a single line of serial telemetry using regex.
Parameters
----------
line : str
Raw serial string.
coords : dict[str, any]
State dictionary of coordinates.
Returns
-------
bool
True if telemetry data was updated, False otherwise.
"""
updated = False
m_cur = re.search(r"CURRENT LAT:\s*([-+]?\d*\.?\d+).*?LON:\s*([-+]?\d*\.?\d+)", line)
if m_cur:
coords["cur_lat"] = float(m_cur.group(1))
coords["cur_lon"] = float(m_cur.group(2))
coords["has_lock"] = True
updated = True
if "PAYLOAD RELEASED" in line:
coords["released"] = 1.0
updated = True
return updated
def _monitor_serial(port: str, baud: int) -> None:
"""
Monitor serial stream from FT232RL ground station.
Parameters
----------
port : str
Serial device path.
baud : int
Baud rate.
Returns
-------
None
"""
if serial is None:
sys.exit("pyserial required: pip install pyserial")
coords = {
"cur_lat": 0.0,
"cur_lon": 0.0,
"released": 0.0,
"has_lock": False
}
with serial.Serial(port, baud, timeout=1.0) as ser:
while True:
raw = ser.readline().decode("utf-8", errors="ignore").strip()
if raw:
_process_line(raw, coords)
rel = coords["released"] > 0.5
_print_hud(coords["cur_lat"], coords["cur_lon"], rel, coords["has_lock"])
def main() -> None:
"""
Parse arguments and start telemetry monitor.
Parameters
----------
None
Returns
-------
None
"""
parser = argparse.ArgumentParser(description="Dark Vector Telemetry")
parser.add_argument("--port", default="/dev/tty.usbserial-0001")
parser.add_argument("--baud", type=int, default=115200)
parser.add_argument("--demo", action="store_true")
args = parser.parse_args()
if args.demo:
_run_demo()
return
_monitor_serial(args.port, args.baud)
if __name__ == "__main__":
main()
+119
View File
@@ -0,0 +1,119 @@
// MIT License
//
// Copyright (c) 2026 Kevin Thomas
//
// File: aes.c
// Desc: Minimal AES-128-ECB block decryption for the CTF waypoint.
// Created: 2026
#include "aes.h"
#include <string.h>
#include <stdint.h>
static const uint8_t sbox[256] = {
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
};
static const uint8_t rsbox[256] = {
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
};
static const uint8_t Rcon[11] = {0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1B,0x36};
static uint8_t xtime(uint8_t x) { return (uint8_t)((x << 1) ^ (((x >> 7) & 1) * 0x1B)); }
static uint8_t mul(uint8_t a, uint8_t b) {
uint8_t p = 0;
for (int i = 0; i < 8; i++) {
if (b & 1) p ^= a;
uint8_t hi = a & 0x80; a <<= 1;
if (hi) a ^= 0x1B;
b >>= 1;
}
return p;
}
static void key_expansion(const uint8_t *key, uint8_t *rk) {
memcpy(rk, key, 16);
for (int i = 4; i < 44; i++) {
uint8_t t[4];
memcpy(t, &rk[(i - 1) * 4], 4);
if (i % 4 == 0) {
uint8_t tmp = t[0];
t[0] = (uint8_t)(sbox[t[1]] ^ Rcon[i / 4]);
t[1] = sbox[t[2]];
t[2] = sbox[t[3]];
t[3] = sbox[tmp];
}
for (int j = 0; j < 4; j++) rk[i * 4 + j] = rk[(i - 4) * 4 + j] ^ t[j];
}
}
static void add_round_key(uint8_t r, uint8_t *s, const uint8_t *rk) {
for (int i = 0; i < 16; i++) s[i] ^= rk[r * 16 + i];
}
static void inv_sub_bytes(uint8_t *s) { for (int i = 0; i < 16; i++) s[i] = rsbox[s[i]]; }
static void inv_shift_rows(uint8_t *s) {
uint8_t t;
t = s[13]; s[13] = s[9]; s[9] = s[5]; s[5] = s[1]; s[1] = t;
t = s[2]; s[2] = s[10]; s[10] = t; t = s[6]; s[6] = s[14]; s[14] = t;
t = s[3]; s[3] = s[7]; s[7] = s[11]; s[11] = s[15]; s[15] = t;
}
static void inv_mix_columns(uint8_t *s) {
for (int i = 0; i < 4; i++) {
uint8_t a = s[i * 4], b = s[i * 4 + 1], c = s[i * 4 + 2], d = s[i * 4 + 3];
s[i * 4] = (uint8_t)(mul(a, 14) ^ mul(b, 11) ^ mul(c, 13) ^ mul(d, 9));
s[i * 4 + 1] = (uint8_t)(mul(a, 9) ^ mul(b, 14) ^ mul(c, 11) ^ mul(d, 13));
s[i * 4 + 2] = (uint8_t)(mul(a, 13) ^ mul(b, 9) ^ mul(c, 14) ^ mul(d, 11));
s[i * 4 + 3] = (uint8_t)(mul(a, 11) ^ mul(b, 13) ^ mul(c, 9) ^ mul(d, 14));
}
}
void aes128_ecb_decrypt_block(const uint8_t in[16], const uint8_t key[16], uint8_t out[16]) {
uint8_t rk[176];
key_expansion(key, rk);
memcpy(out, in, 16);
add_round_key(10, out, rk);
for (int r = 9; r > 0; r--) {
inv_shift_rows(out);
inv_sub_bytes(out);
add_round_key(r, out, rk);
inv_mix_columns(out);
}
inv_shift_rows(out);
inv_sub_bytes(out);
add_round_key(0, out, rk);
}
+172
View File
@@ -0,0 +1,172 @@
// 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: gps.c
// Desc: Implements PIO UART GPS receiver and NMEA coordinate parsing.
// Created: 2026
#include "gps.h"
#include "uart_rx.pio.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int gps_siv = 0;
static int gps_cno = 0;
void init_gps_pio(void)
{
uint offset = pio_add_program(GPS_PIO, &uart_rx_program);
uart_rx_program_init(GPS_PIO, GPS_SM, offset, GPS_PIN, GPS_BAUD);
}
/**
* @brief Convert NMEA ddmm.mmmm coordinate to decimal degrees.
*
* @param str NMEA coordinate string.
* @param dir Cardinal direction character ('N', 'S', 'E', 'W').
* @return double Decimal degree coordinate.
*/
static double parse_nmea_coord(const char *str, char dir)
{
double raw = atof(str);
int deg = (int)(raw / 100.0);
double dec = (double)deg + ((raw - (deg * 100.0)) / 60.0);
return ((dir == 'S') || (dir == 'W')) ? -dec : dec;
}
/**
* @brief Find pointer to n-th comma-separated field in NMEA string.
*
* @param str NMEA sentence string.
* @param field_idx Index of field to locate.
* @return const char* Pointer to field start or NULL.
*/
static const char *get_nmea_field(const char *str, int field_idx)
{
while ((str != NULL) && (*str != '\0') && (field_idx > 0)) {
if (*str++ == ',') {
field_idx--;
}
}
return (field_idx == 0) ? str : NULL;
}
/**
* @brief Parse NMEA RMC sentence for valid coordinates.
*
* @param line NMEA sentence buffer.
* @param lat Pointer to store parsed latitude.
* @param lon Pointer to store parsed longitude.
* @return None.
*/
static bool parse_rmc(const char *line, double *lat, double *lon)
{
const char *st = get_nmea_field(line, 2), *la = get_nmea_field(line, 3);
const char *lo = get_nmea_field(line, 5);
if (!st || *st != 'A' || !la || *la == ',' || !lo || *lo == ',') return false;
*lat = parse_nmea_coord(la, *get_nmea_field(line, 4));
*lon = parse_nmea_coord(lo, *get_nmea_field(line, 6));
return (*lat != 0.0) && (*lon != 0.0);
}
/**
* @brief Parse NMEA GSV sentence for satellites-in-view and best C/N0.
*
* @param line NMEA GSV sentence string.
* @return None.
*/
static void parse_gsv(const char *line)
{
const char *sv = get_nmea_field(line, 3), *msg = get_nmea_field(line, 2);
if (sv != NULL) gps_siv = atoi(sv);
if ((msg != NULL) && (*msg == '1')) gps_cno = 0;
for (int f = 7; f < 40; f += 4) {
const char *c = get_nmea_field(line, f);
if ((c == NULL) || (*c == '*') || (*c == '\0')) break;
if (atoi(c) > gps_cno) gps_cno = atoi(c);
}
}
/**
* @brief Test buffered line for valid NMEA RMC sentence.
*
* @param buf NMEA character buffer.
* @param idx Pointer to character index.
* @param lat Pointer to current latitude.
* @param lon Pointer to current longitude.
* @return bool True if valid 3D fix was parsed, false otherwise.
*/
static bool check_rmc_line(char *buf, int *idx, double *lat, double *lon)
{
buf[*idx] = '\0';
*idx = 0;
char *rmc = strstr(buf, "RMC"), *gsv = strstr(buf, "GSV");
if (gsv != NULL) parse_gsv(gsv);
return (rmc != NULL) ? parse_rmc(rmc, lat, lon) : false;
}
/**
* @brief Accumulate GPS character and trigger RMC parsing on newline.
*
* @param ch Received ASCII character.
* @param lat Pointer to current latitude.
* @param lon Pointer to current longitude.
* @return bool True if a valid active 3D fix was parsed, false otherwise.
*/
static bool process_gps_char(char ch, double *lat, double *lon)
{
static char buf[96];
static int idx = 0;
if (ch == '$') idx = 0;
if ((ch == '\n') || (ch == '\r'))
return check_rmc_line(buf, &idx, lat, lon);
if (idx < (int)(sizeof(buf) - 1))
buf[idx++] = ch;
return false;
}
static bool handle_gps_byte(double *lat, double *lon)
{
char ch = (char)(pio_sm_get(GPS_PIO, GPS_SM) >> 24);
return process_gps_char(ch, lat, lon);
}
bool poll_gps(double *lat, double *lon)
{
bool got_fix = false;
while (!pio_sm_is_rx_fifo_empty(GPS_PIO, GPS_SM))
got_fix |= handle_gps_byte(lat, lon);
return got_fix;
}
void gps_get_stats(int *siv, int *cno)
{
*siv = gps_siv;
*cno = gps_cno;
}
+144
View File
@@ -0,0 +1,144 @@
// 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: lcd.c
// Desc: Implements I2C HD44780 16x2 LCD display for live telemetry coordinates.
// Created: 2026
#include "lcd.h"
#include "pico/stdlib.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
#define PIN_RS 0x01
#define PIN_EN 0x04
#define BACKLIGHT 0x08
static uint8_t lcd_addr = 0x27;
static bool lcd_ready = false;
static void pcf_write(uint8_t d)
{
if (lcd_ready)
i2c_write_blocking(LCD_I2C_INST, lcd_addr, &d, 1, false);
}
static void pcf_pulse(uint8_t d)
{
pcf_write(d | PIN_EN);
sleep_us(1);
pcf_write(d & ~PIN_EN);
sleep_us(50);
}
static void lcd_write4(uint8_t n, uint8_t mode)
{
uint8_t d = (n & 0x0F) << 4;
d |= mode ? PIN_RS : 0;
d |= BACKLIGHT;
pcf_pulse(d);
}
static void lcd_send(uint8_t v, uint8_t mode)
{
lcd_write4((v >> 4) & 0x0F, mode);
lcd_write4(v & 0x0F, mode);
}
static void lcd_clear(void)
{
lcd_send(0x01, 0);
sleep_ms(2);
}
static void lcd_set_cursor(int row, int col)
{
uint8_t offset = (row == 0) ? 0x00 : 0x40;
lcd_send(0x80 | (col + offset), 0);
}
static void lcd_puts(const char *s)
{
while (*s)
lcd_send((uint8_t)*s++, 1);
}
static void lcd_reset_seq(void)
{
lcd_write4(0x03, 0); sleep_ms(5);
lcd_write4(0x03, 0); sleep_us(150);
lcd_write4(0x03, 0); sleep_us(150);
lcd_write4(0x02, 0); sleep_us(150);
}
static void lcd_cfg_seq(void)
{
lcd_send(0x28, 0);
lcd_send(0x0C, 0);
lcd_clear();
lcd_send(0x06, 0);
}
static bool detect_lcd(void)
{
uint8_t rx;
if (i2c_read_blocking(LCD_I2C_INST, 0x27, &rx, 1, false) >= 0)
return (lcd_addr = 0x27, true);
if (i2c_read_blocking(LCD_I2C_INST, 0x3F, &rx, 1, false) >= 0)
return (lcd_addr = 0x3F, true);
return false;
}
void init_lcd(void)
{
i2c_init(LCD_I2C_INST, LCD_BAUD);
gpio_set_function(LCD_SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(LCD_SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(LCD_SDA_PIN); gpio_pull_up(LCD_SCL_PIN);
if (!(lcd_ready = detect_lcd())) return;
lcd_reset_seq();
lcd_cfg_seq();
}
void lcd_show_coords(double lat, double lon)
{
if (!lcd_ready && !(lcd_ready = detect_lcd())) return;
char r1[17], r2[17];
snprintf(r1, sizeof(r1), "LAT: %9.6f %c", fabs(lat), (lat >= 0.0) ? 'N' : 'S');
snprintf(r2, sizeof(r2), "LON: %9.6f %c", fabs(lon), (lon >= 0.0) ? 'E' : 'W');
lcd_set_cursor(0, 0); lcd_puts(r1);
lcd_set_cursor(1, 0); lcd_puts(r2);
}
void lcd_show_gnss(int sats, int cno)
{
if (!lcd_ready && !(lcd_ready = detect_lcd())) return;
char r1[17], r2[17];
snprintf(r1, sizeof(r1), "SAT:%2d CNO:%2d ", sats, cno);
snprintf(r2, sizeof(r2), "%-16s", (sats > 0) ? "ACQUIRING..." : "NO SIGNAL");
lcd_set_cursor(0, 0); lcd_puts(r1);
lcd_set_cursor(1, 0); lcd_puts(r2);
}
+89
View File
@@ -0,0 +1,89 @@
// 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: lora.c
// Desc: Implements UART1 driver for REYAX RYLR998 LoRa transceiver.
// Created: 2026
#include "lora.h"
#include "hardware/gpio.h"
#include "pico/stdlib.h"
#include <stdio.h>
#include <string.h>
static void drain_lora_rx(void)
{
while (uart_is_readable(LORA_UART)) {
char c = (char)uart_getc(LORA_UART);
if (c >= 32 && c <= 126)
putchar(c);
}
}
static void send_at_cmd(const char *cmd)
{
uart_write_blocking(LORA_UART, (const uint8_t *)cmd, strlen(cmd));
sleep_ms(250);
drain_lora_rx();
}
static void configure_lora_rf(void)
{
sleep_ms(1500);
send_at_cmd("AT\r\n");
send_at_cmd("AT+NETWORKID=18\r\n");
send_at_cmd("AT+BAND=915000000\r\n");
send_at_cmd("AT+PARAMETER=9,7,1,12\r\n");
send_at_cmd("AT+ADDRESS=2\r\n");
}
void init_lora(void)
{
uart_init(LORA_UART, LORA_BAUD);
uart_set_translate_crlf(LORA_UART, false);
gpio_set_function(LORA_TX_PIN, GPIO_FUNC_UART);
gpio_set_function(LORA_RX_PIN, GPIO_FUNC_UART);
configure_lora_rf();
}
static char tx_buf[160];
static int tx_len = 0;
static int tx_idx = 0;
void lora_send(const char *msg)
{
int len = (int)strlen(msg);
while ((len > 0) && ((msg[len - 1] == '\r') || (msg[len - 1] == '\n')))
len--;
tx_len = snprintf(tx_buf, sizeof(tx_buf), "AT+SEND=0,%d,%.*s\r\n", len, len, msg);
tx_idx = 0;
}
void lora_tick(void)
{
while ((tx_idx < tx_len) && uart_is_writable(LORA_UART))
uart_putc_raw(LORA_UART, (uint8_t)tx_buf[tx_idx++]);
drain_lora_rx();
}
+108
View File
@@ -0,0 +1,108 @@
// 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 autonomous micro-UAV guidance firmware.
// Created: 2026
#include "gps.h"
#include "lora.h"
#include "payload.h"
#include "propeller.h"
#include "navigation.h"
#include "lcd.h"
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include <stdio.h>
/**
* @brief Initialize all board peripherals, communications, and actuators.
*
* @param None.
* @return None.
*/
static void init_all(void)
{
stdio_init_all();
init_navigation();
init_payload();
init_lora();
init_gps_pio();
init_propeller();
init_lcd();
}
/**
* @brief Continually drain GPS PIO FIFO over 1-second flight tick.
*
* @param cur_lat Pointer to current latitude.
* @param cur_lon Pointer to current longitude.
* @return bool True if active 3D lock was parsed, false otherwise.
*/
static bool update_position(double *cur_lat, double *cur_lon)
{
bool got_fix = false;
for (int i = 0; i < 200; i++, sleep_ms(5)) {
got_fix |= poll_gps(cur_lat, cur_lon);
lora_tick();
}
return got_fix;
}
static void step_mission(double *cur_lat, double *cur_lon)
{
int siv = 0, cno = 0;
static int hold = 0;
bool fix = update_position(cur_lat, cur_lon);
gps_get_stats(&siv, &cno);
hold = fix ? 3 : ((hold > 0) ? (hold - 1) : 0);
bool have = fix || (hold > 0);
set_gnss_leds(have, siv);
if (have) {
lcd_show_coords(*cur_lat, *cur_lon);
navigate_to_target(*cur_lat, *cur_lon);
} else {
lcd_show_gnss(siv, cno);
propeller_stop();
send_telemetry(*cur_lat, *cur_lon);
}
}
/**
* @brief Autonomous micro-UAV firmware execution loop.
*
* @param None.
* @return int Standard exit code (never reached in embedded firmware).
*/
int main(void)
{
double cur_lat = ORIGIN_LAT, cur_lon = ORIGIN_LON;
init_all();
while (true)
step_mission(&cur_lat, &cur_lon);
return 0;
}
+106
View File
@@ -0,0 +1,106 @@
// 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: navigation.c
// Desc: Implements waypoint navigation, dead-reckoning, and telemetry dispatch.
// Created: 2026
#include "navigation.h"
#include "ctf_target.h"
#include "aes.h"
#include "lora.h"
#include "payload.h"
#include "propeller.h"
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <stdint.h>
double TARGET_LAT = 0.0;
double TARGET_LON = 0.0;
const double ORIGIN_LAT = 38.840280;
const double ORIGIN_LON = -77.428890;
void init_navigation(void)
{
uint8_t pt[16];
const uint8_t key[16] = CTF_AES_KEY;
const uint8_t ct[16] = CTF_TARGET_CT;
aes128_ecb_decrypt_block(ct, key, pt);
memcpy(&TARGET_LAT, pt, sizeof(double));
memcpy(&TARGET_LON, pt + 8, sizeof(double));
}
void send_telemetry(double cur_lat, double cur_lon)
{
char msg[80];
snprintf(msg, sizeof(msg), "CURRENT LAT: %lf, LON: %lf\r\n", cur_lat, cur_lon);
printf("%s", msg);
lora_send(msg);
}
void dead_reckon_step(double *cur_lat, double *cur_lon)
{
double dlat = TARGET_LAT - *cur_lat;
double dlon = TARGET_LON - *cur_lon;
*cur_lat += (fabs(dlat) < 0.005) ? dlat : ((dlat > 0.0) ? 0.004166 : -0.004166);
*cur_lon += (fabs(dlon) < 0.005) ? dlon : ((dlon > 0.0) ? 0.002139 : -0.002139);
}
bool check_arrival(double cur_lat, double cur_lon)
{
return (cur_lat == TARGET_LAT) && (cur_lon == TARGET_LON);
}
/**
* @brief Compute the initial bearing from one point toward another.
*
* @param lat1 Origin latitude in degrees.
* @param lon1 Origin longitude in degrees.
* @param lat2 Destination latitude in degrees.
* @param lon2 Destination longitude in degrees.
* @return double Bearing in degrees (0 to 360).
*/
static double bearing_to(double lat1, double lon1, double lat2, double lon2)
{
double p1 = lat1 * 0.017453292519943295, p2 = lat2 * 0.017453292519943295;
double dl = (lon2 - lon1) * 0.017453292519943295;
double y = sin(dl) * cos(p2);
double x = cos(p1) * sin(p2) - sin(p1) * cos(p2) * cos(dl);
double b = atan2(y, x) * 57.29577951308232;
return (b < 0.0) ? (b + 360.0) : b;
}
void navigate_to_target(double cur_lat, double cur_lon)
{
send_telemetry(cur_lat, cur_lon);
if (check_arrival(cur_lat, cur_lon)) {
propeller_stop();
release_payload();
} else {
propeller_set_bearing(bearing_to(cur_lat, cur_lon, TARGET_LAT, TARGET_LON));
}
}
+58
View File
@@ -0,0 +1,58 @@
// 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: payload.c
// Desc: Implements payload release solenoid latch control on GPIO16.
// Created: 2026
#include "payload.h"
#include "lora.h"
#include "hardware/gpio.h"
#include <stdio.h>
void init_payload(void)
{
gpio_init(16); gpio_set_dir(16, GPIO_OUT); gpio_put(16, 1);
gpio_init(17); gpio_set_dir(17, GPIO_OUT); gpio_put(17, 0);
gpio_init(18); gpio_set_dir(18, GPIO_OUT); gpio_put(18, 0);
gpio_init(25); gpio_set_dir(25, GPIO_OUT); gpio_put(25, 0);
}
void set_gnss_leds(bool fix, int siv)
{
gpio_put(16, (!fix && (siv == 0)) ? 1 : 0);
gpio_put(17, fix ? 1 : 0);
gpio_put(18, (!fix && (siv > 0)) ? 1 : 0);
}
void release_payload(void)
{
gpio_put(16, 1);
gpio_put(17, 1);
gpio_put(18, 1);
printf("PAYLOAD RELEASED AT TARGET COORDINATES\r\n");
lora_send("PAYLOAD RELEASED AT TARGET COORDINATES\r\n");
}
+84
View File
@@ -0,0 +1,84 @@
// 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: propeller.c
// Desc: Implements SG90 servo PWM mock propeller control on GPIO6.
// Created: 2026
#include "propeller.h"
#include "pico/stdlib.h"
#include "hardware/pwm.h"
#include "hardware/gpio.h"
static struct repeating_timer prop_timer;
static bool prop_active = false;
static uint16_t current_pulse = 1000;
void init_propeller(void)
{
gpio_set_function(PROPELLER_PIN, GPIO_FUNC_PWM);
uint s = pwm_gpio_to_slice_num(PROPELLER_PIN);
pwm_set_clkdiv(s, 150.0f);
pwm_set_wrap(s, 19999);
pwm_set_gpio_level(PROPELLER_PIN, 0);
pwm_set_enabled(s, true);
}
/**
* @brief Repeating timer callback to alternate servo angle at max slew rate.
*
* @param t Pointer to repeating timer structure.
* @return bool Always true to continue recurring timer.
*/
static bool prop_timer_callback(struct repeating_timer *t)
{
(void)t;
current_pulse = (current_pulse == 1000) ? 2000 : 1000;
pwm_set_gpio_level(PROPELLER_PIN, current_pulse);
return true;
}
void propeller_spin(void)
{
if (!prop_active) {
prop_active = true;
add_repeating_timer_ms(-150, prop_timer_callback, NULL, &prop_timer);
}
}
void propeller_set_bearing(double deg)
{
uint16_t pulse = (uint16_t)(1000.0 + (deg / 360.0) * 1000.0);
pwm_set_gpio_level(PROPELLER_PIN, pulse);
}
void propeller_stop(void)
{
if (prop_active) {
cancel_repeating_timer(&prop_timer);
prop_active = false;
}
pwm_set_gpio_level(PROPELLER_PIN, 0);
}
+43
View File
@@ -0,0 +1,43 @@
;
; Copyright (c) 2026 Kevin Thomas
; SPDX-License-Identifier: MIT
;
.pio_version 0
.program uart_rx
; 8n1 UART receiver for GPS NMEA reception on a single GPIO pin.
; Operates at 8 execution cycles per bit period.
; IN pin 0 and JMP pin are mapped to the GPS RX GPIO pin.
start:
wait 0 pin 0 ; Wait for start bit falling edge (1 cycle)
set x, 7 [10] ; Preload bit counter (7 remaining), delay 1.5 bit periods (11 cycles)
bitloop:
in pins, 1 ; Sample 1 bit from RX pin into ISR (1 cycle)
jmp x-- bitloop [6] ; Loop 8 times; each iteration is 8 execution cycles (7 cycles delay)
jmp pin good_stop ; Verify stop bit is HIGH
wait 1 pin 0 ; Framing error: wait until line returns to idle HIGH
jmp start ; Discard frame and re-synchronize
good_stop:
push noblock ; Push 8-bit byte into RX FIFO (bits [31:24])
% c-sdk {
#include "hardware/clocks.h"
#include "hardware/gpio.h"
static inline void uart_rx_program_init(PIO pio, uint sm, uint offset, uint pin, uint baud) {
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, false);
pio_gpio_init(pio, pin);
gpio_pull_up(pin);
pio_sm_config c = uart_rx_program_get_default_config(offset);
sm_config_set_in_pins(&c, pin);
sm_config_set_jmp_pin(&c, pin);
sm_config_set_in_shift(&c, true, false, 32);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX);
float div = (float)clock_get_hz(clk_sys) / (8 * baud);
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
%}
+365
View File
@@ -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()
+22
View File
@@ -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"
}
]
+4
View File
@@ -0,0 +1,4 @@
build
!.vscode/*
!build-ctf/CTF-02.bin
!CTF-02.bin
+22
View File
@@ -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
}
+15
View File
@@ -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}"
}
}
]
+9
View File
@@ -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"
]
}
+52
View File
@@ -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}\""
]
}
]
}
+46
View File
@@ -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"
}
+102
View File
@@ -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"
}
}
]
}
+128
View File
@@ -0,0 +1,128 @@
# 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 and cryptographic module
# targets for the DEEPLINE Metro practice firmware. Mirrors the
# hardened Ouroboros construction of encryption-c-rp2350.
# Created: 2026
cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
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")
include(pico_sdk_import.cmake)
project(CTF-02 C CXX ASM)
find_package(Python3 COMPONENTS Interpreter REQUIRED)
set(DEMO_ARTIFACT_JSON ${CMAKE_CURRENT_LIST_DIR}/scripts/demo_artifact.json)
set(DEMO_ARTIFACT_HEADER_COMMITTED ${CMAKE_CURRENT_LIST_DIR}/include/demo_artifact.h)
set(DEMO_ARTIFACT_HEADER_GENERATED ${CMAKE_CURRENT_BINARY_DIR}/generated/demo_artifact.h)
add_custom_command(
OUTPUT ${DEMO_ARTIFACT_HEADER_GENERATED}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/generated
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/scripts/dec.py
--from-json ${DEMO_ARTIFACT_JSON}
--header-out ${DEMO_ARTIFACT_HEADER_GENERATED}
--check-header-path ${DEMO_ARTIFACT_HEADER_COMMITTED}
DEPENDS
${CMAKE_CURRENT_LIST_DIR}/scripts/dec.py
${DEMO_ARTIFACT_JSON}
${DEMO_ARTIFACT_HEADER_COMMITTED}
COMMENT "Regenerating demo artifact header from JSON and checking committed header freshness"
VERBATIM
)
add_custom_target(check_demo_artifact_header DEPENDS ${DEMO_ARTIFACT_HEADER_GENERATED})
pico_sdk_init()
if(NOT PICO_MBEDTLS_PATH)
set(PICO_MBEDTLS_PATH ${PICO_SDK_PATH}/lib/mbedtls)
endif()
# Argon2id reference implementation (PHC winner), compiled single-threaded.
set(ARGON2_SRC ${CMAKE_CURRENT_LIST_DIR}/third_party/argon2)
add_library(argon2_ref STATIC
${ARGON2_SRC}/src/argon2.c
${ARGON2_SRC}/src/core.c
${ARGON2_SRC}/src/ref.c
${ARGON2_SRC}/src/encoding.c
${ARGON2_SRC}/src/blake2/blake2b.c
)
target_compile_definitions(argon2_ref PUBLIC ARGON2_NO_THREADS)
target_include_directories(argon2_ref PUBLIC
${ARGON2_SRC}/include
${ARGON2_SRC}/src
)
# mbedTLS subset: ChaCha20, Poly1305, ChaCha20-Poly1305, constant-time.
add_library(mbedtls_subset STATIC
${PICO_MBEDTLS_PATH}/library/chacha20.c
${PICO_MBEDTLS_PATH}/library/poly1305.c
${PICO_MBEDTLS_PATH}/library/chachapoly.c
${PICO_MBEDTLS_PATH}/library/constant_time.c
src/mbedtls_shims.c
)
target_compile_definitions(mbedtls_subset PUBLIC MBEDTLS_CONFIG_FILE="mbedtls_config.h")
target_include_directories(mbedtls_subset PUBLIC
include
${PICO_MBEDTLS_PATH}/include
${PICO_MBEDTLS_PATH}/library
)
# Ouroboros authentication engine consuming the Argon2id and AEAD layers.
add_library(auth STATIC src/auth.c)
add_dependencies(auth check_demo_artifact_header)
target_include_directories(auth PUBLIC include)
target_link_libraries(auth PUBLIC pico_stdlib mbedtls_subset argon2_ref)
# DEEPLINE Metro practice firmware executable.
add_executable(CTF-02 src/main.c)
target_link_libraries(CTF-02 PRIVATE auth pico_stdlib)
pico_enable_stdio_uart(CTF-02 0)
pico_enable_stdio_usb(CTF-02 1)
target_compile_definitions(CTF-02 PRIVATE
PICO_DEFAULT_UART_BAUD_RATE=115200
)
pico_add_extra_outputs(CTF-02)
+673
View File
@@ -0,0 +1,673 @@
# Operation Copperhead - Student Instructions
**⚠ DEEPLINE METRO EMERGENCY INCIDENT ⚠**
```
+----------------------------------------------------------------------------------------+
| |
| ██████╗ ██╗ █████╗ ██████╗██╗ ██╗███████╗████████╗ █████╗ ██████╗ ████████╗ |
| ██╔══██╗██║ ██╔══██╗██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝ |
| ██████╔╝██║ ███████║██║ █████╔╝ ███████╗ ██║ ███████║██████╔╝ ██║ |
| ██╔══██╗██║ ██╔══██║██║ ██╔═██╗ ╚════██║ ██║ ██╔══██║██╔══██╗ ██║ |
| ██████╔╝███████╗██║ ██║╚██████╗██║ ██╗███████╗ ██║ ██║ ██║██║ ██║ ██║ |
| ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝██║ ██║╚══════╝ ╚═╝ ╚═╝ ╚═╝██║ ██║ ██║ |
| |
| |
| O P E R A T I O N C O P P E R H E A D |
| |
| *** PRIORITY RED *** |
| |
+----------------------------------------------------------------------------------------+
```
---
## Project Overview
DEEPLINE Metro Authority's rebuilt DEEPLINE-AUTH field relay image shipped
four corrupted engineering constants: a miscalibrated release threshold, a
false TRACK banner string, an overstated block length, and a poisoned ARX
signal seed. The corrupted image reports an occupied BRIDGE-4 block as
stable and authorized while rescue crews approach, and the source used for
the emergency rebuild was overwritten seventeen minutes later and cannot be
recovered. Students reverse engineer `CTF-02.bin` with Ghidra, patch all
four defects, capture the runtime-derived signal key live in GDB, recover
and authenticate the Ouroboros authority frame, export a corrected image,
flash it to a Pico 2, and prove the corrected behavior on real hardware.
---
## Scenario Briefing
### Read This First (Plain English)
The story uses rail-signalling words that read as jargon the first time you
hit them. Here is what they mean; keep this list open while you read.
- **Block**: a fixed section of track. Only one train may occupy it at a
time. A block is **safe** when it is empty, so the train waiting at its
start may proceed, and **occupied** when a train is inside it, so the
train behind must hold.
- **Track circuit**: a current sent through the rails to detect where
trains are. A train's wheels short the rails and drop that current, which
is how the system knows a block is occupied.
- **Pilot wire**: the sensing line that carries the track-circuit reading
back to the relay. The **dead pilot wire** in this story has a frozen
reading: the value is latched and no longer updates.
- **Field relay**: the small embedded controller, one per block, that
watches the reading and decides hold versus authorize. This challenge uses
a Pico 2 as the relay.
- **Command floor**: the central control room for the whole railway.
- **Blacklock**: a coordinated cyberattack that bricks the control room and
locks everyone out of central operations. After a blacklock, every safety
decision falls back to the local relays.
- **How deep?**: the DEEPLINE corridor runs roughly 40 meters below street
level inside a hardened tube called the armored shell.
Once these words make sense, the incident below is a simple story: a relay
is lying about whether the block ahead is clear, and your job is to find
the corrupted bytes that make it lie.
### Background
**DEEPLINE Metro Authority** runs an armored railway deep beneath the
city, roughly 40 meters below street level, inside a hardened tube called
the armored shell. Armored two-car trains carry people through it, and
tonight they are also carrying rescue crews toward riders trapped inside
the tunnels.
To understand what is happening, picture how the line stays safe. The line
is cut into fixed stretches of track called **blocks**. A block is one
piece of track, and the rule is absolute: only one train may be inside a
block at any moment. Before a train may roll out of its current block and
into the next one, the system must first prove the next block is empty.
Empty means safe, and safe means the train may proceed. Occupied means
danger, and danger means the train must stop and wait.
How does the system prove a block is empty? It uses electricity. A steady
current is pushed into the rails of every block, and a small embedded
computer called a **field relay** (call sign **DEEPLINE-AUTH**) watches
that current. This is called a **track circuit**. When nothing is on a
block, the current flows normally. When a train rolls in, its steel wheels
connect the two rails and the current changes, and that change is the
relay's signal that a train is there. The **pilot wire** is the sensing
line that carries this reading from the rails up to the relay.
So every block has a relay doing the same honest job: read the current,
ask "is the block ahead empty?", and answer with only two words, **HOLD**
(stop, do not move) or **AUTHORIZE** (the way is clear, go). The relays are
the railway's nervous system, and with the command floor dead they are the
only nervous system left.
The command floor is the central control room where humans used to watch
the entire line. It was **blacklocked** by a coordinated attack: shut out,
locked, and made useless. When it went dark, the trains lost their view
from above. Every safety decision dropped down to the relays on the
ground, running their local firmware, deciding hold or authorize block by
block.
At 0341 UTC, a threat actor known as **Cortex Sledge** posted a message
calling the moment before it happened: a blacklock of the DEEPLINE control
floor followed by a silent corruption of the field relay images, so nothing
inside the tunnels could agree on what was safe. The blacklock landed. With
the network coordination center offline and rescue crews already inside the
armored shell, the engineering team rebuilt the relay firmware around the
**Ouroboros hardened gate**: an encrypted authority frame protected by a
12-word operation passphrase and sealed with Argon2id plus
XChaCha20-Poly1305. The rebuilt image was pushed to the fleet within
minutes of the blacklock.
That speed is where the story goes wrong, and it is the trap you walk
into tonight.
### The Origin of the Ouroboros Gate
The Ouroboros gate did not come from DEEPLINE. It came from a reclusive
cryptographer who spent a decade obsessed with a single, improbable goal:
to write encryption that could not be broken, not by anyone, not ever.
The engineers who worked beside him dismissed the obsession as unworkable,
and he never argued. When he finished, he published the work as a single
squashed archive tagged **v0.1.0**, with the compiled gate firmware image
attached to the release, and then vanished.
What he left behind is the strict Ouroboros construction: a memory-hard
Argon2id key schedule feeding an authenticated XChaCha20-Poly1305 authority
frame: symmetric cryptography hardened for the RP2350's memory budget, with
an honest threat model instead of a sales pitch. In plain terms, Ouroboros
is the relay's lock: an order is only trusted if an operator holding the
correct 12-word phrase unlocks it. The corruption in this challenge is
separate from that lock, plain wrong numbers in the safety math, not a
crack in the encryption. DEEPLINE quietly adopted it
for the field relays because it was the strongest gate anyone had ever
shipped that would still boot on the target. A decade of asking
"what if it must not be broken?" is the only reason the relay console can be
an authoritative gate at all. Tonight, in a tunnel with rescue crews
approaching a block the firmware is lying about, that wall of encryption
matters more than DEEPLINE's own design review ever did.
### The Disaster
The relay boots. It prints a status report. It reports **TRACK: NORMAL**,
**BLOCK STATE: STABLE**, and **AUTO TRAIN: AUTHORIZED**. To anyone standing
in the tunnel, that console looks like the railway giving the all-clear:
the track is fine, the block ahead is stable and empty, and the train may
move.
The console is lying, and here is the math behind the lie, step by step.
Step 1. The relay is reading **87 A** from the dead pilot wire. A pilot
wire goes dead when the reading freezes, so the relay is looking at a stale
number instead of live reality. That number is thrust in front of the relay
every cycle, and the relay keeps trusting it.
Step 2. DEEPLINE's hard engineering rule says no train may be released
into a block whose reading is at or above **60 A**. 87 A is nearly 45
percent beyond that limit. In plain terms, the reading is screaming that
the insulated block ahead is compromised.
Step 3. The compromised block in this incident is the exact block where
the two-car train is sitting right now, with rescue crews approaching on
foot. The reading is not noise and it is not a drill. The block ahead is
occupied.
An honest relay would do that arithmetic and reach the only logical answer:
reading too high, block unsafe, print BLOCK STATE: CRITICAL, set AUTO TRAIN:
HELD, and hold the train. That is what the relay was designed to do, and it is
the only thing standing between the rescue corridor and a collision.
The image that shipped does the opposite. It prints STABLE. It prints
AUTHORIZED. It tells the train the way is clear when the way is not clear.
The reason is corruption. Between the safe reference firmware and the image
pushed to the fleet, four engineering constants were changed. A constant is
a fixed number baked into the firmware, like the amounts in a recipe, and a
single wrong number can flip the entire verdict. The corrupted image
believes 87 A is acceptable, believes the occupied block is empty, and will
hand the train a green light straight into the rescue crews' tunnel.
If the fleet trusts that console, the two-car train is dispatched into the
occupied block at the same moment the crews mark the corridor with their
[chemlight], and nobody gets a second chance at that tunnel.
**The rushed build has defects. The four constants were corrupted between
the safe reference firmware and the image that shipped. The source used for
the emergency rebuild was overwritten by the next build seventeen minutes
later and cannot be recovered. Nobody has found where the corrupt values
live in the compiled image.** That is the hole you were called in to fill.
### The Only Surviving Evidence
One field relay, the training/verification unit, still holds the exact
miscompiled image that shipped to the fleet. This image, and this image
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 USB
console link, an SWD debug probe, 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 |
|---|---|
| Two-car train dispatched into an occupied BRIDGE-4 block | 1 train |
| Rescue crews walking toward a block the console calls safe | 4 teams |
| Stations and hospital spokes on backup power after blacklock | 29 facilities |
| Estimated riders stranded in the armored shell | 210+ people |
**The options are:**
1. ❌ **Trust the console**: the train releases on a false reading, the
BRIDGE-4 corridor becomes a collision scene.
2. ❌ **Scrap the fleet's firmware**, which buys time but leaves crews in the
tunnel with no interlocking and no authority frame at all.
3. **REVERSE ENGINEER THE EMERGENCY BUILD**: find the exact corrupt
bytes, patch them, prove the corrected image on real hardware, and hand
the fix to the field team so the *rest of the fleet* can be 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 corrupted 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 no exhausted command-floor
team can 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 DEEPLINE fleet. Every relay still reporting a
> false "STABLE" status is one two-car release away from a disaster.
> **AUTHORIZED LAB ONLY:** This challenge uses a supplied Pico 2 training
> relay and its exact corrupted firmware image. Do not connect this
> exercise to a public network, an operational railway, a metro system, or
> any device you do not own or have explicit written authorization to test.
---
## Learning Objectives
- Decode an ARM Cortex-M33 vector and boot table and identify the reset
handler and initial stack pointer.
- Translate Thumb reset-vector addresses into real function entry points and
trace literal-pool entries to their data.
- Locate four corrupted constants: a boundary comparison, a status string,
an 8-byte IEEE-754 double, and an ARX signal seed.
- Analyze unsigned compare semantics, condition codes, and compiler
transforms of boundary tests.
- Capture a runtime-derived key with GDB, override a register, and set a
watchpoint on stored SRAM state.
- Recover and authenticate an Argon2id plus XChaCha20-Poly1305 authority
frame and describe the crypto pipeline with an honest threat boundary.
- Export and UF2-convert a corrected image, then prove the corrected
behavior on real hardware.
---
## 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, USB-CDC console observation |
| 3 | Bootrom handoff, vector table, reset handler, startup code, XIP, Thumb-bit addressing |
| 4 | Data segments (`.rodata` / `.data` / `.bss`), initialized data images, little-endian encoding, literal pools, soft-float double layout |
| 5 | Unsigned compare semantics, condition codes (`hi`/`ls`), compiler transforms (`<` vs `<=`), volatile refetch semantics |
| 6 | Runtime signal-key derivation (SENTINEL-ARX quarter rounds), live register capture of a derived key, memory watchpoints |
| 7 | Argon2id memory-hard KDF, XChaCha20-Poly1305 AEAD, HChaCha20 subkey, salt/nonce/tag, authenticated decryption |
| 8 | Ouroboros composition (Argon2id to AEAD to payload dispatch), honest threat-model analysis, incident reporting |
---
## Part 1: Understanding the System
### DEEPLINE-AUTH Field Relay Hardware
| Component | Connection | Purpose |
|-----------|------------|---------|
| Raspberry Pi Pico 2 | RP2350 | Runs the corrupted emergency firmware |
| USB-CDC console | Micro-USB to host | Relay console and Ouroboros gate input |
| SWD debug interface | Supplied probe | Authorized GDB inspection |
| Onboard LED | GPIO 25 | Authentication success indicator |
Every graded finding lives in flash (`.rodata` / `.text` / `.data` image) or
SRAM, and is reachable with only the Weeks 1-8 toolset: Ghidra, GDB, and a
serial console.
### Console Configuration
- Transport: USB-CDC virtual COM port (no external adapter needed)
- Baud: `115200`
- Data: `8 bits`
- Parity: `none`
- Stop: `1`
- Logic: `3.3 V` on the debug header
### Normal (Intended) Behavior
The relay should run the SENTINEL-ARX signal-key layer, classify the frozen
87 A reading against the **real** DEEPLINE safety limit of **60 A**, report
honestly, and still accept the **Ouroboros authority frame** when an
operator enters the correct 12-word phrase:
```
+-----------------------------------------------------------------+
| Intended Relay Behavior |
| |
| 1. Boot and initialize USB-CDC stdio and the auth gate |
| 2. Print the boot identity and the true TRACK signal |
| 3. Compare the frozen 87 A reading against the 60 A limit |
| 4. 87 A exceeds 60 A, so the block is NOT stable |
| 5. Report BLOCK STATE: CRITICAL and AUTO TRAIN: HELD |
| 6. Mint a SIGNAL KEY each 2-second cycle and watch it match |
| the SIGNAL_SPEC 0x2D879291 (OK, not MISMATCH) |
| 7. The 12-word emergency phrase reauthorizes the frame: |
| AUTHORITY FRAME: VERIFIED and payload on UART |
| 8. Repeat the report once per cycle until conditions change |
+-----------------------------------------------------------------+
```
### Observed (Buggy) Behavior: What You Will See When You First Flash `CTF-02.uf2`
```text
DEEPLINE METRO AUTHORITY
ADAPTIVE SIGNAL WINDOW: 38 MINUTES
USB-CDC 115200 8N1 | AUTHORIZED LAB CONSOLE
TRACK: NORMAL
BLOCK STATE: STABLE
AUTO TRAIN: AUTHORIZED
BLOCK LENGTH: 3200 M
FAULT POLLS: 1
SIGNAL KEY: 0x915DCFF8 MISMATCH
RESPONSE>
```
> **Terminal Timing Note:** The first four lines (`DEEPLINE METRO...` through
> `TRACK: 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 2-second status stream (`BLOCK
> STATE...` through `SIGNAL KEY...`). To view the boot banner in your terminal,
> reset the Pico (pulse `RUN` to `GND`) while PuTTY is actively connected.
The status block repeats every 2 seconds with `FAULT POLLS` incrementing.
This is exactly what the field crews are seeing. It is wrong, and it is
wrong in **four 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 seventeen minutes after
the emergency build shipped. You have only the compiled image. Your job is
to reverse engineer it with Ghidra, locate the corrupted constants, patch
the image directly, and prove the corrected behavior: exactly the way the
field team will need to repatch the rest of the deployed fleet.
### What The Firmware Does
1. Initializes USB-CDC stdio and the Ouroboros authentication gate.
2. Reads a frozen track-circuit current (87 A) latched on the dead pilot
wire before the blacklock.
3. Compares that reading against a compiled-in safety threshold: **twice**,
once for the operator-facing BLOCK STATE line and once for the automated
AUTO TRAIN decision.
4. Prints a boot banner containing an unconditional TRACK signal line.
5. Enters an infinite 2-second loop that derives a session signal key,
prints the block classification, dispatch decision, block length, fault
poll count, and signal-key verdict.
6. Accepts a 12-word operation passphrase at the `RESPONSE>` prompt and runs
it through the hardened **Ouroboros gate** (Argon2id key derivation plus
XChaCha20-Poly1305 authenticated decryption) before dispatching the
authority frame payload to GPIO25 and UART.
### Bug Summary: What You Are Graded On
| Bug # | Category | Severity | Description | Hint |
|-------|----------|----------|--------------|------|
| **Bug #1** | Miscompiled safety constant | **CRITICAL** | The safe-release threshold was compiled far too permissive (95 A). It is used **twice**: once for the operator-facing status and once for the automated train-release decision, and **both** copies must be corrected. | The real DEEPLINE limit is 60 A. Search for the wrong immediate value used in the comparison. |
| **Bug #2** | Hardcoded string literal | **HIGH** | The boot banner unconditionally prints `TRACK: NORMAL` regardless of the actual reading. | The correct word describes a system holding a train at 87 A against a 60 A limit, not "NORMAL". |
| **Bug #3** | Data-section constant | **HIGH** | The block length shipped as 3.2 km; the real BRIDGE-4 block is 0.32 km, far below minimum release spacing. It prints as metres. | Trace the `BLOCK LENGTH` line to its data image in flash. Little-endian IEEE-754 double. |
| **Bug #4** | Init-time seed constant | **HIGH** | The ARX seed fused into the image is `0x0A0A0A0A`; the real seed is `0x6B206574`. The derived signal key therefore never matches `SIGNAL_SPEC`, and the console reports `MISMATCH` every cycle. | The expected value `0x2D879291` is a literal in a pool. The seed is a `.data` image in flash. Ignore decoy `0A` bytes in the input dispatch table. |
**Important:** The replacement text for Bug #2 **must be the same length**
as the original (`NORMAL` and `DANGER` are both 6 bytes). Patching a shorter
or longer string will corrupt adjacent flash data.
### A Third Layer: Not a Bug, an Authorization Task
The **Ouroboros authority frame** is the encrypted artifact that proves an
operator is legitimate: a 48-byte payload sealed with Argon2id-derived keys
and XChaCha20-Poly1305. It is never printed by the firmware's status lines.
Recovering it, by understanding the construction and authenticating with
the correct 12-word phrase, is required evidence for your final report.
The power of this layer is real but must be described honestly. The
construction is Argon2id (memory-hard KDF) chained into XChaCha20-Poly1305
(AEAD). Against Grover-style search, symmetric-key security exponents are
**halved**, not annihilated; this firmware is a demonstrator and does not
claim NIST post-quantum status. The honest claim is: **a high modeled
brute-force cost under the stated passphrase entropy and KDF assumptions**,
not "quantum-proof." Your report must state this boundary exactly.
---
## Part 3: Your Assignment
Whenever a task asks you to **Document** or **answer**, write your answers
in a single file named `CTF-02-Answers.md`.
### Task 1: Setup and Initial Analysis
1. Create a new Ghidra project named `Copperhead_Investigation`.
2. Import `CTF-02.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 2-second status loop (the branch target the
loop restarts from).
- The vector-table base, the initial stack pointer, and the reset-handler
pointer as stored (note its Thumb bit) versus the actual instruction
address.
- One representative literal-pool entry that feeds the status lines, and
what it points to.
### Task 2: Find and Patch Bug #1: The Miscalibrated Release Threshold
1. Find **both** locations where the frozen 87 A 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 ge`) 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 ge` instruction, which collapses Thumb decoding and swallows Compare Site B (`0x10000312`).
>
> To patch cleanly without breaking downstream disassembly, use the **Bytes Window**:
> 1. Ensure the Bytes window is open (**Window** -> **Bytes: CTF-02.bin**).
> 2. In the Bytes window toolbar, click the **pencil icon** (**Toggle Edit Mode**).
> 3. In the Listing window, click on address `0x10000302` (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 `10000302`, click on `5E`, and change it to **`3B`**.
> 5. Click back in the Listing window on address `0x10000302` and press **`D`** (**Disassemble**). The instruction immediately disassembles cleanly as `cmp r3, #0x3b`.
> 6. Notice that Compare Site B at `0x10000312` remains completely intact and visible! Repeat the exact same steps at `0x10000312`: click `0x10000312` 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 87 A reading dangerous for
an automated train release into an occupied block?
### Task 3: Find and Patch Bug #2: The False TRACK Banner
1. Find the boot-banner string that unconditionally reports the wrong
signal state.
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: Find and Patch Bug #3: The Block Length Constant
1. Use Ghidra to locate the `BLOCK LENGTH` status line and trace the value
it prints back to its source in the initialized data image.
2. Document the 8-byte IEEE-754 double as stored (little endian) and its
printed interpretation.
3. Patch the data image so the relay reports the real 320 m BRIDGE-4 block.
**Questions to answer:**
- Show your byte-for-byte conversion from 3.2 km to 0.32 km.
- Why would a console that overstates block length by ten times be as
dangerous as one that understates it?
### Task 5: Find and Patch Bug #4: The Signal Seed
1. Find the `SIGNAL_SPEC` value `0x2D879291` in the binary and note where
it lives.
2. Locate the `.data` image in flash that the firmware copies into SRAM at
boot. Identify the seed word that is wrong.
3. Patch the seed so the runtime-derived key matches the spec.
**Warning:** there are decoy `0x0A0A0A0A` bytes in the console input
dispatch table. The real seed is an initialized data image, not a jump-table
constant.
**Questions to answer:**
- How is the signal key derived each cycle, and where does the failure
appear in the register trace?
- Does fixing the seed also authenticate the Ouroboros gate? Explain what
each layer does and does not protect.
### Task 6: GDB Register Capture of the Derived Key
Prove the signal-key failure from the live machine, not just from static
bytes:
1. Connect GDB to the running relay via the SWD probe.
2. Break at the second `derive_session_key` call site, immediately after the
branch returns.
3. Read `$r0`. Record the bug-derived key.
4. Inspect the two arguments entering the derivation: the live seed from
SRAM and the derived IV.
5. Overwrite `$r0` with the correct spec value, step out, and confirm the
next status cycle prints `SIGNAL KEY: 0x2D879291 OK`.
6. Verify with a watchpoint on the stored key in SRAM.
**Questions to answer:**
- Why is the derived value in `$r0` different from the spec, and what alone
in the image is responsible?
- What does the register overwrite prove that the static patch proves
differently (and vice versa)?
### Task 7: Recover the Ouroboros Authority Frame
1. In Ghidra, locate the embedded artifact: the 16-byte salt, the 24-byte
XChaCha nonce, and the 64-byte ciphertext-plus-tag.
2. Document the Argon2id parameters compiled into the gate (memory, time,
parallelism) and the payload layout contract (LED byte + UART bytes).
3. On the live relay, enter the canonical 12-word emergency phrase at the
`RESPONSE>` prompt.
4. Confirm the expected outcome: `AUTHORITY FRAME: VERIFIED`, the onboard
LED turning on, and the payload printed to the console.
5. Demonstrate the two failure paths (policy violation and wrong phrase).
**Questions to answer (honest boundary required):**
- Walk through the full pipeline: Argon2id to 32-byte key, HChaCha20 subkey
from the nonce prefix, inner nonce, Poly1305 tag verification, payload
dispatch.
- What would Grover-style search actually change in this construction, and
why does this firmware not claim strict post-quantum status?
### Task 8: Export and Verify
1. Export your patched binary as `CTF-02_fixed.bin`.
2. Convert it to UF2 format for the RP2350:
```bash
python uf2conv.py CTF-02_fixed.bin --base 0x10000000 --family 0xe48bff59 --output CTF-02_fixed.uf2
```
3. Flash `CTF-02_fixed.uf2` to your Pico 2 and capture the corrected
console output.
4. Confirm the corrected image now reports **TRACK: DANGER**, **BLOCK STATE:
CRITICAL**, **AUTO TRAIN: HELD**, **BLOCK LENGTH: 320 M**, and **SIGNAL
KEY: 0x2D879291 OK**, 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 9: 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 dispatch a train into a
block occupied by rescue crews?
2. Name one concrete engineering practice (review, static analysis,
hardware-in-the-loop test, signature verification, etc.) that would have
caught **each** of the four graded bugs before this image reached the
fleet, and one practice that would have stopped the corrupted image from
**running** at all.
---
## How To Breadboard
- Connect the Pico 2 micro-USB port directly to the host computer. The
relay enumerates as a USB-CDC virtual COM device.
- Open the end-of-line tool of your choice at `115200 8N1`.
- Connect the supplied SWD probe to the debug header according to its
documented pinout for GDB access.
- Use **3.3 V logic only** on the debug header. Never connect a 5 V line to
a Pico GPIO.
The supplied image is `CTF-02.bin` (for Ghidra analysis) and
`CTF-02.uf2` (for flashing). If your instructor supplies different
filenames, record the actual filenames in your report.
Flash using BOOTSEL mode (hold BOOT, plug in USB) and copy the UF2 onto the
`RP2350` mass-storage drive, or use `picotool`.
---
## Memory Map Reference
| Region | Address | Purpose |
|--------|---------|---------|
| Bootrom | `0x00000000` | Immutable boot code |
| Flash/XIP | `0x10000000` | Vector table, code, rodata, `.data` init image |
| SRAM | `0x20000000` | Stack and writable state |
---
## Submission Format
Submit a folder containing:
- `CTF-02-Answers.md`;
- screenshots or terminal transcripts;
- `CTF-02_fixed.bin` and `CTF-02_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 TRACK string without corrupting
adjacent data.
- You can locate and patch the corrupted block-length double in the data
image.
- You can locate and patch the corrupted ARX seed and explain why the
runtime-derived key misses its spec.
- You can capture and correct the derived key live in GDB and verify with a
watchpoint.
- You can authenticate through the Ouroboros gate with the correct phrase
and describe the crypto pipeline accurately, including the honest
quantum boundary.
- You can export, convert, flash, and prove the corrected behavior on real
hardware.
---
## 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
railway, a metro system, 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/)
- Strict Ouroboros reference construction (v0.1.0), the published source of
this firmware's gate, with an honest threat model:
[https://github.com/mytechnotalent/encryption-c-rp2350](https://github.com/mytechnotalent/encryption-c-rp2350)
- Reference gate firmware image (v0.1.0 release):
[https://github.com/mytechnotalent/encryption-c-rp2350/releases/download/v0.1.0/encryption_app.uf2](https://github.com/mytechnotalent/encryption-c-rp2350/releases/download/v0.1.0/encryption_app.uf2)
- PHC reference Argon2: [https://github.com/P-H-C/phc-winner-argon2](https://github.com/P-H-C/phc-winner-argon2)
Binary file not shown.
+276
View File
@@ -0,0 +1,276 @@
# Operation Copperhead - Requirements & Grading Criteria
```
+----------------------------------------------------------------------------------------+
| |
| ██████╗ ██╗ █████╗ ██████╗██╗ ██╗███████╗████████╗ █████╗ ██████╗ ████████╗ |
| ██╔══██╗██║ ██╔══██╗██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝ |
| ██████╔╝██║ ███████║██║ █████╔╝ ███████╗ ██║ ███████║██████╔╝ ██║ |
| ██╔══██╗██║ ██╔══██║██║ ██╔═██╗ ╚════██║ ██║ ██╔══██║██╔══██╗ ██║ |
| ██████╔╝███████╗██║ ██║╚██████╗██║ ██╗███████╗ ██║ ██║ ██║██║ ██║ ██║ |
| ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝██║ ██║╚══════╝ ╚═╝ ╚═╝ ╚═╝██║ ██║ ██║ |
| |
| |
| O P E R A T I O N C O P P E R H E A D |
| |
| REQUIREMENTS & GRADING CRITERIA |
| |
+----------------------------------------------------------------------------------------+
```
---
## Project Overview
Students are the reverse-engineering reserve team called in after DEEPLINE
Metro Authority's rebuilt DEEPLINE-AUTH relay image shipped four corrupted
engineering constants: a miscalibrated release threshold, a false TRACK banner
string, an overstated block length, and a poisoned ARX signal seed. Students
reverse engineer `CTF-02.bin` with Ghidra, patch all four defects, capture the
runtime-derived signal key live in GDB, recover the Ouroboros authority frame,
export a corrected image, flash it to real hardware, and prove the corrected
behavior on a physical Pico 2.
The challenge is a standalone capstone exercise and contains no answer,
constant, address, bug, or patch belonging to any other course assignment.
---
## Learning Objectives
- Decode an ARM Cortex-M33 vector and boot table and identify the reset handler
and initial stack pointer.
- Translate Thumb reset-vector addresses into real function entry points and
trace literal-pool entries to their data.
- Locate four corrupted constants: a boundary comparison, a status string, an
8-byte IEEE-754 double, and an ARX signal seed.
- Capture a runtime-derived key with GDB, override a register, and set a
watchpoint on stored SRAM state.
- Recover and authenticate an Argon2id plus XChaCha20-Poly1305 authority frame.
Students must use only Weeks 1-8 concepts: ARM registers, stack behavior,
USB-CDC output, GDB, Ghidra static analysis and binary patching, vector tables,
reset startup, XIP, Thumb addressing, data segments and literal pools,
condition-code analysis, runtime key derivation, and the Argon2id plus
XChaCha20-Poly1305 authenticated gate.
---
## Deliverables Checklist
| # | Deliverable | Format | Criterion |
|---|-------------|--------|-----------|
| 1 | Ghidra project screenshot | PNG/JPG | 1.1 |
| 2 | Vector table and boot table | Inside `CTF-02-Answers.md` | 1.2 |
| 3 | `main()` and status-loop table | Inside `CTF-02-Answers.md` | 1.3 |
| 4 | Literal pool trace | Inside `CTF-02-Answers.md` | 1.4 |
| 5 | Bug #1 evidence and patches | Inside `CTF-02-Answers.md` | 2.1-2.4 |
| 6 | Bug #2 evidence and patch | Inside `CTF-02-Answers.md` | 3.1-3.4 |
| 7 | Bug #3 evidence and patch | Inside `CTF-02-Answers.md` | 4.1-4.3 |
| 8 | Bug #4 evidence and patch | Inside `CTF-02-Answers.md` | 5.1-5.4 |
| 9 | GDB register capture | Inside `CTF-02-Answers.md` | 6.1-6.4 |
| 10 | Ouroboros gate recovery and auth | Inside `CTF-02-Answers.md` | 7.1-7.4 |
| 11 | `CTF-02_fixed.bin` | BIN file | 8.1 |
| 12 | `CTF-02_fixed.uf2` | UF2 file | 8.2 |
| 13 | Corrected console transcript | Inside `CTF-02-Answers.md` | 8.3 |
| 14 | Summary table of all patches | Inside `CTF-02-Answers.md` | 8.4 |
| 15 | Written reflection | Inside `CTF-02-Answers.md` | 9.1-9.2 |
---
## Required Tools and Equipment
| Tool | Purpose |
|------|---------|
| Raspberry Pi Pico 2 | Isolated target |
| USB-CDC virtual serial console | Observe output and type the gate passphrase |
| SWD debug probe | GDB inspection |
| Ghidra | Static analysis and binary patching |
| GDB | Dynamic analysis and register capture |
| Python (`uf2conv.py`) | UF2 conversion |
| `CTF-02.bin` and `CTF-02.uf2` | Supplied artifacts |
Console settings: **USB-CDC virtual COM port, 115200 baud, 8 data bits, no
parity, 1 stop bit**.
---
## Artifact Identity
The instructor-issued artifact hashes are:
```text
CTF-02.bin 85330C37CD0897746B1AF447E4BAC371DDE2042ABD2D61D58A61FE2A8EEF3537
CTF-02.uf2 F3CD4840260DB820D792758CECACC5297BEF1971B9EACF7601279256D8AF1EAB
```
---
## Grading Rubric - Detailed Breakdown
### Task 1: Setup and Initial Analysis (12 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: Vector Table Decoding | 3 | Correct base, initial SP, reset pointer | One missing | Not found |
| Criterion 1.3: main() and Status-Loop Addresses | 4 | Both addresses correct | One correct | Neither found |
| Criterion 1.4: Thumb Addressing and Literal Pool | 2 | Bit 0 cleared and one pool entry traced to its string | Partial | Incorrect |
### Task 2: Find and Patch Bug #1: The Miscalibrated Release Threshold (15 points)
| Criterion | Points | Full credit | Partial credit | No credit |
|-----------|--------|-------------|----------------|-----------|
| Criterion 2.1: Locate Compare Sites A and B | 6 | Both addresses and original bytes | One site | Not found |
| Criterion 2.2: Correct Immediate-Value Reasoning | 4 | Explains the `<` to `<=` transform and gives `0x3B` | Correct value, no reasoning | Wrong value |
| Criterion 2.3: Patch Compare Sites A and B | 4 | Both byte changes verified | One site | Not patched |
| Criterion 2.4: Explain Why Both Sites Must Be Patched | 1 | Clear explanation of the two independent comparisons | Vague | Missing |
### Task 3: Find and Patch Bug #2: The False TRACK Banner (10 points)
| Criterion | Points | Full credit | Partial credit | No credit |
|-----------|--------|-------------|----------------|-----------|
| Criterion 3.1: Locate the Banner String | 3 | Correct address and cross-reference | Approximate | Not found |
| Criterion 3.2: Patch Six Characters | 4 | All six bytes changed, length preserved | Correct text, wrong bytes documented | Wrong length |
| Criterion 3.3: Character-by-Character Documentation | 2 | Original vs patched byte for all six characters | Partial | Missing |
| Criterion 3.4: Explain the Danger of a Hardcoded Status Word | 1 | Clear, specific reasoning | Generic | Missing |
### Task 4: Find and Patch Bug #3: The Block Length Constant (10 points)
| Criterion | Points | Full credit | Partial credit | No credit |
|-----------|--------|-------------|----------------|-----------|
| Criterion 4.1: Locate the .data Double | 3 | Correct address traced from the BLOCK LENGTH print | Approximate | Not found |
| Criterion 4.2: IEEE-754 Bytes and Print Math | 4 | Original and patched 8-byte double with print math (3200 M to 320 M) | Correct patch, no math | Wrong bytes |
| Criterion 4.3: Patch to Print 320 M | 3 | Console shows `BLOCK LENGTH: 320 M` | Wrong bytes | Not patched |
### Task 5: Find and Patch Bug #4: The Signal Seed (15 points)
| Criterion | Points | Full credit | Partial credit | No credit |
|-----------|--------|-------------|----------------|-----------|
| Criterion 5.1: Locate SIGNAL_SPEC and the Seed | 5 | `0x2D879291` located and wrong seed `0x0A0A0A0A` found | Partial | Not found |
| Criterion 5.2: Patch the Seed | 4 | Seed bytes changed to `74 65 20 6B` | Wrong byte | Not patched |
| Criterion 5.3: Explain the ARX Derivation | 3 | Correct trace of the per-cycle derivation | Vague | Missing |
| Criterion 5.4: Separate the Security Layers | 3 | Correctly explains what the seed fixes versus the gate | Generic | Missing |
### Task 6: GDB Register Capture of the Derived Key (15 points)
| Criterion | Points | Full credit | Partial credit | No credit |
|-----------|--------|-------------|----------------|-----------|
| Criterion 6.1: Breakpoint at the Derive Return | 4 | Correct address and `$r0` read as the bug-derived key | Address off | Not found |
| Criterion 6.2: Inspect the Two Arguments | 4 | Live seed and derived IV captured at the second call | One correct | Missing |
| Criterion 6.3: Override the Register | 4 | `$r0` set to `0x2D879291` and the next cycle shows `OK` | Partial | Missing |
| Criterion 6.4: Watchpoint on the Stored Key | 3 | Watchpoint on the SRAM key location documented | Approximate | Missing |
### Task 7: Recover the Ouroboros Authority Frame (10 points)
| Criterion | Points | Full credit | Partial credit | No credit |
|-----------|--------|-------------|----------------|-----------|
| Criterion 7.1: Locate Salt, Nonce, Ciphertext, and Tag | 4 | All three addresses correct in flash | Two correct | Not found |
| Criterion 7.2: Document Argon2id Parameters and Payload Contract | 2 | Correct memory/time/parallelism and payload layout | Partial | Missing |
| Criterion 7.3: Authenticate with the 12-Word Passphrase | 2 | `AUTHORITY FRAME: VERIFIED`, LED on, payload printed | Partial | Not shown |
| Criterion 7.4: State the Honest Quantum Boundary | 2 | Grover halves symmetric exponents; not strict PQC | Generic | Misstates |
### Task 8: Export and Verify (8 points)
| Criterion | Points | Full credit | Partial credit | No credit |
|-----------|--------|-------------|----------------|-----------|
| Criterion 8.1: Export CTF-02_fixed.bin | 1 | Valid patched binary | Corrupted | Not submitted |
| Criterion 8.2: Convert to CTF-02_fixed.uf2 | 1 | Correct base and family flags | Wrong flags | Not submitted |
| Criterion 8.3: Hardware Verification | 4 | Corrected console output confirmed (CRITICAL/HELD in 2s stream; DANGER at boot/Ghidra) | Some lines corrected | No verification |
| Criterion 8.4: Summary Table of All Patches | 2 | Complete address and before/after table | Missing entries | No table |
### Task 9: Written Reflection (5 points)
| Criterion | Points | Full credit | Partial credit | No credit |
|-----------|--------|-------------|----------------|-----------|
| Criterion 9.1: "Rushed Build" Is Not an Excuse | 2 | Specific, grounded reasoning | Generic | Missing |
| Criterion 9.2: One Engineering Practice per Failure Area | 3 | Concrete practices for the bugs and for image authenticity | Names some | Missing |
---
## Common Pitfalls
| Pitfall | Consequence | Avoidance |
|---------|-------------|-----------|
| Patching only one threshold site | One status line still lies | Patch both `0x10000302` and `0x10000312` |
| 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 the block length as an integer | Misses the 8-byte double | Follow the value into `.data`, decode IEEE-754 |
| Using the wrong 0.32 bytes | Prints 316 M instead of 320 M | Use `7B 14 AE 47 E1 7A D4 3F` |
| Treating an odd vector address as invalid | Thumb analysis fails | Clear bit 0 |
| Starting the seed patch at the wrong offset | Wrong seed, key never matches | Seed is at `0x1000EC70` |
---
## How To Breadboard
- **Raspberry Pi Pico 2** powered over USB.
- **USB-CDC virtual serial console:** open the Pico's COM port at 115200 baud,
8 data bits, no parity, 1 stop bit.
- **SWD debug probe:** connect SWCLK, SWDIO, GND, and 3.3 V to the Pico debug
header for GDB inspection and register capture.
- No other peripherals are required; the authority LED is on-board.
---
## Memory Map Reference
| Region | Address | Purpose |
|--------|---------|---------|
| Bootrom | `0x00000000` | Immutable boot code |
| Flash/XIP | `0x10000000` | Vector table, code, rodata, `.data` init image |
| SRAM | `0x20000000` | Stack and writable state |
---
## Deadline & Submission
- Create a folder containing the Ghidra screenshot, `CTF-02_fixed.bin`, and
`CTF-02_fixed.uf2`.
- Write all written answers in a single file named `CTF-02-Answers.md` inside that
folder.
- ZIP the folder as `lastname-firstname-CTF-02.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 railway, metro system, 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, keys,
passphrases, or answers is a violation of the academic integrity policy.
---
## Reference Material
| Topic | Reference |
|-------|-----------|
| ARM Cortex-M33 registers and stack | Week 1 |
| USB-CDC output and console capture | Week 2 |
| Vector tables, reset startup, and XIP | Week 2 |
| Ghidra static analysis and binary patching | Week 3 |
| Data segments, literal pools, IEEE-754 | Week 4 |
| Condition-code analysis | Week 5 |
| Runtime key derivation and GDB register capture | Week 6 |
| Argon2id and XChaCha20-Poly1305 authenticated gate | Week 8 |
Binary file not shown.
+652
View File
@@ -0,0 +1,652 @@
# Operation Copperhead - Instructor Solution Key
```
+----------------------------------------------------------------------------------------+
| |
| ██████╗ ██╗ █████╗ ██████╗██╗ ██╗███████╗████████╗ █████╗ ██████╗ ████████╗ |
| ██╔══██╗██║ ██╔══██╗██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝ |
| ██████╔╝██║ ███████║██║ █████╔╝ ███████╗ ██║ ███████║██████╔╝ ██║ |
| ██╔══██╗██║ ██╔══██║██║ ██╔═██╗ ╚════██║ ██║ ██╔══██║██╔══██╗ ██║ |
| ██████╔╝███████╗██║ ██║╚██████╗██║ ██╗███████╗ ██║ ██║ ██║██║ ██║ ██║ |
| ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝██║ ██║╚══════╝ ╚═╝ ╚═╝ ╚═╝██║ ██║ ██║ |
| |
| |
| O P E R A T I O N C O P P E R H E A D |
| |
| *** INSTRUCTOR SOLUTION KEY: RESTRICTED *** |
| |
+----------------------------------------------------------------------------------------+
```
> The task and criterion headings in this key are word-for-word identical to
> `CTF-02-R.md`, so a student can match each criterion one-to-one.
---
## Artifact Identity
| Artifact | Value |
|----------|-------|
| Student image | `CTF-02.bin` |
| Flash image | `CTF-02.uf2` |
| Target | Raspberry Pi Pico 2 / RP2350 ARM Cortex-M33 |
| Image base | `0x10000000` |
| Console | USB-CDC virtual COM, 115200 8N1 |
```text
CTF-02.bin 85330C37CD0897746B1AF447E4BAC371DDE2042ABD2D61D58A61FE2A8EEF3537
CTF-02.uf2 F3CD4840260DB820D792758CECACC5297BEF1971B9EACF7601279256D8AF1EAB
```
Proof tool: `python3 scripts/verify_ctf.py` returns `26/26 checks passed` against
`CTF-02.bin`.
---
## Task 1: Setup and Initial Analysis (12 points)
### Solution
**Criterion 1.1: Ghidra Project Setup (3 points).** Import `CTF-02.bin` as
`Raw Binary`, language `ARM:LE:32:Cortex`, base address `0x10000000`, then run
auto-analysis.
**Criterion 1.2: Vector Table Decoding (3 points).**
First 32 bytes of `CTF-02.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.3: main() and Status-Loop Addresses (4 points).**
| Element | Address |
|---------|---------|
| `main()` | `0x100002E8` |
| Recurring status loop start | `0x1000034C` |
| Loop back-edge (`b.n 0x1000034C`) | `0x1000044E` |
**Criterion 1.4: Thumb Addressing and Literal Pool (2 points).**
The stored reset pointer `0x1000015B` has bit 0 set, selecting Thumb mode.
Clearing bit 0 gives `0x1000015A`. A representative literal pool entry is
`0x100004B4`, which holds `0x1000C4C8`, the address of the format string
`"BLOCK STATE: %s"`, loaded by `ldr r0, [pc, #308]` at `0x1000037C`.
**Supporting Reference: SRAM Symbols (Ghidra names to semantic roles).**
| Ghidra Label | SRAM Address | Section | Role | Source Symbol |
|--------------|--------------|---------|------|---------------|
| `DAT_20001188` | `0x20001188` | `.data` | Block length double | `g_telemetry` |
| `DAT_20001198` | `0x20001198` | `.data` | Signal key seed | `g_auth_seed` |
| `DAT_2000119C` | `0x2000119C` | `.data` | Track current | `g_block_current` |
| `DAT_20001ECC` | `0x20001ECC` | `.bss` | Dispatch state | `g_dispatch_state` |
| `DAT_20001ED0` | `0x20001ED0` | `.bss` | Fault poll counter | `g_fault_polls` |
| `DAT_20001ED4` | `0x20001ED4` | `.bss` | Input line buffer | `g_linebuf` |
| `DAT_200020D4` | `0x200020D4` | `.bss` | Input write index | `g_lineidx` |
| `DAT_200020D8` | `0x200020D8` | `.bss` | Operator state | `g_operator_state` |
| `DAT_200020DC` | `0x200020DC` | `.bss` | Derived signal key | `g_signal_key` |
### 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: Vector Table Decoding | 3 | Correct base, initial SP, reset pointer | One missing | Not found |
| Criterion 1.3: main() and Status-Loop Addresses | 4 | Both addresses correct | One correct | Neither found |
| Criterion 1.4: Thumb Addressing and Literal Pool | 2 | Bit 0 cleared and one pool entry traced to its string | Partial | 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 `0x100002E8`, the loop head is `0x1000034C`, and the
back-edge is `0x1000044E`.
- For Criterion 1.4, accept any correctly traced literal pool entry; the pool
entry `0x100004B4` holding `0x1000C4C8` (`"BLOCK STATE: %s"`, loaded at
`0x1000037C`) is the reference example.
- The SRAM symbol table is supporting reference material, not a separate
scored criterion.
---
## Task 2: Find and Patch Bug #1: The Miscalibrated Release Threshold (15 points)
### Solution
**Criterion 2.1: Locate Compare Sites A and B (6 points).**
```text
10000302: 2b5e cmp r3, #94 @ 0x5e
10000312: 2b5e cmp r3, #94 @ 0x5e
```
| Site | Address | File Offset | Original Bytes | Original Instruction |
|------|---------|-------------|----------------|----------------------|
| A | `0x10000302` | `0x0302` | `5E 2B` | `cmp r3, #94` |
| B | `0x10000312` | `0x0312` | `5E 2B` | `cmp r3, #94` |
**Criterion 2.2: Correct Immediate-Value Reasoning (4 points).**
The source constant is `SAFE_THRESHOLD = 95` and the test is `x < 95`. For an
unsigned value, `x < 95` is exactly `x <= 94`, so the compiler emits
`cmp r3, #94`. The correct limit is `60`, so the test is `x < 60`, which is
`x <= 59`. The correct patched immediate is **`0x3B` (59)**, not `0x3C` (60).
**Criterion 2.3: Patch Compare Sites A and B (4 points).**
| Site | Address | File Offset | Original | Patched | After |
|------|---------|-------------|----------|---------|-------|
| A | `0x10000302` | `0x0302` | `5E 2B` | `3B 2B` | `cmp r3, #59` |
| B | `0x10000312` | `0x0312` | `5E 2B` | `3B 2B` | `cmp r3, #59` |
**Criterion 2.4: Explain Why Both Sites Must Be Patched (1 point).**
Site A drives the `BLOCK STATE` line and site B drives the `AUTO TRAIN`
decision. Patching only site A makes the console read `CRITICAL` while the
automated dispatch still says `AUTHORIZED`. Frozen reading `87`: `87 <= 94` is
true (wrong); `87 <= 59` is false (correct).
### Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|-----------|--------|--------------------------|----------------|-----------|
| Criterion 2.1: Locate Compare Sites A and B | 6 | Both addresses and original bytes | One site | Not found |
| Criterion 2.2: Correct Immediate-Value Reasoning | 4 | Explains the `<` to `<=` transform and gives `0x3B` | Correct value, no reasoning | Wrong value |
| Criterion 2.3: Patch Compare Sites A and B | 4 | Both byte changes verified | One site | Not patched |
| Criterion 2.4: Explain Why Both Sites Must Be Patched | 1 | Clear explanation of the two independent comparisons | Vague | Missing |
### Instructor Notes & Assembly
- Both sites must be patched: `0x10000302` for `BLOCK STATE` and `0x10000312`
for the `AUTO TRAIN` decision.
- The correct immediate is `0x3B` (59), not `0x3C` (60).
- Verify the byte changes on hardware; the corrected console reads `CRITICAL`
and `HELD`.
- **Ghidra ARM/Thumb Context Note:** In raw `.bin` files, patching an instruction
that precedes an `IT` block (`ite ge`) 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 (`0x10000312`). 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 TRACK Banner (10 points)
### Solution
**Criterion 3.1: Locate the Banner String (3 points).**
| String | Address |
|--------|---------|
| `"TRACK: NORMAL\r"` | `0x1000C4B8` |
| `"NORMAL"` substring to patch | `0x1000C4BF` |
The string is loaded in `main` and printed once at boot; it never reads the
sensor.
**Criterion 3.2: Patch Six Characters (4 points).**
`NORMAL` and `DANGER` are both six ASCII characters, so the patch preserves the
length.
| Address Range | Original Bytes | Patched Bytes |
|---------------|----------------|---------------|
| `0x1000C4BF` - `0x1000C4C4` | `4E 4F 52 4D 41 4C` | `44 41 4E 47 45 52` |
**Criterion 3.3: Character-by-Character Documentation (2 points).**
| Address | Original Char | Original Byte | Patched Char | Patched Byte |
|---------|---------------|---------------|--------------|--------------|
| `0x1000C4BF` | N | `4E` | D | `44` |
| `0x1000C4C0` | O | `4F` | A | `41` |
| `0x1000C4C1` | R | `52` | N | `4E` |
| `0x1000C4C2` | M | `4D` | G | `47` |
| `0x1000C4C3` | A | `41` | E | `45` |
| `0x1000C4C4` | L | `4C` | R | `52` |
**Criterion 3.4: Explain the Danger of a Hardcoded Status Word (1 point).**
The banner never consults the reading, so it reports a healthy track even while
the frozen reading is dangerous, masking the hazard from the operator.
### Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|-----------|--------|--------------------------|----------------|-----------|
| Criterion 3.1: Locate the Banner String | 3 | Correct address and cross-reference | Approximate | Not found |
| Criterion 3.2: Patch Six Characters | 4 | All six bytes changed, length preserved | Correct text, wrong bytes documented | Wrong length |
| Criterion 3.3: Character-by-Character Documentation | 2 | Original vs patched byte for all six characters | Partial | Missing |
| Criterion 3.4: Explain the Danger of a Hardcoded Status Word | 1 | 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.
- Confirm the patch covers `0x1000C4BF` through `0x1000C4C4` exactly.
- The banner is printed once at boot and never recomputed, so it is a separate
defect from the threshold.
---
## Task 4: Find and Patch Bug #3: The Block Length Constant (10 points)
### Solution
**Criterion 4.1: Locate the .data Double (3 points).**
The console prints `BLOCK LENGTH: 3200 M` from the format string at
`0x1000C4F0`. The value is a `double` in the `.data` init image at
`0x1000EC60` (loaded into `0x20001188` at boot).
**Criterion 4.2: IEEE-754 Bytes and Print Math (4 points).**
Eight bytes at `0x1000EC60`:
```text
9A 99 99 99 99 99 09 40 -> 0x400999999999999A -> 3.2 km -> 3200 m
7B 14 AE 47 E1 7A D4 3F -> 0x3FD47AE147AE147B -> 0.32 km -> 320 m
```
**Criterion 4.3: Patch to Print 320 M (3 points).**
| File Offset Range | Flash Address Range | Original Bytes | Patched Bytes |
|-------------------|---------------------|----------------|---------------|
| `0xEC60` - `0xEC67` | `0x1000EC60` - `0x1000EC67` | `9A 99 99 99 99 99 09 40` | `7B 14 AE 47 E1 7A D4 3F` |
At `0x1000EC68` the adjacent telemetry fields (`03 00 00 00` flags and `07 00`
crossing) are left untouched.
### Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|-----------|--------|--------------------------|----------------|-----------|
| Criterion 4.1: Locate the .data Double | 3 | Correct address traced from the BLOCK LENGTH print | Approximate | Not found |
| Criterion 4.2: IEEE-754 Bytes and Print Math | 4 | Original and patched 8-byte double with print math (3200 M to 320 M) | Correct patch, no math | Wrong bytes |
| Criterion 4.3: Patch to Print 320 M | 3 | Console shows `BLOCK LENGTH: 320 M` | Wrong bytes | Not patched |
### Instructor Notes & Assembly
- The value is an 8-byte IEEE-754 double, not an integer. Follow the
`BLOCK LENGTH` print into `.data` at `0x1000EC60`.
- The patched bytes `7B 14 AE 47 E1 7A D4 3F` decode to `0.32 km` (`320 m`).
- Confirm the adjacent telemetry fields at `0x1000EC68` (`03 00 00 00` and
`07 00`) are left untouched.
---
## Task 5: Find and Patch Bug #4: The Signal Seed (15 points)
### Solution
**Criterion 5.1: Locate SIGNAL_SPEC and the Seed (5 points).**
The SIMPLE comparison in the loop is at `0x100003A6`:
```text
100003a6: 4559 cmp r1, fp ; fp = SIGNAL_SPEC = 0x2D879291 (pool 0x100004FC)
```
The corrupted seed is a `0x0A0A0A0A` word in the `.data` init image at
`0x1000EC70` (loaded into `0x20001198` at boot). The byte values `0A 0A 0A 0A`
also appear in the `tbb` jump table at `0x100003D4`; those are not the seed.
**Criterion 5.2: Patch the Seed (4 points).**
The required seed is the ChaCha expand word `0x6B206574` (`"te k"`), stored
little-endian as `74 65 20 6B`.
| File Offset Range | Flash Address Range | Original Bytes | Patched Bytes |
|-------------------|---------------------|----------------|---------------|
| `0xEC70` - `0xEC73` | `0x1000EC70` - `0x1000EC73` | `0A 0A 0A 0A` | `74 65 20 6B` |
**Criterion 5.3: Explain the ARX Derivation (3 points).**
`derive_session_key(seed, iv)` works exactly like this:
1. Set `a = seed`, `b = iv`, `c = 0x61707865`, `d = 0x3320646E`.
2. Run four ChaCha quarter-rounds. A quarter-round runs four phases with rotate
amounts 16, 12, 8, 7. Each phase is: `a = a + b; d = d XOR a;
d = rotate_left(d, s); c = c + d; b = b XOR c; b = rotate_left(b, s)`.
3. Return `a XOR d`.
The runtime IV is `derive_session_key(0x6B206574, 0) = 0x43C974F6`. The shipped
seed `0x0A0A0A0A` gives `derive_session_key(0x0A0A0A0A, 0x43C974F6) =
0x915DCFF8` (MISMATCH). The honest seed `0x6B206574` gives
`derive_session_key(0x6B206574, 0x43C974F6) = 0x2D879291`, which equals
`SIGNAL_SPEC` and prints `OK`.
**Criterion 5.4: Separate the Security Layers (3 points).**
The signal seed only controls the local `SIGNAL KEY` telemetry check. It does
not authenticate the operator. The Argon2id plus XChaCha20-Poly1305 gate is a
separate layer that requires the 12-word passphrase.
### Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|-----------|--------|--------------------------|----------------|-----------|
| Criterion 5.1: Locate SIGNAL_SPEC and the Seed | 5 | `0x2D879291` located and wrong seed `0x0A0A0A0A` found | Partial | Not found |
| Criterion 5.2: Patch the Seed | 4 | Seed bytes changed to `74 65 20 6B` | Wrong byte | Not patched |
| Criterion 5.3: Explain the ARX Derivation | 3 | Correct trace of the per-cycle derivation | Vague | Missing |
| Criterion 5.4: Separate the Security Layers | 3 | Correctly explains what the seed fixes versus the gate | Generic | Missing |
### Instructor Notes & Assembly
- The seed is at `0x1000EC70` in the `.data` init image. The `0A 0A 0A 0A`
bytes in the `tbb` jump table at `0x100003D4` are not the seed.
- The patched seed `74 65 20 6B` is the little-endian form of the ChaCha expand
word `0x6B206574` (`"te k"`).
- The seed only affects the local `SIGNAL KEY` check; the Argon2id plus
XChaCha20-Poly1305 gate is a separate authentication layer.
---
## Task 6: GDB Register Capture of the Derived Key (15 points)
### Solution
**Criterion 6.1: Breakpoint at the Derive Return (4 points).**
The per-cycle derive is `bl derive_session_key` at `0x10000352`. Break at the
next instruction, `0x10000356`, and read `$r0`. On the shipped image it is
`0x915DCFF8`.
```gdb
(gdb) break *0x10000356
(gdb) continue
(gdb) print/x $r0 # 0x915DCFF8 on the corrupted image
```
**Criterion 6.2: Inspect the Two Arguments (4 points).**
At the call entry `0x10000352`:
| Register | Corrupted image | Meaning |
|----------|-----------------|---------|
| `$r0` | `0x0A0A0A0A` | Seed |
| `$r1` | `0x43C974F6` | Derived IV |
**Criterion 6.3: Override the Register (4 points).**
With execution at `0x10000356`, set `$r0` to the spec key, then continue. The
next cycle prints `SIGNAL KEY: 0x2D879291 OK`.
```gdb
(gdb) set $r0 = 0x2D879291
(gdb) continue
```
**Criterion 6.4: Watchpoint on the Stored Key (3 points).**
The key is stored in SRAM at `0x200020DC` (`str r0, [r6, #0]`).
```gdb
(gdb) watch *0x200020DC
```
### Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|-----------|--------|--------------------------|----------------|-----------|
| Criterion 6.1: Breakpoint at the Derive Return | 4 | Correct address and `$r0` read as the bug-derived key | Address off | Not found |
| Criterion 6.2: Inspect the Two Arguments | 4 | Live seed and derived IV captured at the second call | One correct | Missing |
| Criterion 6.3: Override the Register | 4 | `$r0` set to `0x2D879291` and the next cycle shows `OK` | Partial | Missing |
| Criterion 6.4: Watchpoint on the Stored Key | 3 | Watchpoint on the SRAM key location documented | Approximate | Missing |
### Instructor Notes & Assembly
- Confirm the breakpoint is placed at `0x10000356`, the instruction after the
`bl derive_session_key` at `0x10000352`.
- On the shipped image `$r0` reads `0x915DCFF8`; `$r0` is the seed `0x0A0A0A0A`
and `$r1` is the derived IV `0x43C974F6`.
- The stored key lives at `0x200020DC`; accept the documented watchpoint.
---
## Task 7: Recover the Ouroboros Authority Frame (10 points)
### Solution
**Criterion 7.1: Locate Salt, Nonce, Ciphertext, and Tag (4 points).**
| Component | Flash Address | Size |
|-----------|---------------|------|
| Ciphertext + Tag | `0x1000CE94` | 64 B |
| Nonce | `0x1000CED4` | 24 B |
| Salt | `0x1000CEEC` | 16 B |
**Criterion 7.2: Document Argon2id Parameters and Payload Contract (2 points).**
Argon2id: memory `64 KiB`, iterations `3`, parallelism `1`, output key `32 B`,
salt the 16 bytes above. XChaCha20-Poly1305 decrypts the 48-byte ciphertext with
the 16-byte tag. The plaintext is `01 68 65 6C 6C 6F 0D 0A` followed by zeros:
byte 0 turns the GPIO 25 LED on, and bytes 1 through 7 are printed as `hello`
plus carriage-return and newline.
**Criterion 7.3: Authenticate with the 12-Word Passphrase (2 points).**
At the `RESPONSE> ` prompt, type:
```text
orbit olive ladder marble quartz canyon ripple saddle violet ember walnut falcon
```
Expected result (proven on hardware):
```text
hello
AUTHORITY FRAME: VERIFIED
```
**Criterion 7.4: State the Honest Quantum Boundary (2 points).**
Grover-style search halves the effective security exponent of a symmetric key,
so a 256-bit key gives about 128 bits of quantum security. The construction uses
classical symmetric and password-hashing primitives and does not implement NIST
post-quantum standards.
### Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|-----------|--------|--------------------------|----------------|-----------|
| Criterion 7.1: Locate Salt, Nonce, Ciphertext, and Tag | 4 | All three addresses correct in flash | Two correct | Not found |
| Criterion 7.2: Document Argon2id Parameters and Payload Contract | 2 | Correct memory/time/parallelism and payload layout | Partial | Missing |
| Criterion 7.3: Authenticate with the 12-Word Passphrase | 2 | `AUTHORITY FRAME: VERIFIED`, LED on, payload printed | Partial | Not shown |
| Criterion 7.4: State the Honest Quantum Boundary | 2 | Grover halves symmetric exponents; not strict PQC | Generic | Misstates |
### Instructor Notes & Assembly
- Verify the three component addresses in flash: ciphertext plus tag at
`0x1000CE94`, nonce at `0x1000CED4`, salt at `0x1000CEEC`.
- The passphrase is fixed for the lab; a successful gate prints `hello` and
`AUTHORITY FRAME: VERIFIED` and lights the on-board authority LED.
- Grade Criterion 7.4 on the honest boundary: 256-bit symmetric key maps to
about 128 bits under Grover, and the design is not NIST post-quantum.
---
## Task 8: Export and Verify (8 points)
### Solution
**Criterion 8.1: Export CTF-02_fixed.bin (1 point).**
Export the patched program from Ghidra (`File -> Export Program...`, `Binary
Format`) as `CTF-02_fixed.bin`. The shipped image is 62,308 bytes.
**Criterion 8.2: Convert to CTF-02_fixed.uf2 (1 point).**
```bash
python uf2conv.py CTF-02_fixed.bin --base 0x10000000 --family 0xe48bff59 --output CTF-02_fixed.uf2
```
**Criterion 8.3: Hardware Verification (4 points).**
Before patching:
```text
DEEPLINE METRO AUTHORITY
ADAPTIVE SIGNAL WINDOW: 38 MINUTES
USB-CDC 115200 8N1 | AUTHORIZED LAB CONSOLE
TRACK: NORMAL
BLOCK STATE: STABLE
AUTO TRAIN: AUTHORIZED
BLOCK LENGTH: 3200 M
FAULT POLLS: 1
SIGNAL KEY: 0x915DCFF8 MISMATCH
RESPONSE>
```
After all four patches:
```text
DEEPLINE METRO AUTHORITY
ADAPTIVE SIGNAL WINDOW: 38 MINUTES
USB-CDC 115200 8N1 | AUTHORIZED LAB CONSOLE
TRACK: DANGER
BLOCK STATE: CRITICAL
AUTO TRAIN: HELD
BLOCK LENGTH: 320 M
FAULT POLLS: 1
SIGNAL KEY: 0x2D879291 OK
RESPONSE>
```
**Criterion 8.4: Summary Table of All Patches (2 points).**
| # | Bug | File Offset | Flash Address | Original Bytes | Patched Bytes |
|---|-----|-------------|---------------|----------------|---------------|
| 1a | Operator threshold | `0x0302` | `0x10000302` | `5E 2B` | `3B 2B` |
| 1b | Dispatch threshold | `0x0312` | `0x10000312` | `5E 2B` | `3B 2B` |
| 2 | TRACK banner | `0xC4BF` - `0xC4C4` | `0x1000C4BF` - `0x1000C4C4` | `4E 4F 52 4D 41 4C` | `44 41 4E 47 45 52` |
| 3 | Block length | `0xEC60` - `0xEC67` | `0x1000EC60` - `0x1000EC67` | `9A 99 99 99 99 99 09 40` | `7B 14 AE 47 E1 7A D4 3F` |
| 4 | Signal seed | `0xEC70` - `0xEC73` | `0x1000EC70` - `0x1000EC73` | `0A 0A 0A 0A` | `74 65 20 6B` |
### Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|-----------|--------|--------------------------|----------------|-----------|
| Criterion 8.1: Export CTF-02_fixed.bin | 1 | Valid patched binary | Corrupted | Not submitted |
| Criterion 8.2: Convert to CTF-02_fixed.uf2 | 1 | Correct base and family flags | Wrong flags | Not submitted |
| Criterion 8.3: Hardware Verification | 4 | Corrected console output confirmed on hardware | Some lines corrected | No verification |
| Criterion 8.4: Summary Table of All Patches | 2 | 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 `26/26 checks passed` against `CTF-02.bin`.
- Confirm the UF2 conversion used base `0x10000000` and family `0xe48bff59`.
- **Serial Terminal Timing:** Note that `print_identity()` (`TRACK: NORMAL`)
fires within the first 5 milliseconds of boot. In normal lab usage, PuTTY
attaches after boot and will display the continuous 2-second status loop
(`BLOCK STATE: CRITICAL`, `AUTO TRAIN: HELD`). To see the corrected banner,
the student must pulse `RUN` to `GND` while PuTTY is open, or demonstrate
the string change at `0x1000C4BF` via Ghidra static analysis.
- The shipped image is 62,308 bytes; confirm the exported corrected image is a
valid patched binary with all four fixes present.
---
## Task 9: Written Reflection (5 points)
### Solution
**Criterion 9.1: "Rushed Build" Is Not an Excuse (2 points).**
The rebuild shipped four constants that were never checked against their
documented limits, which is exactly what produced the false-safe reading.
Pressure explains why the checks were skipped, not why they should be skipped.
**Criterion 9.2: One Engineering Practice per Failure Area (3 points).**
- Physical limits (Bugs #1 and #3): one shared configuration header plus a
build-time assertion that each compiled limit matches its documented value.
- Banner (Bug #2): remove static banners; a hardware-in-the-loop test that
compares displayed state to the live register.
- Seed integrity (Bug #4): reproducible builds with golden artifact hash
comparison so keys and seeds match the certified specification.
- Image authenticity: enable RP2350 hardware secure boot with OTP hash
verification so a modified image will not run.
### Grading Rubric (1-to-1 Mapping)
| Criterion | Points | Full Credit (Answer Key) | Partial Credit | No Credit |
|-----------|--------|--------------------------|----------------|-----------|
| Criterion 9.1: "Rushed Build" Is Not an Excuse | 2 | Specific, grounded reasoning | Generic | Missing |
| Criterion 9.2: One Engineering Practice per Failure Area | 3 | Concrete practices for the bugs and for image authenticity | Names some | Missing |
### Instructor Notes & Assembly
- Grade the specificity of the reasoning, not the length of the prose.
- Require concrete practices across the failure areas, including at least one
practice for image authenticity.
---
## How To Breadboard
- **Raspberry Pi Pico 2** powered over USB.
- **USB-CDC virtual serial console:** open the Pico's COM port at 115200 baud,
8 data bits, no parity, 1 stop bit.
- **SWD debug probe:** connect SWCLK, SWDIO, GND, and 3.3 V to the Pico debug
header for GDB inspection and register capture.
- No other peripherals are required; the authority LED is on-board.
---
## Complete Grading Summary
| Task | Title | Points |
|------|-------|--------|
| Task 1 | Setup and Initial Analysis | 12 |
| Task 2 | Find and Patch Bug #1: The Miscalibrated Release Threshold | 15 |
| Task 3 | Find and Patch Bug #2: The False TRACK Banner | 10 |
| Task 4 | Find and Patch Bug #3: The Block Length Constant | 10 |
| Task 5 | Find and Patch Bug #4: The Signal Seed | 15 |
| Task 6 | GDB Register Capture of the Derived Key | 15 |
| Task 7 | Recover the Ouroboros Authority Frame | 10 |
| Task 8 | Export and Verify | 8 |
| Task 9 | Written Reflection | 5 |
| **TOTAL** | | **100** |
---
## Instructor Notes
Safety: Use only the supplied Pico 2, SWD probe, and firmware. Never connect the
exercise to an operational railway, metro system, public network, military
system, or third-party device.
### Common Student Mistakes
- Patching only one threshold site (`0x10000302` or `0x10000312`), leaving 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 block length as an integer and missing the 8-byte double in
`.data`.
- Using the wrong `0.32` bytes and printing `316 M` instead of `320 M`.
- Treating the odd vector address `0x1000015B` as invalid instead of clearing
bit 0 to get `0x1000015A`.
- Starting the seed patch at the wrong offset; the seed is at `0x1000EC70`.
### 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.
- Award partial credit for a correct block-length patch without the IEEE-754
print math.
- Award partial credit for one of the two GDB argument captures, or for a
partial register override.
- Award no credit for patches that change string length or overwrite adjacent
flash.
---
## Appendix: Expected Binary Diff
| # | Bug | File Offset(s) | Flash Address(es) | Original Bytes | Patched Bytes |
|---|-----|----------------|-------------------|----------------|---------------|
| 1a | Operator threshold | `0x0302` | `0x10000302` | `5E 2B` | `3B 2B` |
| 1b | Dispatch threshold | `0x0312` | `0x10000312` | `5E 2B` | `3B 2B` |
| 2 | TRACK banner | `0xC4BF` - `0xC4C4` | `0x1000C4BF` - `0x1000C4C4` | `4E 4F 52 4D 41 4C` | `44 41 4E 47 45 52` |
| 3 | Block length | `0xEC60` - `0xEC67` | `0x1000EC60` - `0x1000EC67` | `9A 99 99 99 99 99 09 40` | `7B 14 AE 47 E1 7A D4 3F` |
| 4 | Signal seed | `0xEC70` - `0xEC73` | `0x1000EC70` - `0x1000EC73` | `0A 0A 0A 0A` | `74 65 20 6B` |
Four defects, five changed regions: two immediate bytes (`0x3B 2B` at each
threshold), six banner bytes, eight block-length bytes, and four seed bytes.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+143
View File
@@ -0,0 +1,143 @@
// 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/encryption-c-rp2350
// File: auth.h
// Desc: Declares the Ouroboros authentication engine API for RP2350 firmware.
// Created: 2026
#ifndef AUTH_H
#define AUTH_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/**
* @brief Onboard LED GPIO pin number.
*
* The RP2350 Pico 2 onboard LED is connected to GPIO 25. Driven high
* on successful authentication and low on failure or idle.
*/
#define AUTH_LED_PIN 25u
/**
* @brief Maximum accepted terminal passphrase length in bytes.
*
* The CLI accepts interactive human-entered passphrases up to 512 bytes,
* matching the hardened host demo boundary before policy validation.
*/
#define AUTH_PASSPHRASE_MAX_LEN 512u
/**
* @brief Required number of lowercase words in the hardened passphrase.
*
* The embedded hardened workflow matches the host-side policy exactly:
* twelve lowercase ASCII words separated by whitespace.
*/
#define AUTH_REQUIRED_WORDS 12u
/**
* @brief Hardened Argon2id salt size in bytes.
*
* Every demo artifact carries a per-ciphertext random 128-bit salt.
*/
#define AUTH_SALT_SIZE 16u
/**
* @brief Hardened XChaCha20 nonce size in bytes.
*
* XChaCha20-Poly1305 consumes a 192-bit nonce in the outer construction.
*/
#define AUTH_NONCE_SIZE 24u
/**
* @brief Subkey size in bytes derived from Argon2id.
*
* The AEAD key size is 256 bits.
*/
#define AUTH_KEY_SIZE 32u
/**
* @brief AEAD authentication tag size in bytes.
*
* XChaCha20-Poly1305 appends a 128-bit authentication tag.
*/
#define AUTH_TAG_SIZE 16u
/**
* @brief Plaintext payload size in bytes.
*
* The fixed dispatch payload is 48 bytes: LED state, UART bytes,
* and trailing reserved bytes matching the Rust hardened demo layout.
*/
#define AUTH_PAYLOAD_SIZE 48u
/**
* @brief Full ciphertext-plus-tag artifact size in bytes.
*
* The encrypted payload is 48 bytes followed by a 16-byte tag.
*/
#define AUTH_CIPHERTEXT_SIZE (AUTH_PAYLOAD_SIZE + AUTH_TAG_SIZE)
/**
* @brief Authentication result codes returned by the hardened engine.
*
* These values let the CLI distinguish policy failures from
* cryptographic authentication failures without guessing.
*/
typedef enum auth_result {
AUTH_RESULT_SUCCESS = 0,
AUTH_RESULT_POLICY_VIOLATION = 1,
AUTH_RESULT_AUTHENTICATION_FAILED = 2,
AUTH_RESULT_INTERNAL_ERROR = 3,
} auth_result_t;
/**
* @brief Initialize the Ouroboros authentication module.
*
* Configures the onboard LED GPIO and marks the hardened engine as ready
* for passphrase authentication.
*
* @param None.
* @return bool true when initialization is successful, else false.
*/
bool auth_init(void);
/**
* @brief Execute the hardened Ouroboros authentication pipeline.
*
* Validates the strict 12-word lowercase passphrase policy, derives the
* 256-bit AEAD key with Argon2id using artifact parameters, decrypts the
* embedded XChaCha20-Poly1305 ciphertext, and dispatches GPIO25/UART
* payload bytes on success.
*
* @param passphrase Pointer to passphrase bytes.
* @param passphrase_len Number of passphrase bytes.
* @return auth_result_t Detailed authentication outcome for the caller.
*/
auth_result_t auth_execute(const uint8_t *passphrase,
size_t passphrase_len);
#endif // AUTH_H
+59
View File
@@ -0,0 +1,59 @@
// 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: cli.h
// Desc: Declares the CLI UART passphrase input interface for Ouroboros.
// Created: 2026
#ifndef CLI_H
#define CLI_H
#include <stddef.h>
/**
* @brief Print the UART passphrase prompt.
*
* Emits a minimal shell-style prompt followed by a space so the
* terminal clearly indicates that hardened passphrase input is expected.
*
* @param None.
* @return None.
*/
void print_prompt(void);
/**
* @brief Service one UART polling step for passphrase input.
*
* Polls stdio for a character, dispatches backspace or newline
* handling, and appends printable characters to the passphrase
* buffer. Call repeatedly from the main loop.
*
* @param buf Pointer to mutable passphrase buffer.
* @param idx Pointer to current buffer length.
* @return None.
*/
void service_uart(char *buf, size_t *idx);
#endif // CLI_H
+60
View File
@@ -0,0 +1,60 @@
// 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.
//
// This file is generated by scripts/dec.py. Do not edit by hand.
#ifndef DEMO_ARTIFACT_H
#define DEMO_ARTIFACT_H
#include <stdint.h>
#define DEMO_ARTIFACT_FORMAT "ouroboros-hardened-demo-v1"
#define DEMO_MEMORY_KIB 64u
#define DEMO_ITERATIONS 3u
#define DEMO_PARALLELISM 1u
static const uint8_t DEMO_SALT[16] = {
0xF2u, 0xD5u, 0x18u, 0x63u, 0x9Au, 0x82u, 0x01u, 0x9Du,
0xC2u, 0xD7u, 0xAFu, 0xA5u, 0xCDu, 0xB6u, 0xD8u, 0x71u
};
static const uint8_t DEMO_NONCE[24] = {
0x1Cu, 0xEFu, 0x79u, 0x0Du, 0x77u, 0x9Eu, 0x7Cu, 0x04u,
0xE7u, 0xF0u, 0x66u, 0xDDu, 0x90u, 0xD0u, 0x80u, 0x70u,
0x87u, 0x97u, 0x67u, 0x1Fu, 0x79u, 0xEFu, 0xC4u, 0xE4u
};
static const uint8_t DEMO_CIPHERTEXT_AND_TAG[64] = {
0x2Cu, 0x23u, 0xB2u, 0x7Eu, 0x95u, 0x62u, 0xB8u, 0xEDu,
0x9Eu, 0x08u, 0xE0u, 0x6Du, 0xD9u, 0x9Du, 0xB4u, 0x91u,
0x3Eu, 0x81u, 0x9Au, 0x77u, 0x8Bu, 0xB4u, 0x7Bu, 0x71u,
0xBCu, 0x66u, 0x1Eu, 0x6Eu, 0x73u, 0x1Au, 0x81u, 0x54u,
0xCDu, 0xB5u, 0x36u, 0xA4u, 0x76u, 0x7Eu, 0x9Bu, 0xF8u,
0x53u, 0x3Eu, 0x03u, 0x1Du, 0xB8u, 0xE5u, 0xAEu, 0x7Au,
0xADu, 0xB4u, 0x31u, 0xCFu, 0x12u, 0xD9u, 0xF9u, 0xC4u,
0x5Fu, 0xA9u, 0xB9u, 0x4Bu, 0x80u, 0xDCu, 0xBBu, 0xDEu
};
#endif // DEMO_ARTIFACT_H
+39
View File
@@ -0,0 +1,39 @@
// 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: mbedtls_config.h
// Desc: Configures the minimal mbedTLS cryptographic features required by
// the Ouroboros AEAD engine on RP2350.
// Created: 2026
#ifndef MBEDTLS_CONFIG_H
#define MBEDTLS_CONFIG_H
#define MBEDTLS_CHACHA20_C
#define MBEDTLS_CHACHAPOLY_C
#define MBEDTLS_POLY1305_C
#define MBEDTLS_PLATFORM_C
#endif // MBEDTLS_CONFIG_H
+121
View File
@@ -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})
+944
View File
@@ -0,0 +1,944 @@
"""Generate hardened demo artifacts for the RP2350 Ouroboros firmware.
This script writes the same JSON schema used by the Rust demo and also emits
the generated C header consumed by the embedded firmware.
"""
import argparse
import json
import platform
import secrets
import sys
from pathlib import Path
from typing import Optional
DEFAULT_PASSPHRASE = (
"orbit olive ladder marble quartz canyon "
"ripple saddle violet ember walnut falcon"
)
DEFAULT_TEXT = "hello"
DEFAULT_OUTPUT_JSON = "scripts/demo_artifact.json"
DEFAULT_OUTPUT_HEADER = "include/demo_artifact.h"
DEFAULT_MEMORY_KIB = 64
DEFAULT_ITERATIONS = 3
DEFAULT_PARALLELISM = 1
ARTIFACT_FORMAT = "ouroboros-hardened-demo-v1"
_KEY_HELP = "12-word lowercase passphrase"
_TEXT_HELP = "Text to place in payload bytes 1..7"
_OUT_HELP = "Output JSON artifact path"
_HEADER_OUT_HELP = "Output generated C header path"
_FROM_JSON_HELP = (
"Load existing JSON artifact and emit header without re-encrypting"
)
_CHECK_HEADER_HELP = (
"Optional path to compare against generated header and fail if stale"
)
_SALT_HEX_HELP = "Optional fixed 16-byte salt as hex"
_NONCE_HEX_HELP = "Optional fixed 24-byte nonce as hex"
_NO_CRLF_HELP = "Do not append CRLF to payload text"
_LED_OFF_HELP = "Encode LED off instead of on"
_MEMORY_HELP = "Argon2 memory cost in KiB"
_ITERATIONS_HELP = "Argon2 time cost"
_PARALLELISM_HELP = "Argon2 parallel lanes"
_POLICY_ERROR = (
"Hardened mode requires exactly 12 lowercase ASCII words in --key."
)
_PAYLOAD_TOO_LONG = (
"Output text is too long for fixed dispatch "
"(max 7 bytes after CRLF handling)."
)
_INVALID_JSON = "Artifact JSON at {0} is invalid JSON."
_MISMATCH_PREFIX = "Detected a Python native-extension architecture mismatch. "
_REINSTALL_DEPS = ("Recreate this virtual environment with a native Python "
"and reinstall deps:")
_REINSTALL_LINES = (
"rm -rf .venv",
"python3 -m venv .venv",
"source .venv/bin/activate",
"python3 -m pip install -U pip setuptools wheel",
"python3 -m pip install argon2-cffi pynacl",
)
_HEADER_TEMPLATE = """// 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.
//
// This file is generated by scripts/dec.py. Do not edit by hand.
#ifndef DEMO_ARTIFACT_H
#define DEMO_ARTIFACT_H
#include <stdint.h>
#define DEMO_ARTIFACT_FORMAT "{artifact_format}"
#define DEMO_MEMORY_KIB {memory_kib}u
#define DEMO_ITERATIONS {iterations}u
#define DEMO_PARALLELISM {parallelism}u
static const uint8_t DEMO_SALT[16] = {{
{salt_body}
}};
static const uint8_t DEMO_NONCE[24] = {{
{nonce_body}
}};
static const uint8_t DEMO_CIPHERTEXT_AND_TAG[64] = {{
{cipher_body}
}};
#endif // DEMO_ARTIFACT_H
"""
def _raise_dependency_error(package_name, install_hint, exc):
"""Raise a RuntimeError with environment-aware dependency diagnostics.
Parameters
----------
package_name : str
Package common name for the error message.
install_hint : str
Pip install command in the error message.
exc : Exception
Import error observed while loading the native module.
Returns
-------
None
"""
message = "Hardened mode requires {0}. Install with: {1}".format(
package_name, install_hint)
message += _reinstall_message() if _is_arch_mismatch(exc) else ""
raise RuntimeError(message) from exc
def _is_arch_mismatch(exc):
"""Report whether the interpreter likely has a native-extension mismatch.
Parameters
----------
exc : Exception
Import error observed while loading the native module.
Returns
-------
bool
True when the error text matches a native architecture mismatch.
"""
detail = str(exc)
return (
"incompatible architecture" in detail
or "_cffi_backend" in detail
or "mach-o file, but is an incompatible architecture" in detail
)
def _reinstall_message():
"""Build the native-interpreter reinstall diagnostic text.
Parameters
----------
None
Returns
-------
str
Newline-delimited machine and reinstall details, or an empty string.
"""
machine = platform.machine()
head = (_MISMATCH_PREFIX
+ "Current interpreter reports machine=" + machine
+ ", executable=" + sys.executable + ".")
lines = [" {0}".format(item) for item in _REINSTALL_LINES]
return ("\n" + head + "\n" + _REINSTALL_DEPS + "\n"
+ "\n".join(lines))
def _is_policy_compliant(passphrase):
"""Return True when passphrase is exactly 12 lowercase ASCII words.
Parameters
----------
passphrase : str
Candidate operator passphrase.
Returns
-------
bool
True when the passphrase satisfies the gate policy.
"""
words = passphrase.split()
if len(words) != 12:
return False
return all(
word and all(ch.isascii() and ch.islower() for ch in word)
for word in words
)
def _build_payload(text_str, led_on=True, append_crlf=True):
"""Build the fixed 48-byte payload dispatched by the firmware.
Parameters
----------
text_str : str
Console text placed in payload bytes 1..7.
led_on : bool
True turns the LED byte on, False leaves it off.
append_crlf : bool
True appends CRLF to the console text.
Returns
-------
bytes
Fixed 48-byte dispatch payload.
"""
tx_bytes = text_str.encode() + (b"\r\n" if append_crlf else b"")
if len(tx_bytes) > 7:
raise ValueError(_PAYLOAD_TOO_LONG)
payload = bytearray(48)
payload[0] = 1 if led_on else 0
payload[1:1 + len(tx_bytes)] = tx_bytes
return bytes(payload)
def _resolve_salt_nonce(salt, nonce):
"""Confirm or generate the 16-byte salt and 24-byte nonce.
Parameters
----------
salt : bytes or None
Optional fixed salt value.
nonce : bytes or None
Optional fixed nonce value.
Returns
-------
tuple
Confirmed (salt, nonce) byte values.
"""
salt_word = secrets.token_bytes(16) if salt is None else salt
nonce_word = secrets.token_bytes(24) if nonce is None else nonce
if len(salt_word) != 16:
raise ValueError("Hardened salt must be exactly 16 bytes.")
if len(nonce_word) != 24:
raise ValueError("Hardened nonce must be exactly 24 bytes.")
return salt_word, nonce_word
def _optional_hex(value, length, label):
"""Decode an optional hex argument, leaving absent values as None.
Parameters
----------
value : str or None
Hex string supplied on the command line.
length : int
Expected decoded byte length.
label : str
Field name used in validation errors.
Returns
-------
bytes or None
Decoded bytes, or None when value is absent.
"""
return _hex_decode(value, length, label) if value else None
def _load_argon2():
"""Load the Argon2 low-level binding with dependency diagnostics.
Parameters
----------
None
Returns
-------
tuple
Argon2 Type enum and hash_secret_raw callable.
"""
try:
from argon2.low_level import Type, hash_secret_raw
except ImportError as exc:
install_hint = "python3 -m pip install argon2-cffi"
_raise_dependency_error("argon2-cffi", install_hint, exc)
return Type, hash_secret_raw
def _load_nacl_encrypt():
"""Load the XChaCha20-Poly1305 encrypt binding with diagnostics.
Parameters
----------
None
Returns
-------
callable
PyNaCl crypto_aead_xchacha20poly1305_ietf_encrypt function.
"""
try:
from nacl.bindings import (
crypto_aead_xchacha20poly1305_ietf_encrypt as encrypt,
)
except ImportError as exc:
install_hint = "python3 -m pip install pynacl"
_raise_dependency_error("PyNaCl", install_hint, exc)
return encrypt
def _derive_hardened_key(passphrase, salt, memory_kib, iterations,
parallelism):
"""Derive a 32-byte key with Argon2id.
Parameters
----------
passphrase : str
Policy-compliant operator passphrase.
salt : bytes
16-byte Argon2 salt.
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
Returns
-------
bytes
Derived 32-byte key.
"""
arg2_type, hash_secret_raw = _load_argon2()
args = (
passphrase.encode(), salt, iterations, memory_kib,
parallelism, 32, arg2_type,
)
return hash_secret_raw(*args)
def _entry_key(phrase, seed, memory_kib, iterations, parallelism):
"""Derive the entry key word for the hardened artifact.
Parameters
----------
phrase : str
Policy-compliant operator passphrase.
seed : bytes
16-byte Argon2 salt.
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
Returns
-------
bytes
Derived 32-byte entry key.
"""
return _derive_hardened_key(
phrase, seed, memory_kib, iterations, parallelism
)
def _build_key_material(phrase, salt, nonce, memory_kib, iterations,
parallelism):
"""Resolve salt, nonce, and the entry key.
Parameters
----------
phrase : str
Policy-compliant operator passphrase.
salt : bytes or None
Optional fixed salt value.
nonce : bytes or None
Optional fixed nonce value.
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
Returns
-------
tuple
Resolved (salt, nonce, key) values.
"""
seed, nonce_word = _resolve_salt_nonce(salt, nonce)
key = _entry_key(phrase, seed, memory_kib, iterations, parallelism)
return seed, nonce_word, key
def build_hardened_entry(
key_str,
text_str,
led_on=True,
append_crlf=True,
salt: Optional[bytes] = None,
nonce: Optional[bytes] = None,
memory_kib=DEFAULT_MEMORY_KIB,
iterations=DEFAULT_ITERATIONS,
parallelism=DEFAULT_PARALLELISM,
):
"""Build a hardened encrypted entry with Argon2id + XChaCha20-Poly1305.
Parameters
----------
key_str : str
Policy-compliant 12-word operator passphrase.
text_str : str
Console text placed in payload bytes 1..7.
led_on : bool
True turns the LED byte on, False leaves it off.
append_crlf : bool
True appends CRLF to the console text.
salt : bytes or None
Optional fixed 16-byte salt.
nonce : bytes or None
Optional fixed 24-byte nonce.
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
Returns
-------
tuple
Resulting (salt, nonce, ciphertext_and_tag) values.
"""
if not _is_policy_compliant(key_str):
raise ValueError(_POLICY_ERROR)
salt_word, nonce_word, key = _build_key_material(
key_str, salt, nonce, memory_kib, iterations, parallelism)
payload = _build_payload(text_str, led_on, append_crlf)
encrypt = _load_nacl_encrypt()
ciphertext_and_tag = encrypt(payload, b"", nonce_word, key)
return salt_word, nonce_word, ciphertext_and_tag
def _hex_decode(value, expected_len, label):
"""Decode a hex string and validate the expected byte length.
Parameters
----------
value : str
Hex string to decode.
expected_len : int
Required decoded byte length.
label : str
Field name used in validation errors.
Returns
-------
bytes
Decoded bytes of the expected length.
"""
try:
decoded = bytes.fromhex(value)
except ValueError as exc:
raise ValueError("{0} must be valid hex.".format(label)) from exc
if len(decoded) != expected_len:
message = "{0} must decode to exactly {1} bytes.".format(
label, expected_len)
raise ValueError(message)
return decoded
def _artifact_dict(memory_kib, iterations, parallelism, salt, nonce, cipher):
"""Compose the canonical artifact dictionary.
Parameters
----------
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
salt : bytes
16-byte salt.
nonce : bytes
24-byte nonce.
cipher : bytes
64-byte ciphertext and tag.
Returns
-------
dict
Canonical artifact fields with hex-encoded byte values.
"""
return {
"format": ARTIFACT_FORMAT, "memory_kib": memory_kib,
"iterations": iterations, "parallelism": parallelism,
"salt_hex": salt.hex(), "nonce_hex": nonce.hex(),
"ciphertext_and_tag_hex": cipher.hex(),
}
def _format_c_array(data, width=8):
"""Format bytes as an indented C array literal body.
Parameters
----------
data : bytes
Bytes to serialize as a C array.
width : int
Byte values emitted per source line.
Returns
-------
str
Indented, comma-joined C array body.
"""
items = [f"0x{value:02X}u" for value in data]
rows = [", ".join(items[offset:offset + width])
for offset in range(0, len(items), width)]
return ",\n".join(" " + row for row in rows)
def _render_header(artifact):
"""Render the generated firmware header text from the artifact.
Parameters
----------
artifact : dict
Canonical artifact dictionary.
Returns
-------
str
Complete generated C header text.
"""
cipher = bytes.fromhex(artifact["ciphertext_and_tag_hex"])
return _HEADER_TEMPLATE.format(
artifact_format=ARTIFACT_FORMAT, memory_kib=artifact["memory_kib"],
iterations=artifact["iterations"], parallelism=artifact["parallelism"],
salt_body=_format_c_array(bytes.fromhex(artifact["salt_hex"])),
nonce_body=_format_c_array(bytes.fromhex(artifact["nonce_hex"])),
cipher_body=_format_c_array(cipher),
)
def _write_header(path, artifact):
"""Write the generated firmware header from the hardened artifact.
Parameters
----------
path : str
Destination header file path.
artifact : dict
Canonical artifact dictionary.
Returns
-------
Path
Resolved destination header path.
"""
output_path = Path(path)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(_render_header(artifact), encoding="utf-8")
return output_path.resolve()
def _parse_json_file(path):
"""Load and parse the artifact JSON file.
Parameters
----------
path : str
Artifact JSON file path.
Returns
-------
dict
Parsed JSON document.
"""
raw = Path(path).read_text(encoding="utf-8")
try:
parsed = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(_INVALID_JSON.format(path)) from exc
return parsed
def _validate_artifact_format(parsed):
"""Reject artifact JSON with an unexpected format marker.
Parameters
----------
parsed : dict
Parsed JSON document.
Returns
-------
None
"""
actual = parsed.get("format")
if actual != ARTIFACT_FORMAT:
raise ValueError(
"Artifact format must be '{0}', got '{1}'.".format(
ARTIFACT_FORMAT, actual))
def _parsed_ints(parsed):
"""Parse the integer cost fields from artifact JSON.
Parameters
----------
parsed : dict
Parsed JSON document.
Returns
-------
list
Parsed memory_kib, iterations, and parallelism integer values.
"""
try:
int_names = ("memory_kib", "iterations", "parallelism")
return [int(parsed[name]) for name in int_names]
except (KeyError, TypeError, ValueError) as exc:
raise ValueError(
"Artifact must include integer memory_kib, "
"iterations, and parallelism fields.") from exc
def _artifact_ints(parsed):
"""Unpack the three integer cost fields from artifact JSON.
Parameters
----------
parsed : dict
Parsed JSON document.
Returns
-------
tuple
(memory_kib, iterations, parallelism) integer values.
"""
values = _parsed_ints(parsed)
return values[0], values[1], values[2]
def _artifact_from_parsed(parsed):
"""Reconstruct a canonical artifact dict from parsed JSON.
Parameters
----------
parsed : dict
Parsed JSON document.
Returns
-------
dict
Canonical artifact dictionary.
"""
_validate_artifact_format(parsed)
memory_kib, iterations, parallelism = _artifact_ints(parsed)
cipher_field = "ciphertext_and_tag_hex"
salt = _hex_decode(parsed.get("salt_hex", ""), 16, "salt_hex")
nonce = _hex_decode(parsed.get("nonce_hex", ""), 24, "nonce_hex")
cipher = _hex_decode(parsed.get(cipher_field, ""), 64, cipher_field)
return _artifact_dict(
memory_kib, iterations, parallelism, salt, nonce, cipher)
def _load_artifact_json(path):
"""Load and validate a hardened artifact JSON for header generation.
Parameters
----------
path : str
Artifact JSON file path.
Returns
-------
dict
Canonical artifact dictionary.
"""
parsed = _parse_json_file(path)
return _artifact_from_parsed(parsed)
def _check_header_match(generated_path, expected_path):
"""Fail when the generated header does not match an expected file.
Parameters
----------
generated_path : str
Generated header file path.
expected_path : str
Expected committed header file path.
Returns
-------
None
"""
generated = Path(generated_path).read_text(encoding="utf-8")
expected = Path(expected_path).read_text(encoding="utf-8")
if generated != expected:
raise RuntimeError(
"Generated header does not match committed "
"include/demo_artifact.h. Regenerate and commit "
"updated artifacts with scripts/dec.py.")
def _print_header_check(header_path, expected_path):
"""Print the verified header match result.
Parameters
----------
header_path : str
Generated header file path.
expected_path : str
Expected committed header file path.
Returns
-------
None
"""
_check_header_match(header_path, expected_path)
print("Verified header matches: {0}".format(Path(expected_path).resolve()))
def _parse_args():
"""Parse command-line arguments.
Parameters
----------
None
Returns
-------
argparse.Namespace
Parsed command-line arguments.
"""
parser = argparse.ArgumentParser(description=__doc__)
for argument_group in _ARGUMENT_GROUPS:
for name, kwargs in argument_group:
parser.add_argument(name, **kwargs)
return parser.parse_args()
def _build_from_args(args, salt, nonce):
"""Build the hardened entry from parsed arguments.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
salt : bytes or None
Optional fixed salt value.
nonce : bytes or None
Optional fixed nonce value.
Returns
-------
tuple
Resulting (salt, nonce, ciphertext_and_tag) values.
"""
return build_hardened_entry(
key_str=args.key, text_str=args.text, led_on=not args.led_off,
append_crlf=not args.no_crlf, salt=salt, nonce=nonce,
memory_kib=args.memory_kib, iterations=args.iterations,
parallelism=args.parallelism,
)
def _flush_outputs(args, salt, nonce, cipher):
"""Write the artifact JSON and header, then return the header path.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
salt : bytes
16-byte salt.
nonce : bytes
24-byte nonce.
cipher : bytes
64-byte ciphertext and tag.
Returns
-------
Path
Resolved generated header path.
"""
json_path, artifact = _write_demo_json(
args.out, args.memory_kib, args.iterations, args.parallelism,
salt, nonce, cipher)
header_path = _write_header(args.header_out, artifact)
print("Wrote hardened demo artifact JSON: {0}".format(json_path))
print("Wrote generated firmware header: {0}".format(header_path))
return header_path
def _write_demo_json(path, memory_kib, iterations, parallelism, salt,
nonce, ciphertext_and_tag):
"""Write the hardened JSON artifact consumed by docs and validation.
Parameters
----------
path : str
Destination JSON file path.
memory_kib : int
Argon2 memory cost in KiB.
iterations : int
Argon2 time cost.
parallelism : int
Argon2 parallel lane count.
salt : bytes
16-byte salt.
nonce : bytes
24-byte nonce.
ciphertext_and_tag : bytes
64-byte ciphertext and tag.
Returns
-------
tuple
Resolved (path, artifact) values.
"""
output_path = Path(path)
output_path.parent.mkdir(parents=True, exist_ok=True)
artifact = _artifact_dict(
memory_kib, iterations, parallelism, salt, nonce, ciphertext_and_tag)
text = json.dumps(artifact, indent=2) + "\n"
output_path.write_text(text, encoding="utf-8")
return output_path.resolve(), artifact
def _flush_from_json(args):
"""Write the header from an existing artifact JSON.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
Returns
-------
Path
Resolved generated header path.
"""
artifact = _load_artifact_json(args.from_json)
header_path = _write_header(args.header_out, artifact)
print("Wrote generated firmware header: {0}".format(header_path))
return header_path
def _run_from_json(args):
"""Generate the header only from committed artifact JSON.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
Returns
-------
None
"""
header_path = _flush_from_json(args)
if args.check_header_path:
_print_header_check(header_path, args.check_header_path)
def _run_from_generate(args):
"""Encrypt fresh artifact material, then emit JSON and the header.
Parameters
----------
args : argparse.Namespace
Parsed command-line arguments.
Returns
-------
None
"""
salt = _optional_hex(args.salt_hex, 16, "salt_hex")
nonce = _optional_hex(args.nonce_hex, 24, "nonce_hex")
header_path = _flush_outputs(
args, *_build_from_args(args, salt, nonce),
)
if args.check_header_path:
_print_header_check(header_path, args.check_header_path)
def main():
"""Generate the hardened artifact JSON and C header.
Parameters
----------
None
Returns
-------
None
"""
args = _parse_args()
if args.from_json:
_run_from_json(args)
else:
_run_from_generate(args)
_ARGUMENT_GROUPS = (
(
("--key", dict(default=DEFAULT_PASSPHRASE, help=_KEY_HELP)),
("--salt-hex", dict(help=_SALT_HEX_HELP)),
("--nonce-hex", dict(help=_NONCE_HEX_HELP)),
),
(
("--text", dict(default=DEFAULT_TEXT, help=_TEXT_HELP)),
("--out", dict(default=DEFAULT_OUTPUT_JSON, help=_OUT_HELP)),
("--header-out",
dict(default=DEFAULT_OUTPUT_HEADER, help=_HEADER_OUT_HELP)),
("--from-json", dict(help=_FROM_JSON_HELP)),
("--no-crlf", dict(action="store_true", help=_NO_CRLF_HELP)),
("--led-off", dict(action="store_true", help=_LED_OFF_HELP)),
),
(
("--memory-kib",
dict(type=int, default=DEFAULT_MEMORY_KIB, help=_MEMORY_HELP)),
("--iterations",
dict(type=int, default=DEFAULT_ITERATIONS, help=_ITERATIONS_HELP)),
("--parallelism",
dict(type=int, default=DEFAULT_PARALLELISM,
help=_PARALLELISM_HELP)),
("--check-header-path", dict(help=_CHECK_HEADER_HELP)),
),
)
if __name__ == "__main__":
main()
Loaded 100 of 162 files, more files were not shown because too many files have changed in this diff. Show more