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