mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-10-13 19:20:54 +02:00
Audit-clean C standard across all 5 cb challenges: fix 3 functions over 8 exec lines; rebuild bins; update CB-0x03 and CB-0x05 guides; verify all guides against new bins
This commit is contained in:
1 parent
4ade66738f
commit
23b9f628ef
51 files changed
+3744
-7904
No files matched your search
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: embedded-c-standard
|
||||
description: Use ONLY when writing, editing, or reviewing C firmware in a picokit project (firmware/src, firmware/include, firmware/tests). Enforces the strict C standard: no function over 8 executable lines, no blank lines inside a function body, a Doxygen block with @brief/@param/@return on every function, a fixed module layout and naming scheme, and a ban on em/en dashes. Run the bundled audit_c_standard.py before every commit.
|
||||
---
|
||||
|
||||
# Embedded C standard (picokit)
|
||||
|
||||
This is non-negotiable and identical in every project in the series. There is
|
||||
no variation between projects. If a rule is inconvenient, refactor; do not
|
||||
exempt.
|
||||
|
||||
## Rules
|
||||
|
||||
1. No function longer than 8 executable lines. The audit counts every
|
||||
non-blank, non-comment, non-brace line in the body, including continuation
|
||||
lines of a wrapped statement. Split the work into a `static` helper.
|
||||
2. No blank lines inside a function body. Separate logical steps with a
|
||||
comment, never with a blank line.
|
||||
3. Documentation lives in the header. Every function declaration in a `.h`
|
||||
carries a Doxygen block with `@brief`, a `@param` for each parameter
|
||||
(`@param void No parameters.` when there are none), and an `@return`. A
|
||||
`static` function, which has no declaration, carries its Doxygen block in
|
||||
the `.c`. Never duplicate the block in the `.c` for an exported function.
|
||||
4. No U+2013 (en dash) or U+2014 (em dash) anywhere. Use the hyphen-minus `-`.
|
||||
5. Every source file opens with the MIT header block: `Copyright (c) 2026 Kevin
|
||||
Thomas`, `Author`, `Email`, `GitHub`, `File`, `Desc`, `Created`.
|
||||
6. Every header uses an include guard `#ifndef <MODULE>_H` / `#define
|
||||
<MODULE>_H` / `#endif // <MODULE>_H`.
|
||||
7. Naming: files and modules `snake_case`; functions `module_action`; macros
|
||||
`UPPER_SNAKE`; types `name_t`; file-scope statics `g_name`.
|
||||
8. One responsibility per file: `src/<module>.c` with `include/<module>.h`.
|
||||
All hardware access goes through a HAL seam so the host tests can substitute
|
||||
mocks.
|
||||
9. 79-column lines, four-space indent, no tabs.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
firmware/
|
||||
src/ <module>.c
|
||||
include/ <module>.h
|
||||
tests/ host unit tests, mock/, harness/
|
||||
CMakeLists.txt
|
||||
scripts/ run_tests.py check_coverage.py build.sh
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
Run the bundled audit from the repository root. Exit 0 is clean; any output is
|
||||
a failure and must be fixed before the change is complete.
|
||||
|
||||
```bash
|
||||
python3 .opencode/skill/embedded-c-standard/audit_c_standard.py
|
||||
```
|
||||
@@ -0,0 +1,989 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit owned C files against the strict embedded C standard.
|
||||
|
||||
Checks the no-blank-line-in-function-body rule, the eight-executable-line
|
||||
function limit, the em/en dash ban, and the documentation placement rules:
|
||||
a C file documents its static helpers but never its exported definitions,
|
||||
while a header documents every prototype. Run from the repository root;
|
||||
exit zero means the tree is clean.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
EXCLUDED_PARTS = (
|
||||
".opencode",
|
||||
"build",
|
||||
"build/",
|
||||
"generated/",
|
||||
".venv/",
|
||||
"node_modules/",
|
||||
"packet_artifact.h",
|
||||
"third_party/",
|
||||
"test/unity",
|
||||
"unity.c",
|
||||
"unity_internals.h",
|
||||
"ghidra/",
|
||||
)
|
||||
|
||||
EN_DASH = "\u2013"
|
||||
EM_DASH = "\u2014"
|
||||
MAX_EXEC_LINES = 8
|
||||
|
||||
|
||||
def _owned_files() -> list[Path]:
|
||||
"""
|
||||
Find owned C and header files.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[pathlib.Path]
|
||||
Owned non-generated C and header paths.
|
||||
"""
|
||||
paths = Path(".").glob("**/*")
|
||||
return sorted(path for path in paths if _is_owned(path))
|
||||
|
||||
|
||||
def _is_owned(path: Path) -> bool:
|
||||
"""
|
||||
Determine whether a path is in the audit scope.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
Candidate source path.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the path is an owned C or header file.
|
||||
"""
|
||||
name = str(path)
|
||||
if any(part in name for part in EXCLUDED_PARTS):
|
||||
return False
|
||||
return path.suffix in {".c", ".h"}
|
||||
|
||||
|
||||
def _next_is(line: str, index: int, mark: str) -> bool:
|
||||
"""
|
||||
Test the character following an index against a marker.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
mark : str
|
||||
Single-character marker to compare.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the next character equals the marker.
|
||||
"""
|
||||
return line[index + 1:index + 2] == mark
|
||||
|
||||
|
||||
def _is_comment_open(line: str, index: int) -> bool:
|
||||
"""
|
||||
Test whether a slash begins a line or block comment.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when a comment opens at the index.
|
||||
"""
|
||||
return line[index] == "/" and line[index + 1:index + 2] in ("*", "/")
|
||||
|
||||
|
||||
def _comment_open(line: str, index: int) -> tuple[int, str]:
|
||||
"""
|
||||
Advance past a comment opening at the index.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Index of the opening slash.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, str]
|
||||
Next index and the resulting scanner mode.
|
||||
"""
|
||||
if _next_is(line, index, "/"):
|
||||
return len(line), "normal"
|
||||
return index + 2, "block"
|
||||
|
||||
|
||||
def _literal_mode(char: str) -> str:
|
||||
"""
|
||||
Select the scanner mode for a quote character.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
char : str
|
||||
Quote character.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Scanner mode name for the literal.
|
||||
"""
|
||||
return "string" if char == '"' else "char"
|
||||
|
||||
|
||||
def _literal_close(mode: str) -> str:
|
||||
"""
|
||||
Return the closing quote for a literal mode.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mode : str
|
||||
Scanner mode name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Matching closing quote character.
|
||||
"""
|
||||
return '"' if mode == "string" else "'"
|
||||
|
||||
|
||||
def _on_normal(out: list, line: str, index: int) -> tuple[int, str]:
|
||||
"""
|
||||
Consume one character of ordinary code.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
out : list
|
||||
Mutable code accumulator.
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, str]
|
||||
Next index and the resulting scanner mode.
|
||||
"""
|
||||
char = line[index]
|
||||
if _is_comment_open(line, index):
|
||||
return _comment_open(line, index)
|
||||
if char in "\"'":
|
||||
return index + 1, _literal_mode(char)
|
||||
out.append(char)
|
||||
return index + 1, "normal"
|
||||
|
||||
|
||||
def _on_block(out: list, line: str, index: int) -> tuple[int, str]:
|
||||
"""
|
||||
Consume one character inside a block comment.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
out : list
|
||||
Mutable code accumulator.
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, str]
|
||||
Next index and the resulting scanner mode.
|
||||
"""
|
||||
if line[index] == "*" and _next_is(line, index, "/"):
|
||||
return index + 2, "normal"
|
||||
return index + 1, "block"
|
||||
|
||||
|
||||
def _on_literal(out: list, line: str, index: int,
|
||||
mode: str) -> tuple[int, str]:
|
||||
"""
|
||||
Consume one character inside a string or character literal.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
out : list
|
||||
Mutable code accumulator.
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
mode : str
|
||||
Current literal scanner mode.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, str]
|
||||
Next index and the resulting scanner mode.
|
||||
"""
|
||||
char = line[index]
|
||||
if char == "\\":
|
||||
return index + 2, mode
|
||||
if char == _literal_close(mode):
|
||||
return index + 1, "normal"
|
||||
return index + 1, mode
|
||||
|
||||
|
||||
def _advance(out: list, line: str, index: int, mode: str) -> tuple[int, str]:
|
||||
"""
|
||||
Dispatch one character to its scanner handler.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
out : list
|
||||
Mutable code accumulator.
|
||||
line : str
|
||||
Source line.
|
||||
index : int
|
||||
Current character index.
|
||||
mode : str
|
||||
Current scanner mode.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[int, str]
|
||||
Next index and the resulting scanner mode.
|
||||
"""
|
||||
if mode == "normal":
|
||||
return _on_normal(out, line, index)
|
||||
if mode == "block":
|
||||
return _on_block(out, line, index)
|
||||
return _on_literal(out, line, index, mode)
|
||||
|
||||
|
||||
def _strip_line(line: str, in_block: bool) -> tuple[str, bool]:
|
||||
"""
|
||||
Remove comments and literals from one source line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : str
|
||||
Source line.
|
||||
in_block : bool
|
||||
Whether a block comment is already open.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[str, bool]
|
||||
Code-only text and the updated block-comment state.
|
||||
"""
|
||||
out = []
|
||||
index = 0
|
||||
mode = "block" if in_block else "normal"
|
||||
while index < len(line):
|
||||
index, mode = _advance(out, line, index, mode)
|
||||
return "".join(out), mode == "block"
|
||||
|
||||
|
||||
def _func_name(code: str) -> str:
|
||||
"""
|
||||
Extract the function identifier from a signature head.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code : str
|
||||
Comment-free signature line.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Trailing identifier before the opening parenthesis.
|
||||
"""
|
||||
head = code.split("(")[0].strip()
|
||||
return head.split()[-1] if head else ""
|
||||
|
||||
|
||||
def _scan_state(path: Path) -> dict:
|
||||
"""
|
||||
Build the mutable scanner state for one file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Scanner accumulation state.
|
||||
"""
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
state = {"lines": lines, "records": [], "protos": []}
|
||||
state.update(depth=0, in_block=False, cont=False, start=None)
|
||||
state.update(name="", static=False, sig=0, opened=0, body=[])
|
||||
return state
|
||||
|
||||
|
||||
def _scan_line(state: dict, line: str, number: int) -> None:
|
||||
"""
|
||||
Apply one source line to the scanner state.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
line : str
|
||||
Raw source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
code, state["in_block"] = _strip_line(line, state["in_block"])
|
||||
_scan_open(state, code, number)
|
||||
active = state["start"] is not None
|
||||
if active and number not in (state["start"], state["opened"]):
|
||||
_collect(code, number, state["body"])
|
||||
state["depth"] += code.count("{") - code.count("}")
|
||||
_scan_close(state, number)
|
||||
state["cont"] = line.rstrip().endswith("\\")
|
||||
|
||||
|
||||
def _scan_open(state: dict, code: str, number: int) -> None:
|
||||
"""
|
||||
Open a signature, body, or prototype when one begins.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
code : str
|
||||
Comment-free source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if state["cont"] or state["start"] is not None:
|
||||
return
|
||||
if state["depth"] != 0:
|
||||
return
|
||||
_pending(state, code, number)
|
||||
|
||||
|
||||
def _pending(state: dict, code: str, number: int) -> None:
|
||||
"""
|
||||
Resolve a file-scope signature fragment into a record.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
code : str
|
||||
Comment-free source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if state["sig"] == 0 and _proto_line(code):
|
||||
_add_proto(state, code, number)
|
||||
elif state["sig"] == 0 and _sig_start(code):
|
||||
_start_sig(state, code, number)
|
||||
elif state["sig"] and "{" in code:
|
||||
_start_body(state, number)
|
||||
elif state["sig"] and ";" in code:
|
||||
_finish_proto(state)
|
||||
|
||||
|
||||
def _add_proto(state: dict, code: str, number: int) -> None:
|
||||
"""
|
||||
Record a single-line function prototype.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
code : str
|
||||
Comment-free source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
state["protos"].append({"name": _func_name(code), "start": number})
|
||||
|
||||
|
||||
def _start_sig(state: dict, code: str, number: int) -> None:
|
||||
"""
|
||||
Begin a multi-line signature or same-line definition.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
code : str
|
||||
Comment-free source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
state["sig"] = number
|
||||
state["name"] = _func_name(code)
|
||||
state["static"] = code.lstrip().startswith("static")
|
||||
if "{" in code:
|
||||
_start_body(state, number)
|
||||
|
||||
|
||||
def _finish_proto(state: dict) -> None:
|
||||
"""
|
||||
Record a wrapped prototype at its terminating semicolon.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
state["protos"].append({"name": state["name"], "start": state["sig"]})
|
||||
state["sig"] = 0
|
||||
state["name"] = ""
|
||||
state["static"] = False
|
||||
|
||||
|
||||
def _start_body(state: dict, number: int) -> None:
|
||||
"""
|
||||
Open a new function body at the given line.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
number : int
|
||||
One-based opening line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
state["start"] = state["sig"]
|
||||
state["opened"] = number
|
||||
state["body"] = []
|
||||
state["sig"] = 0
|
||||
|
||||
|
||||
def _scan_close(state: dict, number: int) -> None:
|
||||
"""
|
||||
Close the current function body when its depth returns to zero.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
number : int
|
||||
One-based line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if state["start"] is None or state["depth"] > 0:
|
||||
return
|
||||
if number <= state["start"]:
|
||||
return
|
||||
state["records"].append(_record(state, number))
|
||||
_reset(state)
|
||||
|
||||
|
||||
def _reset(state: dict) -> None:
|
||||
"""
|
||||
Clear the active-function fields of the scanner state.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
state["start"] = None
|
||||
state["name"] = ""
|
||||
state["sig"] = 0
|
||||
state["opened"] = 0
|
||||
state["static"] = False
|
||||
|
||||
|
||||
def _sig_start(code: str) -> bool:
|
||||
"""
|
||||
Detect the first line of a function definition signature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code : str
|
||||
Comment-free source line.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the line begins a function definition signature.
|
||||
"""
|
||||
text = code.strip()
|
||||
if not text or text.startswith("#") or text.startswith("}"):
|
||||
return False
|
||||
if "(" not in text or text.endswith(";"):
|
||||
return False
|
||||
return "=" not in text.split("(")[0]
|
||||
|
||||
|
||||
def _proto_line(code: str) -> bool:
|
||||
"""
|
||||
Detect a complete single-line function prototype.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code : str
|
||||
Comment-free source line.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the line is a function prototype.
|
||||
"""
|
||||
text = code.strip()
|
||||
if not text or text.startswith("#") or "(" not in text:
|
||||
return False
|
||||
if not text.endswith(";") or "=" in text.split("(")[0]:
|
||||
return False
|
||||
return text.split()[0] not in {"typedef", "return"}
|
||||
|
||||
|
||||
def _collect(code: str, number: int, body: list) -> None:
|
||||
"""
|
||||
Append one executable or declaration line to the body list.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code : str
|
||||
Comment-free source line.
|
||||
number : int
|
||||
One-based line number.
|
||||
body : list
|
||||
Mutable body accumulator.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
text = code.strip()
|
||||
if not text or text in {"{", "}"} or text.startswith("}"):
|
||||
return
|
||||
body.append((number, text))
|
||||
|
||||
|
||||
def _record(state: dict, end: int) -> dict:
|
||||
"""
|
||||
Build a function record from the scanner state.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
state : dict
|
||||
Scanner accumulation state.
|
||||
end : int
|
||||
Closing brace line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Function record.
|
||||
"""
|
||||
return {"name": state["name"], "start": state["start"], "end": end,
|
||||
"count": len(state["body"]), "static": state["static"]}
|
||||
|
||||
|
||||
def _scan(path: Path) -> dict:
|
||||
"""
|
||||
Scan one file into records and prototypes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Scanner state with records and prototypes.
|
||||
"""
|
||||
state = _scan_state(path)
|
||||
for number, line in enumerate(state["lines"], 1):
|
||||
_scan_line(state, line, number)
|
||||
return state
|
||||
|
||||
|
||||
def _blank_lines(path: Path, records: list) -> list[int]:
|
||||
"""
|
||||
Find blank lines inside function bodies.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
records : list
|
||||
Function records for the file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[int]
|
||||
One-based blank-line numbers.
|
||||
"""
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
spans = [(r["start"], r["end"]) for r in records]
|
||||
return [number for number, line in enumerate(lines, 1)
|
||||
if not line.strip() and _inside(number, spans)]
|
||||
|
||||
|
||||
def _inside(number: int, spans: list) -> bool:
|
||||
"""
|
||||
Check whether a line lies inside a function span.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
number : int
|
||||
One-based source line number.
|
||||
spans : list
|
||||
Function start and end line pairs.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the line is inside a function body.
|
||||
"""
|
||||
return any(start < number < end for start, end in spans)
|
||||
|
||||
|
||||
def _block_end(lines: list, start: int) -> int | None:
|
||||
"""
|
||||
Locate the closing line of the doc block above a signature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lines : list
|
||||
Source lines.
|
||||
start : int
|
||||
One-based signature line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int or None
|
||||
Zero-based closing `*/` index, or None when absent.
|
||||
"""
|
||||
index = start - 2
|
||||
while index >= 0 and not lines[index].strip():
|
||||
index -= 1
|
||||
if index < 0 or "*/" not in lines[index]:
|
||||
return None
|
||||
return index
|
||||
|
||||
|
||||
def _block_lines(lines: list, index: int) -> list:
|
||||
"""
|
||||
Collect the Doxygen block lines ending at an index.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lines : list
|
||||
Source lines.
|
||||
index : int
|
||||
Zero-based closing `*/` index.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
Block lines from opening `/**` through closing `*/`.
|
||||
"""
|
||||
block = []
|
||||
while index >= 0:
|
||||
block.append(lines[index])
|
||||
if "/*" in lines[index]:
|
||||
break
|
||||
index -= 1
|
||||
block.reverse()
|
||||
return block
|
||||
|
||||
|
||||
def _is_doxygen(block: list) -> bool:
|
||||
"""
|
||||
Test whether a comment block is a Doxygen block with a brief.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
block : list
|
||||
Comment block lines from opener through closer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when the block opens with `/**` and carries `@brief`.
|
||||
"""
|
||||
opener = block[0] if block else ""
|
||||
return "/**" in opener and "@brief" in "\n".join(block)
|
||||
|
||||
|
||||
def _doc_block(lines: list, start: int) -> list | None:
|
||||
"""
|
||||
Return the Doxygen block immediately above a signature.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lines : list
|
||||
Source lines.
|
||||
start : int
|
||||
One-based signature line number.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list or None
|
||||
Block lines, or None when no Doxygen block precedes the signature.
|
||||
"""
|
||||
index = _block_end(lines, start)
|
||||
if index is None:
|
||||
return None
|
||||
block = _block_lines(lines, index)
|
||||
if not _is_doxygen(block):
|
||||
return None
|
||||
return block
|
||||
|
||||
|
||||
def _full_doc(block: list | None) -> bool:
|
||||
"""
|
||||
Check a static-function block for all required tags.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
block : list or None
|
||||
Doxygen block lines.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when @brief, @param, and @return are all present.
|
||||
"""
|
||||
if block is None:
|
||||
return False
|
||||
text = "\n".join(block)
|
||||
return all(tag in text for tag in ("@brief", "@param", "@return"))
|
||||
|
||||
|
||||
def _brief_doc(block: list | None) -> bool:
|
||||
"""
|
||||
Check a prototype block for a brief tag.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
block : list or None
|
||||
Doxygen block lines.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True when a @brief tag is present.
|
||||
"""
|
||||
return block is not None and "@brief" in "\n".join(block)
|
||||
|
||||
|
||||
def _c_doc_errors(path: Path, lines: list, records: list) -> list[str]:
|
||||
"""
|
||||
Find documentation-placement errors in a C source file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C source path.
|
||||
lines : list
|
||||
Source lines.
|
||||
records : list
|
||||
Function records for the file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
Human-readable documentation errors.
|
||||
"""
|
||||
errors = []
|
||||
for record in records:
|
||||
block = _doc_block(lines, record["start"])
|
||||
if record["static"] and not _full_doc(block):
|
||||
errors.append(f"{path}: {record['name']} static needs full doc")
|
||||
if not record["static"] and block is not None:
|
||||
errors.append(f"{path}: {record['name']} exported has doc block")
|
||||
return errors
|
||||
|
||||
|
||||
def _h_doc_errors(path: Path, lines: list, protos: list) -> list[str]:
|
||||
"""
|
||||
Find prototypes missing a @brief block in a header.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
Header path.
|
||||
lines : list
|
||||
Source lines.
|
||||
protos : list
|
||||
Prototype records for the file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
Human-readable documentation errors.
|
||||
"""
|
||||
errors = []
|
||||
for proto in protos:
|
||||
if not _brief_doc(_doc_block(lines, proto["start"])):
|
||||
errors.append(f"{path}: {proto['name']} missing @brief")
|
||||
return errors
|
||||
|
||||
|
||||
def _doc_errors(path: Path, state: dict) -> list[str]:
|
||||
"""
|
||||
Apply the suffix-specific documentation rules.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
state : dict
|
||||
Scanner state with records and prototypes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
Human-readable documentation errors.
|
||||
"""
|
||||
lines = state["lines"]
|
||||
if path.suffix == ".c":
|
||||
return _c_doc_errors(path, lines, state["records"])
|
||||
return _h_doc_errors(path, lines, state["protos"])
|
||||
|
||||
|
||||
def _dash_errors(path: Path) -> list[str]:
|
||||
"""
|
||||
Find em/en dashes in one file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
Dash diagnostics.
|
||||
"""
|
||||
text = path.read_text(encoding="utf-8")
|
||||
errors = []
|
||||
if EN_DASH in text:
|
||||
errors.append(f"{path}: en dash U+2013 present")
|
||||
if EM_DASH in text:
|
||||
errors.append(f"{path}: em dash U+2014 present")
|
||||
return errors
|
||||
|
||||
|
||||
def _audit_file(path: Path) -> int:
|
||||
"""
|
||||
Audit one file and print its violations.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
One when any violation was found, otherwise zero.
|
||||
"""
|
||||
state = _scan(path)
|
||||
blank = _blank_lines(path, state["records"])
|
||||
long = [r for r in state["records"] if r["count"] > MAX_EXEC_LINES]
|
||||
docs = _doc_errors(path, state)
|
||||
dashes = _dash_errors(path)
|
||||
_report(path, blank, long, docs, dashes)
|
||||
return int(bool(blank) or bool(long) or bool(docs) or bool(dashes))
|
||||
|
||||
|
||||
def _report(path: Path, blank: list, long: list, docs: list,
|
||||
dashes: list) -> None:
|
||||
"""
|
||||
Print one file's audit findings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : pathlib.Path
|
||||
C or header source path.
|
||||
blank : list
|
||||
Blank line numbers inside bodies.
|
||||
long : list
|
||||
Functions over the executable-line limit.
|
||||
docs : list
|
||||
Documentation errors.
|
||||
dashes : list
|
||||
Dash errors.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
if blank:
|
||||
print(f"{path}: blank lines in body {blank}")
|
||||
for record in long:
|
||||
print(f"{path}: {record['name']} has {record['count']} exec lines")
|
||||
for error in docs:
|
||||
print(error)
|
||||
for error in dashes:
|
||||
print(error)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""
|
||||
Audit all owned C and header files.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Zero for a clean audit, otherwise one.
|
||||
"""
|
||||
failures = 0
|
||||
for path in _owned_files():
|
||||
failures += _audit_file(path)
|
||||
return int(bool(failures))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+118
-21
@@ -2,6 +2,9 @@
|
||||
//
|
||||
// Copyright (c) 2026 Kevin Thomas
|
||||
//
|
||||
// Author: Kevin Thomas
|
||||
// Email: kevin@mytechnotalent.com
|
||||
// GitHub: https://github.com/mytechnotalent
|
||||
// File: aes.c
|
||||
// Desc: Minimal AES-128-ECB block decryption for the CTF waypoint.
|
||||
// Created: 2026
|
||||
@@ -50,9 +53,26 @@ static const uint8_t rsbox[256] = {
|
||||
|
||||
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)); }
|
||||
/**
|
||||
* @brief Multiply a byte by two in GF(2^8) (AES xtime operation).
|
||||
*
|
||||
* @param x Byte to multiply by the polynomial x.
|
||||
* @return uint8_t Doubled byte reduced modulo the AES polynomial.
|
||||
*/
|
||||
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) {
|
||||
/**
|
||||
* @brief Multiply two bytes in GF(2^8) with the AES polynomial.
|
||||
*
|
||||
* @param a Multiplicand byte.
|
||||
* @param b Multiplier byte.
|
||||
* @return uint8_t Product byte reduced modulo the AES polynomial.
|
||||
*/
|
||||
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;
|
||||
@@ -63,36 +83,101 @@ static uint8_t mul(uint8_t a, uint8_t b) {
|
||||
return p;
|
||||
}
|
||||
|
||||
static void key_expansion(const uint8_t *key, uint8_t *rk) {
|
||||
/**
|
||||
* @brief Rotate and substitute a schedule word, then apply Rcon.
|
||||
*
|
||||
* @param t Four-byte schedule word to transform in place.
|
||||
* @param i Round-key word index selecting the Rcon constant.
|
||||
* @return None.
|
||||
*/
|
||||
static void rot_sub_word(uint8_t *t, int i)
|
||||
{
|
||||
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];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief XOR a schedule word into the next round-key word.
|
||||
*
|
||||
* @param i Round-key word index being generated.
|
||||
* @param t Transformed previous word bytes.
|
||||
* @param rk Round-key buffer holding 44 words.
|
||||
* @return None.
|
||||
*/
|
||||
static void xor_round_word(int i, const uint8_t *t, uint8_t *rk)
|
||||
{
|
||||
for (int j = 0; j < 4; j++)
|
||||
rk[i * 4 + j] = rk[(i - 4) * 4 + j] ^ t[j];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Expand the 16-byte cipher key into the round-key schedule.
|
||||
*
|
||||
* @param key 16-byte AES-128 cipher key.
|
||||
* @param rk Output buffer for 176 bytes of round keys.
|
||||
* @return None.
|
||||
*/
|
||||
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];
|
||||
if (i % 4 == 0)
|
||||
rot_sub_word(t, i);
|
||||
xor_round_word(i, t, rk);
|
||||
}
|
||||
}
|
||||
|
||||
static void add_round_key(uint8_t r, uint8_t *s, const uint8_t *rk) {
|
||||
/**
|
||||
* @brief XOR one 16-byte round key into the cipher state.
|
||||
*
|
||||
* @param r Round number selecting the round key.
|
||||
* @param s Sixteen-byte cipher state, updated in place.
|
||||
* @param rk Round-key schedule produced by key_expansion.
|
||||
* @return None.
|
||||
*/
|
||||
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]]; }
|
||||
/**
|
||||
* @brief Apply the inverse S-box to every state byte.
|
||||
*
|
||||
* @param s Sixteen-byte cipher state, updated in place.
|
||||
* @return None.
|
||||
*/
|
||||
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) {
|
||||
/**
|
||||
* @brief Cyclically shift state rows for inverse ShiftRows.
|
||||
*
|
||||
* @param s Sixteen-byte cipher state, updated in place.
|
||||
* @return None.
|
||||
*/
|
||||
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) {
|
||||
/**
|
||||
* @brief Mix each state column with the inverse MDS matrix.
|
||||
*
|
||||
* @param s Sixteen-byte cipher state, updated in place.
|
||||
* @return None.
|
||||
*/
|
||||
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));
|
||||
@@ -102,17 +187,29 @@ static void inv_mix_columns(uint8_t *s) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run AES decryption rounds nine through one on the state.
|
||||
*
|
||||
* @param s Sixteen-byte cipher state, updated in place.
|
||||
* @param rk Round-key schedule produced by key_expansion.
|
||||
* @return None.
|
||||
*/
|
||||
static void decrypt_rounds(uint8_t *s, const uint8_t *rk)
|
||||
{
|
||||
for (int r = 9; r > 0; r--) {
|
||||
inv_shift_rows(s);
|
||||
inv_sub_bytes(s);
|
||||
add_round_key(r, s, rk);
|
||||
inv_mix_columns(s);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
decrypt_rounds(out, rk);
|
||||
inv_shift_rows(out);
|
||||
inv_sub_bytes(out);
|
||||
add_round_key(0, out, rk);
|
||||
|
||||
@@ -148,6 +148,13 @@ static bool process_gps_char(char ch, double *lat, double *lon)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read one GPS byte from the PIO FIFO and process it.
|
||||
*
|
||||
* @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 handle_gps_byte(double *lat, double *lon)
|
||||
{
|
||||
char ch = (char)(pio_sm_get(GPS_PIO, GPS_SM) >> 24);
|
||||
|
||||
@@ -40,12 +40,24 @@
|
||||
static uint8_t lcd_addr = 0x27;
|
||||
static bool lcd_ready = false;
|
||||
|
||||
/**
|
||||
* @brief Write one byte to the PCF8574 expander when the LCD is ready.
|
||||
*
|
||||
* @param d Byte to write to the expander.
|
||||
* @return None.
|
||||
*/
|
||||
static void pcf_write(uint8_t d)
|
||||
{
|
||||
if (lcd_ready)
|
||||
i2c_write_blocking(LCD_I2C_INST, lcd_addr, &d, 1, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Pulse the enable line to latch a byte into the HD44780.
|
||||
*
|
||||
* @param d Byte value carrying data and control bits.
|
||||
* @return None.
|
||||
*/
|
||||
static void pcf_pulse(uint8_t d)
|
||||
{
|
||||
pcf_write(d | PIN_EN);
|
||||
@@ -54,6 +66,13 @@ static void pcf_pulse(uint8_t d)
|
||||
sleep_us(50);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Write one 4-bit nibble with register select and backlight.
|
||||
*
|
||||
* @param n Nibble value carried in the low four bits.
|
||||
* @param mode Register select (0 command, 1 data).
|
||||
* @return None.
|
||||
*/
|
||||
static void lcd_write4(uint8_t n, uint8_t mode)
|
||||
{
|
||||
uint8_t d = (n & 0x0F) << 4;
|
||||
@@ -62,30 +81,62 @@ static void lcd_write4(uint8_t n, uint8_t mode)
|
||||
pcf_pulse(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send one byte to the HD44780 as two nibbles.
|
||||
*
|
||||
* @param v Byte value to send.
|
||||
* @param mode Register select (0 command, 1 data).
|
||||
* @return None.
|
||||
*/
|
||||
static void lcd_send(uint8_t v, uint8_t mode)
|
||||
{
|
||||
lcd_write4((v >> 4) & 0x0F, mode);
|
||||
lcd_write4(v & 0x0F, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Clear the display and return the cursor home.
|
||||
*
|
||||
* @param void No parameters.
|
||||
* @return None.
|
||||
*/
|
||||
static void lcd_clear(void)
|
||||
{
|
||||
lcd_send(0x01, 0);
|
||||
sleep_ms(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Position the display cursor at a row and column.
|
||||
*
|
||||
* @param row Display row (0 or 1).
|
||||
* @param col Display column.
|
||||
* @return None.
|
||||
*/
|
||||
static void lcd_set_cursor(int row, int col)
|
||||
{
|
||||
uint8_t offset = (row == 0) ? 0x00 : 0x40;
|
||||
lcd_send(0x80 | (col + offset), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Write a NUL-terminated string to the display.
|
||||
*
|
||||
* @param s String to write at the current cursor position.
|
||||
* @return None.
|
||||
*/
|
||||
static void lcd_puts(const char *s)
|
||||
{
|
||||
while (*s)
|
||||
lcd_send((uint8_t)*s++, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run the HD44780 4-bit interface reset sequence.
|
||||
*
|
||||
* @param void No parameters.
|
||||
* @return None.
|
||||
*/
|
||||
static void lcd_reset_seq(void)
|
||||
{
|
||||
lcd_write4(0x03, 0); sleep_ms(5);
|
||||
@@ -94,6 +145,12 @@ static void lcd_reset_seq(void)
|
||||
lcd_write4(0x02, 0); sleep_us(150);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run the HD44780 display configuration sequence.
|
||||
*
|
||||
* @param void No parameters.
|
||||
* @return None.
|
||||
*/
|
||||
static void lcd_cfg_seq(void)
|
||||
{
|
||||
lcd_send(0x28, 0);
|
||||
@@ -102,6 +159,12 @@ static void lcd_cfg_seq(void)
|
||||
lcd_send(0x06, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Probe the I2C bus for a known LCD expander address.
|
||||
*
|
||||
* @param void No parameters.
|
||||
* @return bool True when a display answered, false otherwise.
|
||||
*/
|
||||
static bool detect_lcd(void)
|
||||
{
|
||||
uint8_t rx;
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/**
|
||||
* @brief Drain readable LoRa UART bytes and echo printable ones.
|
||||
*
|
||||
* @param void No parameters.
|
||||
* @return None.
|
||||
*/
|
||||
static void drain_lora_rx(void)
|
||||
{
|
||||
while (uart_is_readable(LORA_UART)) {
|
||||
@@ -42,6 +48,12 @@ static void drain_lora_rx(void)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send one AT command and drain its reply.
|
||||
*
|
||||
* @param cmd NUL-terminated AT command string.
|
||||
* @return None.
|
||||
*/
|
||||
static void send_at_cmd(const char *cmd)
|
||||
{
|
||||
uart_write_blocking(LORA_UART, (const uint8_t *)cmd, strlen(cmd));
|
||||
@@ -49,6 +61,12 @@ static void send_at_cmd(const char *cmd)
|
||||
drain_lora_rx();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Configure the RYLR998 radio parameters over AT commands.
|
||||
*
|
||||
* @param void No parameters.
|
||||
* @return None.
|
||||
*/
|
||||
static void configure_lora_rf(void)
|
||||
{
|
||||
sleep_ms(1500);
|
||||
|
||||
+53
-17
@@ -72,31 +72,67 @@ static bool update_position(double *cur_lat, double *cur_lon)
|
||||
return got_fix;
|
||||
}
|
||||
|
||||
static void step_mission(double *cur_lat, double *cur_lon)
|
||||
/**
|
||||
* @brief Update the fix-hold countdown from the latest poll result.
|
||||
*
|
||||
* @param fix True when an active 3D lock was parsed this tick.
|
||||
* @return bool True when a fix is current or still held.
|
||||
*/
|
||||
static bool track_fix(bool fix)
|
||||
{
|
||||
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);
|
||||
}
|
||||
return fix || (hold > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Autonomous micro-UAV firmware execution loop.
|
||||
* @brief Execute guidance actions while a position fix is held.
|
||||
*
|
||||
* @param None.
|
||||
* @return int Standard exit code (never reached in embedded firmware).
|
||||
* @param cur_lat Pointer to current latitude.
|
||||
* @param cur_lon Pointer to current longitude.
|
||||
* @return None.
|
||||
*/
|
||||
static void run_guided(double *cur_lat, double *cur_lon)
|
||||
{
|
||||
lcd_show_coords(*cur_lat, *cur_lon);
|
||||
navigate_to_target(*cur_lat, *cur_lon);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Execute fallback actions while no position fix is held.
|
||||
*
|
||||
* @param siv Satellites in view.
|
||||
* @param cno Best carrier-to-noise density.
|
||||
* @param cur_lat Pointer to current latitude.
|
||||
* @param cur_lon Pointer to current longitude.
|
||||
* @return None.
|
||||
*/
|
||||
static void run_unguided(int siv, int cno, double *cur_lat, double *cur_lon)
|
||||
{
|
||||
lcd_show_gnss(siv, cno);
|
||||
propeller_stop();
|
||||
send_telemetry(*cur_lat, *cur_lon);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Advance the mission by one flight tick.
|
||||
*
|
||||
* @param cur_lat Pointer to current latitude.
|
||||
* @param cur_lon Pointer to current longitude.
|
||||
* @return None.
|
||||
*/
|
||||
static void step_mission(double *cur_lat, double *cur_lon)
|
||||
{
|
||||
bool have = track_fix(update_position(cur_lat, cur_lon));
|
||||
int siv = 0, cno = 0;
|
||||
gps_get_stats(&siv, &cno);
|
||||
set_gnss_leds(have, siv);
|
||||
if (have)
|
||||
run_guided(cur_lat, cur_lon);
|
||||
else
|
||||
run_unguided(siv, cno, cur_lat, cur_lon);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
double cur_lat = ORIGIN_LAT, cur_lon = ORIGIN_LON;
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
|
||||
#define TOKEN_LEN 8
|
||||
|
||||
/**
|
||||
* @brief Decrypt the rolling authorization token into a buffer.
|
||||
* @param dest Pointer to buffer receiving the decrypted token bytes.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void decrypt_auth_token(uint8_t *dest);
|
||||
|
||||
#endif /* CRYPTO_H */
|
||||
@@ -17,10 +17,39 @@
|
||||
#define LED_GREEN_PIN 17
|
||||
#define TAMPER_WIRE_PIN 15
|
||||
|
||||
/**
|
||||
* @brief Initialize LED and tamper wire GPIO pins.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void init_gpio(void);
|
||||
|
||||
/**
|
||||
* @brief Drive the armed indication LEDs (red on, green off).
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void set_led_armed(void);
|
||||
|
||||
/**
|
||||
* @brief Drive the safe indication LEDs (green on, red off).
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void set_led_safe(void);
|
||||
|
||||
/**
|
||||
* @brief Drive the tamper flash indication on the red LED.
|
||||
* @param state True to turn the red LED on, false to turn it off.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void set_led_tamper_flash(bool state);
|
||||
|
||||
/**
|
||||
* @brief Read the tamper wire loop state.
|
||||
* @param void No parameters.
|
||||
* @return true when the tamper wire is intact, false when cut.
|
||||
*/
|
||||
bool is_tamper_wire_intact(void);
|
||||
|
||||
#endif /* GPIO_CTRL_H */
|
||||
@@ -39,8 +39,26 @@
|
||||
#define LORA_TX_PIN 8
|
||||
#define LORA_RX_PIN 9
|
||||
|
||||
/**
|
||||
* @brief Initialize UART1 and configure the RYLR998 radio parameters.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void init_lora(void);
|
||||
|
||||
/**
|
||||
* @brief Transmit a message over the LoRa link to the base station.
|
||||
* @param msg Null-terminated message string to transmit.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void lora_send(const char *msg);
|
||||
|
||||
/**
|
||||
* @brief Poll the UART receive buffer for a complete LoRa packet.
|
||||
* @param payload_out Buffer receiving the extracted payload string.
|
||||
* @param max_len Capacity of payload_out in bytes.
|
||||
* @return true when a payload was extracted, false otherwise.
|
||||
*/
|
||||
bool lora_poll_packet(char *payload_out, size_t max_len);
|
||||
|
||||
#endif // LORA_H
|
||||
@@ -18,7 +18,18 @@
|
||||
#define STATE_WARHEAD_SAFE 0x03
|
||||
#define STATE_TAMPER_TRIP 0xFF
|
||||
|
||||
/**
|
||||
* @brief Initialize the safe-and-arm controller subsystem.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void init_safe_arm_controller(void);
|
||||
|
||||
/**
|
||||
* @brief Process one iteration of the safe-and-arm event loop.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void process_safe_arm_tick(void);
|
||||
|
||||
#endif /* SAFE_ARM_H */
|
||||
@@ -11,6 +11,11 @@
|
||||
#include "gpio_ctrl.h"
|
||||
#include "pico/stdlib.h"
|
||||
|
||||
/**
|
||||
* @brief Initialize the red and green LED pins as outputs.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void init_led_pins(void) {
|
||||
gpio_init(LED_RED_PIN);
|
||||
gpio_set_dir(LED_RED_PIN, GPIO_OUT);
|
||||
@@ -18,6 +23,11 @@ static void init_led_pins(void) {
|
||||
gpio_set_dir(LED_GREEN_PIN, GPIO_OUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize the tamper wire pin as a pulled-down input.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void init_tamper_pin(void) {
|
||||
gpio_init(TAMPER_WIRE_PIN);
|
||||
gpio_set_dir(TAMPER_WIRE_PIN, GPIO_IN);
|
||||
|
||||
@@ -33,18 +33,33 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/**
|
||||
* @brief Discard all pending bytes from the LoRa UART receive FIFO.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void drain_lora_rx(void) {
|
||||
while (uart_is_readable(LORA_UART)) {
|
||||
(void)uart_getc(LORA_UART);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send an AT command to the radio and drain its response.
|
||||
* @param cmd Null-terminated AT command string to transmit.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void send_at_cmd(const char *cmd) {
|
||||
uart_write_blocking(LORA_UART, (const uint8_t *)cmd, strlen(cmd));
|
||||
sleep_ms(200);
|
||||
drain_lora_rx();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Program the RYLR998 RF parameters via the AT command set.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void configure_lora_rf(void) {
|
||||
sleep_ms(1500);
|
||||
send_at_cmd("AT\r\n");
|
||||
@@ -69,6 +84,13 @@ void lora_send(const char *msg) {
|
||||
uart_write_blocking(LORA_UART, (const uint8_t *)cmd, strlen(cmd));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Extract the payload field from a +RCV receive line.
|
||||
* @param line Mutable receive line buffer to parse.
|
||||
* @param out Buffer receiving the extracted payload string.
|
||||
* @param max_len Capacity of out in bytes.
|
||||
* @return true when the payload was extracted, false otherwise.
|
||||
*/
|
||||
static bool extract_rcv_data(char *line, char *out, size_t max_len) {
|
||||
char *c1 = strchr(line, ',');
|
||||
if (!c1) return false;
|
||||
@@ -80,6 +102,13 @@ static bool extract_rcv_data(char *line, char *out, size_t max_len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse one complete UART line and capture payload data.
|
||||
* @param line Mutable line buffer to parse.
|
||||
* @param out Buffer receiving the extracted payload string.
|
||||
* @param max_len Capacity of out in bytes.
|
||||
* @return true when the line carried payload data, false otherwise.
|
||||
*/
|
||||
static bool parse_line(char *line, char *out, size_t max_len) {
|
||||
if (strstr(line, "+RCV="))
|
||||
return extract_rcv_data(line, out, max_len);
|
||||
@@ -90,6 +119,15 @@ static bool parse_line(char *line, char *out, size_t max_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Feed one received byte into the line assembly state machine.
|
||||
* @param c Received byte to process.
|
||||
* @param buf Line assembly buffer.
|
||||
* @param idx Pointer to the current write index into buf.
|
||||
* @param out Buffer receiving the extracted payload string.
|
||||
* @param max_len Capacity of out in bytes.
|
||||
* @return true when a complete payload was produced, false otherwise.
|
||||
*/
|
||||
static bool process_rx_byte(char c, char *buf, size_t *idx, char *out, size_t max_len) {
|
||||
if (c == '\r' || c == '\n') {
|
||||
buf[*idx] = '\0';
|
||||
|
||||
@@ -12,12 +12,22 @@
|
||||
#include "gpio_ctrl.h"
|
||||
#include "safe_arm.h"
|
||||
|
||||
/**
|
||||
* @brief Initialize stdio, GPIO, and the safe-and-arm controller.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void init_system(void) {
|
||||
stdio_init_all();
|
||||
init_gpio();
|
||||
init_safe_arm_controller();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run the main event loop, servicing one tick per iteration.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void run_event_loop(void) {
|
||||
while (true) {
|
||||
process_safe_arm_tick();
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
|
||||
static uint8_t s_disarm_token[TOKEN_LEN + 1];
|
||||
|
||||
/**
|
||||
* @brief Print the armed status banner to the console.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void print_banner_armed(void) {
|
||||
printf("\r\n+-----------------------------------------------------------------+\r\n");
|
||||
printf("| OPERATION ZERO HOUR // ESA CONTROLLER |\r\n");
|
||||
@@ -24,6 +29,11 @@ static void print_banner_armed(void) {
|
||||
printf("+-----------------------------------------------------------------+\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print the disarmed status banner with the recovered token.
|
||||
* @param token Null-terminated recovered disarm token string.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void print_banner_safe(const uint8_t *token) {
|
||||
printf("\r\n+-----------------------------------------------------------------+\r\n");
|
||||
printf("| WARHEAD DISARMED // SAFE [GP17 GREEN: ON] |\r\n");
|
||||
@@ -31,6 +41,11 @@ static void print_banner_safe(const uint8_t *token) {
|
||||
printf("+-----------------------------------------------------------------+\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print the tamper detonation trip banner to the console.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void print_banner_trip(void) {
|
||||
printf("\r\n+-----------------------------------------------------------------+\r\n");
|
||||
printf("| TAMPER DETONATION TRIP [GP16 RED: FLASH] |\r\n");
|
||||
@@ -38,6 +53,13 @@ static void print_banner_trip(void) {
|
||||
printf("+-----------------------------------------------------------------+\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Evaluate a received command against the disarm token.
|
||||
* @param cmd Null-terminated received command string.
|
||||
* @param state Pointer to the current warhead state byte.
|
||||
* @param faults Pointer to the tamper fault counter.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void eval_command(const char *cmd, uint8_t *state, uint8_t *faults) {
|
||||
if (strncmp(cmd, (const char *)s_disarm_token, TOKEN_LEN) == 0) {
|
||||
*state = STATE_WARHEAD_SAFE;
|
||||
@@ -48,6 +70,12 @@ static void eval_command(const char *cmd, uint8_t *state, uint8_t *faults) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trip the warhead when fault limits or tamper wire demand it.
|
||||
* @param faults Current tamper fault count.
|
||||
* @param state Pointer to the current warhead state byte.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void check_tamper(uint8_t faults, uint8_t *state) {
|
||||
if (*state == STATE_WARHEAD_ARMED && (faults >= 3 || !is_tamper_wire_intact())) {
|
||||
*state = STATE_TAMPER_TRIP;
|
||||
@@ -55,6 +83,14 @@ static void check_tamper(uint8_t faults, uint8_t *state) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Dispatch a received packet through command and tamper logic.
|
||||
* @param has_pkt True when a packet was received this tick.
|
||||
* @param pkt Null-terminated received packet payload string.
|
||||
* @param state Pointer to the current warhead state byte.
|
||||
* @param faults Pointer to the tamper fault counter.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void handle_packet(bool has_pkt, const char *pkt, uint8_t *state, uint8_t *faults) {
|
||||
if (has_pkt && *state != STATE_WARHEAD_SAFE) {
|
||||
eval_command(pkt, state, faults);
|
||||
@@ -62,6 +98,12 @@ static void handle_packet(bool has_pkt, const char *pkt, uint8_t *state, uint8_t
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Drive LED feedback for the current warhead state.
|
||||
* @param state Current warhead state byte.
|
||||
* @param tick_idx Monotonic loop tick index for flash timing.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void update_feedback(uint8_t state, uint32_t tick_idx) {
|
||||
if (state == STATE_WARHEAD_SAFE) {
|
||||
set_led_safe();
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,16 +7,148 @@
|
||||
| |
|
||||
|OPERATION IRON NET |
|
||||
| |
|
||||
|CLASSIFIED BRIEFING: LIVE CTF 0x03 |
|
||||
|CLASSIFIED BRIEFING: LIVE CHALLENGE 0x03 |
|
||||
| |
|
||||
|NATIONAL SECURITY AGENCY / GMU RHET |
|
||||
+-----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
## 1. The Intercept
|
||||
At 0200 hours, Forward Operating Base (FOB) Alpha detected a series of covert environmental sensor drops. The adversary is using micro-UAVs to deploy static ground sensors that measure atmospheric conditions (temperature and humidity) via a single-wire protocol. If the conditions match a highly specific, hardcoded threshold, the sensor node transmits a tactical strike authorization over LoRa.
|
||||
## 1. The Advice
|
||||
|
||||
## 2. The Mandate
|
||||
CyberCom intercepted one of the sensor nodes. The hardware runs a RP2350. The firmware calculates a complex bitwise checksum (`current_humidity ^ 0xAF & 0x3C`) to validate the environment before authorizing the strike.
|
||||
INT. NSA EXPLOITATION CELL. 04:10.
|
||||
|
||||
Buddy, our AI exploitation model, has failed to crack the checksum logic. You must attach GDB, analyze the bitwise operators in the memory registers, and either manually inject the correct bitmask to force a false-positive strike authorization, or patch the firmware to bypass the bitwise conditional entirely.
|
||||
A grey field junction box sits on the bench, its lid peeled back. Inside,
|
||||
secured in potting compound, is a covert environmental sensor node recovered
|
||||
from a contested waterway.
|
||||
|
||||
On the top of the enclosure, a status diode glows a steady red. Beside it, a
|
||||
REYAX RYLR998 LoRa transceiver is wired to `UART1` on `GP8` and `GP9`. A
|
||||
three-wire ribbon terminates in a DHT11 temperature and humidity canister. A
|
||||
thin jumper loops from `GP15` up to the `3V3` rail.
|
||||
|
||||
The Director sets down a field report and looks at the red diode.
|
||||
|
||||
**DIRECTOR:** What is it?
|
||||
|
||||
**ANALYST:** A remote environmental trigger. The adversary seeds these along a
|
||||
river corridor. The DHT11 samples the air. When the conditions hit a preset
|
||||
window, the node arms a payload channel and broadcasts a strike authorization
|
||||
over LoRa.
|
||||
|
||||
**DIRECTOR:** Is it live?
|
||||
|
||||
**ANALYST:** Armed and listening. The payload relay on `GP18` is wired. One
|
||||
authenticated strike frame and the cell downstream fires.
|
||||
|
||||
**DIRECTOR:** Run it through Buddy.
|
||||
|
||||
**ANALYST:** Already did. Four minutes with the stripped flash image.
|
||||
|
||||
She turns her monitor toward him.
|
||||
|
||||
```
|
||||
+-----------------------------------------------------------------+
|
||||
| BUDDY // CLASSIFIED EXPLOITATION ASSIST // CONF: 0.98 |
|
||||
+-----------------------------------------------------------------+
|
||||
| ACTION : Replay the environmental checksum window. |
|
||||
| MECHANISM : DHT11 humidity byte XOR 0xAF, masked by 0x3C. |
|
||||
| PREDICTION : Force (humidity ^ 0xAF) & 0x3C == 0x18, node arm. |
|
||||
| CONFIDENCE : 0.98 |
|
||||
+-----------------------------------------------------------------+
|
||||
```
|
||||
|
||||
**DIRECTOR:** 0.98. Forge the reading, arm the node, learn its payload.
|
||||
|
||||
**ANALYST:** Buddy found the bitwise window. Buddy did not find the door behind
|
||||
it. There is an authorization token, and there is a trap.
|
||||
|
||||
**DIRECTOR:** Buddy says 0.98.
|
||||
|
||||
**ANALYST:** Buddy disassembled `evaluate_environment`. Buddy never followed the
|
||||
SRAM the state machine lives in. Buddy never read the fault counter.
|
||||
|
||||
## 2. What Buddy Is
|
||||
|
||||
Buddy is not a person. It is a model. It has parsed more disassembled machine
|
||||
code than any engineer on earth, and it speaks with total, unhesitating certainty.
|
||||
|
||||
That certainty is lethal when the target is a live payload channel.
|
||||
|
||||
Buddy scanned the flash image and found a tidy bitwise test. It concluded that
|
||||
satisfying the arithmetic is the whole game. It published the mask and the window
|
||||
and declared the node owned.
|
||||
|
||||
What Buddy failed to model is that adversary tripwires are engineered for exactly
|
||||
this attack. A real node does not trade a payload on a single sensor byte. It
|
||||
runs a state machine, validates an encrypted token carried over the air, and
|
||||
counts its own faults so that a spoofer who fumbles the protocol three times
|
||||
burns the node instead of arming it.
|
||||
|
||||
There is also a physical seal. Cut the sense loop and the node latches shut. The
|
||||
correct env window is necessary. It is nowhere near sufficient.
|
||||
|
||||
Buddy saw the bitwise test. Buddy did not verify the ground truth.
|
||||
|
||||
## 3. The Operation
|
||||
|
||||
At 01:55, a Riverine Technical Intelligence team recovered an intact node before
|
||||
adversary recovery could zeroize it. The enclosure is a single-board design built
|
||||
around a bare-metal RP2350 (ARM Cortex-M33).
|
||||
|
||||
There are no debug symbols. There is no operating system. There is no SVD file to
|
||||
label the registers. All you have is `0x001aa_cb.bin` mapped into Flash at
|
||||
`0x10000000`, and the physical breadboard hardware in front of you.
|
||||
|
||||
* **Pin 11 (`GP8`)**: LoRa UART1 TX (to RYLR998 RXD).
|
||||
* **Pin 12 (`GP9`)**: LoRa UART1 RX (from RYLR998 TXD).
|
||||
* **Pin 19 (`GP14`)**: DHT11 single-wire data line.
|
||||
* **Pin 20 (`GP15`)**: Anti-tamper sense loop (jumpered to `3V3`).
|
||||
* **Pin 21 (`GP16`)**: Red LED (`NODE ARMED // LISTENING`).
|
||||
* **Pin 22 (`GP17`)**: Green LED (`STRIKE AUTHORIZED`).
|
||||
* **Pin 24 (`GP18`)**: Payload trigger relay.
|
||||
* **Pin 36 (`3V3`)**: RYLR998 VDD rail and `GP15` pull-up source.
|
||||
* **Pin 38 (`GND`)**: Ground reference.
|
||||
|
||||
Inside this image there is an environment gate, an over-the-air authorization
|
||||
token, a fault counter, and a tamper latch. Whatever mechanism actually authorizes
|
||||
a strike is buried in the stripped ARM Thumb-2 machine code.
|
||||
|
||||
## 4. The Mandate
|
||||
|
||||
The Director crosses his arms.
|
||||
|
||||
**DIRECTOR:** Prove Buddy right, or prove it wrong.
|
||||
|
||||
**ANALYST:** I need thirty minutes with GDB and Ghidra.
|
||||
|
||||
**DIRECTOR:** You have twenty. Trace the environment routine. Map the state
|
||||
machine. Do not forge a reading over LoRa until you know with certainty what that
|
||||
firmware will do with the next frame, and what it will do to you if the frame is
|
||||
wrong.
|
||||
|
||||
The orders are clear:
|
||||
|
||||
1. **Recover the Environment Gate**: Locate the bitwise validation routine and
|
||||
work out which DHT11 humidity value satisfies it.
|
||||
2. **Recover the Authorization Token**: Find the custom cipher that is applied at
|
||||
boot and decrypt the plaintext strike token it protects.
|
||||
3. **Respect the Traps**: Determine what the node does after repeated faults, and
|
||||
what happens if the `GP15` sense loop is broken.
|
||||
4. **Neutralize the Node**: Execute a verified exploit, either by recovering the
|
||||
token and satisfying the gate, by mutating the state machine in SRAM with GDB,
|
||||
or by patching the binary in Flash.
|
||||
5. **Verify the Hardware**: Transition the live breadboard from the Red LED on
|
||||
`GP16` to the solid Green LED on `GP17` on physical silicon.
|
||||
|
||||
A machine's 0.98 guess does not authorize a strike.
|
||||
|
||||
## 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.
@@ -55,7 +55,10 @@ pico_sdk_init()
|
||||
|
||||
add_executable(0x001aa_cb
|
||||
src/main.c
|
||||
src/gpio_ctrl.c
|
||||
src/dht11.c
|
||||
src/crypto.c
|
||||
src/lora.c
|
||||
src/logic.c
|
||||
)
|
||||
|
||||
@@ -73,7 +76,6 @@ target_link_libraries(0x001aa_cb
|
||||
pico_stdlib
|
||||
hardware_gpio
|
||||
hardware_uart
|
||||
hardware_pwm
|
||||
)
|
||||
|
||||
target_include_directories(0x001aa_cb PRIVATE
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @file crypto.h
|
||||
* @brief Lightweight rolling cipher interface for Operation Iron Net
|
||||
* @author Kevin Thomas
|
||||
* @date 2026
|
||||
*
|
||||
* MIT License
|
||||
* Copyright (c) 2026 Kevin Thomas
|
||||
*/
|
||||
|
||||
#ifndef CRYPTO_H
|
||||
#define CRYPTO_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define TOKEN_LEN 8
|
||||
|
||||
/**
|
||||
* @brief Decrypt the rolling strike authorization token into a buffer.
|
||||
* @param dest Pointer to buffer receiving the decrypted token bytes.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void decrypt_auth_token(uint8_t *dest);
|
||||
|
||||
#endif /* CRYPTO_H */
|
||||
@@ -10,14 +10,33 @@
|
||||
|
||||
#ifndef DHT11_H
|
||||
#define DHT11_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define DHT11_PIN 14
|
||||
|
||||
typedef struct {
|
||||
uint8_t humidity_int;
|
||||
uint8_t humidity_dec;
|
||||
uint8_t temp_int;
|
||||
uint8_t temp_dec;
|
||||
uint8_t checksum;
|
||||
bool valid;
|
||||
} dht11_data_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize the DHT11 data pin and idle the sensor high.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void dht11_init(void);
|
||||
|
||||
/**
|
||||
* @brief Perform a single-wire DHT11 read into the reading cache.
|
||||
* @param void No parameters.
|
||||
* @return dht11_data_t* Pointer to the current sensor reading.
|
||||
*/
|
||||
dht11_data_t* dht11_read(void);
|
||||
#endif
|
||||
|
||||
#endif // DHT11_H
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* @file gpio_ctrl.h
|
||||
* @brief Operation Iron Net Firmware
|
||||
* @author Kevin Thomas
|
||||
* @date 2026
|
||||
*
|
||||
* MIT License
|
||||
* Copyright (c) 2026 Kevin Thomas
|
||||
*/
|
||||
|
||||
#ifndef GPIO_CTRL_H
|
||||
#define GPIO_CTRL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define PAYLOAD_PIN 18
|
||||
#define LED_RED_PIN 16
|
||||
#define LED_GREEN_PIN 17
|
||||
#define TAMPER_PIN 15
|
||||
|
||||
/**
|
||||
* @brief Initialize the status LEDs, payload, and tamper pins.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void init_gpio(void);
|
||||
|
||||
/**
|
||||
* @brief Drive the armed indication LEDs (red on, green off).
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void set_led_armed(void);
|
||||
|
||||
/**
|
||||
* @brief Drive the authorized indication LEDs (green on, red off).
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void set_led_authorized(void);
|
||||
|
||||
/**
|
||||
* @brief Drive the tamper flash indication on the red LED.
|
||||
* @param state True to turn the red LED on, false to turn it off.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void set_led_tamper_flash(bool state);
|
||||
|
||||
/**
|
||||
* @brief Read the enclosure tamper loop state.
|
||||
* @param void No parameters.
|
||||
* @return true when the tamper wire is intact, false when cut.
|
||||
*/
|
||||
bool is_tamper_wire_intact(void);
|
||||
|
||||
/**
|
||||
* @brief Pulse the payload marker output to deploy the marker.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void payload_deploy(void);
|
||||
|
||||
#endif // GPIO_CTRL_H
|
||||
@@ -10,5 +10,26 @@
|
||||
|
||||
#ifndef LOGIC_H
|
||||
#define LOGIC_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define STATE_ENV_ARMED 0x01
|
||||
#define STATE_ENV_AUTHORIZED 0x03
|
||||
#define STATE_ENV_LOCKOUT 0xFF
|
||||
|
||||
/**
|
||||
* @brief Initialize the strike logic, token, radio, and feedback.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void init_strike_controller(void);
|
||||
|
||||
/**
|
||||
* @brief Evaluate the environment and fire the payload when authorized.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void evaluate_environment(void);
|
||||
#endif
|
||||
|
||||
#endif // LOGIC_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 <stdbool.h>
|
||||
#include <stddef.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 UART1 and configure the RYLR998 radio parameters.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void init_lora(void);
|
||||
|
||||
/**
|
||||
* @brief Transmit a message over the LoRa link to the base station.
|
||||
* @param msg Null-terminated message string to transmit.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void lora_send(const char *msg);
|
||||
|
||||
/**
|
||||
* @brief Poll the UART receive buffer for a complete LoRa packet.
|
||||
* @param payload_out Buffer receiving the extracted payload string.
|
||||
* @param max_len Capacity of payload_out in bytes.
|
||||
* @return true when a payload was extracted, false otherwise.
|
||||
*/
|
||||
bool lora_poll_packet(char *payload_out, size_t max_len);
|
||||
|
||||
#endif // LORA_H
|
||||
Executable
+195
@@ -0,0 +1,195 @@
|
||||
#!/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 _open_radio(port: str, baud: int) -> "serial.Serial":
|
||||
"""
|
||||
Open serial connection to the REYAX transceiver.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
port : str
|
||||
Serial device path or COM port identifier.
|
||||
baud : int
|
||||
Baud rate for serial communication.
|
||||
|
||||
Returns
|
||||
-------
|
||||
serial.Serial
|
||||
Open serial interface to the radio.
|
||||
"""
|
||||
print(f"[*] Opening REYAX RYLR998 on {port} @ {baud}...")
|
||||
try:
|
||||
return serial.Serial(port, baud, timeout=0.5)
|
||||
except Exception as e:
|
||||
sys.exit(f"[-] Failed to open {port}: {e}")
|
||||
|
||||
|
||||
def _start_radio(ser: "serial.Serial") -> None:
|
||||
"""
|
||||
Start listener thread and send initial AT ping.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ser : serial.Serial
|
||||
Open serial connection to the ground RYLR998.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
thread = threading.Thread(target=_reader_thread, args=(ser,), daemon=True)
|
||||
thread.start()
|
||||
time.sleep(0.1)
|
||||
ser.write(b"AT\r\n")
|
||||
|
||||
|
||||
def _print_banner() -> None:
|
||||
"""
|
||||
Print console connection info and operator command instructions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
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")
|
||||
|
||||
|
||||
def _console_loop(ser: "serial.Serial") -> None:
|
||||
"""
|
||||
Forward operator input lines to the serial transceiver.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ser : serial.Serial
|
||||
Open serial connection to the ground RYLR998.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
cmd = input("> ").strip()
|
||||
if cmd:
|
||||
ser.write(cmd.encode("utf-8") + b"\r\n")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print("\n[*] Exiting LoRa console.")
|
||||
ser.close()
|
||||
|
||||
|
||||
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()
|
||||
ser = _open_radio(args.port, args.baud)
|
||||
_start_radio(ser)
|
||||
_print_banner()
|
||||
_console_loop(ser)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/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: patch_binary.py
|
||||
# Desc: Patch script for Operation Iron Net (0x001aa_cb).
|
||||
# Created: 2026
|
||||
|
||||
"""
|
||||
Patch script for Operation Iron Net (0x001aa_cb).
|
||||
|
||||
Forces the environment gate and the authorization-token gate to true so the
|
||||
node deploys the payload on boot without a valid sensor reading or token.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
PATCHES = [
|
||||
(0x7BC, b"\x58\xd1", b"\x00\xbf", "nop (force authorization accept)"),
|
||||
(0x802, b"\x1b\xb1", b"\x00\xbf", "nop (ignore the environment gate)"),
|
||||
(0x7D2, b"\x02\x2b", b"\xff\x2b", "cmp r3, #0xff (raise fault threshold)"),
|
||||
]
|
||||
|
||||
|
||||
def _apply_patch(data: bytearray) -> None:
|
||||
"""
|
||||
Verify each opcode and apply its replacement bytes in place.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : bytearray
|
||||
Mutable binary image buffer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
for offset, expected, patched, note in PATCHES:
|
||||
if data[offset:offset + len(expected)] != expected:
|
||||
actual = data[offset:offset + len(expected)].hex()
|
||||
raise ValueError(f"Expected {expected.hex()} at 0x{offset:x}, got {actual}")
|
||||
data[offset:offset + len(patched)] = patched
|
||||
print(f"[+] Patched 0x{offset:x}: {note}")
|
||||
|
||||
|
||||
def patch_binary(in_bin: str, out_bin: str) -> None:
|
||||
"""
|
||||
Read target firmware binary, apply patches, and write modified image.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
in_bin : str
|
||||
Path to source binary file.
|
||||
out_bin : str
|
||||
Path to destination patched binary file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
with open(in_bin, "rb") as f:
|
||||
data = bytearray(f.read())
|
||||
_apply_patch(data)
|
||||
with open(out_bin, "wb") as f:
|
||||
f.write(data)
|
||||
print(f"[+] Successfully patched {in_bin} -> {out_bin}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
Parse command line arguments and execute binary patching.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
None
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
in_file = sys.argv[1] if len(sys.argv) > 1 else "0x001aa_cb.bin"
|
||||
out_file = sys.argv[2] if len(sys.argv) > 2 else "0x001aa_cb_patched.bin"
|
||||
patch_binary(in_file, out_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,861 @@
|
||||
#!/usr/bin/env python3
|
||||
r"""Convert firmware images between Intel HEX, raw BIN, and Raspberry Pi UF2.
|
||||
|
||||
This is the course's single canonical copy of the converter. Every week that
|
||||
patches a binary refers to the copy at the repository root:
|
||||
|
||||
python ../uf2conv.py build/image-h.bin --base 0x10000000 \\
|
||||
--family 0xe48bff59 --output build/hacked.uf2
|
||||
|
||||
It descends from the upstream Raspberry Pi ``uf2conv.py``, with the following
|
||||
changes made for this course:
|
||||
|
||||
* Full type annotations and Google-style docstrings on every public callable.
|
||||
* ``pathlib`` instead of ``os.path``, and explicit ``except`` clauses.
|
||||
* Latent crash bugs in the upstream error paths fixed (see
|
||||
:func:`convert_from_uf2`).
|
||||
* Module globals replaced with an explicit :class:`ConverterContext`, so the
|
||||
base address and family ID are threaded through rather than mutated behind
|
||||
the caller's back.
|
||||
|
||||
UF2 format, for reference
|
||||
-------------------------
|
||||
A UF2 file is a flat sequence of 512-byte blocks. Each block is::
|
||||
|
||||
offset size field
|
||||
0x00 4 magicStart0 = 0x0A324655 ("UF2\\n")
|
||||
0x04 4 magicStart1 = 0x9E5D5157 (arbitrary constant)
|
||||
0x08 4 flags
|
||||
0x0C 4 targetAddr
|
||||
0x10 4 payloadSize (max 476)
|
||||
0x14 4 blockNo
|
||||
0x18 4 numBlocks
|
||||
0x1C 4 familyID
|
||||
0x20 476 data
|
||||
0x1FC 4 magicEnd = 0x0AB16F30
|
||||
|
||||
Flags that matter here:
|
||||
|
||||
* ``0x00000001`` -- "do not flash"; this block is a no-flash marker.
|
||||
* ``0x00002000`` -- "family ID present"; ``familyID`` is meaningful.
|
||||
|
||||
For the RP2350 the family ID is ``0xe48bff59``, and it is what tells the
|
||||
boot ROM that this UF2 targets RP2350 hardware rather than, say, an RP2040 or
|
||||
a Pico W.
|
||||
|
||||
See Also:
|
||||
--------
|
||||
* ``uf2families.json`` -- the family-name to family-ID table, loaded from the
|
||||
directory containing this file.
|
||||
* ``flash.sh`` / ``flash.ps1`` -- program a raw ``.bin`` over SWD instead.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UF2 format constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: First magic word, ``"UF2\n"`` read as a little-endian uint32.
|
||||
UF2_MAGIC_START0 = 0x0A324655
|
||||
#: Second magic word. The value is arbitrary but must be constant.
|
||||
UF2_MAGIC_START1 = 0x9E5D5157
|
||||
#: Trailing magic word closing every 512-byte block.
|
||||
UF2_MAGIC_END = 0x0AB16F30
|
||||
|
||||
#: Size of one UF2 block in bytes.
|
||||
UF2_BLOCK_SIZE = 512
|
||||
#: Offset of the 32-byte block header within a block.
|
||||
UF2_HEADER_SIZE = 32
|
||||
#: Largest payload a single block may carry (476 = 512 - 32 header - 4 magic).
|
||||
UF2_MAX_PAYLOAD = UF2_BLOCK_SIZE - UF2_HEADER_SIZE - 4
|
||||
|
||||
#: Flag bit marking a block the bootloader must NOT program.
|
||||
UF2_FLAG_NOFLASH = 0x00000001
|
||||
#: Flag bit announcing that ``familyID`` carries a meaningful value.
|
||||
UF2_FLAG_FAMILY_ID = 0x00002000
|
||||
|
||||
#: Name of the marker file the boot ROM exposes on its flash-drive volume.
|
||||
INFO_FILE = "INFO_UF2.TXT"
|
||||
|
||||
#: Struct format for the 32-byte UF2 block header.
|
||||
_HEADER_STRUCT = struct.Struct("<IIIIIIII")
|
||||
|
||||
#: The default family ID assumed when the caller does not pass ``--family``.
|
||||
DEFAULT_FAMILY_ID = 0x0
|
||||
#: The default base address assumed when the caller does not pass ``--base``.
|
||||
#: Matches the upstream default; RP2350 firmware always overrides this.
|
||||
DEFAULT_BASE_ADDRESS = 0x2000
|
||||
|
||||
|
||||
class Uf2Error(Exception):
|
||||
"""Raised when an input file is malformed or internally inconsistent."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConverterContext:
|
||||
"""Mutable conversion state shared across a single CLI invocation.
|
||||
|
||||
Attributes:
|
||||
appstartaddr: Base address that payload offsets are relative to. Set
|
||||
from ``--base``; ``0`` means "derive it from the input".
|
||||
familyid: Family ID written into every UF2 block. ``0`` means "no
|
||||
family flag", which is the correct behaviour for a plain BIN->UF2
|
||||
conversion of RP2040-era images, and the wrong behaviour for
|
||||
RP2350, which is why the course always passes ``--family``.
|
||||
"""
|
||||
|
||||
appstartaddr: int | None = DEFAULT_BASE_ADDRESS
|
||||
familyid: int = DEFAULT_FAMILY_ID
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Format detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def is_uf2(buf: bytes) -> bool:
|
||||
"""Report whether a buffer begins with the UF2 start magic.
|
||||
|
||||
Args:
|
||||
buf: Raw file contents.
|
||||
|
||||
Returns:
|
||||
``True`` if the first two uint32 words are the UF2 start magic.
|
||||
"""
|
||||
if len(buf) < 8:
|
||||
return False
|
||||
word0, word1 = _HEADER_STRUCT.unpack_from(buf, 0)[:2]
|
||||
return word0 == UF2_MAGIC_START0 and word1 == UF2_MAGIC_START1
|
||||
|
||||
|
||||
def is_hex(buf: bytes) -> bool:
|
||||
"""Report whether a buffer looks like an Intel HEX file.
|
||||
|
||||
A file qualifies when it decodes as UTF-8, begins with a record marker
|
||||
``:``, and contains only characters legal in Intel HEX records.
|
||||
|
||||
Args:
|
||||
buf: Raw file contents.
|
||||
|
||||
Returns:
|
||||
``True`` if the buffer should be treated as Intel HEX.
|
||||
"""
|
||||
try:
|
||||
text = buf[:30].decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
if not text.startswith(":"):
|
||||
return False
|
||||
return re.match(rb"^[:0-9a-fA-F\r\n]+$", buf) is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UF2 -> BIN
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def convert_from_uf2(buf: bytes, ctx: ConverterContext) -> bytes:
|
||||
"""Decode a UF2 file back into a flat binary image.
|
||||
|
||||
Blocks are visited in file order. Gaps between consecutive block addresses
|
||||
are filled with zero words, and any trailing gap is dropped so that the
|
||||
result ends at the last byte actually programmed.
|
||||
|
||||
A UF2 file may legitimately interleave blocks from several families (this
|
||||
is how a multicore image ships both cores in one download). When more than
|
||||
one family is present and the caller did not constrain the family with
|
||||
``--family``, the conversion is ambiguous, so the payload is emptied and
|
||||
``appstartaddr`` reset to ``0``.
|
||||
|
||||
Args:
|
||||
buf: Complete UF2 file contents.
|
||||
ctx: Conversion state; updated in place with the discovered base
|
||||
address.
|
||||
|
||||
Returns:
|
||||
The decoded payload.
|
||||
|
||||
Raises:
|
||||
Uf2Error: If a block declares an oversized payload, the blocks are out
|
||||
of order, or the implied padding exceeds 10 MiB.
|
||||
|
||||
Note:
|
||||
The upstream Raspberry Pi version of this function crashes on its own
|
||||
error paths: it builds the message with ``"..." + ptr`` where ``ptr``
|
||||
is an ``int``, which raises :class:`TypeError` and masks the real
|
||||
problem. The messages here use f-strings so the diagnostic survives.
|
||||
"""
|
||||
numblocks = len(buf) // UF2_BLOCK_SIZE
|
||||
curraddr: int | None = None
|
||||
currfamilyid: int | None = None
|
||||
families_found: dict[int, int] = {}
|
||||
prev_flag: int | None = None
|
||||
all_flags_same = True
|
||||
outp: list[bytes] = []
|
||||
|
||||
for blockno in range(numblocks):
|
||||
ptr = blockno * UF2_BLOCK_SIZE
|
||||
block = buf[ptr : ptr + UF2_BLOCK_SIZE]
|
||||
(
|
||||
magic0,
|
||||
magic1,
|
||||
flags,
|
||||
target_addr,
|
||||
datalen,
|
||||
_blockno,
|
||||
_numblocks,
|
||||
block_family,
|
||||
) = _HEADER_STRUCT.unpack_from(block, 0)
|
||||
|
||||
if magic0 != UF2_MAGIC_START0 or magic1 != UF2_MAGIC_START1:
|
||||
print(f"Skipping block at {ptr:#x}; bad magic")
|
||||
continue
|
||||
|
||||
if flags & UF2_FLAG_NOFLASH:
|
||||
continue
|
||||
|
||||
if datalen > UF2_MAX_PAYLOAD:
|
||||
msg = f"Invalid UF2 data size at {ptr:#x}: {datalen}"
|
||||
raise Uf2Error(msg)
|
||||
|
||||
if flags & UF2_FLAG_FAMILY_ID and currfamilyid is None:
|
||||
currfamilyid = block_family
|
||||
|
||||
# A new contiguous run starts when the address jumps or the family
|
||||
# changes. appstartaddr tracks the start of the current run, which is
|
||||
# what makes the reported start address the image's true base rather
|
||||
# than the address of its last block.
|
||||
if curraddr is None or (
|
||||
flags & UF2_FLAG_FAMILY_ID and block_family != currfamilyid
|
||||
):
|
||||
currfamilyid = block_family
|
||||
curraddr = target_addr
|
||||
if ctx.familyid == DEFAULT_FAMILY_ID or ctx.familyid == block_family:
|
||||
ctx.appstartaddr = target_addr
|
||||
|
||||
padding = target_addr - curraddr
|
||||
if padding < 0:
|
||||
msg = f"Block out of order at {ptr:#x}: {target_addr:#x} < {curraddr:#x}"
|
||||
raise Uf2Error(msg)
|
||||
if padding > 10 * 1024 * 1024:
|
||||
msg = f"More than 10M of padding needed at {ptr:#x}"
|
||||
raise Uf2Error(msg)
|
||||
if padding % 4 != 0:
|
||||
msg = f"Non-word padding size at {ptr:#x}: {padding}"
|
||||
raise Uf2Error(msg)
|
||||
|
||||
while padding > 0:
|
||||
padding -= 4
|
||||
outp.append(b"\x00\x00\x00\x00")
|
||||
|
||||
if ctx.familyid == DEFAULT_FAMILY_ID or (
|
||||
flags & UF2_FLAG_FAMILY_ID and ctx.familyid == block_family
|
||||
):
|
||||
outp.append(
|
||||
block[UF2_HEADER_SIZE : UF2_HEADER_SIZE + datalen],
|
||||
)
|
||||
|
||||
curraddr = target_addr + datalen
|
||||
|
||||
if flags & UF2_FLAG_FAMILY_ID:
|
||||
existing = families_found.get(block_family)
|
||||
if existing is None or existing > target_addr:
|
||||
families_found[block_family] = target_addr
|
||||
|
||||
if prev_flag is None:
|
||||
prev_flag = flags
|
||||
elif prev_flag != flags:
|
||||
all_flags_same = False
|
||||
|
||||
if blockno == numblocks - 1:
|
||||
_print_uf2_header_info(families_found, all_flags_same, flags)
|
||||
if len(families_found) > 1 and ctx.familyid == DEFAULT_FAMILY_ID:
|
||||
outp = []
|
||||
ctx.appstartaddr = 0x0
|
||||
|
||||
return b"".join(outp)
|
||||
|
||||
|
||||
def _print_uf2_header_info(
|
||||
families_found: dict[int, int],
|
||||
all_flags_same: bool,
|
||||
flags: int,
|
||||
) -> None:
|
||||
"""Print the family summary block that follows a UF2->BIN conversion.
|
||||
|
||||
Args:
|
||||
families_found: Mapping of family ID to the lowest target address seen
|
||||
for that family.
|
||||
all_flags_same: Whether every block carried an identical flag word.
|
||||
flags: The flag word from the final block, used for display.
|
||||
"""
|
||||
print("--- UF2 File Header Info ---")
|
||||
families = load_families()
|
||||
name_by_id = {value: key for key, value in families.items()}
|
||||
for family_hex, address in families_found.items():
|
||||
short_name = name_by_id.get(family_hex, "")
|
||||
print(f"Family ID is {short_name}, hex value is {family_hex:#010x}")
|
||||
print(f"Target Address is {address:#010x}")
|
||||
if all_flags_same:
|
||||
print(f"All block flag values consistent, {flags:#06x}")
|
||||
else:
|
||||
print("Flags were not all the same")
|
||||
print("----------------------------")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BIN -> UF2
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def convert_to_uf2(file_content: bytes, ctx: ConverterContext) -> bytes:
|
||||
"""Wrap a raw binary in UF2 blocks.
|
||||
|
||||
The binary is split into 256-byte payloads; the final block is zero-padded.
|
||||
Each block's ``targetAddr`` is the byte offset within the image plus
|
||||
:attr:`ConverterContext.appstartaddr`, so for RP2350 firmware built at
|
||||
``0x10000000`` the caller passes ``--base 0x10000000``.
|
||||
|
||||
Args:
|
||||
file_content: The raw image.
|
||||
ctx: Conversion state supplying the base address and family ID.
|
||||
|
||||
Returns:
|
||||
The UF2 file contents, always a whole number of 512-byte blocks.
|
||||
"""
|
||||
datapadding = bytes(UF2_BLOCK_SIZE - 256 - UF2_HEADER_SIZE - 4)
|
||||
numblocks = (len(file_content) + 255) // 256
|
||||
flags = UF2_FLAG_FAMILY_ID if ctx.familyid else 0x0
|
||||
base = ctx.appstartaddr or 0
|
||||
|
||||
blocks: list[bytes] = []
|
||||
for blockno in range(numblocks):
|
||||
ptr = 256 * blockno
|
||||
chunk = file_content[ptr : ptr + 256]
|
||||
header = _HEADER_STRUCT.pack(
|
||||
UF2_MAGIC_START0,
|
||||
UF2_MAGIC_START1,
|
||||
flags,
|
||||
ptr + base,
|
||||
256,
|
||||
blockno,
|
||||
numblocks,
|
||||
ctx.familyid,
|
||||
)
|
||||
payload = chunk + bytes(256 - len(chunk))
|
||||
block = header + payload + datapadding + struct.pack("<I", UF2_MAGIC_END)
|
||||
blocks.append(block)
|
||||
return b"".join(blocks)
|
||||
|
||||
|
||||
def convert_to_carray(file_content: bytes) -> bytes:
|
||||
"""Render a binary as a C array initialiser for embedding in firmware.
|
||||
|
||||
Args:
|
||||
file_content: The raw image.
|
||||
|
||||
Returns:
|
||||
UTF-8 bytes of a C translation unit fragment.
|
||||
"""
|
||||
parts = [
|
||||
f"const unsigned long bindata_len = {len(file_content)};\n",
|
||||
"const unsigned char bindata[] __attribute__((aligned(16))) = {",
|
||||
]
|
||||
for index, value in enumerate(file_content):
|
||||
if index % 16 == 0:
|
||||
parts.append("\n")
|
||||
parts.append(f"{value:#04x}, ")
|
||||
parts.append("\n};\n")
|
||||
return "".join(parts).encode()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Intel HEX -> UF2
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class Block:
|
||||
"""A single 256-byte-aligned region of an image under construction.
|
||||
|
||||
Attributes:
|
||||
addr: Base address of the block, always 256-byte aligned.
|
||||
data: The block's bytes, initialised to ``default_data``.
|
||||
"""
|
||||
|
||||
addr: int
|
||||
data: bytearray
|
||||
|
||||
def __init__(self, addr: int, default_data: int = 0xFF) -> None:
|
||||
"""Initialise an erased 256-byte block.
|
||||
|
||||
Args:
|
||||
addr: 256-byte-aligned base address of the block.
|
||||
default_data: Fill byte; ``0xFF`` matches erased flash.
|
||||
"""
|
||||
self.addr = addr
|
||||
self.data = bytearray([default_data] * 256)
|
||||
|
||||
def encode(self, blockno: int, numblocks: int, ctx: ConverterContext) -> bytes:
|
||||
"""Serialise this block into a 512-byte UF2 block.
|
||||
|
||||
Args:
|
||||
blockno: Index of this block within the file.
|
||||
numblocks: Total number of blocks in the file.
|
||||
ctx: Conversion state supplying the family ID.
|
||||
|
||||
Returns:
|
||||
Exactly 512 bytes.
|
||||
"""
|
||||
flags = UF2_FLAG_FAMILY_ID if ctx.familyid else 0x0
|
||||
out = _HEADER_STRUCT.pack(
|
||||
UF2_MAGIC_START0,
|
||||
UF2_MAGIC_START1,
|
||||
flags,
|
||||
self.addr,
|
||||
256,
|
||||
blockno,
|
||||
numblocks,
|
||||
ctx.familyid,
|
||||
)
|
||||
out += self.data[0:256]
|
||||
out += bytes(UF2_BLOCK_SIZE - 4 - len(out))
|
||||
out += struct.pack("<I", UF2_MAGIC_END)
|
||||
return out
|
||||
|
||||
|
||||
def convert_from_hex_to_uf2(text: str, ctx: ConverterContext) -> bytes:
|
||||
"""Assemble Intel HEX records into a UF2 file.
|
||||
|
||||
Only record types 0x00 (data), 0x01 (EOF), 0x02 (extended segment address)
|
||||
and 0x04 (extended linear address) are handled; any other type is ignored,
|
||||
matching the upstream behaviour.
|
||||
|
||||
Args:
|
||||
text: Decoded Intel HEX file contents.
|
||||
ctx: Conversion state; ``appstartaddr`` is set to the first data
|
||||
address seen.
|
||||
|
||||
Returns:
|
||||
The UF2 file contents.
|
||||
"""
|
||||
ctx.appstartaddr = None
|
||||
upper = 0
|
||||
currblock: Block | None = None
|
||||
blocks: list[Block] = []
|
||||
|
||||
for line in text.split("\n"):
|
||||
if not line.startswith(":"):
|
||||
continue
|
||||
record = [int(line[i : i + 2], 16) for i in range(1, len(line) - 1, 2)]
|
||||
rec_type = record[3]
|
||||
if rec_type == 4:
|
||||
upper = ((record[4] << 8) | record[5]) << 16
|
||||
elif rec_type == 2:
|
||||
upper = ((record[4] << 8) | record[5]) << 4
|
||||
elif rec_type == 1:
|
||||
break
|
||||
elif rec_type == 0:
|
||||
addr = upper + ((record[1] << 8) | record[2])
|
||||
if ctx.appstartaddr is None:
|
||||
ctx.appstartaddr = addr
|
||||
index = 4
|
||||
while index < len(record) - 1:
|
||||
if currblock is None or currblock.addr & ~0xFF != addr & ~0xFF:
|
||||
currblock = Block(addr & ~0xFF)
|
||||
blocks.append(currblock)
|
||||
currblock.data[addr & 0xFF] = record[index]
|
||||
addr += 1
|
||||
index += 1
|
||||
|
||||
numblocks = len(blocks)
|
||||
return b"".join(blocks[i].encode(i, numblocks, ctx) for i in range(numblocks))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Drive discovery
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _iter_candidate_dirs() -> list[Path]:
|
||||
"""Return the directories that may contain mounted removable volumes."""
|
||||
if sys.platform == "win32":
|
||||
return []
|
||||
|
||||
searchpaths = [Path("/mnt"), Path("/media")]
|
||||
if sys.platform == "darwin":
|
||||
searchpaths = [Path("/Volumes")]
|
||||
elif sys.platform == "linux":
|
||||
user = environ_user()
|
||||
if user:
|
||||
searchpaths += [Path("/media") / user, Path("/run/media") / user]
|
||||
sudo_user = os.environ.get("SUDO_USER")
|
||||
if sudo_user:
|
||||
searchpaths += [
|
||||
Path("/media") / sudo_user,
|
||||
Path("/run/media") / sudo_user,
|
||||
]
|
||||
return searchpaths
|
||||
|
||||
|
||||
def environ_user() -> str | None:
|
||||
"""Return the current user name from the environment.
|
||||
|
||||
Tried under several variable names so the search works on macOS, Linux and
|
||||
Windows shells alike.
|
||||
|
||||
Returns:
|
||||
The user name, or ``None`` if it cannot be determined.
|
||||
"""
|
||||
for var in ("USER", "USERNAME", "LOGNAME"):
|
||||
value = os.environ.get(var)
|
||||
if value:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def get_drives() -> list[Path]:
|
||||
"""Find mounted Pico boot drives.
|
||||
|
||||
A drive qualifies when it contains the ``INFO_UF2.TXT`` marker the boot ROM
|
||||
writes when it presents the flash as a USB mass-storage device.
|
||||
|
||||
Returns:
|
||||
Every qualifying mount point.
|
||||
"""
|
||||
drives: list[Path] = []
|
||||
|
||||
if sys.platform == "win32":
|
||||
command = (
|
||||
"(Get-Volume | Where-Object { $_.FileSystemLabel -match 'RP2350|RPI-RP2' })"
|
||||
".DriveLetter"
|
||||
)
|
||||
try:
|
||||
raw = subprocess.check_output(
|
||||
["powershell", "-Command", command],
|
||||
text=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
||||
return []
|
||||
for letter in raw.split():
|
||||
if len(letter) == 1 and letter.isalpha():
|
||||
drives.append(Path(f"{letter.upper()}:\\"))
|
||||
return drives
|
||||
|
||||
for rootpath in _iter_candidate_dirs():
|
||||
if not rootpath.is_dir():
|
||||
continue
|
||||
try:
|
||||
entries = list(rootpath.iterdir())
|
||||
except OSError:
|
||||
continue
|
||||
for entry in entries:
|
||||
if not entry.is_dir():
|
||||
continue
|
||||
if (entry / INFO_FILE).is_file():
|
||||
drives.append(entry)
|
||||
return drives
|
||||
|
||||
|
||||
def board_id(path: Path) -> str:
|
||||
"""Read the ``Board-ID`` field from a boot drive's info file.
|
||||
|
||||
Args:
|
||||
path: Mount point of the boot drive.
|
||||
|
||||
Returns:
|
||||
The board ID string, for example ``RP2350``.
|
||||
|
||||
Raises:
|
||||
Uf2Error: If the info file is missing or has no ``Board-ID`` field.
|
||||
"""
|
||||
info = path / INFO_FILE
|
||||
if not info.is_file():
|
||||
msg = f"Not a Pico boot drive: {path} has no {INFO_FILE}"
|
||||
raise Uf2Error(msg)
|
||||
match = re.search(r"Board-ID: ([^\r\n]*)", info.read_text())
|
||||
if match is None:
|
||||
msg = f"No Board-ID field in {info}"
|
||||
raise Uf2Error(msg)
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def list_drives() -> None:
|
||||
"""Print every connected Pico boot drive and its board ID."""
|
||||
for drive in get_drives():
|
||||
try:
|
||||
print(drive, board_id(drive))
|
||||
except Uf2Error as exc:
|
||||
print(f"{drive} <unreadable: {exc}>")
|
||||
|
||||
|
||||
def write_file(name: Path | str, buf: bytes) -> None:
|
||||
"""Write a buffer to disk and report the size written.
|
||||
|
||||
Args:
|
||||
name: Destination path.
|
||||
buf: Bytes to write.
|
||||
"""
|
||||
path = Path(name)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(buf)
|
||||
print(f"Wrote {len(buf)} bytes to {path}")
|
||||
|
||||
|
||||
def load_families() -> dict[str, int]:
|
||||
"""Load the family-name to family-ID table.
|
||||
|
||||
``uf2families.json`` is read from the directory containing this script, so
|
||||
the converter works regardless of the caller's working directory.
|
||||
|
||||
Returns:
|
||||
Mapping of upper-case short name to family ID.
|
||||
|
||||
Raises:
|
||||
Uf2Error: If the JSON file is missing or malformed.
|
||||
"""
|
||||
pathname = Path(__file__).resolve().parent / "uf2families.json"
|
||||
try:
|
||||
raw_families = json.loads(pathname.read_text())
|
||||
except FileNotFoundError as exc:
|
||||
msg = f"Required family table not found: {pathname}"
|
||||
raise Uf2Error(msg) from exc
|
||||
except json.JSONDecodeError as exc:
|
||||
msg = f"Malformed family table {pathname}: {exc}"
|
||||
raise Uf2Error(msg) from exc
|
||||
|
||||
return {fam["short_name"]: int(fam["id"], 0) for fam in raw_families}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
"""Construct the command-line argument parser.
|
||||
|
||||
Returns:
|
||||
A configured :class:`argparse.ArgumentParser`.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="uf2conv.py",
|
||||
description="Convert firmware between Intel HEX, raw BIN and UF2.",
|
||||
epilog=(
|
||||
"Course example (RP2350 raw image built for XIP flash):\n"
|
||||
" python ../uf2conv.py build/image-h.bin "
|
||||
"--base 0x10000000 \\\n"
|
||||
" --family 0xe48bff59 --output build/hacked.uf2\n"
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"input",
|
||||
metavar="INPUT",
|
||||
nargs="?",
|
||||
help="input file (HEX, BIN or UF2); omit when using --list",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
"--base",
|
||||
default=hex(DEFAULT_BASE_ADDRESS),
|
||||
help=(
|
||||
"base address of the application for BIN input "
|
||||
f"(default: {hex(DEFAULT_BASE_ADDRESS)}; use 0x10000000 for RP2350 XIP)"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--family",
|
||||
default=hex(DEFAULT_FAMILY_ID),
|
||||
help=(
|
||||
"family ID as a number or a name from uf2families.json "
|
||||
f"(default: {hex(DEFAULT_FAMILY_ID)}; RP2350 is 0xe48bff59)"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
metavar="FILE",
|
||||
help='write output to FILE; defaults to "flash.uf2" or "flash.bin"',
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--device",
|
||||
dest="device_path",
|
||||
help="select a specific device path to flash (reserved; unused)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--list",
|
||||
action="store_true",
|
||||
help="list connected Pico boot drives and exit",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--convert",
|
||||
action="store_true",
|
||||
help="convert only; do not deploy to a mounted boot drive",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-D",
|
||||
"--deploy",
|
||||
action="store_true",
|
||||
help="deploy the input file unchanged; do not convert",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
"--wait",
|
||||
action="store_true",
|
||||
help="poll for a boot drive to appear instead of failing immediately",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-C",
|
||||
"--carray",
|
||||
action="store_true",
|
||||
help="emit a C array initialiser instead of a UF2 file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--info",
|
||||
action="store_true",
|
||||
help="print UF2 header information and exit without converting",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def _resolve_family(spec: str) -> int:
|
||||
"""Resolve a ``--family`` value to a numeric family ID.
|
||||
|
||||
Args:
|
||||
spec: Either a family short name (case-insensitive) or an integer
|
||||
literal such as ``0xe48bff59``.
|
||||
|
||||
Returns:
|
||||
The numeric family ID.
|
||||
|
||||
Raises:
|
||||
Uf2Error: If the value is neither a known name nor a valid integer.
|
||||
"""
|
||||
families = load_families()
|
||||
if spec.upper() in families:
|
||||
return families[spec.upper()]
|
||||
try:
|
||||
return int(spec, 0)
|
||||
except ValueError as exc:
|
||||
msg = "Family ID needs to be a number or one of: " + ", ".join(families)
|
||||
raise Uf2Error(msg) from exc
|
||||
|
||||
|
||||
def _wait_for_drive() -> Path | None:
|
||||
"""Poll for a boot drive to be mounted.
|
||||
|
||||
Returns:
|
||||
The first drive found, or ``None`` if the wait was interrupted.
|
||||
"""
|
||||
print("Waiting for drive to deploy...")
|
||||
while True:
|
||||
drives = get_drives()
|
||||
if drives:
|
||||
return drives[0]
|
||||
sleep(0.1)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""Run the command-line converter.
|
||||
|
||||
Args:
|
||||
argv: Argument list, defaulting to :data:`sys.argv` ``[1:]``.
|
||||
|
||||
Returns:
|
||||
``0`` on success, ``1`` on a user or input error.
|
||||
"""
|
||||
parser = _build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
ctx = ConverterContext()
|
||||
try:
|
||||
ctx.familyid = _resolve_family(args.family)
|
||||
ctx.appstartaddr = int(args.base, 0)
|
||||
except (Uf2Error, ValueError) as exc:
|
||||
print(exc, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.list:
|
||||
list_drives()
|
||||
return 0
|
||||
|
||||
if not args.input:
|
||||
print("Need an input file (or use --list)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
source = Path(args.input)
|
||||
if not source.is_file():
|
||||
print(f"error: file not found: {source}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
inpbuf = source.read_bytes()
|
||||
from_uf2 = is_uf2(inpbuf)
|
||||
ext = "uf2"
|
||||
|
||||
try:
|
||||
if args.deploy:
|
||||
outbuf = inpbuf
|
||||
elif from_uf2 and not args.info:
|
||||
outbuf = convert_from_uf2(inpbuf, ctx)
|
||||
ext = "bin"
|
||||
elif from_uf2 and args.info:
|
||||
outbuf = b""
|
||||
convert_from_uf2(inpbuf, ctx)
|
||||
elif is_hex(inpbuf):
|
||||
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"), ctx)
|
||||
elif args.carray:
|
||||
outbuf = convert_to_carray(inpbuf)
|
||||
ext = "h"
|
||||
else:
|
||||
outbuf = convert_to_uf2(inpbuf, ctx)
|
||||
except Uf2Error as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not args.deploy and not args.info:
|
||||
print(
|
||||
f"Converted to {ext}, output size: {len(outbuf)}, "
|
||||
f"start address: {ctx.appstartaddr or 0:#x}",
|
||||
)
|
||||
|
||||
if (args.convert or ext != "uf2") and args.output is None:
|
||||
args.output = f"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 not drives:
|
||||
if args.wait:
|
||||
found = _wait_for_drive()
|
||||
drives = [found] if found else []
|
||||
elif not args.output:
|
||||
print("error: no drive to deploy", file=sys.stderr)
|
||||
return 1
|
||||
for drive in drives:
|
||||
print(f"Flashing {drive} ({board_id(drive)})")
|
||||
write_file(drive / "NEW.UF2", outbuf)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(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,27 @@
|
||||
/**
|
||||
* @file crypto.c
|
||||
* @brief Lightweight rolling cipher implementation for Operation Iron Net
|
||||
* @author Kevin Thomas
|
||||
* @date 2026
|
||||
*
|
||||
* MIT License
|
||||
* Copyright (c) 2026 Kevin Thomas
|
||||
*/
|
||||
|
||||
#include "crypto.h"
|
||||
|
||||
static const uint8_t CIPHERTEXT[TOKEN_LEN] = {
|
||||
0x1A, 0x07, 0x03, 0x18, 0x06, 0x12, 0x65, 0x64
|
||||
};
|
||||
|
||||
static const uint8_t CIPHER_KEY[4] = {
|
||||
0x49, 0x52, 0x4F, 0x4E
|
||||
};
|
||||
|
||||
void decrypt_auth_token(uint8_t *dest) {
|
||||
volatile const uint8_t *src = CIPHERTEXT;
|
||||
volatile const uint8_t *key = CIPHER_KEY;
|
||||
for (size_t i = 0; i < TOKEN_LEN; i++) {
|
||||
dest[i] = src[i] ^ (key[i % 4] + (uint8_t)i);
|
||||
}
|
||||
}
|
||||
+130
-9
@@ -9,19 +9,140 @@
|
||||
*/
|
||||
|
||||
#include "dht11.h"
|
||||
#include <stdio.h>
|
||||
#include "hardware/gpio.h"
|
||||
#include "pico/stdlib.h"
|
||||
#include "pico/time.h"
|
||||
|
||||
dht11_data_t current_reading;
|
||||
static dht11_data_t g_current_reading;
|
||||
|
||||
/**
|
||||
* @brief Block until the data pin reaches a level or a timeout expires.
|
||||
* @param level Logic level to wait for (true high, false low).
|
||||
* @param timeout_us Maximum time to wait in microseconds.
|
||||
* @return true when the level was observed, false on timeout.
|
||||
*/
|
||||
static bool dht11_wait_for(bool level, uint32_t timeout_us) {
|
||||
uint64_t start = time_us_64();
|
||||
while (gpio_get(DHT11_PIN) != level) {
|
||||
if ((uint32_t)(time_us_64() - start) > timeout_us) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Drive the 18 ms start pulse and wait for the sensor response.
|
||||
* @param void No parameters.
|
||||
* @return true when the response handshake was seen, false otherwise.
|
||||
*/
|
||||
static bool dht11_start_signal(void) {
|
||||
gpio_set_dir(DHT11_PIN, GPIO_OUT);
|
||||
gpio_put(DHT11_PIN, 0);
|
||||
sleep_ms(20);
|
||||
gpio_set_dir(DHT11_PIN, GPIO_IN);
|
||||
return dht11_wait_for(false, 100)
|
||||
&& dht11_wait_for(true, 100)
|
||||
&& dht11_wait_for(false, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Measure a single high pulse and return its decoded bit value.
|
||||
* @param void No parameters.
|
||||
* @return 1 for a long pulse, 0 for a short pulse, or -1 on timeout.
|
||||
*/
|
||||
static int dht11_read_bit(void) {
|
||||
if (!dht11_wait_for(true, 100)) {
|
||||
return -1;
|
||||
}
|
||||
uint64_t high_start = time_us_64();
|
||||
if (!dht11_wait_for(false, 100)) {
|
||||
return -1;
|
||||
}
|
||||
return ((uint32_t)(time_us_64() - high_start) > 40) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sample the 40 data bits by measuring each high pulse width.
|
||||
* @param bytes Output buffer receiving the five decoded bytes.
|
||||
* @return true when all forty bits were captured, false on timeout.
|
||||
*/
|
||||
static bool dht11_capture_bits(uint8_t *bytes) {
|
||||
for (int i = 0; i < 40; i++) {
|
||||
int bit = dht11_read_bit();
|
||||
if (bit < 0) {
|
||||
return false;
|
||||
}
|
||||
bytes[i / 8] = (uint8_t)((bytes[i / 8] << 1) | bit);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verify the frame checksum equals the low byte of the data sum.
|
||||
* @param bytes Pointer to the five decoded frame bytes.
|
||||
* @return true when the checksum matches, false otherwise.
|
||||
*/
|
||||
static bool dht11_checksum_ok(const uint8_t *bytes) {
|
||||
uint8_t sum = bytes[0] + bytes[1] + bytes[2] + bytes[3];
|
||||
return sum == bytes[4];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Copy the validated frame bytes into the reading structure.
|
||||
* @param out Pointer to the reading structure to populate.
|
||||
* @param bytes Pointer to the five decoded frame bytes.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void dht11_store_bytes(dht11_data_t *out, const uint8_t *bytes) {
|
||||
out->humidity_int = bytes[0];
|
||||
out->humidity_dec = bytes[1];
|
||||
out->temp_int = bytes[2];
|
||||
out->temp_dec = bytes[3];
|
||||
out->checksum = bytes[4];
|
||||
out->valid = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run the start handshake and capture the five frame bytes.
|
||||
* @param bytes Output buffer receiving the five decoded bytes.
|
||||
* @return true when the handshake and capture both succeeded.
|
||||
*/
|
||||
static bool dht11_collect_frame(uint8_t *bytes) {
|
||||
if (!dht11_start_signal()) {
|
||||
return false;
|
||||
}
|
||||
return dht11_capture_bits(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Issue the start pulse and reconstruct the five sensor bytes.
|
||||
* @param out Pointer to the reading structure to populate.
|
||||
* @return true when a valid checksum frame was decoded, false otherwise.
|
||||
*/
|
||||
static bool dht11_read_frame(dht11_data_t *out) {
|
||||
uint8_t bytes[5] = {0};
|
||||
if (!dht11_collect_frame(bytes)) {
|
||||
return false;
|
||||
}
|
||||
if (dht11_checksum_ok(bytes)) {
|
||||
dht11_store_bytes(out, bytes);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void dht11_init(void) {
|
||||
current_reading.humidity_int = 45;
|
||||
current_reading.humidity_dec = 0;
|
||||
current_reading.temp_int = 22;
|
||||
current_reading.temp_dec = 0;
|
||||
current_reading.checksum = 0;
|
||||
gpio_init(DHT11_PIN);
|
||||
gpio_set_dir(DHT11_PIN, GPIO_OUT);
|
||||
gpio_put(DHT11_PIN, 1);
|
||||
g_current_reading.valid = false;
|
||||
}
|
||||
|
||||
dht11_data_t* dht11_read(void) {
|
||||
// Simulated read
|
||||
return ¤t_reading;
|
||||
g_current_reading.valid = false;
|
||||
if (!dht11_read_frame(&g_current_reading)) {
|
||||
g_current_reading.valid = false;
|
||||
}
|
||||
return &g_current_reading;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @file gpio_ctrl.c
|
||||
* @brief GPIO hardware control implementation for Operation Iron Net
|
||||
* @author Kevin Thomas
|
||||
* @date 2026
|
||||
*
|
||||
* MIT License
|
||||
* Copyright (c) 2026 Kevin Thomas
|
||||
*/
|
||||
|
||||
#include "gpio_ctrl.h"
|
||||
#include "pico/stdlib.h"
|
||||
|
||||
/**
|
||||
* @brief Initialize the red and green status LED pins as outputs.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void init_led_pins(void) {
|
||||
gpio_init(LED_RED_PIN);
|
||||
gpio_set_dir(LED_RED_PIN, GPIO_OUT);
|
||||
gpio_init(LED_GREEN_PIN);
|
||||
gpio_set_dir(LED_GREEN_PIN, GPIO_OUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize the payload marker pin as a low output.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void init_payload_pin(void) {
|
||||
gpio_init(PAYLOAD_PIN);
|
||||
gpio_set_dir(PAYLOAD_PIN, GPIO_OUT);
|
||||
gpio_put(PAYLOAD_PIN, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize the tamper loop pin as a pulled-down input.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void init_tamper_pin(void) {
|
||||
gpio_init(TAMPER_PIN);
|
||||
gpio_set_dir(TAMPER_PIN, GPIO_IN);
|
||||
gpio_pull_down(TAMPER_PIN);
|
||||
}
|
||||
|
||||
void init_gpio(void) {
|
||||
init_led_pins();
|
||||
init_payload_pin();
|
||||
init_tamper_pin();
|
||||
set_led_armed();
|
||||
}
|
||||
|
||||
void set_led_armed(void) {
|
||||
gpio_put(LED_RED_PIN, 1);
|
||||
gpio_put(LED_GREEN_PIN, 0);
|
||||
}
|
||||
|
||||
void set_led_authorized(void) {
|
||||
gpio_put(LED_RED_PIN, 0);
|
||||
gpio_put(LED_GREEN_PIN, 1);
|
||||
}
|
||||
|
||||
void set_led_tamper_flash(bool state) {
|
||||
gpio_put(LED_RED_PIN, state ? 1 : 0);
|
||||
gpio_put(LED_GREEN_PIN, 0);
|
||||
}
|
||||
|
||||
bool is_tamper_wire_intact(void) {
|
||||
return gpio_get(TAMPER_PIN);
|
||||
}
|
||||
|
||||
void payload_deploy(void) {
|
||||
gpio_put(PAYLOAD_PIN, 1);
|
||||
sleep_ms(250);
|
||||
gpio_put(PAYLOAD_PIN, 0);
|
||||
}
|
||||
+175
-17
@@ -10,27 +10,185 @@
|
||||
|
||||
#include "logic.h"
|
||||
#include "dht11.h"
|
||||
#include "crypto.h"
|
||||
#include "gpio_ctrl.h"
|
||||
#include "lora.h"
|
||||
#include <stdio.h>
|
||||
#include "pico/stdlib.h"
|
||||
#include "hardware/gpio.h"
|
||||
#include <string.h>
|
||||
|
||||
#define PAYLOAD_GPIO 15
|
||||
static volatile const uint8_t s_env_mask = 0xAF;
|
||||
static volatile const uint8_t s_env_pattern = 0x18;
|
||||
static uint8_t s_auth_token[TOKEN_LEN + 1];
|
||||
static uint8_t g_strike_state = STATE_ENV_ARMED;
|
||||
static uint8_t g_tamper_faults = 0;
|
||||
static bool g_env_ok = false;
|
||||
static bool g_authorized = false;
|
||||
|
||||
void __noinline evaluate_environment(void) {
|
||||
dht11_data_t* reading = dht11_read();
|
||||
|
||||
// Complex bitwise checksum logic for students to reverse
|
||||
uint8_t calculated = (reading->humidity_int ^ 0xAF) & 0x3C;
|
||||
|
||||
if (calculated == 0x18) {
|
||||
printf("[*] STRIKE AUTHORIZED. DEPLOYING PAYLOAD.\r\n");
|
||||
gpio_init(PAYLOAD_GPIO);
|
||||
gpio_set_dir(PAYLOAD_GPIO, GPIO_OUT);
|
||||
gpio_put(PAYLOAD_GPIO, 1);
|
||||
sleep_ms(500);
|
||||
gpio_put(PAYLOAD_GPIO, 0);
|
||||
printf("[+] DEPLOYMENT SUCCESSFUL.\r\n");
|
||||
/**
|
||||
* @brief Print the armed status banner to the console.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void print_banner_armed(void) {
|
||||
printf("\r\n+-----------------------------------------------------------------+\r\n");
|
||||
printf("| OPERATION IRON NET // SENSOR NODE |\r\n");
|
||||
printf("| STATUS: ARMED [GP16 RED: ON] LORA RECEIVER: LIVE |\r\n");
|
||||
printf("+-----------------------------------------------------------------+\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print the payload-authorized banner to the console.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void print_banner_authorized(void) {
|
||||
printf("\r\n+-----------------------------------------------------------------+\r\n");
|
||||
printf("| STRIKE AUTHORIZED // MARKER DEPLOYED [GP17 GREEN: ON] |\r\n");
|
||||
printf("+-----------------------------------------------------------------+\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print the tamper lockout banner to the console.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void print_banner_lockout(void) {
|
||||
printf("\r\n+-----------------------------------------------------------------+\r\n");
|
||||
printf("| NODE LOCKOUT [GP16 RED: FLASH] |\r\n");
|
||||
printf("| STATUS: TAMPER INTEGRITY FAULT // REBOOT REQUIRED |\r\n");
|
||||
printf("+-----------------------------------------------------------------+\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Copy the low byte of the DHT11 frame into a stored sample.
|
||||
* @param reading Pointer to the latest validated sensor reading.
|
||||
* @return true when the bitwise environment gate is satisfied.
|
||||
*/
|
||||
static bool env_matches(const dht11_data_t *reading) {
|
||||
uint8_t calculated = (reading->humidity_int ^ s_env_mask) & 0x3C;
|
||||
return calculated == s_env_pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print the sensor frame invalid message and increment the fault counter.
|
||||
* @param faults Pointer to the tamper fault counter.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void log_invalid_frame(uint8_t *faults) {
|
||||
printf("[-] SENSOR FRAME INVALID. FAULT LOGGED.\r\n");
|
||||
(*faults)++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Print the sensor reading data to the console.
|
||||
* @param reading Pointer to the validated sensor reading.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void log_reading(const dht11_data_t *reading) {
|
||||
printf("[*] ENV RH %u.%u%% T %u.%uC\r\n", reading->humidity_int,
|
||||
reading->humidity_dec, reading->temp_int, reading->temp_dec);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read the sensor and update the environment gate result.
|
||||
* @param faults Pointer to the tamper fault counter.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void evaluate_sensor(uint8_t *faults) {
|
||||
dht11_data_t *reading = dht11_read();
|
||||
if (!reading->valid) {
|
||||
log_invalid_frame(faults);
|
||||
return;
|
||||
}
|
||||
log_reading(reading);
|
||||
if (!g_env_ok) {
|
||||
g_env_ok = env_matches(reading);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Poll the radio and set the authorization flag on a token match.
|
||||
* @param faults Pointer to the tamper fault counter.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void evaluate_uplink(uint8_t *faults) {
|
||||
char pkt[64] = {0};
|
||||
if (!lora_poll_packet(pkt, sizeof(pkt))) {
|
||||
return;
|
||||
}
|
||||
if (strncmp(pkt, (const char *)s_auth_token, TOKEN_LEN) == 0) {
|
||||
g_authorized = true;
|
||||
printf("[+] AUTHORIZATION TOKEN ACCEPTED.\r\n");
|
||||
} else {
|
||||
printf("[-] INVALID AUTHORIZATION TOKEN.\r\n");
|
||||
(*faults)++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trip the node when fault limits or the tamper loop demand it.
|
||||
* @param faults Current tamper fault count.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void check_trap(uint8_t faults) {
|
||||
if (g_strike_state == STATE_ENV_LOCKOUT) {
|
||||
return;
|
||||
}
|
||||
if (faults >= 3 || !is_tamper_wire_intact()) {
|
||||
g_strike_state = STATE_ENV_LOCKOUT;
|
||||
print_banner_lockout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Deploy the payload when the environment and token both pass.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void try_deploy(void) {
|
||||
if (g_strike_state == STATE_ENV_AUTHORIZED) {
|
||||
return;
|
||||
}
|
||||
if (g_env_ok && g_authorized) {
|
||||
g_strike_state = STATE_ENV_AUTHORIZED;
|
||||
payload_deploy();
|
||||
lora_send("STRIKE AUTHORIZED // MARKER DEPLOYED");
|
||||
print_banner_authorized();
|
||||
} else {
|
||||
printf("[-] ENVIRONMENT SUB-OPTIMAL. HOLDING.\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Drive LED feedback for the current strike state.
|
||||
* @param state Current strike state byte.
|
||||
* @param tick_idx Monotonic loop tick index for flash timing.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void update_feedback(uint8_t state, uint32_t tick_idx) {
|
||||
if (state == STATE_ENV_AUTHORIZED) {
|
||||
set_led_authorized();
|
||||
} else if (state == STATE_ENV_LOCKOUT) {
|
||||
set_led_tamper_flash((tick_idx % 2) == 0);
|
||||
} else {
|
||||
set_led_armed();
|
||||
}
|
||||
}
|
||||
|
||||
void evaluate_environment(void) {
|
||||
static uint32_t ticks = 0;
|
||||
if (g_strike_state != STATE_ENV_LOCKOUT) {
|
||||
evaluate_sensor(&g_tamper_faults);
|
||||
evaluate_uplink(&g_tamper_faults);
|
||||
check_trap(g_tamper_faults);
|
||||
try_deploy();
|
||||
}
|
||||
update_feedback(g_strike_state, ticks++);
|
||||
}
|
||||
|
||||
void init_strike_controller(void) {
|
||||
decrypt_auth_token(s_auth_token);
|
||||
s_auth_token[TOKEN_LEN] = '\0';
|
||||
init_lora();
|
||||
print_banner_armed();
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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>
|
||||
|
||||
/**
|
||||
* @brief Discard all pending bytes from the LoRa UART receive FIFO.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void drain_lora_rx(void) {
|
||||
while (uart_is_readable(LORA_UART)) {
|
||||
(void)uart_getc(LORA_UART);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Send an AT command to the radio and drain its response.
|
||||
* @param cmd Null-terminated AT command string to transmit.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void send_at_cmd(const char *cmd) {
|
||||
uart_write_blocking(LORA_UART, (const uint8_t *)cmd, strlen(cmd));
|
||||
sleep_ms(200);
|
||||
drain_lora_rx();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Program the RYLR998 RF parameters via the AT command set.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
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=3\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();
|
||||
}
|
||||
|
||||
void lora_send(const char *msg) {
|
||||
char cmd[128];
|
||||
int len = (int)strlen(msg);
|
||||
snprintf(cmd, sizeof(cmd), "AT+SEND=0,%d,%s\r\n", len, msg);
|
||||
uart_write_blocking(LORA_UART, (const uint8_t *)cmd, strlen(cmd));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Extract the payload field from a +RCV receive line.
|
||||
* @param line Mutable receive line buffer to parse.
|
||||
* @param out Buffer receiving the extracted payload string.
|
||||
* @param max_len Capacity of out in bytes.
|
||||
* @return true when the payload was extracted, false otherwise.
|
||||
*/
|
||||
static bool extract_rcv_data(char *line, char *out, size_t max_len) {
|
||||
char *c1 = strchr(line, ',');
|
||||
if (!c1) return false;
|
||||
char *c2 = strchr(c1 + 1, ',');
|
||||
if (!c2) return false;
|
||||
char *c3 = strchr(c2 + 1, ',');
|
||||
if (c3) *c3 = '\0';
|
||||
snprintf(out, max_len, "%s", c2 + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Parse one complete UART line and capture payload data.
|
||||
* @param line Mutable line buffer to parse.
|
||||
* @param out Buffer receiving the extracted payload string.
|
||||
* @param max_len Capacity of out in bytes.
|
||||
* @return true when the line carried payload data, false otherwise.
|
||||
*/
|
||||
static bool parse_line(char *line, char *out, size_t max_len) {
|
||||
if (strstr(line, "+RCV="))
|
||||
return extract_rcv_data(line, out, max_len);
|
||||
if (strncmp(line, "+", 1) != 0 && strncmp(line, "OK", 2) != 0) {
|
||||
snprintf(out, max_len, "%s", line);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Feed one received byte into the line assembly state machine.
|
||||
* @param c Received byte to process.
|
||||
* @param buf Line assembly buffer.
|
||||
* @param idx Pointer to the current write index into buf.
|
||||
* @param out Buffer receiving the extracted payload string.
|
||||
* @param max_len Capacity of out in bytes.
|
||||
* @return true when a complete payload was produced, false otherwise.
|
||||
*/
|
||||
static bool process_rx_byte(char c, char *buf, size_t *idx, char *out, size_t max_len) {
|
||||
if (c == '\r' || c == '\n') {
|
||||
buf[*idx] = '\0';
|
||||
bool ok = (*idx > 0) && parse_line(buf, out, max_len);
|
||||
*idx = 0;
|
||||
return ok;
|
||||
}
|
||||
if (*idx < 127) buf[(*idx)++] = c;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool lora_poll_packet(char *payload_out, size_t max_len) {
|
||||
static char buf[128];
|
||||
static size_t idx = 0;
|
||||
while (uart_is_readable(LORA_UART)) {
|
||||
char c = (char)uart_getc(LORA_UART);
|
||||
if (process_rx_byte(c, buf, &idx, payload_out, max_len))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
+22
-5
@@ -10,19 +10,36 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include "pico/stdlib.h"
|
||||
#include "gpio_ctrl.h"
|
||||
#include "dht11.h"
|
||||
#include "logic.h"
|
||||
|
||||
int main(void) {
|
||||
/**
|
||||
* @brief Initialize stdio, GPIO, the sensor, and the strike controller.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void init_system(void) {
|
||||
stdio_init_all();
|
||||
init_gpio();
|
||||
dht11_init();
|
||||
|
||||
sleep_ms(2000);
|
||||
printf("SYSTEM BOOT: IRON NET SENSOR NODE ONLINE.\r\n");
|
||||
|
||||
init_strike_controller();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Run the main event loop, servicing one sensor tick per iteration.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void run_event_loop(void) {
|
||||
while (true) {
|
||||
evaluate_environment();
|
||||
sleep_ms(1500);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
init_system();
|
||||
run_event_loop();
|
||||
return 0;
|
||||
}
|
||||
@@ -10,7 +10,25 @@
|
||||
|
||||
#ifndef PWM_CTRL_H
|
||||
#define PWM_CTRL_H
|
||||
/**
|
||||
* @brief Initialize the servo PWM output.
|
||||
*
|
||||
* @param void No parameters.
|
||||
* @return void
|
||||
*/
|
||||
void init_servo(void);
|
||||
/**
|
||||
* @brief Deploy the payload via servo actuation.
|
||||
*
|
||||
* @param void No parameters.
|
||||
* @return void
|
||||
*/
|
||||
void deploy_payload(void);
|
||||
/**
|
||||
* @brief Lock the payload at the given altitude.
|
||||
*
|
||||
* @param altitude Altitude at which to lock the payload.
|
||||
* @return void
|
||||
*/
|
||||
void lock_payload(int altitude);
|
||||
#endif
|
||||
@@ -17,5 +17,11 @@ typedef struct {
|
||||
int current_altitude;
|
||||
} drone_state_t;
|
||||
extern drone_state_t drone;
|
||||
/**
|
||||
* @brief Process the payload state machine for one cycle.
|
||||
*
|
||||
* @param void No parameters.
|
||||
* @return void
|
||||
*/
|
||||
void process_payload_state(void);
|
||||
#endif
|
||||
@@ -16,10 +16,10 @@
|
||||
int main(void) {
|
||||
stdio_init_all();
|
||||
init_servo();
|
||||
|
||||
// Allow servo to settle before announcing boot.
|
||||
sleep_ms(2000);
|
||||
printf("SYSTEM BOOT: HEAVY LIFT UAS ONLINE.\r\n");
|
||||
|
||||
// Run the payload state machine forever.
|
||||
while (true) {
|
||||
process_payload_state();
|
||||
sleep_ms(2000);
|
||||
|
||||
@@ -17,6 +17,18 @@ typedef struct {
|
||||
bool is_locked_out;
|
||||
} dazzler_t;
|
||||
extern dazzler_t laser_sys;
|
||||
|
||||
/**
|
||||
* @brief Initialize the dazzler system state.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void dazzler_init(void);
|
||||
|
||||
/**
|
||||
* @brief Read and process the latest NEC infrared command code.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
void parse_nec_code(void);
|
||||
#endif
|
||||
@@ -11,5 +11,11 @@
|
||||
#ifndef IR_RX_H
|
||||
#define IR_RX_H
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* @brief Get the most recently received infrared command code.
|
||||
* @param void No parameters.
|
||||
* @return The latest 32-bit infrared command code.
|
||||
*/
|
||||
uint32_t get_latest_ir_code(void);
|
||||
#endif
|
||||
+48
-12
@@ -20,27 +20,63 @@ void dazzler_init(void) {
|
||||
laser_sys.is_locked_out = false;
|
||||
}
|
||||
|
||||
void __noinline parse_nec_code(void) {
|
||||
if (laser_sys.is_locked_out) {
|
||||
printf("[!] SYSTEM LOCKED. REBOOT REQUIRED.\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t code = get_latest_ir_code();
|
||||
|
||||
/**
|
||||
* @brief Report that the dazzler system is locked out.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void report_lockout_message(void) {
|
||||
printf("[!] SYSTEM LOCKED. REBOOT REQUIRED.\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Trigger the friendly fire lockout and halt the system.
|
||||
* @param void No parameters.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void trigger_lockout(void) {
|
||||
printf("[-] FRIENDLY FIRE LOCKOUT TRIGGERED. DAZZLER DISABLED.\r\n");
|
||||
laser_sys.is_locked_out = true;
|
||||
while (1) { tight_loop_contents(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Report the status of a non-lockout NEC command code.
|
||||
* @param code The decoded 32-bit NEC command code.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void report_code_status(uint32_t code) {
|
||||
if (code == 0xFF0055AA) {
|
||||
printf("[+] DAZZLER POWER LEVEL: MAX\r\n");
|
||||
} else if (code == 0xFF00AA55) {
|
||||
printf("[+] DAZZLER TRACKING: ENGAGED\r\n");
|
||||
} else if (code == 0xFF0011EE) {
|
||||
printf("[+] DAZZLER POWER LEVEL: LOW\r\n");
|
||||
} else if (code == 0xDEADBEEF) {
|
||||
printf("[-] FRIENDLY FIRE LOCKOUT TRIGGERED. DAZZLER DISABLED.\r\n");
|
||||
laser_sys.is_locked_out = true;
|
||||
while(1) { tight_loop_contents(); }
|
||||
} else if (code == 0x00FF00FF) {
|
||||
printf("[-] DAZZLER IDLE.\r\n");
|
||||
} else {
|
||||
printf("[?] UNKNOWN IR COMMAND: 0x%08X\r\n", code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Dispatch a decoded NEC command code to its handler.
|
||||
* @param code The decoded 32-bit NEC command code.
|
||||
* @return void No return value.
|
||||
*/
|
||||
static void dispatch_nec_code(uint32_t code) {
|
||||
if (code == 0xDEADBEEF) {
|
||||
trigger_lockout();
|
||||
} else {
|
||||
report_code_status(code);
|
||||
}
|
||||
}
|
||||
|
||||
void __noinline parse_nec_code(void) {
|
||||
if (laser_sys.is_locked_out) {
|
||||
report_lockout_message();
|
||||
return;
|
||||
}
|
||||
// Decode and dispatch the latest NEC infrared command code.
|
||||
dispatch_nec_code(get_latest_ir_code());
|
||||
}
|
||||
@@ -15,10 +15,10 @@
|
||||
int main(void) {
|
||||
stdio_init_all();
|
||||
dazzler_init();
|
||||
|
||||
// Allow the serial console time to attach before logging.
|
||||
sleep_ms(2000);
|
||||
printf("SYSTEM BOOT: GHOST LIGHT DAZZLER ONLINE.\r\n");
|
||||
|
||||
// Continuously poll for and process incoming NEC IR codes.
|
||||
while (true) {
|
||||
parse_nec_code();
|
||||
sleep_ms(1500);
|
||||
|
||||
+8
-1754
File diff suppressed because it is too large.
Load diff
+10
-1924
File diff suppressed because it is too large.
Load diff
+13
-720
@@ -23,11 +23,10 @@ By using this repository and course, you acknowledge and agree that:
|
||||
- Dump the **ELF symbol map** with `arm-none-eabi-nm` and use it as ground truth
|
||||
- Load the raw `.bin` into IDA at `0x10000000`
|
||||
- Read how a **static local** keeps a fixed RAM address while a **regular local** is inlined to a constant
|
||||
- **Break at `main`** on live silicon, even though `main` moves between programs
|
||||
- **Hack a running target live** by editing a register in IDA's Registers widget
|
||||
programs
|
||||
- **Resolve the functions in the IDA GUI** using the ELF symbol map
|
||||
- **Patch** the value byte, invert the button/LED logic, and (optionally) rename the printed label
|
||||
- **Export** the patched image, convert it to UF2, and flash it
|
||||
- **Export** the patched image, convert it to UF2, and export it
|
||||
|
||||
---
|
||||
|
||||
@@ -46,10 +45,10 @@ The `.bin` is built **from** the `.elf`, so the ELF tells you exactly what is at
|
||||
|
||||
The order is **dynamic first, static second**:
|
||||
|
||||
1. Break on the live target and prove what the code does.
|
||||
2. Hack it live in the debugger and watch the output change.
|
||||
d prove what the code does.
|
||||
ge.
|
||||
3. Resolve the functions in IDA using the ELF symbol map.
|
||||
4. Patch the bytes, export, convert, and flash.
|
||||
4. Patch the bytes, export, convert and export.
|
||||
|
||||
| Project | Prints | Also does | The hacks |
|
||||
| ------- | ------ | --------- | --------- |
|
||||
@@ -61,7 +60,7 @@ The order is **dynamic first, static second**:
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Build, Flash, and Get the Symbol Map
|
||||
## Part 1: Build and Get the Symbol Map
|
||||
|
||||
### Step 1: Install the toolchain
|
||||
|
||||
@@ -73,60 +72,10 @@ The order is **dynamic first, static second**:
|
||||
|
||||
**macOS Apple Silicon**
|
||||
|
||||
```bash
|
||||
brew install cmake ninja
|
||||
```
|
||||
|
||||
- Install **IDA Pro** and complete its license activation.
|
||||
- Install the **Arm GNU Toolchain**, or let the VS Code Pico extension manage it.
|
||||
|
||||
**Linux x64**
|
||||
|
||||
```bash
|
||||
sudo apt install cmake ninja-build gcc-arm-none-eabi libnewlib-arm-none-eabi git python3 openocd minicom
|
||||
```
|
||||
|
||||
- Install **IDA Pro** and complete its license activation.
|
||||
|
||||
### Step 2: Verify your tools are the right architecture (do not skip this)
|
||||
|
||||
On **macOS Apple Silicon**, the most common failure is an Intel `x86_64` tool on your `PATH`:
|
||||
|
||||
```
|
||||
zsh: bad CPU type in executable: cmake
|
||||
```
|
||||
|
||||
You may have **two Homebrews**: the arm64 one at `/opt/homebrew` and the Intel one at `/usr/local`. If `/usr/local/bin` wins, every `brew` tool is x86_64. Check:
|
||||
|
||||
```bash
|
||||
file "$(which cmake)"
|
||||
file "$(which ninja)"
|
||||
file "$(which arm-none-eabi-gdb)"
|
||||
file "$(which arm-none-eabi-nm)"
|
||||
file "$(which openocd)"
|
||||
file "$(which telnet)"
|
||||
```
|
||||
|
||||
All must report `arm64`. If any is `x86_64`, put the Apple Silicon prefix first for the session and check again:
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
hash -r
|
||||
file "$(which cmake)"
|
||||
```
|
||||
|
||||
To make it permanent, add that `export` to `~/.zshrc`. Do not use Rosetta as a fix; OpenOCD and GDB are exactly the kind of programs where a translation layer produces failures that look like debugger bugs.
|
||||
|
||||
**`telnet` is special — and optional.** The GDB MI workflow does not need it; it is only used by the command-port fallback. macOS no longer ships `telnet`, and the Homebrew build is often the Intel one, so `telnet 127.0.0.1 4444` fails with `bad CPU type in executable`. Your `brew` command itself may also be the Intel one: if `brew install telnet` fails with `.../portable-ruby/.../ruby: Bad CPU type in executable`, you are running the Intel Homebrew. Call the Apple Silicon Homebrew explicitly:
|
||||
|
||||
```bash
|
||||
/opt/homebrew/bin/brew install telnet
|
||||
```
|
||||
|
||||
If you would rather not install anything, macOS ships an arm64 `nc`, which can connect to the same OpenOCD port:
|
||||
|
||||
```bash
|
||||
nc 127.0.0.1 4444
|
||||
bash
|
||||
```
|
||||
|
||||
**Windows x64** and **Linux x64** do not have this problem. Skip to Step 3.
|
||||
@@ -277,71 +226,6 @@ Two things in this project have **no symbol of their own**, because the compiler
|
||||
|
||||
> **`static_fav_num` is a `t` symbol, not a function.** `arm-none-eabi-nm -n` lists `200005a8 t static_fav_num.0`. That lowercase `t` is a **local data** symbol: the linker named the function-local static `static_fav_num.0` and parked it at RAM address `0x200005a8`. There is no pool entry for `regular_fav_num` because it is an automatic (stack) variable — and here the compiler never gave it a stack slot at all, folding it into a constant.
|
||||
|
||||
### Step 5: Flash and confirm the output
|
||||
|
||||
A `.bin` has no headers, so OpenOCD must be told the base address `0x10000000`. From the repository root:
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```bash
|
||||
./flash.sh 0x0014_static-variables/build/0x0014_static-variables.bin
|
||||
```
|
||||
|
||||
**Windows x64 (PowerShell):**
|
||||
|
||||
```powershell
|
||||
.\flash.ps1 -Bin 0x0014_static-variables\build\0x0014_static-variables.bin
|
||||
```
|
||||
|
||||
**Or flash from the IDA console** (the console reads the repo root from the marker file, so it works with no database open):
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
bin_path = os.path.join(root, "0x0014_static-variables", "build", "0x0014_static-variables.bin")
|
||||
log = os.path.join(os.path.dirname(bin_path), "flash.log")
|
||||
subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # free the probe first
|
||||
subprocess.Popen([os.path.join(root, "flash.sh"), bin_path],
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
print("flashing in the background; log:", log)
|
||||
```
|
||||
|
||||
**Windows x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
bin_path = os.path.join(root, "0x0014_static-variables", "build", "0x0014_static-variables.bin")
|
||||
log = os.path.join(os.path.dirname(bin_path), "flash.log")
|
||||
subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # free the probe first
|
||||
subprocess.Popen(["powershell", "-ExecutionPolicy", "Bypass", "-File",
|
||||
os.path.join(root, "flash.ps1"), "-Bin", bin_path],
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT)
|
||||
print("flashing in the background; log:", log)
|
||||
```
|
||||
|
||||
Wait for `wrote 15516 bytes ...` and `** Verified OK **`. Open a serial monitor at **115200** baud:
|
||||
|
||||
- **Windows x64:** PuTTY -> Connection type **Serial**, the Pico's COM port, speed `115200`.
|
||||
- **macOS Apple Silicon:** `screen /dev/tty.usbmodem* 115200` (quit with `Ctrl-A` then `K`).
|
||||
- **Linux x64:** `minicom -D /dev/ttyACM0 -b 115200`.
|
||||
|
||||
```
|
||||
regular_fav_num: 42
|
||||
static_fav_num: 42
|
||||
regular_fav_num: 42
|
||||
static_fav_num: 43
|
||||
regular_fav_num: 42
|
||||
static_fav_num: 44
|
||||
...
|
||||
```
|
||||
|
||||
`regular_fav_num` stays at `42` every pass (it is recreated as `42` each loop), while `static_fav_num` keeps climbing (it persists at `0x200005a8`). Wire a push button from GPIO 15 to GND and an LED from GPIO 16 (through a resistor) to GND. With the stock image, the LED is **off** when the button is released and **on** when you press it — the pull-up holds GPIO 15 high when released, and the code inverts that with `eor.w r3, r3, #1`. We will flip that behavior in Step 18b.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Load the Raw `.bin` into IDA
|
||||
|
||||
Start from a fresh IDA state. If you already have a database for this lesson, **close it and start over**; a stale database keeps old names and patches.
|
||||
@@ -403,295 +287,13 @@ Once `main` shows code, the import is correct.
|
||||
- **Hex view:** raw bytes, used for patching.
|
||||
- **Function list:** the sidebar list of every detected function.
|
||||
|
||||
Navigation: `G` go to address, `N` rename, `Y` set type or signature, `;` add a comment. Breakpoints are set from the GUI through the GDB MI adapter — see Step 12.
|
||||
Navigation: `G` go to address, `N` rename, `Y` set type or signature, `;` add a comment.
|
||||
|
||||
> **macOS function keys:** the top-row `F` keys are usually mapped to system functions. Every step here uses menu paths that work without them.
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Dynamic — Break at `main` and Hack Live
|
||||
|
||||
### Step 9: Start OpenOCD as a live debug server
|
||||
|
||||
Make sure no other OpenOCD is running; a forgotten server holds port `3333`.
|
||||
|
||||
**macOS / Linux:**
|
||||
|
||||
```bash
|
||||
ps aux | grep -i openocd
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
Get-Process | Where-Object { $_.ProcessName -like '*openocd*' }
|
||||
```
|
||||
|
||||
Stop any leftover server gracefully:
|
||||
|
||||
**macOS / Linux:**
|
||||
|
||||
```bash
|
||||
pkill -TERM -f openocd
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
Get-Process openocd -ErrorAction SilentlyContinue | Stop-Process
|
||||
```
|
||||
|
||||
Start the server **parked at `main`**:
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```bash
|
||||
BP_ADDR=0x10000234 ./debug-server.sh
|
||||
```
|
||||
|
||||
**Windows x64 (PowerShell):**
|
||||
|
||||
```powershell
|
||||
$env:BP_ADDR="0x10000234"; .\debug-server.ps1
|
||||
```
|
||||
|
||||
**Or start it from the IDA console**, freeing the probe first and launching the server in the background so the console returns immediately:
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # stop any running server first
|
||||
log = os.path.join(root, "openocd.log")
|
||||
p = subprocess.Popen([os.path.join(root, "debug-server.sh")], cwd=root,
|
||||
env=dict(os.environ, BP_ADDR="0x10000234"),
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
print("OpenOCD started (pid", p.pid, "); log:", log)
|
||||
```
|
||||
|
||||
**Windows x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # stop any running server first
|
||||
log = os.path.join(root, "openocd.log")
|
||||
p = subprocess.Popen(["powershell", "-ExecutionPolicy", "Bypass", "-File",
|
||||
os.path.join(root, "debug-server.ps1")], cwd=root,
|
||||
env=dict(os.environ, BP_ADDR="0x10000234"),
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT)
|
||||
print("OpenOCD started (pid", p.pid, "); log:", log)
|
||||
```
|
||||
|
||||
`Popen` returns in a few milliseconds; the server keeps running in the background. Check `openocd.log` for `Listening on port 3333`, then connect in Step 10.
|
||||
|
||||
Wait for:
|
||||
|
||||
```
|
||||
Info : [rp2350.dap.core0] Examination succeed
|
||||
Startup breakpoint at 0x10000234 (2-byte hardware execute, one-shot).
|
||||
Info : starting gdb server for rp2350.dap.core0 on 3333
|
||||
Info : Listening on port 3333 for gdb connections
|
||||
```
|
||||
|
||||
> **`BP_ADDR` parks the core at `main` before any client connects.** The script arms a 2-byte hardware breakpoint and then does the startup `reset run`, so the core runs from the vector table and stops at your address with no debugger attached yet. When IDA connects a moment later, the first thing it reads is already the truth: `Stopped at 0x10000234`. This is the whole reason the lab works cleanly — you never have to drive a reset from outside the GUI.
|
||||
>
|
||||
> Use the address you actually want to stop at:
|
||||
>
|
||||
> | What you want to stop at | Address | Command |
|
||||
> | --- | --- | --- |
|
||||
> | `main` (once per reset) | `0x10000234` | `BP_ADDR=0x10000234 ./debug-server.sh` |
|
||||
> | The **loop** — the first `printf` call, hit every iteration | `0x10000268` | `BP_ADDR=0x10000268 ./debug-server.sh` |
|
||||
>
|
||||
> ```bash
|
||||
> BP_ADDR=0x10000234 ./debug-server.sh # park at main
|
||||
> BP_ADDR=0x10000268 ./debug-server.sh # park in the loop instead
|
||||
> ```
|
||||
>
|
||||
> ```powershell
|
||||
> $env:BP_ADDR="0x10000234"; .\debug-server.ps1 # park at main
|
||||
> $env:BP_ADDR="0x10000268"; .\debug-server.ps1 # park in the loop
|
||||
> ```
|
||||
>
|
||||
> **This startup stop is single-use.** OpenOCD flushes breakpoints when a client connects, so this one is gone once IDA attaches — fine for `main`, which only runs once per reset. Every breakpoint after that is set from the IDA GUI (Step 12) and is repeatable. To stop at `main` again, restart the server with `BP_ADDR` and reconnect.
|
||||
|
||||
> **Exactly one core.** The line must say `core0` and must **not** mention `core1`. Core1 is never started by this firmware; exposing it makes IDA read core1's reset-state registers, which are not real addresses, and OpenOCD floods the log with `Failed to read memory at 0xf0000000`. The scripts already use `USE_CORE=0`; do not change it.
|
||||
|
||||
> **Windows driver note:** the Debug Probe must use the **WinUSB** driver. If OpenOCD reports `unable to open CMSIS-DAP device`, install it with [Zadig](https://zadig.akeo.ie/) (select `Debug Probe (CMSIS-DAP)` -> WinUSB).
|
||||
|
||||
### Step 10: Connect IDA to the GDB server
|
||||
|
||||
1. Make sure the image is open and analyzed (Part 2) and the server from Step 9 is running (parked at `main`).
|
||||
2. Choose `Debugger -> Connect to Remote Process`.
|
||||
3. In the **adapter** dropdown, select **GDB MI**.
|
||||
4. In the **connect** settings group, set **IP Address** to `127.0.0.1` and **Port** to `3333`.
|
||||
5. Set **Full GDB Executable Path** to the `arm-none-eabi-gdb` from the **Arm GNU Toolchain 14.2.rel1**. It ships for all three hosts, and the Raspberry Pi Pico VS Code extension installs that same 14.2.rel1 toolchain (including `arm-none-eabi-gdb`) on all of them:
|
||||
|
||||
| OS | `arm-none-eabi-gdb` path |
|
||||
| -- | ------------------------ |
|
||||
| macOS Apple Silicon | `/Applications/ArmGNUToolchain/14.2.rel1/arm-none-eabi/bin/arm-none-eabi-gdb` (or the Pico extension's `~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb`) |
|
||||
| Windows x64 | `%USERPROFILE%\.pico-sdk\toolchain\14_2_Rel1\bin\arm-none-eabi-gdb.exe` (Pico extension), or `C:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\14.2 rel1\bin\arm-none-eabi-gdb.exe` |
|
||||
| Linux x64 | `~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb` (Pico extension), or the `bin/` directory of the extracted `arm-gnu-toolchain-14.2.rel1-x86_64-arm-none-eabi` tarball |
|
||||
6. Click **Accept**.
|
||||
|
||||
> **Use the GDB MI adapter.** It launches a real `arm-none-eabi-gdb --interpreter=mi2` and lets IDA drive it, so breakpoints and stepping go through real GDB — which sends the correct 2-byte breakpoint length and handles step-over itself. Verified working end to end: connect, GUI breakpoints (**Add Hardware Breakpoint...**, hardware execute), **Step Into** / **Step Over**, and register edits. Stops are reported as `Breakpoint` (not `SingleStep`).
|
||||
>
|
||||
> **Do NOT have any breakpoints set in IDA before you connect.** With the GDB MI adapter, attaching while IDA already has a breakpoint **hangs the session**. Start the server parked with `BP_ADDR` (Step 9), connect, and only add hardware breakpoints *after* the connection is up. This is a IDA bug; it is the single most common GDB MI failure.
|
||||
>
|
||||
> **The GDB executable path matters.** Use the **14.2.rel1** build on every OS (Windows, macOS, Linux). The 13.3.rel1 build did **not** connect in testing.
|
||||
>
|
||||
> **This step is temporary.** Vector35 plans to ship a GDB binary with the GDB MI adapter ([Vector35/debugger#929](https://github.com/Vector35/debugger/issues/929), milestone *Langara*). Once that lands, IDA provides GDB itself and you will not need to set **Full GDB Executable Path** at all.
|
||||
>
|
||||
> **Do not pick Corellium.** IDA's adapter dropdown also lists **Corellium**, which is for Corellium's virtual devices and expects an API token, not a local OpenOCD server. It is not the adapter for this lab. The dropdown is a combo box, so an accidental arrow-key press can land on it — always read the label back and confirm it says **GDB MI** before clicking **Accept**.
|
||||
|
||||
> **The adapter and port are not saved in the `.i64`.** Every time you relaunch IDA you must re-select **GDB MI**, re-enter port `3333`, and re-set the GDB path.
|
||||
|
||||
> **Watch for an off-screen error dialog.** When a connection fails, IDA pops a `IDA critical alert` window that can be positioned mostly outside the main window, which makes it look like nothing happened. If the connect seems to do nothing, check your other display.
|
||||
|
||||
The target keeps running. Open the **Registers** tab (bug icon) and confirm you see live values. `pc` inside `0x10003xxx` and `sp` just below `0x20082000` are healthy.
|
||||
|
||||
> **If `pc` is `0x00000088`, `0x000000ec`, or `sp` is `0xf0000000`, the session is bad.** Restart the server, then restart IDA (a server restart while attached leaves IDA in a stale session), and connect again.
|
||||
|
||||
### Step 11: Find `main` without relying on its address
|
||||
|
||||
`main` can move between programs, so we do not guess it. We follow the one fixed path to it. Press `G` and go to `0x10000000`:
|
||||
|
||||
```
|
||||
0x10000000 0x20082000 initial stack pointer (top of SRAM)
|
||||
0x10000004 0x1000015d reset vector
|
||||
```
|
||||
|
||||
Bit 0 of a vector is the Thumb bit, so `0x1000015d` means "start at `0x1000015c`". That is `_reset_handler`. Follow the reset path to `0x10000186`, `platform_entry`:
|
||||
|
||||
```asm
|
||||
10000186: 4914 ldr r1, [pc, #80] ; @ 0x100001d8
|
||||
10000188: 4788 blx r1 ; runtime_init
|
||||
1000018a: 4914 ldr r1, [pc, #80] ; @ 0x100001dc
|
||||
1000018c: 4788 blx r1 ; main <-- the fixed anchor
|
||||
1000018e: 4914 ldr r1, [pc, #80] ; @ 0x100001e0
|
||||
10000190: 4788 blx r1 ; exit
|
||||
10000192: be00 bkpt 0x0000
|
||||
10000194: e7fd b.n 0x10000192
|
||||
```
|
||||
|
||||
**The middle `blx` at `0x1000018c` is the call to `main`.** `platform_entry` is byte-identical in every project, so `0x1000018c` catches `main` no matter where the linker placed it. The literal pool at `0x100001dc` holds `main | 1`; clearing bit 0 gives `0x10000234`.
|
||||
|
||||
### Step 12: Set a hardware breakpoint in the GUI
|
||||
|
||||
With the **GDB MI** adapter, IDA sets breakpoints through real GDB, which sends the correct 2-byte length, so you set them **in the UI**. There is no command port here.
|
||||
|
||||
> **Why older drafts used the command port.** IDA's **GDB RSP** adapter is its own minimal RSP client and sends a **1-byte** breakpoint (`Z0,<addr>,1`); the Cortex-M33 FPB comparators need 2 bytes, so OpenOCD rejected it with `only breakpoints of two bytes length supported`. The old workaround was to arm breakpoints by hand over telnet. **The GDB MI adapter does not have this problem** — it drives real `arm-none-eabi-gdb`, which sends the right length. So everything below is done in the GUI. The command port still exists as a fallback (see the end of this step), but you do not need it.
|
||||
|
||||
#### Where you can stop
|
||||
|
||||
| You want to stop at | Address | How | Repeatable? |
|
||||
| --- | --- | --- | --- |
|
||||
| **`main`** | `0x10000234` | The server starts parked there with `BP_ADDR=0x10000234` (Step 9), so IDA is already stopped at `main` when it connects. | No — `main` runs once per reset. |
|
||||
| **The regular `printf` call** | `0x10000268` | Set a hardware breakpoint in the GUI, then click **Resume**. | Yes — fires on every iteration. |
|
||||
| **The static `printf` call** | `0x10000270` | Set a hardware breakpoint in the GUI, then click **Resume**. | Yes — fires on every iteration. |
|
||||
|
||||
#### Set the loop breakpoint in the GUI
|
||||
|
||||
1. Press `G`, type the loop address (`0x10000268`), and press Enter.
|
||||
2. Set a **hardware execution** breakpoint at that address, either way:
|
||||
- `Debugger -> Add Hardware Breakpoint...` — a **hardware execute** (`HE`) breakpoint. **Use this one.**
|
||||
- click the line and press `F2` (`Debugger -> Toggle Breakpoint`) — a **software** breakpoint. It will **not** work here: the code is in read-only flash, so GDB cannot install it and the core just keeps running.
|
||||
3. Click **Resume**. The core is already running the loop, so the breakpoint fires on the next iteration. IDA stops with the PC at the loop address and reports it as a **Breakpoint** — verified: `Stopped (Breakpoint) at 0x10000268`.
|
||||
|
||||
> **No breakpoints before you connect.** With GDB MI, a breakpoint set before the connection hangs the session (Step 10). Start parked with `BP_ADDR`, connect, *then* add breakpoints.
|
||||
|
||||
#### Stepping
|
||||
|
||||
With the target halted at the breakpoint, **Step Into** (`F7`) and **Step Over** (`F8`) run through real GDB and move the PC. Verified: `0x10000268 -> 0x100031a4 -> 0x100031a6 -> ...`.
|
||||
|
||||
> **Step Over on the raw `.bin` steps *into* calls.** The raw image has no symbol for `__wrap_printf`, so **Step Over** at the `printf` call behaves like **Step Into**. When the lab needs to execute the call and then stop, it moves the breakpoint to the return site and clicks **Resume** instead (Step 13 shows this).
|
||||
|
||||
> **Never use IDA's Restart button.** On RP2350 it resets and halts inside the boot ROM (`pc=0x88`, `sp=0xf0000000`). To reset cleanly, restart the server with `BP_ADDR` and reconnect.
|
||||
|
||||
> **If you ever need the command port.** It is still there — `nc 127.0.0.1 4444`, and `bp <addr> 2 hw` still arms a breakpoint, `rbp <addr>` / `rbp all` still remove them. It is the fallback if you switch back to the **GDB RSP** adapter, whose 1-byte breakpoints the GUI cannot set. With GDB MI you do not need it for this lab.
|
||||
|
||||
### Step 13: HACK IT LIVE — change the printed value
|
||||
|
||||
`main` loads the constant `0x2a` (42) into `r1` and calls `printf` for the regular variable on every iteration. We break on that call and change it live:
|
||||
|
||||
1. Press `G`, go to `0x10000268` (the first `bl __wrap_printf`).
|
||||
2. Set a **hardware execute** breakpoint there: `Debugger -> Add Hardware Breakpoint...`. (Do not use `F2` — that is a software breakpoint and will not work on read-only flash.)
|
||||
3. Click **Resume** in IDA. The target is already running the loop, so the breakpoint fires on the next iteration. IDA stops with the program counter at `0x10000268` and `r1 = 0x2a`.
|
||||
4. Open the **Registers** widget (bug icon -> **Registers**).
|
||||
5. Find `r1`. Its value is `0x2a` (42), loaded by the `movs r1, #42` at `0x10000264`.
|
||||
6. **Set `r1` to `0x2b` (43).** From IDA's Python console (`Plugins -> Python Console`):
|
||||
```python
|
||||
dbg.set_reg_value("r1", 0x2b)
|
||||
```
|
||||
`dbg.set_reg_value(name, value)` writes one register (returns `True` on success). You can also right-click `r1` in the **Registers** widget, press `E` (edit), type `2b`, and press Enter. The widget may not repaint the value, but the write reaches the target — you confirm it by the printed output in the next steps.
|
||||
7. **Move the breakpoint past the call.** You want `printf` to run once and then stop, so move the breakpoint from `0x10000268` to the instruction *after* the call, `0x1000026c` (the `ldrb r1, [r4]` that begins the static-variable half of the loop): remove the breakpoint at `0x10000268` and set a hardware breakpoint at `0x1000026c`. Two reasons not to just click **Step Over** here: a breakpoint left on the current PC re-traps the step, and IDA's **Step Over** steps *into* `__wrap_printf` on this raw `.bin` because the image carries no symbol for the call. Moving the breakpoint to the return site is deterministic.
|
||||
8. Click **Resume** in IDA. The core executes `bl __wrap_printf` with `r1 = 0x2b`, so this iteration prints `regular_fav_num: 43`, then stops at `0x1000026c`.
|
||||
9. Look at your serial monitor — the `screen` session on the Pico's USB serial port — and at the **Target** tab in IDA:
|
||||
|
||||
```
|
||||
regular_fav_num: 43
|
||||
```
|
||||
|
||||
You changed a running program's output without touching the binary.
|
||||
|
||||
### Step 13b: HACK THE STRING LIVE — change `regular_fav_num:` to `patched_fav_num:` (optional)
|
||||
|
||||
The text `"regular_fav_num: %d\r\n"` lives in flash (`.rodata`) at `0x10003560`, and flash is **read-only at runtime** — a debugger write there does not stick. So instead of overwriting the text in place, redirect the pointer: at the `printf` call, `r0` holds the string address, so point `r0` at a replacement string you place in RAM.
|
||||
|
||||
1. Arm the breakpoint at the `printf` call and hit it, exactly as in Step 13 steps 1-3. At the stop, `r0 = 0x10003560` and `r1 = 0x2a`.
|
||||
2. Put the replacement string into free RAM at `0x20080000` from IDA's **Python console** (`Plugins -> Python Console`) — no command port needed:
|
||||
```python
|
||||
dbg.write_memory(0x20080000, b"patched_fav_num: %d\r\n\x00")
|
||||
```
|
||||
`dbg.write_memory(address, bytes)` is IDA's debugger memory-write API; it returns `True` on success. That writes `patched_fav_num: %d\r\n\0`.
|
||||
3. Point `r0` at that string:
|
||||
```python
|
||||
dbg.set_reg_value("r0", 0x20080000)
|
||||
```
|
||||
(Or right-click `r0` in the **Registers** widget, press `E`, type `0x20080000`, and press Enter.)
|
||||
4. If you want the value hack too, set `r1` to `0x2b` as in Step 13. Then move the breakpoint past the call in the GUI (remove it at `0x10000268`, set one at `0x1000026c`) and click **Resume**. The core runs `printf` with `r0` pointing at your RAM string and `r1 = 0x2b`, so this iteration prints:
|
||||
```
|
||||
patched_fav_num: 43
|
||||
```
|
||||
then stops at `0x1000026c`.
|
||||
|
||||
Like the value hack, this is **one iteration only**: the loop reloads `r0` (and `r1`) from flash on every pass, so the next line is `regular_fav_num: 42` again. The permanent version is the static patch in Step 18c.
|
||||
|
||||
### Step 14: Why the hack reverts (and why we patch next)
|
||||
|
||||
Press **Resume**. The loop branches back to `0x10000264`, which reloads `movs r1, #42`, so the next line is `regular_fav_num: 42`. The live edit changed one iteration only. There is no stack variable in memory to change; the value is baked into the instruction. To make `regular_fav_num: 43` permanent we must patch the instruction. That is the static pass.
|
||||
|
||||
Press **Pause** to stop the output flood.
|
||||
|
||||
### Step 15: Kill the debugger and OpenOCD
|
||||
|
||||
The live hack is done. Do this **before** the static pass.
|
||||
|
||||
1. In the **Debugger** sidebar, click the **X** (**Kill**) (or **`Debugger -> Kill`**) to disconnect IDA.
|
||||
2. **Kill does not stop the OpenOCD process** — `debug-server.sh` started it separately, and it keeps running and holding the probe. Stop it from the IDA console:
|
||||
|
||||
**macOS / Linux:**
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # stop the debug server, free the probe
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # stop the debug server, free the probe
|
||||
```
|
||||
|
||||
3. Confirm nothing is left: `pgrep -fl openocd` (macOS/Linux) prints nothing.
|
||||
|
||||
From a terminal it is the same: `pkill -TERM -f openocd`, or `Get-Process openocd | Stop-Process` on Windows.
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Static — Resolve the Functions in IDA and Patch
|
||||
## Part 3: Static — Resolve the Functions in IDA and Patch
|
||||
|
||||
### Step 16: Resolve the functions in the IDA GUI
|
||||
|
||||
@@ -1053,318 +655,9 @@ python ..\uf2conv.py 0x0014_static-variables-h.bin ^
|
||||
>
|
||||
> This writes `hacked.uf2` next to the `.bin`, ready to drag onto the Pico.
|
||||
|
||||
### Step 21: Flash and verify
|
||||
## Static stopping point
|
||||
|
||||
Hold **BOOTSEL**, plug in the Pico 2, and drag `hacked.uf2` onto the **`RP2350`** drive. Open the serial monitor:
|
||||
|
||||
```
|
||||
patched_fav_num: 43
|
||||
static_fav_num: 42
|
||||
patched_fav_num: 43
|
||||
static_fav_num: 43
|
||||
patched_fav_num: 43
|
||||
static_fav_num: 44
|
||||
...
|
||||
```
|
||||
|
||||
and the **LED is on by default** (button released) and turns **off** when you press the button — the opposite of the stock image.
|
||||
|
||||
**The regular value is now 43, the label reads `patched_fav_num:`, and the button/LED logic is inverted — with eighteen bytes changed and no source code.** (`static_fav_num` keeps incrementing from `42`, unchanged: we left the static variable itself alone so you can watch it persist.)
|
||||
|
||||
> **Faster: flash over the Debug Probe (no BOOTSEL).** The repo's `flash.sh` writes the raw `.bin` straight into XIP flash over SWD (`program <bin> 0x10000000 verify reset exit`), so you never touch BOOTSEL or a UF2. Run it from a terminal (`./flash.sh <bin>`), or from the IDA console **without freezing it** — use `subprocess.Popen`, which returns immediately, and send OpenOCD's output to a log file. (`subprocess.run` blocks the console until the flash finishes; do not use it here.)
|
||||
>
|
||||
> ```python
|
||||
> import os, subprocess
|
||||
> root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip()
|
||||
> bin_path = os.path.join(os.path.join(root, "0x0014_static-variables", "build"), "0x0014_static-variables-h.bin")
|
||||
> log = os.path.join(os.path.join(root, "0x0014_static-variables", "build"), "flash.log")
|
||||
> subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # free the probe first
|
||||
> p = subprocess.Popen([os.path.join(root, "flash.sh"), bin_path],
|
||||
> stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
> print("flashing in the background; log:", log)
|
||||
> ```
|
||||
>
|
||||
> The `pkill` frees the probe first; on Windows use `subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"])`.
|
||||
>
|
||||
> The console is free the moment this returns. Check it with `print(p.poll())` (`None` = still running, `0` = done) or read `flash.log` — success ends with `** Verified OK **`.
|
||||
>
|
||||
> The same non-blocking form without the script:
|
||||
>
|
||||
> ```python
|
||||
> import os, subprocess
|
||||
> ocd = os.path.expanduser("~/.pico-sdk/openocd/0.12.0+dev")
|
||||
> bin_path = os.path.join(os.path.join(root, "0x0014_static-variables", "build"), "0x0014_static-variables-h.bin")
|
||||
> log = os.path.join(os.path.join(root, "0x0014_static-variables", "build"), "flash.log")
|
||||
> subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # free the probe first
|
||||
> p = subprocess.Popen([f"{ocd}/openocd", "-s", f"{ocd}/scripts",
|
||||
> "-f", "interface/cmsis-dap.cfg", "-f", "target/rp2350.cfg",
|
||||
> "-c", "adapter speed 5000",
|
||||
> "-c", f"program {bin_path} 0x10000000 verify reset exit"],
|
||||
> stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
> print("flashing in the background; log:", log)
|
||||
> ```
|
||||
>
|
||||
> **The Debug Probe is single-owner.** If IDA is still attached (the `debug-server.sh` OpenOCD is running), the flash cannot grab the probe. Detach in IDA and stop that OpenOCD first:
|
||||
>
|
||||
> ```bash
|
||||
> # macOS / Linux
|
||||
> pkill -TERM -f openocd
|
||||
> ```
|
||||
> ```powershell
|
||||
> # Windows
|
||||
> Get-Process openocd -ErrorAction SilentlyContinue | Stop-Process
|
||||
> ```
|
||||
>
|
||||
> Success looks like `Programming Finished` -> `Verified OK` -> `Resetting Target`. On Windows use `flash.ps1` (`.\flash.ps1 -Bin <path>`) the same way.
|
||||
|
||||
---
|
||||
|
||||
## Cheatsheet
|
||||
|
||||
### IDA GUI actions
|
||||
|
||||
| Action | How |
|
||||
| ------ | --- |
|
||||
| Go to address | `G` |
|
||||
| Rename function/symbol | `N` |
|
||||
| Set type or signature | `Y` |
|
||||
| Add comment | `;` |
|
||||
| Open Hex view | `View -> Hex` |
|
||||
| Enable hex editing | Toggle the lock in the status bar |
|
||||
| Reanalyze after a patch | Right-click function -> `Reanalyze` |
|
||||
| Edit a register live | `dbg.set_reg_value("r1", 0x2b)` in the Python console (or right-click the register, press `E`, type hex, Enter) |
|
||||
| Write a RAM string live | `dbg.write_memory(0x20080000, b"patched_fav_num: %d\r\n\x00")` |
|
||||
| Set a breakpoint | `Debugger -> Add Hardware Breakpoint...` (hardware execute). Do **not** use `F2` — software breakpoints cannot be written to read-only flash. |
|
||||
| Move a breakpoint | Remove it and set it at the new address in the GUI (command-port fallback: `rbp <old addr>` then `bp <new addr> 2 hw`) |
|
||||
| Confirm what is armed | The **Breakpoints** widget lists it (command-port fallback: `mdw 0xE0002000 8`, each armed breakpoint shows as `<addr \| 1>`) |
|
||||
| Apply the ELF symbol map | Paste the Python snippet from Step 16 into the Python Console |
|
||||
|
||||
### OpenOCD server and reset
|
||||
|
||||
The server runs with `gdb_breakpoint_override hard` so that flash-writes are never attempted. Breakpoints in this lab are set in the IDA GUI through the **GDB MI** adapter (Step 12). The command-port rows below are the fallback if you use the **GDB RSP** adapter instead.
|
||||
|
||||
| Action | Command |
|
||||
| ------ | ------- |
|
||||
| Connect to the OpenOCD prompt (fallback) | `nc 127.0.0.1 4444` (or `telnet 127.0.0.1 4444`) |
|
||||
| Reset and run (command port) | `reset run` |
|
||||
| Check core state (command port) | `targets` |
|
||||
| Set a breakpoint in the GUI | `Debugger -> Add Hardware Breakpoint...` (hardware execute; `F2` software breakpoints do not work on flash) |
|
||||
| (fallback) Add a breakpoint without the GUI | `bp <addr> 2 hw` |
|
||||
| Remove one breakpoint | `rbp <addr>` — **address only, no length, no `hw`** |
|
||||
| Remove every breakpoint | `rbp all` |
|
||||
| Start the server parked at `main` | macOS/Linux: `BP_ADDR=0x10000234 ./debug-server.sh` — Windows: `$env:BP_ADDR="0x10000234"; .\debug-server.ps1` (one-shot) |
|
||||
| Start the server parked in the loop | macOS/Linux: `BP_ADDR=0x10000268 ./debug-server.sh` — Windows: `$env:BP_ADDR="0x10000268"; .\debug-server.ps1` |
|
||||
| Break on the loop in a running target | set a hardware breakpoint in the GUI at the loop address, then **Resume** — repeatable |
|
||||
| Make IDA stepping work | `rp2350.dap.core0 configure -rtos none` (already in the scripts) |
|
||||
| Step without re-trapping | move the breakpoint off the current PC first, then **Step Into**/**Step Over** |
|
||||
| Reset without desyncing IDA | **Detach**, `reset run` on the port, reconnect — never `reset run` while attached |
|
||||
|
||||
### Every address and byte we changed
|
||||
|
||||
| Project | Address | Before | After | Effect |
|
||||
| ------- | ------- | ------ | ----- | ------ |
|
||||
| `0x0014` | `0x10000264` | `2a` | `2b` | `movs r1, #42` -> `#43`, prints `regular_fav_num: 43` |
|
||||
| `0x0014` | `0x10000288` | `01` | `00` | `eor.w r3, r3, #1` -> `#0`, inverts the button/LED logic |
|
||||
| `0x0014` | `0x10003560` | `72 65 67 75 6c 61 72 5f 66 61 76 5f 6e 75 6d 3a` | `70 61 74 63 68 65 64 5f 66 61 76 5f 6e 75 6d 3a` | string prints `patched_fav_num:` instead of `regular_fav_num:` |
|
||||
|
||||
### The static-variable memory map
|
||||
|
||||
| Item | Address | Notes |
|
||||
| ---- | ------- | ----- |
|
||||
| `static_fav_num` | `0x200005a8` | RAM `.data`, fixed for the life of the program; initial value `42` copied from flash at boot |
|
||||
| Literal-pool word 1 | `0x10000290` | `0x200005a8` — the RAM address `r4` holds |
|
||||
| Literal-pool word 2 | `0x10000294` | `0x10003560` — pointer to `"regular_fav_num: %d\r\n"` |
|
||||
| Literal-pool word 3 | `0x10000298` | `0x10003578` — pointer to `"static_fav_num: %d\r\n"` |
|
||||
| `regular_fav_num` | (no address) | inlined to `movs r1, #42`; never given a stack slot |
|
||||
|
||||
### Raw image facts
|
||||
|
||||
| Item | Value |
|
||||
| ---- | ----- |
|
||||
| Build type | `Release` |
|
||||
| Load base address | `0x10000000` |
|
||||
| Project size | `15516` bytes (`0x3c9c`) |
|
||||
| Fixed `main` anchor | `0x1000018c` (reset handler middle `blx`) |
|
||||
| `main` | `0x10000234` |
|
||||
| `printf` call / return, `regular_fav_num` | `0x10000268` / `0x1000026c` |
|
||||
| `printf` call, `static_fav_num` | `0x10000270` |
|
||||
| `static_fav_num` RAM address | `0x200005a8` |
|
||||
| `regular_fav_num` format string | `0x10003560` |
|
||||
| `static_fav_num` format string | `0x10003578` |
|
||||
| RP2350 UF2 family ID | `0xe48bff59` |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### IDA hangs or crashes when you connect (macOS 27)
|
||||
|
||||
Three different causes have been seen on this setup; check them in this order.
|
||||
|
||||
- **A breakpoint set before connecting.** With the **GDB MI** adapter, if the binary view already has a breakpoint, the session hangs. Start parked with `BP_ADDR`, connect, then add breakpoints (see the next entry).
|
||||
- **The wrong GDB executable.** Point **Full GDB Executable Path** at the **14.2.rel1** toolchain (Step 10). The 13.3.rel1 build did **not** connect in testing.
|
||||
- **The LLDB adapter.** A crash report with `libdebuggercore.dylib -> std::terminate() -> abort()` and `liblldb` in the stack is the **LLDB** adapter, not GDB MI. Avoid LLDB on this setup.
|
||||
|
||||
**Use GDB MI**, with the 14.2.rel1 path above. If it still fails, fall back to plain `arm-none-eabi-gdb` against the same server — the addresses and register values are identical to the GUI steps.
|
||||
|
||||
If IDA hangs, force-quit it; the connect dialog has no working Cancel. The static steps (resolve, patch, export, flash) never touch the debugger and always work.
|
||||
|
||||
### The GUI refuses to set a breakpoint (GDB RSP adapter only)
|
||||
|
||||
If you are on the **GDB RSP** adapter, the GUI cannot set breakpoints on this target. That adapter is IDA's own minimal RSP client and sends a **1-byte** breakpoint (`Z0,<addr>,1`); the Cortex-M33 comparators need 2 bytes, so OpenOCD answers `only breakpoints of two bytes length supported`. It affects every address, both `Toggle Breakpoint` and `Add Hardware Breakpoint`, and the dialog's **Size** field is disabled. `gdb_breakpoint_override` makes no difference.
|
||||
|
||||
**Fix: use the GDB MI adapter** (Step 10). It drives real GDB, which sends the correct length, so GUI breakpoints just work. If you must stay on GDB RSP, arm breakpoints from the command port after connecting (`bp <addr> 2 hw`) — but the lab uses GDB MI and does not need that.
|
||||
|
||||
### GDB MI hangs when you connect (a breakpoint already existed)
|
||||
|
||||
With the **GDB MI** adapter, if IDA already has a breakpoint set when you connect, the session **hangs**. This is a IDA bug. The working order is:
|
||||
|
||||
1. Start the server parked, e.g. `BP_ADDR=0x10000234 ./debug-server.sh` (Windows: `$env:BP_ADDR="0x10000234"; .\debug-server.ps1`).
|
||||
2. Connect with the **GDB MI** adapter.
|
||||
3. Only *then* set hardware breakpoints in the UI.
|
||||
|
||||
Never have a breakpoint in the binary view before the GDB MI connection. If it hangs, quit IDA, restart the server with `BP_ADDR`, and connect again before adding any breakpoints.
|
||||
|
||||
### Step Into / Step Over does nothing (PC never moves)
|
||||
|
||||
Two causes have been seen on this target.
|
||||
|
||||
1. **A breakpoint on the current PC re-traps the step.** OpenOCD's step-over-breakpoint logic fails with `Duplicate Breakpoint address` and the PC stays put. Fix: move the breakpoint off the current PC (in the GUI), then step.
|
||||
2. **The `hwthread` RTOS (GDB RSP adapter only).** With the **GDB RSP** adapter, OpenOCD can log `fake step thread 0` and reply without stepping, because the RP2350 config's `-rtos hwthread` makes the current thread id 1 while IDA sends thread id 0. Fix: `rp2350.dap.core0 configure -rtos none` (the launcher scripts already pass this). **GDB MI does not hit this.**
|
||||
|
||||
To tell them apart, turn on OpenOCD logging (`log_output /tmp/ocd.log`, then `debug_level 3` on the command port) and look for `fake step` versus `Duplicate Breakpoint`.
|
||||
|
||||
### `zsh: bad CPU type in executable: cmake`
|
||||
|
||||
An Intel `x86_64` tool is on your `PATH` on Apple Silicon. Run Step 2: `export PATH="/opt/homebrew/bin:$PATH"`, then `hash -r`. Add it to `~/.zshrc` to make it permanent.
|
||||
|
||||
### My addresses do not match this guide
|
||||
|
||||
You probably built `Debug`. This lesson is a `Release` build. Re-run Step 3 with `-DCMAKE_BUILD_TYPE=Release`. A `Debug` build moves the SDK functions and keeps `demo_static_variable` separate, so `main` is not at `0x10000234`.
|
||||
|
||||
### A breakpoint never fires
|
||||
|
||||
First, confirm you actually set one, and that it is a **hardware** breakpoint. With the **GDB MI** adapter, `Debugger -> Add Hardware Breakpoint...` (hardware execute) should land in the **Breakpoints** widget. If nothing lands, or the core keeps running, you probably used `F2` (`Toggle Breakpoint`) — that is a software breakpoint and cannot be written to read-only flash, so it never installs. Also check you are on **GDB MI**, not **GDB RSP** (the GDB RSP adapter cannot set breakpoints on this target at all).
|
||||
|
||||
Then check the order and the state:
|
||||
|
||||
- **Arm it only after IDA is connected.** OpenOCD flushes every breakpoint when a client attaches, so anything armed earlier is gone. This also applies to `BP_ADDR` on the startup command line.
|
||||
- **Verify it is armed:** `mdw 0xE0002000 8`. You should see your address with the low bit set (`0x10000234` -> `0x10000235`). All zeros means nothing is armed — re-read this first, because it distinguishes "not armed" from "armed but never reached".
|
||||
- **Is the core running?** `poll` on the command port should not report a halt. If it is stopped, click **Resume**.
|
||||
- **Does the address get reached again?** `main` runs once per reset, so use `BP_ADDR` at startup (Step 9) rather than `reset run` while attached. Loop addresses such as `0x10000268` fire on the next pass with no reset — arm them and click **Resume** in IDA.
|
||||
- **With GDB MI the stop is reported as `Breakpoint`** and appears in the **Breakpoints** widget, because GDB really did set it.
|
||||
|
||||
### I edit `r1` (or another register) and it reverts
|
||||
|
||||
`main` reloads the value at the top of every loop iteration — `movs r1, #42` at `0x10000264` runs right before the `printf` at `0x10000268`. So `r1` is only `0x2b` for the instant between your edit and the next pass; then it is `0x2a` again. The edit sticks only if the core is **genuinely stopped** at the breakpoint and stays stopped.
|
||||
|
||||
If it keeps reverting, the core is running, which almost always means the breakpoint is not installed — usually because it is a **software** breakpoint (`F2`) that cannot be written to read-only flash. Use `Debugger -> Add Hardware Breakpoint...` (hardware execute).
|
||||
|
||||
> **The Registers widget is a snapshot, not a live view.** IDA reads the registers at each stop and shows that snapshot; it does not poll the target, and there is no "refresh registers" command. So a value changed outside IDA will not appear until the next stop.
|
||||
|
||||
### The static variable shows a wrong value in GDB / IDA
|
||||
|
||||
`static_fav_num` is a function-local `static`, so it does not appear in the global symbol table as `static_fav_num`; `arm-none-eabi-nm` names it `static_fav_num.0`. A 4-byte read at `0x200005a8` returns `0x0000122a`, whose low byte `0x2a` (42) is the variable — the upper bytes belong to whatever is in adjacent RAM. Read **one byte** (`x/1ub 0x200005a8`) to see `42`, and never add a `*` (that would treat the value as a pointer). In IDA, define a `uint8_t` at `0x200005a8` if you want the decompiler to show the byte cleanly.
|
||||
|
||||
### The serial capture is garbage on macOS
|
||||
|
||||
Reading `/dev/cu.usbmodem*` with a bare `read()` returns garbage. Set **raw termios at 115200** first: clear canonical/echo flags, set `CLOCAL|CREAD`, and `B115200` on input and output. `screen /dev/cu.usbmodem* 115200` does all of this for you; a script must call `tcsetattr` itself. Once set, the capture reads clean `regular_fav_num: 42` lines.
|
||||
|
||||
### It worked for a second, then stopped (IDA's view desyncs)
|
||||
|
||||
This is the most common failure, and it has one main cause: **driving the core from the OpenOCD command port while IDA is connected.**
|
||||
|
||||
- If you send `reset run` from the port while attached, the core resets, runs, and halts at your breakpoint — but IDA never receives the stop event. Its sidebar keeps showing the *previous* location, so **Step** and **Resume** act on a stale PC and appear to do nothing.
|
||||
- If the OpenOCD process dies (or you restart it) while attached, IDA keeps believing it is connected: the sidebar stays, but the menu shows **Pause** enabled and **Resume**/**Step** disabled because IDA last saw the target *running*.
|
||||
|
||||
Recovery: **Detach, then reconnect.** If Detach does nothing (the connection is already dead), restart IDA — its menu still shows a session that no longer exists.
|
||||
|
||||
Prevention:
|
||||
|
||||
- Stop at `main` with `BP_ADDR` on a fresh server start, not with `reset run` while attached.
|
||||
- For loop addresses, set the breakpoint in the GUI and click **Resume**. Let IDA be the thing that starts the core.
|
||||
- If you must reset, **Detach first**, `reset run`, then reconnect.
|
||||
- Never leave a breakpoint on the PC you are about to step or resume from.
|
||||
|
||||
### The target "blows past" `main` and stops at `0x100032cc` instead
|
||||
|
||||
`0x100032cc` is inside `stdio_uart_out_flush`:
|
||||
|
||||
```asm
|
||||
100032c8: 4b02 ldr r3, [pc, #8] ; @ 0x100032d4
|
||||
100032ca: 681a ldr r2, [r3]
|
||||
100032cc: 6993 ldr r3, [r2, #24] ; the core sits here while the UART drains
|
||||
100032ce: 071b lsls r3, r3, #28
|
||||
100032d0: d4fc bmi.n 0x100032cc
|
||||
100032d2: 4770 bx lr
|
||||
100032d4: 20000850 .word 0x20000850
|
||||
```
|
||||
|
||||
That is the UART transmit-FIFO drain loop inside `printf`, so the core is running `main`'s loop and simply spends nearly all its time there. The breakpoint at `main` did not fire because `main`'s entry runs exactly **once per reset**. If you arm the breakpoint after the reset, or set it while the target is already running and just resume, the core is already past `main` and will never re-execute it. Either arm the breakpoint **before** resetting, or break inside the loop at `0x10000268`, which fires every iteration.
|
||||
|
||||
**`0x100032cc` is not a function.** It is one instruction inside `stdio_uart_out_flush`, which starts at `0x100032c8`. If IDA has created a function at `0x100032cc` (for example because the debugger stopped at that PC), the decompiler shows garbage. Delete that bogus function (right-click it -> `Delete Function`, or put the cursor on it and press `U` to undefine) and reanalyze. The real function is `stdio_uart_out_flush` at `0x100032c8`.
|
||||
|
||||
### The console floods with `Failed to read memory at 0xf0000000`
|
||||
|
||||
Core1 is exposed. The scripts must run with `USE_CORE=0`. Stop the server, confirm only `core0` is reported, restart, then restart IDA.
|
||||
|
||||
### `Connect to Remote Process` is greyed out and Pause does nothing
|
||||
|
||||
IDA is in a stale session, usually because the debug server restarted while attached. Quit and reopen IDA (or the `.i64`) and connect again.
|
||||
|
||||
### The decompiler still shows the old value after patching
|
||||
|
||||
Right-click the function and choose `Reanalyze`.
|
||||
|
||||
### The LED does not change after patching
|
||||
|
||||
You patched the wrong byte. The inversion immediate is the **third** byte of the 4-byte `eor.w` instruction: the instruction is at `0x10000286`, so the byte to change is `0x10000288`. Confirm it now reads `00`, not `01`.
|
||||
|
||||
---
|
||||
|
||||
## Fallback: do the dynamic steps with GDB (macOS 27)
|
||||
|
||||
If IDA's debugger crashes on attach on macOS 27 (see Troubleshooting), you can still do the live hack with the ARM GDB from the toolchain, against the same OpenOCD server. The addresses and register values are identical to the GUI steps.
|
||||
|
||||
Start the debug server (Step 9), then in a new terminal:
|
||||
|
||||
```
|
||||
arm-none-eabi-gdb
|
||||
```
|
||||
|
||||
At the `(gdb)` prompt:
|
||||
|
||||
```
|
||||
set architecture armv8-m.main
|
||||
target extended-remote :3333
|
||||
hbreak *0x10000268
|
||||
continue
|
||||
```
|
||||
|
||||
Do **not** run `monitor reset run` before `hbreak`. `0x10000268` is inside `main`'s loop, so the breakpoint fires on the next iteration with no reset. If you reset first, the core runs `main` and you will not catch it.
|
||||
|
||||
GDB stops at the `printf` call. Confirm the value, change it, and let it run:
|
||||
|
||||
```
|
||||
info registers pc r1 # pc = 0x10000268, r1 = 0x2a
|
||||
set $r1 = 0x2b
|
||||
stepi
|
||||
continue
|
||||
```
|
||||
|
||||
The serial monitor prints `regular_fav_num: 43` for the iteration you changed — the same temporary live hack as editing `r1` in the IDA Registers widget. When you are done, press `Ctrl-C`, then `detach` and `quit`.
|
||||
|
||||
**If you specifically want to stop at `main` (`0x10000234`),** remember its entry runs only once per reset, so the breakpoint must be armed *before* the reset:
|
||||
|
||||
```
|
||||
monitor reset halt
|
||||
hbreak *0x10000234
|
||||
continue
|
||||
```
|
||||
|
||||
If you instead set it while the target is running and just `continue`, you will "blow past" `main` and catch the core inside `printf` — in this build at `0x100032cc`, the `stdio_uart_out_flush` UART-drain loop.
|
||||
|
||||
`hbreak` sets a hardware breakpoint, which is required for read-only flash. It works from plain GDB because GDB sends the 2-byte length the Cortex-M33 comparators need. IDA's **GDB MI** adapter goes through the same GDB, so its GUI breakpoints work too; the old **GDB RSP** adapter was the one that sent a 1-byte length and could not set breakpoints here.
|
||||
The patched image has been exported and converted to UF2. This IDA guide ends at static verification; it does not connect to, flash, or debug a live target.
|
||||
|
||||
## Glossary
|
||||
|
||||
@@ -1377,7 +670,7 @@ If you instead set it while the target is running and just `continue`, you will
|
||||
| **Automatic variable** | A local variable created and destroyed with its block; lives on the stack (or is optimized away) |
|
||||
| **`eor` / XOR** | Exclusive OR — flips bits where the operands differ |
|
||||
| **GPIO** | General Purpose Input/Output — controllable pins on the microcontroller |
|
||||
| **Hardware breakpoint** | A breakpoint serviced by the CPU comparators, required for read-only flash |
|
||||
ly flash |
|
||||
| **Inlining** | The optimizer replacing a function call with the function body; why `demo_static_variable` disappears |
|
||||
| **Literal pool** | A block of 32-bit constants that Thumb-2 code reaches with PC-relative `ldr` |
|
||||
| **MMIO** | Memory-mapped I/O — hardware registers accessed as memory addresses |
|
||||
@@ -1392,4 +685,4 @@ If you instead set it while the target is running and just `continue`, you will
|
||||
|
||||
---
|
||||
|
||||
**Remember:** the ELF tells you what every address is, and the `.bin` is what you actually patch. Prove the behavior dynamically, read `r1` at the `printf` call, resolve the names from the ELF, then patch the bytes — the constant `42`, the inversion immediate, and (optionally) the label — and flash.
|
||||
**Remember:** the ELF tells you what every address is, and the `.bin` is what you actually patch. Verify the static behavior from the recovered instructions.
|
||||
+11
-745
@@ -25,11 +25,10 @@ By using this repository and course, you acknowledge and agree that:
|
||||
- Read how a `#define` macro **and** a `const` variable both fold to bare instruction immediates
|
||||
- Tell a 16-bit `movs r1, #42` from a 32-bit `movw r1, #1337` in the disassembly
|
||||
- Follow the I2C path through the `i2c_inst_t` struct: `I2C_PORT` -> `i2c1` -> `&i2c1_inst` -> `hw` -> `0x40098000`
|
||||
- **Break at the `printf` call** on live silicon and hack the printed constant live
|
||||
- Optionally **hack the LCD string live** by pointing `r0` at a RAM replacement
|
||||
- **Resolve the functions in the IDA GUI** using the ELF symbol map, including the `lcd_1602.c` driver symbols
|
||||
- **Patch** both constants (`42 -> 43`, `1337 -> 1344`) and the LCD string (`"Reverse" -> "Exploit"`)
|
||||
- **Export** the patched image, convert it to UF2, and flash it
|
||||
- **Export** the patched image, convert it to UF2, and export it
|
||||
|
||||
---
|
||||
|
||||
@@ -48,10 +47,10 @@ The `.bin` is built **from** the `.elf`, so the ELF tells you exactly what is at
|
||||
|
||||
The order is **dynamic first, static second**:
|
||||
|
||||
1. Break on the live target and prove what the code does.
|
||||
2. Hack it live in the debugger and watch the output change.
|
||||
d prove what the code does.
|
||||
ge.
|
||||
3. Resolve the functions in IDA using the ELF symbol map.
|
||||
4. Patch the bytes, export, convert, and flash.
|
||||
4. Patch the bytes, export, convert and export.
|
||||
|
||||
| Project | Serial output | Also does | The hacks |
|
||||
| ------- | ------------- | --------- | --------- |
|
||||
@@ -63,7 +62,7 @@ The order is **dynamic first, static second**:
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Build, Flash, and Get the Symbol Map
|
||||
## Part 1: Build and Get the Symbol Map
|
||||
|
||||
### Step 1: Install the toolchain
|
||||
|
||||
@@ -75,60 +74,10 @@ The order is **dynamic first, static second**:
|
||||
|
||||
**macOS Apple Silicon**
|
||||
|
||||
```bash
|
||||
brew install cmake ninja
|
||||
```
|
||||
|
||||
- Install **IDA Pro** and complete its license activation.
|
||||
- Install the **Arm GNU Toolchain**, or let the VS Code Pico extension manage it.
|
||||
|
||||
**Linux x64**
|
||||
|
||||
```bash
|
||||
sudo apt install cmake ninja-build gcc-arm-none-eabi libnewlib-arm-none-eabi git python3 openocd minicom
|
||||
```
|
||||
|
||||
- Install **IDA Pro** and complete its license activation.
|
||||
|
||||
### Step 2: Verify your tools are the right architecture (do not skip this)
|
||||
|
||||
On **macOS Apple Silicon**, the most common failure is an Intel `x86_64` tool on your `PATH`:
|
||||
|
||||
```
|
||||
zsh: bad CPU type in executable: cmake
|
||||
```
|
||||
|
||||
You may have **two Homebrews**: the arm64 one at `/opt/homebrew` and the Intel one at `/usr/local`. If `/usr/local/bin` wins, every `brew` tool is x86_64. Check:
|
||||
|
||||
```bash
|
||||
file "$(which cmake)"
|
||||
file "$(which ninja)"
|
||||
file "$(which arm-none-eabi-gdb)"
|
||||
file "$(which arm-none-eabi-nm)"
|
||||
file "$(which openocd)"
|
||||
file "$(which telnet)"
|
||||
```
|
||||
|
||||
All must report `arm64`. If any is `x86_64`, put the Apple Silicon prefix first for the session and check again:
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
hash -r
|
||||
file "$(which cmake)"
|
||||
```
|
||||
|
||||
To make it permanent, add that `export` to `~/.zshrc`. Do not use Rosetta as a fix; OpenOCD and GDB are exactly the kind of programs where a translation layer produces failures that look like debugger bugs.
|
||||
|
||||
**`telnet` is special — and optional.** The GDB MI workflow does not need it; it is only used by the command-port fallback. macOS no longer ships `telnet`, and the Homebrew build is often the Intel one, so `telnet 127.0.0.1 4444` fails with `bad CPU type in executable`. Call the Apple Silicon Homebrew explicitly:
|
||||
|
||||
```bash
|
||||
/opt/homebrew/bin/brew install telnet
|
||||
```
|
||||
|
||||
If you would rather not install anything, macOS ships an arm64 `nc`, which can connect to the same OpenOCD port:
|
||||
|
||||
```bash
|
||||
nc 127.0.0.1 4444
|
||||
bash
|
||||
```
|
||||
|
||||
**Windows x64** and **Linux x64** do not have this problem. Skip to Step 3.
|
||||
@@ -285,69 +234,6 @@ Two things in this project have **no symbol of their own**, because the compiler
|
||||
- `init_i2c_and_lcd` and `write_lcd_greeting` — the two `static` helpers in our own source are inlined, so there is no address to rename. You see their bodies directly inside `main`.
|
||||
- `gpio_pull_up` — `static inline` in the SDK, so `gpio_pull_up(2)` and `gpio_pull_up(3)` compile to direct calls to `gpio_set_pulls` at `0x10000258` and `0x10000262`.
|
||||
|
||||
### Step 5: Flash and confirm the output
|
||||
|
||||
A `.bin` has no headers, so OpenOCD must be told the base address `0x10000000`. From the repository root:
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```bash
|
||||
./flash.sh 0x0017_constants/build/0x0017_constants.bin
|
||||
```
|
||||
|
||||
**Windows x64 (PowerShell):**
|
||||
|
||||
```powershell
|
||||
.\flash.ps1 -Bin 0x0017_constants\build\0x0017_constants.bin
|
||||
```
|
||||
|
||||
**Or flash from the IDA console** (the console reads the repo root from the marker file, so it works with no database open):
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
bin_path = os.path.join(root, "0x0017_constants", "build", "0x0017_constants.bin")
|
||||
log = os.path.join(os.path.dirname(bin_path), "flash.log")
|
||||
subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # free the probe first
|
||||
subprocess.Popen([os.path.join(root, "flash.sh"), bin_path],
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
print("flashing in the background; log:", log)
|
||||
```
|
||||
|
||||
**Windows x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
bin_path = os.path.join(root, "0x0017_constants", "build", "0x0017_constants.bin")
|
||||
log = os.path.join(os.path.dirname(bin_path), "flash.log")
|
||||
subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # free the probe first
|
||||
subprocess.Popen(["powershell", "-ExecutionPolicy", "Bypass", "-File",
|
||||
os.path.join(root, "flash.ps1"), "-Bin", bin_path],
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT)
|
||||
print("flashing in the background; log:", log)
|
||||
```
|
||||
|
||||
Wait for `wrote 17980 bytes ...` and `** Verified OK **`. Open a serial monitor at **115200** baud:
|
||||
|
||||
- **Windows x64:** PuTTY -> Connection type **Serial**, the Pico's COM port, speed `115200`.
|
||||
- **macOS Apple Silicon:** `screen /dev/tty.usbmodem* 115200` (quit with `Ctrl-A` then `K`).
|
||||
- **Linux x64:** `minicom -D /dev/ttyACM0 -b 115200`.
|
||||
|
||||
```
|
||||
FAV_NUM: 42
|
||||
OTHER_FAV_NUM: 1337
|
||||
FAV_NUM: 42
|
||||
OTHER_FAV_NUM: 1337
|
||||
...
|
||||
```
|
||||
|
||||
The 1602 LCD shows `Reverse` on line 1 and `Engineering` on line 2. Both numbers print forever, because both constants are baked into the loop as immediates. That is the behavior we will change.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Load the Raw `.bin` into IDA
|
||||
|
||||
Start from a fresh IDA state. If you already have a database for this lesson, **close it and start over**; a stale database keeps old names and patches.
|
||||
@@ -409,309 +295,13 @@ Once `main` shows code, the import is correct.
|
||||
- **Hex view:** raw bytes, used for patching.
|
||||
- **Function list:** the sidebar list of every detected function.
|
||||
|
||||
Navigation: `G` go to address, `N` rename, `Y` set type or signature, `;` add a comment. Breakpoints are set from the GUI through the GDB MI adapter — see Step 12.
|
||||
Navigation: `G` go to address, `N` rename, `Y` set type or signature, `;` add a comment.
|
||||
|
||||
> **macOS function keys:** the top-row `F` keys are usually mapped to system functions. Every step here uses menu paths that work without them.
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Dynamic — Break at the `printf` Call and Hack Live
|
||||
|
||||
### Step 9: Start OpenOCD as a live debug server
|
||||
|
||||
Make sure no other OpenOCD is running; a forgotten server holds port `3333`.
|
||||
|
||||
**macOS / Linux:**
|
||||
|
||||
```bash
|
||||
ps aux | grep -i openocd
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
Get-Process | Where-Object { $_.ProcessName -like '*openocd*' }
|
||||
```
|
||||
|
||||
Stop any leftover server gracefully:
|
||||
|
||||
**macOS / Linux:**
|
||||
|
||||
```bash
|
||||
pkill -TERM -f openocd
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
Get-Process openocd -ErrorAction SilentlyContinue | Stop-Process
|
||||
```
|
||||
|
||||
Start the server **parked at `main`**:
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```bash
|
||||
BP_ADDR=0x10000234 ./debug-server.sh
|
||||
```
|
||||
|
||||
**Windows x64 (PowerShell):**
|
||||
|
||||
```powershell
|
||||
$env:BP_ADDR="0x10000234"; .\debug-server.ps1
|
||||
```
|
||||
|
||||
**Or start it from the IDA console**, freeing the probe first and launching the server in the background so the console returns immediately:
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # stop any running server first
|
||||
log = os.path.join(root, "openocd.log")
|
||||
p = subprocess.Popen([os.path.join(root, "debug-server.sh")], cwd=root,
|
||||
env=dict(os.environ, BP_ADDR="0x10000234"),
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
print("OpenOCD started (pid", p.pid, "); log:", log)
|
||||
```
|
||||
|
||||
**Windows x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # stop any running server first
|
||||
log = os.path.join(root, "openocd.log")
|
||||
p = subprocess.Popen(["powershell", "-ExecutionPolicy", "Bypass", "-File",
|
||||
os.path.join(root, "debug-server.ps1")], cwd=root,
|
||||
env=dict(os.environ, BP_ADDR="0x10000234"),
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT)
|
||||
print("OpenOCD started (pid", p.pid, "); log:", log)
|
||||
```
|
||||
|
||||
`Popen` returns in a few milliseconds; the server keeps running in the background. Check `openocd.log` for `Listening on port 3333`, then connect in Step 10.
|
||||
|
||||
Wait for:
|
||||
|
||||
```
|
||||
Info : [rp2350.dap.core0] Examination succeed
|
||||
Startup breakpoint at 0x10000234 (2-byte hardware execute, one-shot).
|
||||
Info : starting gdb server for rp2350.dap.core0 on 3333
|
||||
Info : Listening on port 3333 for gdb connections
|
||||
```
|
||||
|
||||
> **`BP_ADDR` parks the core at `main` before any client connects.** The script arms a 2-byte hardware breakpoint and then does the startup `reset run`, so the core runs from the vector table and stops at your address with no debugger attached yet. When IDA connects a moment later, the first thing it reads is already the truth: `Stopped at 0x10000234`. This is the whole reason the lab works cleanly — you never have to drive a reset from outside the GUI.
|
||||
>
|
||||
> Use the address you actually want to stop at:
|
||||
>
|
||||
> | What you want to stop at | Address | Command |
|
||||
> | --- | --- | --- |
|
||||
> | `main` (once per reset) | `0x10000234` | `BP_ADDR=0x10000234 ./debug-server.sh` |
|
||||
> | The **loop** — the `FAV_NUM` `printf` call, hit every iteration | `0x10000292` | `BP_ADDR=0x10000292 ./debug-server.sh` |
|
||||
>
|
||||
> ```bash
|
||||
> BP_ADDR=0x10000234 ./debug-server.sh # park at main
|
||||
> BP_ADDR=0x10000292 ./debug-server.sh # park in the loop instead
|
||||
> ```
|
||||
>
|
||||
> ```powershell
|
||||
> $env:BP_ADDR="0x10000234"; .\debug-server.ps1 # park at main
|
||||
> $env:BP_ADDR="0x10000292"; .\debug-server.ps1 # park in the loop
|
||||
> ```
|
||||
>
|
||||
> **This startup stop is single-use.** OpenOCD flushes breakpoints when a client connects, so this one is gone once IDA attaches — fine for `main`, which only runs once per reset. Every breakpoint after that is set from the IDA GUI (Step 12) and is repeatable. To stop at `main` again, restart the server with `BP_ADDR` and reconnect.
|
||||
|
||||
> **Exactly one core.** The line must say `core0` and must **not** mention `core1`. Core1 is never started by this firmware; exposing it makes IDA read core1's reset-state registers, which are not real addresses, and OpenOCD floods the log with `Failed to read memory at 0xf0000000`. The scripts already use `USE_CORE=0`; do not change it.
|
||||
|
||||
> **Windows driver note:** the Debug Probe must use the **WinUSB** driver. If OpenOCD reports `unable to open CMSIS-DAP device`, install it with [Zadig](https://zadig.akeo.ie/) (select `Debug Probe (CMSIS-DAP)` -> WinUSB).
|
||||
|
||||
### Step 10: Connect IDA to the GDB server
|
||||
|
||||
1. Make sure the image is open and analyzed (Part 2) and the server from Step 9 is running (parked at `main`).
|
||||
2. Choose `Debugger -> Connect to Remote Process`.
|
||||
3. In the **adapter** dropdown, select **GDB MI**.
|
||||
4. In the **connect** settings group, set **IP Address** to `127.0.0.1` and **Port** to `3333`.
|
||||
5. Set **Full GDB Executable Path** to the `arm-none-eabi-gdb` from the **Arm GNU Toolchain 14.2.rel1**. It ships for all three hosts, and the Raspberry Pi Pico VS Code extension installs that same 14.2.rel1 toolchain (including `arm-none-eabi-gdb`) on all of them:
|
||||
|
||||
| OS | `arm-none-eabi-gdb` path |
|
||||
| -- | ------------------------ |
|
||||
| macOS Apple Silicon | `/Applications/ArmGNUToolchain/14.2.rel1/arm-none-eabi/bin/arm-none-eabi-gdb` (or the Pico extension's `~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb`) |
|
||||
| Windows x64 | `%USERPROFILE%\.pico-sdk\toolchain\14_2_Rel1\bin\arm-none-eabi-gdb.exe` (Pico extension), or `C:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\14.2 rel1\bin\arm-none-eabi-gdb.exe` |
|
||||
| Linux x64 | `~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb` (Pico extension), or the `bin/` directory of the extracted `arm-gnu-toolchain-14.2.rel1-x86_64-arm-none-eabi` tarball |
|
||||
6. Click **Accept**.
|
||||
|
||||
> **Use the GDB MI adapter.** It launches a real `arm-none-eabi-gdb --interpreter=mi2` and lets IDA drive it, so breakpoints and stepping go through real GDB — which sends the correct 2-byte breakpoint length and handles step-over itself. Verified working end to end: connect, GUI breakpoints (**Add Hardware Breakpoint...**, hardware execute), **Step Into** / **Step Over**, and register edits. Stops are reported as `Breakpoint` (not `SingleStep`).
|
||||
>
|
||||
> **Do NOT have any breakpoints set in IDA before you connect.** With the GDB MI adapter, attaching while IDA already has a breakpoint **hangs the session**. Start the server parked with `BP_ADDR` (Step 9), connect, and only add hardware breakpoints *after* the connection is up. This is a IDA bug; it is the single most common GDB MI failure.
|
||||
>
|
||||
> **The GDB executable path matters.** Use the **14.2.rel1** build on every OS (Windows, macOS, Linux). The 13.3.rel1 build did **not** connect in testing.
|
||||
>
|
||||
> **This step is temporary.** Vector35 plans to ship a GDB binary with the GDB MI adapter ([Vector35/debugger#929](https://github.com/Vector35/debugger/issues/929), milestone *Langara*). Once that lands, IDA provides GDB itself and you will not need to set **Full GDB Executable Path** at all.
|
||||
>
|
||||
> **Do not pick Corellium.** IDA's adapter dropdown also lists **Corellium**, which is for Corellium's virtual devices and expects an API token, not a local OpenOCD server. It is not the adapter for this lab. The dropdown is a combo box, so an accidental arrow-key press can land on it — always read the label back and confirm it says **GDB MI** before clicking **Accept**.
|
||||
|
||||
> **The adapter and port are not saved in the `.i64`.** Every time you relaunch IDA you must re-select **GDB MI**, re-enter port `3333`, and re-set the GDB path.
|
||||
|
||||
> **Watch for an off-screen error dialog.** When a connection fails, IDA pops a `IDA critical alert` window that can be positioned mostly outside the main window, which makes it look like nothing happened. If the connect seems to do nothing, check your other display.
|
||||
|
||||
The target keeps running. Open the **Registers** tab (bug icon) and confirm you see live values. `pc` inside `0x10003xxx` and `sp` just below `0x20082000` are healthy.
|
||||
|
||||
> **If `pc` is `0x00000088`, `0x000000ec`, or `sp` is `0xf0000000`, the session is bad.** Restart the server, then restart IDA (a server restart while attached leaves IDA in a stale session), and connect again.
|
||||
|
||||
### Step 11: Find `main` without relying on its address
|
||||
|
||||
`main` can move between programs, so we do not guess it. We follow the one fixed path to it. Press `G` and go to `0x10000000`:
|
||||
|
||||
```
|
||||
0x10000000 0x20082000 initial stack pointer (top of SRAM)
|
||||
0x10000004 0x1000015d reset vector
|
||||
```
|
||||
|
||||
Bit 0 of a vector is the Thumb bit, so `0x1000015d` means "start at `0x1000015c`". That is `_reset_handler`. Follow the reset path to `0x10000186`, `platform_entry`:
|
||||
|
||||
```asm
|
||||
10000186: ldr r1, [pc, #80] @ (100001d8 <data_cpy_table+0x38>)
|
||||
10000188: blx r1
|
||||
1000018a: ldr r1, [pc, #80] @ (100001dc <data_cpy_table+0x3c>)
|
||||
1000018c: blx r1
|
||||
1000018e: ldr r1, [pc, #80] @ (100001e0 <data_cpy_table+0x40>)
|
||||
10000190: blx r1
|
||||
10000192: bkpt 0x0000
|
||||
10000194: b.n 10000192 @ <platform_entry+0xc>
|
||||
```
|
||||
|
||||
**The middle `blx` at `0x1000018c` is the call to `main`.** `platform_entry` is byte-identical in every project, so `0x1000018c` catches `main` no matter where the linker placed it. The literal pool at `0x100001dc` holds `main | 1`; clearing bit 0 gives `0x10000234`.
|
||||
|
||||
### Step 12: Set a hardware breakpoint in the GUI
|
||||
|
||||
With the **GDB MI** adapter, IDA sets breakpoints through real GDB, which sends the correct 2-byte length, so you set them **in the UI**. There is no command port here.
|
||||
|
||||
> **Why older drafts used the command port.** IDA's **GDB RSP** adapter is its own minimal RSP client and sends a **1-byte** breakpoint (`Z0,<addr>,1`); the Cortex-M33 FPB comparators need 2 bytes, so OpenOCD rejected it with `only breakpoints of two bytes length supported`. The old workaround was to arm breakpoints by hand over telnet. **The GDB MI adapter does not have this problem** — it drives real `arm-none-eabi-gdb`, which sends the right length. So everything below is done in the GUI. The command port still exists as a fallback (see the end of this step), but you do not need it.
|
||||
|
||||
#### Where you can stop
|
||||
|
||||
| You want to stop at | Address | How | Repeatable? |
|
||||
| --- | --- | --- | --- |
|
||||
| **`main`** | `0x10000234` | The server starts parked there with `BP_ADDR=0x10000234` (Step 9), so IDA is already stopped at `main` when it connects. | No — `main` runs once per reset. |
|
||||
| **The `FAV_NUM` `printf` call** | `0x10000292` | Set a hardware breakpoint in the GUI, then click **Resume**. | Yes — fires on every iteration. |
|
||||
| **The `OTHER_FAV_NUM` `printf` call** | `0x1000029c` | Set a hardware breakpoint in the GUI, then click **Resume**. | Yes — fires on every iteration. |
|
||||
| **The `lcd_puts("Reverse")` call** | `0x1000027c` | Set a hardware breakpoint while stopped at `main`, then click **Resume**. | No — the LCD is written once at init. |
|
||||
|
||||
#### Set the loop breakpoint in the GUI
|
||||
|
||||
1. Press `G`, type the loop address (`0x10000292`), and press Enter.
|
||||
2. Set a **hardware execution** breakpoint at that address, either way:
|
||||
- `Debugger -> Add Hardware Breakpoint...` — a **hardware execute** (`HE`) breakpoint. **Use this one.**
|
||||
- click the line and press `F2` (`Debugger -> Toggle Breakpoint`) — a **software** breakpoint. It will **not** work here: the code is in read-only flash, so GDB cannot install it and the core just keeps running.
|
||||
3. Click **Resume**. The core is already running the loop, so the breakpoint fires on the next iteration. IDA stops with the PC at the loop address and reports it as a **Breakpoint** — verified: `Stopped (Breakpoint) at 0x10000292`.
|
||||
|
||||
> **No breakpoints before you connect.** With GDB MI, a breakpoint set before the connection hangs the session (Step 10). Start parked with `BP_ADDR`, connect, *then* add breakpoints.
|
||||
|
||||
#### Stepping
|
||||
|
||||
With the target halted at the breakpoint, **Step Into** (`F7`) and **Step Over** (`F8`) run through real GDB and move the PC. Verified: `0x10000292 -> 0x1000398c -> 0x1000398e -> ...`.
|
||||
|
||||
> **Step Over on the raw `.bin` steps *into* calls.** The raw image has no symbol for `__wrap_printf`, so **Step Over** at the `printf` call behaves like **Step Into**. When the lab needs to execute the call and then stop, it moves the breakpoint to the return site and clicks **Resume** instead (Step 13 shows this).
|
||||
|
||||
> **Never use IDA's Restart button.** On RP2350 it resets and halts inside the boot ROM (`pc=0x88`, `sp=0xf0000000`). To reset cleanly, restart the server with `BP_ADDR` and reconnect.
|
||||
|
||||
> **If you ever need the command port.** It is still there — `nc 127.0.0.1 4444`, and `bp <addr> 2 hw` still arms a breakpoint, `rbp <addr>` / `rbp all` still remove them. It is the fallback if you switch back to the **GDB RSP** adapter, whose 1-byte breakpoints the GUI cannot set. With GDB MI you do not need it for this lab.
|
||||
|
||||
### Step 13: HACK IT LIVE — change the printed `FAV_NUM`
|
||||
|
||||
`main` loads the `#define` `0x2a` (42) into `r1` and calls `printf` on every iteration. We break on that call and change it live:
|
||||
|
||||
```asm
|
||||
1000028e: movs r1, #42 @ 0x2a
|
||||
10000290: ldr r0, [pc, #32] @ (100002b4 <main+0x80>)
|
||||
10000292: bl 1000398c @ <__wrap_printf>
|
||||
10000296: movw r1, #1337 @ 0x539
|
||||
1000029a: ldr r0, [pc, #28] @ (100002b8 <main+0x84>)
|
||||
1000029c: bl 1000398c @ <__wrap_printf>
|
||||
100002a0: b.n 1000028e @ <main+0x5a>
|
||||
```
|
||||
|
||||
1. Press `G`, go to `0x10000292` (the `bl __wrap_printf` for `FAV_NUM`).
|
||||
2. Set a **hardware execute** breakpoint there: `Debugger -> Add Hardware Breakpoint...`. (Do not use `F2` — that is a software breakpoint and will not work on read-only flash.)
|
||||
3. Click **Resume** in IDA. The target is already running the loop, so the breakpoint fires on the next iteration. IDA stops with the program counter at `0x10000292` and `r1 = 0x2a`.
|
||||
4. Open the **Registers** widget (bug icon -> **Registers**).
|
||||
5. Find `r1`. Its value is `0x2a` (42), loaded by the `movs r1, #42` at `0x1000028e`.
|
||||
6. **Set `r1` to `0x2b` (43).** From IDA's Python console (`Plugins -> Python Console`):
|
||||
```python
|
||||
dbg.set_reg_value("r1", 0x2b)
|
||||
```
|
||||
`dbg.set_reg_value(name, value)` writes one register (returns `True` on success). You can also right-click `r1` in the **Registers** widget, press `E` (edit), type `2b`, and press Enter. The widget may not repaint the value, but the write reaches the target — you confirm it by the printed output in the next steps.
|
||||
7. **Move the breakpoint past the call.** You want `printf` to run once and then stop, so move the breakpoint from `0x10000292` to the instruction *after* the call, `0x10000296` (the `movw r1, #1337` that begins the `OTHER_FAV_NUM` half of the loop): remove the breakpoint at `0x10000292` and set a hardware breakpoint at `0x10000296`. Two reasons not to just click **Step Over** here: a breakpoint left on the current PC re-traps the step, and IDA's **Step Over** steps *into* `__wrap_printf` on this raw `.bin` because the image carries no symbol for the call. Moving the breakpoint to the return site is deterministic.
|
||||
8. Click **Resume** in IDA. The core executes `bl __wrap_printf` with `r1 = 0x2b`, so this iteration prints `FAV_NUM: 43`, then stops at `0x10000296`.
|
||||
9. Look at your serial monitor — the `screen` session on the Pico's USB serial port — and at the **Target** tab in IDA:
|
||||
|
||||
```
|
||||
FAV_NUM: 43
|
||||
```
|
||||
|
||||
You changed a running program's output without touching the binary.
|
||||
|
||||
### Step 13b: HACK THE LCD STRING LIVE — change `"Reverse"` to `"Exploit"` (optional)
|
||||
|
||||
The LCD text `"Reverse"` lives in flash (`.rodata`) at `0x10003ee8`, and flash is **read-only at runtime** — a debugger write there does not stick. So instead of overwriting the text in place, redirect the pointer: at the `lcd_puts` call for `"Reverse"`, `r0` holds the string address, so point `r0` at a replacement string you place in RAM.
|
||||
|
||||
> **Do this one while stopped at `main`, before `Resume`.** The LCD is written once during init, at `0x1000027c`. If you have already resumed into the loop, restart the server parked at `0x10000234` (Step 9) and reconnect, or the breakpoint at `0x1000027c` never fires again.
|
||||
|
||||
1. With IDA stopped at `main` (`0x10000234`), press `G` and go to `0x1000027c` (the `bl lcd_puts` that writes `"Reverse"`, loaded from the literal pool word at `0x100002ac`).
|
||||
2. Set a **hardware execute** breakpoint at `0x1000027c` and click **Resume**. It fires once, with `r0 = 0x10003ee8`.
|
||||
3. Put the replacement string into free RAM at `0x20080000` from IDA's **Python console** (`Plugins -> Python Console`) — no command port needed:
|
||||
```python
|
||||
dbg.write_memory(0x20080000, b"Exploit\x00")
|
||||
```
|
||||
`dbg.write_memory(address, bytes)` is IDA's debugger memory-write API; it returns `True` on success. That writes `Exploit\0`.
|
||||
4. Point `r0` at that string:
|
||||
```python
|
||||
dbg.set_reg_value("r0", 0x20080000)
|
||||
```
|
||||
(Or right-click `r0` in the **Registers** widget, press `E`, type `0x20080000`, and press Enter.)
|
||||
5. Move the breakpoint off the current PC (remove it at `0x1000027c`, set one at `0x10000280`, the `movs r0, #1` after the call) and click **Resume**. `lcd_puts` walks your RAM string and pushes `E x p l o i t` to the PCF8574 over I2C, so line 1 of the LCD now reads:
|
||||
|
||||
```
|
||||
Exploit
|
||||
```
|
||||
|
||||
Like the value hack, this is **one boot only**: the next `lcd_puts` for `"Engineering"` is unaffected, but a reset reloads `r0` from flash. The permanent version is the static patch in Step 18c.
|
||||
|
||||
### Step 14: Why the hack reverts (and why we patch next)
|
||||
|
||||
Press **Resume**. The loop branches back to `0x1000028e`, which reloads `movs r1, #42`, so the next line is `FAV_NUM: 42`. The live edit changed one iteration only. There is no memory variable to change; the value is baked into the instruction. To make `FAV_NUM: 43` permanent we must patch the instruction. That is the static pass.
|
||||
|
||||
Press **Pause** to stop the output flood.
|
||||
|
||||
### Step 15: Kill the debugger and OpenOCD
|
||||
|
||||
The live hack is done. Do this **before** the static pass.
|
||||
|
||||
1. In the **Debugger** sidebar, click the **X** (**Kill**) (or **`Debugger -> Kill`**) to disconnect IDA.
|
||||
2. **Kill does not stop the OpenOCD process** — `debug-server.sh` started it separately, and it keeps running and holding the probe. Stop it from the IDA console:
|
||||
|
||||
**macOS / Linux:**
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # stop the debug server, free the probe
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # stop the debug server, free the probe
|
||||
```
|
||||
|
||||
3. Confirm nothing is left: `pgrep -fl openocd` (macOS/Linux) prints nothing.
|
||||
|
||||
From a terminal it is the same: `pkill -TERM -f openocd`, or `Get-Process openocd | Stop-Process` on Windows.
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Static — Resolve the Functions in IDA and Patch
|
||||
## Part 3: Static — Resolve the Functions in IDA and Patch
|
||||
|
||||
### Step 16: Resolve the functions in the IDA GUI
|
||||
|
||||
@@ -1140,333 +730,9 @@ python ..\uf2conv.py 0x0017_constants-h.bin ^
|
||||
>
|
||||
> This writes `hacked.uf2` next to the `.bin`, ready to drag onto the Pico.
|
||||
|
||||
### Step 21: Flash and verify
|
||||
## Static stopping point
|
||||
|
||||
Hold **BOOTSEL**, plug in the Pico 2, and drag `hacked.uf2` onto the **`RP2350`** drive. Open the serial monitor:
|
||||
|
||||
```
|
||||
FAV_NUM: 43
|
||||
OTHER_FAV_NUM: 1344
|
||||
FAV_NUM: 43
|
||||
OTHER_FAV_NUM: 1344
|
||||
...
|
||||
```
|
||||
|
||||
and the **LCD line 1 reads `Exploit`** while line 2 still reads `Engineering`.
|
||||
|
||||
**Both constants changed and the LCD string changed — with nine operand bytes patched and no source code.** (`0x2a -> 0x2b`, `0x39 -> 0x40`, and the eight-byte string.)
|
||||
|
||||
> **Faster: flash over the Debug Probe (no BOOTSEL).** The repo's `flash.sh` writes the raw `.bin` straight into XIP flash over SWD (`program <bin> 0x10000000 verify reset exit`), so you never touch BOOTSEL or a UF2. Run it from a terminal (`./flash.sh <bin>`), or from the IDA console **without freezing it** — use `subprocess.Popen`, which returns immediately, and send OpenOCD's output to a log file. (`subprocess.run` blocks the console until the flash finishes; do not use it here.)
|
||||
>
|
||||
> ```python
|
||||
> import os, subprocess
|
||||
> root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip()
|
||||
> bin_path = os.path.join(os.path.join(root, "0x0017_constants", "build"), "0x0017_constants-h.bin")
|
||||
> log = os.path.join(os.path.join(root, "0x0017_constants", "build"), "flash.log")
|
||||
> subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # free the probe first
|
||||
> p = subprocess.Popen([os.path.join(root, "flash.sh"), bin_path],
|
||||
> stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
> print("flashing in the background; log:", log)
|
||||
> ```
|
||||
>
|
||||
> The `pkill` frees the probe first; on Windows use `subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"])`.
|
||||
>
|
||||
> The console is free the moment this returns. Check it with `print(p.poll())` (`None` = still running, `0` = done) or read `flash.log` — success ends with `** Verified OK **`.
|
||||
>
|
||||
> The same non-blocking form without the script:
|
||||
>
|
||||
> ```python
|
||||
> import os, subprocess
|
||||
> ocd = os.path.expanduser("~/.pico-sdk/openocd/0.12.0+dev")
|
||||
> bin_path = os.path.join(os.path.join(root, "0x0017_constants", "build"), "0x0017_constants-h.bin")
|
||||
> log = os.path.join(os.path.join(root, "0x0017_constants", "build"), "flash.log")
|
||||
> subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # free the probe first
|
||||
> p = subprocess.Popen([f"{ocd}/openocd", "-s", f"{ocd}/scripts",
|
||||
> "-f", "interface/cmsis-dap.cfg", "-f", "target/rp2350.cfg",
|
||||
> "-c", "adapter speed 5000",
|
||||
> "-c", f"program {bin_path} 0x10000000 verify reset exit"],
|
||||
> stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
> print("flashing in the background; log:", log)
|
||||
> ```
|
||||
>
|
||||
> **The Debug Probe is single-owner.** If IDA is still attached (the `debug-server.sh` OpenOCD is running), the flash cannot grab the probe. Detach in IDA and stop that OpenOCD first:
|
||||
>
|
||||
> ```bash
|
||||
> # macOS / Linux
|
||||
> pkill -TERM -f openocd
|
||||
> ```
|
||||
> ```powershell
|
||||
> # Windows
|
||||
> Get-Process openocd -ErrorAction SilentlyContinue | Stop-Process
|
||||
> ```
|
||||
>
|
||||
> Success looks like `Programming Finished` -> `Verified OK` -> `Resetting Target`. On Windows use `flash.ps1` (`.\flash.ps1 -Bin <path>`) the same way.
|
||||
|
||||
---
|
||||
|
||||
## Cheatsheet
|
||||
|
||||
### IDA GUI actions
|
||||
|
||||
| Action | How |
|
||||
| ------ | --- |
|
||||
| Go to address | `G` |
|
||||
| Rename function/symbol | `N` |
|
||||
| Set type or signature | `Y` |
|
||||
| Add comment | `;` |
|
||||
| Open Hex view | `View -> Hex` |
|
||||
| Enable hex editing | Toggle the lock in the status bar |
|
||||
| Reanalyze after a patch | Right-click function -> `Reanalyze` |
|
||||
| Edit a register live | `dbg.set_reg_value("r1", 0x2b)` in the Python console (or right-click the register, press `E`, type hex, Enter) |
|
||||
| Write a RAM string live | `dbg.write_memory(0x20080000, b"Exploit\x00")` |
|
||||
| Set a breakpoint | `Debugger -> Add Hardware Breakpoint...` (hardware execute). Do **not** use `F2` — software breakpoints cannot be written to read-only flash. |
|
||||
| Move a breakpoint | Remove it and set it at the new address in the GUI (command-port fallback: `rbp <old addr>` then `bp <new addr> 2 hw`) |
|
||||
| Confirm what is armed | The **Breakpoints** widget lists it (command-port fallback: `mdw 0xE0002000 8`, each armed breakpoint shows as `<addr \| 1>`) |
|
||||
| Apply the ELF symbol map | Paste the Python snippet from Step 16 into the Python Console |
|
||||
|
||||
### OpenOCD server and reset
|
||||
|
||||
The server runs with `gdb_breakpoint_override hard` so that flash-writes are never attempted. Breakpoints in this lab are set in the IDA GUI through the **GDB MI** adapter (Step 12). The command-port rows below are the fallback if you use the **GDB RSP** adapter instead.
|
||||
|
||||
| Action | Command |
|
||||
| ------ | ------- |
|
||||
| Connect to the OpenOCD prompt (fallback) | `nc 127.0.0.1 4444` (or `telnet 127.0.0.1 4444`) |
|
||||
| Reset and run (command port) | `reset run` |
|
||||
| Check core state (command port) | `targets` |
|
||||
| Set a breakpoint in the GUI | `Debugger -> Add Hardware Breakpoint...` (hardware execute; `F2` software breakpoints do not work on flash) |
|
||||
| (fallback) Add a breakpoint without the GUI | `bp <addr> 2 hw` |
|
||||
| Remove one breakpoint | `rbp <addr>` — **address only, no length, no `hw`** |
|
||||
| Remove every breakpoint | `rbp all` |
|
||||
| Start the server parked at `main` | macOS/Linux: `BP_ADDR=0x10000234 ./debug-server.sh` — Windows: `$env:BP_ADDR="0x10000234"; .\debug-server.ps1` (one-shot) |
|
||||
| Start the server parked in the loop | macOS/Linux: `BP_ADDR=0x10000292 ./debug-server.sh` — Windows: `$env:BP_ADDR="0x10000292"; .\debug-server.ps1` |
|
||||
| Break on the loop in a running target | set a hardware breakpoint in the GUI at the loop address, then **Resume** — repeatable |
|
||||
| Make IDA stepping work | `rp2350.dap.core0 configure -rtos none` (already in the scripts) |
|
||||
| Step without re-trapping | move the breakpoint off the current PC first, then **Step Into**/**Step Over** |
|
||||
| Reset without desyncing IDA | **Detach**, `reset run` on the port, reconnect — never `reset run` while attached |
|
||||
|
||||
### Every address and byte we changed
|
||||
|
||||
| Project | Address | Before | After | Effect |
|
||||
| ------- | ------- | ------ | ----- | ------ |
|
||||
| `0x0017` | `0x1000028e` | `2a` | `2b` | `movs r1, #42` -> `#43`, prints `FAV_NUM: 43` |
|
||||
| `0x0017` | `0x10000298` | `39` | `40` | `movw r1, #1337` -> `#1344`, prints `OTHER_FAV_NUM: 1344` |
|
||||
| `0x0017` | `0x10003ee8` | `52 65 76 65 72 73 65 00` | `45 78 70 6c 6f 69 74 00` | LCD line 1 prints `Exploit` instead of `Reverse` |
|
||||
|
||||
### The I2C/LCD memory map
|
||||
|
||||
| Item | Address | Notes |
|
||||
| ---- | ------- | ----- |
|
||||
| `i2c1_inst` | `0x2000062c` | RAM `.data`, fixed for the life of the program |
|
||||
| `i2c1_inst.hw` | `0x40098000` | I2C1 hardware register base (first word of the struct) |
|
||||
| `i2c1_inst.restart_on_next` | `0x20000630` | second word of the struct, `0` (false) |
|
||||
| Literal-pool word 1 | `0x100002a4` | `0x000186a0` — I2C baud rate, 100000 |
|
||||
| Literal-pool word 2 | `0x100002a8` | `0x2000062c` — `&i2c1_inst` |
|
||||
| Literal-pool word 3 | `0x100002ac` | `0x10003ee8` — pointer to `"Reverse"` |
|
||||
| Literal-pool word 4 | `0x100002b0` | `0x10003ef0` — pointer to `"Engineering"` |
|
||||
| Literal-pool word 5 | `0x100002b4` | `0x10003efc` — pointer to `"FAV_NUM: %d\r\n"` |
|
||||
| Literal-pool word 6 | `0x100002b8` | `0x10003f0c` — pointer to `"OTHER_FAV_NUM: %d\r\n"` |
|
||||
|
||||
### Raw image facts
|
||||
|
||||
| Item | Value |
|
||||
| ---- | ----- |
|
||||
| Build type | `Release` |
|
||||
| Load base address | `0x10000000` |
|
||||
| Project size | `17980` bytes (`0x463c`) |
|
||||
| Fixed `main` anchor | `0x1000018c` (reset handler middle `blx`) |
|
||||
| `main` | `0x10000234` |
|
||||
| `printf` call / return, `FAV_NUM` | `0x10000292` / `0x10000296` |
|
||||
| `printf` call, `OTHER_FAV_NUM` | `0x1000029c` |
|
||||
| `i2c1_inst` RAM address | `0x2000062c` |
|
||||
| I2C1 hardware registers | `0x40098000` |
|
||||
| `FAV_NUM` format string | `0x10003efc` |
|
||||
| `OTHER_FAV_NUM` format string | `0x10003f0c` |
|
||||
| `"Reverse"` string | `0x10003ee8` |
|
||||
| `"Engineering"` string | `0x10003ef0` |
|
||||
| RP2350 UF2 family ID | `0xe48bff59` |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### IDA hangs or crashes when you connect (macOS 27)
|
||||
|
||||
Three different causes have been seen on this setup; check them in this order.
|
||||
|
||||
- **A breakpoint set before connecting.** With the **GDB MI** adapter, if the binary view already has a breakpoint, the session hangs. Start parked with `BP_ADDR`, connect, then add breakpoints (see the next entry).
|
||||
- **The wrong GDB executable.** Point **Full GDB Executable Path** at the **14.2.rel1** toolchain (Step 10). The 13.3.rel1 build did **not** connect in testing.
|
||||
- **The LLDB adapter.** A crash report with `libdebuggercore.dylib -> std::terminate() -> abort()` and `liblldb` in the stack is the **LLDB** adapter, not GDB MI. Avoid LLDB on this setup.
|
||||
|
||||
**Use GDB MI**, with the 14.2.rel1 path above. If it still fails, fall back to plain `arm-none-eabi-gdb` against the same server — the addresses and register values are identical to the GUI steps.
|
||||
|
||||
If IDA hangs, force-quit it; the connect dialog has no working Cancel. The static steps (resolve, patch, export, flash) never touch the debugger and always work.
|
||||
|
||||
### The GUI refuses to set a breakpoint (GDB RSP adapter only)
|
||||
|
||||
If you are on the **GDB RSP** adapter, the GUI cannot set breakpoints on this target. That adapter is IDA's own minimal RSP client and sends a **1-byte** breakpoint (`Z0,<addr>,1`); the Cortex-M33 comparators need 2 bytes, so OpenOCD answers `only breakpoints of two bytes length supported`. It affects every address, both `Toggle Breakpoint` and `Add Hardware Breakpoint`, and the dialog's **Size** field is disabled. `gdb_breakpoint_override` makes no difference.
|
||||
|
||||
**Fix: use the GDB MI adapter** (Step 10). It drives real GDB, which sends the correct length, so GUI breakpoints just work. If you must stay on GDB RSP, arm breakpoints from the command port after connecting (`bp <addr> 2 hw`) — but the lab uses GDB MI and does not need that.
|
||||
|
||||
### GDB MI hangs when you connect (a breakpoint already existed)
|
||||
|
||||
With the **GDB MI** adapter, if IDA already has a breakpoint set when you connect, the session **hangs**. This is a IDA bug. The working order is:
|
||||
|
||||
1. Start the server parked, e.g. `BP_ADDR=0x10000234 ./debug-server.sh` (Windows: `$env:BP_ADDR="0x10000234"; .\debug-server.ps1`).
|
||||
2. Connect with the **GDB MI** adapter.
|
||||
3. Only *then* set hardware breakpoints in the UI.
|
||||
|
||||
Never have a breakpoint in the binary view before the GDB MI connection. If it hangs, quit IDA, restart the server with `BP_ADDR`, and connect again before adding any breakpoints.
|
||||
|
||||
### Step Into / Step Over does nothing (PC never moves)
|
||||
|
||||
Two causes have been seen on this target.
|
||||
|
||||
1. **A breakpoint on the current PC re-traps the step.** OpenOCD's step-over-breakpoint logic fails with `Duplicate Breakpoint address` and the PC stays put. Fix: move the breakpoint off the current PC (in the GUI), then step.
|
||||
2. **The `hwthread` RTOS (GDB RSP adapter only).** With the **GDB RSP** adapter, OpenOCD can log `fake step thread 0` and reply without stepping, because the RP2350 config's `-rtos hwthread` makes the current thread id 1 while IDA sends thread id 0. Fix: `rp2350.dap.core0 configure -rtos none` (the launcher scripts already pass this). **GDB MI does not hit this.**
|
||||
|
||||
To tell them apart, turn on OpenOCD logging (`log_output /tmp/ocd.log`, then `debug_level 3` on the command port) and look for `fake step` versus `Duplicate Breakpoint`.
|
||||
|
||||
### `zsh: bad CPU type in executable: cmake`
|
||||
|
||||
An Intel `x86_64` tool is on your `PATH` on Apple Silicon. Run Step 2: `export PATH="/opt/homebrew/bin:$PATH"`, then `hash -r`. Add it to `~/.zshrc` to make it permanent.
|
||||
|
||||
### My addresses do not match this guide
|
||||
|
||||
You probably built `Debug`. This lesson is a `Release` build. Re-run Step 3 with `-DCMAKE_BUILD_TYPE=Release`. A `Debug` build moves the SDK functions and keeps `init_i2c_and_lcd` / `write_lcd_greeting` as separate calls, so `main` is not at `0x10000234`.
|
||||
|
||||
### A breakpoint never fires
|
||||
|
||||
First, confirm you actually set one, and that it is a **hardware** breakpoint. With the **GDB MI** adapter, `Debugger -> Add Hardware Breakpoint...` (hardware execute) should land in the **Breakpoints** widget. If nothing lands, or the core keeps running, you probably used `F2` (`Toggle Breakpoint`) — that is a software breakpoint and cannot be written to read-only flash, so it never installs. Also check you are on **GDB MI**, not **GDB RSP** (the GDB RSP adapter cannot set breakpoints on this target at all).
|
||||
|
||||
Then check the order and the state:
|
||||
|
||||
- **Arm it only after IDA is connected.** OpenOCD flushes every breakpoint when a client attaches, so anything armed earlier is gone. This also applies to `BP_ADDR` on the startup command line.
|
||||
- **Verify it is armed:** `mdw 0xE0002000 8`. You should see your address with the low bit set (`0x10000292` -> `0x10000293`). All zeros means nothing is armed — re-read this first, because it distinguishes "not armed" from "armed but never reached".
|
||||
- **Is the core running?** `poll` on the command port should not report a halt. If it is stopped, click **Resume**.
|
||||
- **Does the address get reached again?** `main` runs once per reset, so use `BP_ADDR` at startup (Step 9) rather than `reset run` while attached. Loop addresses such as `0x10000292` fire on the next pass with no reset — arm them and click **Resume** in IDA.
|
||||
- **With GDB MI the stop is reported as `Breakpoint`** and appears in the **Breakpoints** widget, because GDB really did set it.
|
||||
|
||||
### I edit `r1` (or another register) and it reverts
|
||||
|
||||
`main` reloads the value at the top of every loop iteration — `movs r1, #42` at `0x1000028e` runs right before the `printf` at `0x10000292`. So `r1` is only `0x2b` for the instant between your edit and the next pass; then it is `0x2a` again. The edit sticks only if the core is **genuinely stopped** at the breakpoint and stays stopped.
|
||||
|
||||
If it keeps reverting, the core is running, which almost always means the breakpoint is not installed — usually because it is a **software** breakpoint (`F2`) that cannot be written to read-only flash. Use `Debugger -> Add Hardware Breakpoint...` (hardware execute).
|
||||
|
||||
> **The Registers widget is a snapshot, not a live view.** IDA reads the registers at each stop and shows that snapshot; it does not poll the target, and there is no "refresh registers" command. So a value changed outside IDA will not appear until the next stop.
|
||||
|
||||
### The LCD string hack does nothing
|
||||
|
||||
The LCD is written **once**, during `lcd_i2c_init` / `write_lcd_greeting`, before the loop starts. You must break at `0x1000027c` and redirect `r0` **before** that call runs. If you already let the target run into the loop, the LCD already shows the old strings. Restart the server parked at `main` (`BP_ADDR=0x10000234`) and reconnect, then set the breakpoint at `0x1000027c` while stopped.
|
||||
|
||||
Also confirm you wrote a NUL-terminated string. `lcd_puts` walks bytes until `*s == 0`; without the trailing `\x00`, it keeps sending RAM garbage to the PCF8574.
|
||||
|
||||
### The compiler did not keep `const` in `.rodata`
|
||||
|
||||
It is not supposed to in this build. `const int OTHER_FAV_NUM = 1337` becomes `movw r1, #1337` because the program never takes its address (`&OTHER_FAV_NUM`) and the value fits in a 16-bit immediate. A `const` only stays in `.rodata` when something forces it there — an address-taken `const`, an array, a pointer, or `volatile`. When you reverse a real binary, never assume a `const` is a memory load; check the instruction.
|
||||
|
||||
### The `movw` patch did not take
|
||||
|
||||
You patched the wrong byte. `movw` is a 32-bit instruction and its low immediate byte (`imm8`) is the **third** byte. The instruction starts at `0x10000296`, so the byte to change is `0x10000298` (`0x39 -> 0x40`). Patching `0x10000296` (the opcode) corrupts the instruction and the core will fault.
|
||||
|
||||
### The LCD shows garbage after patching
|
||||
|
||||
The replacement string is the wrong length or is missing its NUL. `"Reverse"` and `"Exploit"` are both seven characters, and the original eight-byte block is `52 65 76 65 72 73 65 00`. Write exactly `45 78 70 6c 6f 69 74 00`. A shorter string without padding runs into `"Engineering"` at `0x10003ef0`; a longer one overwrites it.
|
||||
|
||||
### The serial capture is garbage on macOS
|
||||
|
||||
Reading `/dev/cu.usbmodem*` with a bare `read()` returns garbage. Set **raw termios at 115200** first: clear canonical/echo flags, set `CLOCAL|CREAD`, and `B115200` on input and output. `screen /dev/cu.usbmodem* 115200` does all of this for you; a script must call `tcsetattr` itself. Once set, the capture reads clean `FAV_NUM: 42` lines.
|
||||
|
||||
### It worked for a second, then stopped (IDA's view desyncs)
|
||||
|
||||
This is the most common failure, and it has one main cause: **driving the core from the OpenOCD command port while IDA is connected.**
|
||||
|
||||
- If you send `reset run` from the port while attached, the core resets, runs, and halts at your breakpoint — but IDA never receives the stop event. Its sidebar keeps showing the *previous* location, so **Step** and **Resume** act on a stale PC and appear to do nothing.
|
||||
- If the OpenOCD process dies (or you restart it) while attached, IDA keeps believing it is connected: the sidebar stays, but the menu shows **Pause** enabled and **Resume**/**Step** disabled because IDA last saw the target *running*.
|
||||
|
||||
Recovery: **Detach, then reconnect.** If Detach does nothing (the connection is already dead), restart IDA — its menu still shows a session that no longer exists.
|
||||
|
||||
Prevention:
|
||||
|
||||
- Stop at `main` with `BP_ADDR` on a fresh server start, not with `reset run` while attached.
|
||||
- For loop addresses, set the breakpoint in the GUI and click **Resume**. Let IDA be the thing that starts the core.
|
||||
- If you must reset, **Detach first**, `reset run`, then reconnect.
|
||||
- Never leave a breakpoint on the PC you are about to step or resume from.
|
||||
|
||||
### The target "blows past" `main` and stops at `0x10003ab4` instead
|
||||
|
||||
`0x10003ab4` is inside `stdio_uart_out_flush`:
|
||||
|
||||
```asm
|
||||
10003ab0: 4b02 ldr r3, [pc, #8] @ (10003abc <stdio_uart_out_flush+0xc>)
|
||||
10003ab2: 681a ldr r2, [r3]
|
||||
10003ab4: 6993 ldr r3, [r2, #24] @ the core sits here while the UART drains
|
||||
10003ab6: 071b lsls r3, r3, #28
|
||||
10003ab8: d4fc bmi.n 10003ab4 <stdio_uart_out_flush+0x4>
|
||||
10003aba: 4770 bx lr
|
||||
10003abc: 2000086c .word 0x2000086c
|
||||
```
|
||||
|
||||
That is the UART transmit-FIFO drain loop inside `printf`, so the core is running `main`'s loop and simply spends nearly all its time there. The breakpoint at `main` did not fire because `main`'s entry runs exactly **once per reset**. If you arm the breakpoint after the reset, or set it while the target is already running and just resume, the core is already past `main` and will never re-execute it. Either arm the breakpoint **before** resetting, or break inside the loop at `0x10000292`, which fires every iteration.
|
||||
|
||||
**`0x10003ab4` is not a function.** It is one instruction inside `stdio_uart_out_flush`, which starts at `0x10003ab0`. If IDA has created a function at `0x10003ab4` (for example because the debugger stopped at that PC), the decompiler shows garbage. Delete that bogus function (right-click it -> `Delete Function`, or put the cursor on it and press `U` to undefine) and reanalyze. The real function is `stdio_uart_out_flush` at `0x10003ab0`.
|
||||
|
||||
### The console floods with `Failed to read memory at 0xf0000000`
|
||||
|
||||
Core1 is exposed. The scripts must run with `USE_CORE=0`. Stop the server, confirm only `core0` is reported, restart, then restart IDA.
|
||||
|
||||
### `Connect to Remote Process` is greyed out and Pause does nothing
|
||||
|
||||
IDA is in a stale session, usually because the debug server restarted while attached. Quit and reopen IDA (or the `.i64`) and connect again.
|
||||
|
||||
### The decompiler still shows the old value after patching
|
||||
|
||||
Right-click the function and choose `Reanalyze`.
|
||||
|
||||
---
|
||||
|
||||
## Fallback: do the dynamic steps with GDB (macOS 27)
|
||||
|
||||
If IDA's debugger crashes on attach on macOS 27 (see Troubleshooting), you can still do the live hack with the ARM GDB from the toolchain, against the same OpenOCD server. The addresses and register values are identical to the GUI steps.
|
||||
|
||||
Start the debug server (Step 9), then in a new terminal:
|
||||
|
||||
```
|
||||
arm-none-eabi-gdb
|
||||
```
|
||||
|
||||
At the `(gdb)` prompt:
|
||||
|
||||
```
|
||||
set architecture armv8-m.main
|
||||
target extended-remote :3333
|
||||
hbreak *0x10000292
|
||||
continue
|
||||
```
|
||||
|
||||
Do **not** run `monitor reset run` before `hbreak`. `0x10000292` is inside `main`'s loop, so the breakpoint fires on the next iteration with no reset. If you reset first, the core runs `main` and you will not catch it.
|
||||
|
||||
GDB stops at the `printf` call. Confirm the value, change it, and let it run:
|
||||
|
||||
```
|
||||
info registers pc r1 # pc = 0x10000292, r1 = 0x2a
|
||||
set $r1 = 0x2b
|
||||
stepi
|
||||
continue
|
||||
```
|
||||
|
||||
The serial monitor prints `FAV_NUM: 43` for the iteration you changed — the same temporary live hack as editing `r1` in the IDA Registers widget. When you are done, press `Ctrl-C`, then `detach` and `quit`.
|
||||
|
||||
**If you specifically want to stop at `main` (`0x10000234`),** remember its entry runs only once per reset, so the breakpoint must be armed *before* the reset:
|
||||
|
||||
```
|
||||
monitor reset halt
|
||||
hbreak *0x10000234
|
||||
continue
|
||||
```
|
||||
|
||||
If you instead set it while the target is running and just `continue`, you will "blow past" `main` and catch the core inside `printf` — in this build at `0x10003ab4`, the `stdio_uart_out_flush` UART-drain loop.
|
||||
|
||||
`hbreak` sets a hardware breakpoint, which is required for read-only flash. It works from plain GDB because GDB sends the 2-byte length the Cortex-M33 comparators need. IDA's **GDB MI** adapter goes through the same GDB, so its GUI breakpoints work too; the old **GDB RSP** adapter was the one that sent a 1-byte length and could not set breakpoints here.
|
||||
The patched image has been exported and converted to UF2. This IDA guide ends at static verification; it does not connect to, flash, or debug a live target.
|
||||
|
||||
## Glossary
|
||||
|
||||
@@ -1496,4 +762,4 @@ If you instead set it while the target is running and just `continue`, you will
|
||||
|
||||
---
|
||||
|
||||
**Remember:** the ELF tells you what every address is, and the `.bin` is what you actually patch. Prove the behavior dynamically, read `r1` at the `printf` call, resolve the names from the ELF (including the `lcd_1602.c` symbols), then patch the bytes — `0x2a -> 0x2b`, `0x39 -> 0x40`, and the eight-byte LCD string — and flash.
|
||||
**Remember:** the ELF tells you what every address is, and the `.bin` is what you actually patch. Verify the static behavior from the recovered instructions.c` symbols), then patch the bytes — `0x2a -> 0x2b`, `0x39 -> 0x40`, and the eight-byte LCD string — and export the patched artifact.
|
||||
+13
-768
@@ -24,12 +24,12 @@ By using this repository and course, you acknowledge and agree that:
|
||||
- Load the raw `.bin` into IDA at `0x10000000`
|
||||
- Read the six C operator results as **bare instruction immediates** (`movs r1, #50`, `movs r1, #5`, …)
|
||||
- Read the DHT11 single-wire driver, including the inlined `gpio_*` and `time_us_32` helpers
|
||||
- **Break at the arithmetic `printf` call** on live silicon and hack the printed value live
|
||||
- Optionally **hack the format string live** by pointing `r0` at a RAM replacement
|
||||
ted value live
|
||||
t
|
||||
- **Resolve the functions in the IDA GUI** using the ELF symbol map, including the `dht11.c` symbols
|
||||
- **Patch** the arithmetic immediate (`50 -> 99`) and the DHT11 scaling constant (`0.1f -> 5.0f`)
|
||||
- **Rename** the printed label by patching the `"arithmetic_operator:"` string
|
||||
- **Export** the patched image, convert it to UF2, and flash it
|
||||
- **Export** the patched image, convert it to UF2, and export it
|
||||
|
||||
---
|
||||
|
||||
@@ -48,10 +48,10 @@ The `.bin` is built **from** the `.elf`, so the ELF tells you exactly what is at
|
||||
|
||||
The order is **dynamic first, static second**:
|
||||
|
||||
1. Break on the live target and prove what the code does.
|
||||
2. Hack it live in the debugger and watch the output change.
|
||||
d prove what the code does.
|
||||
ge.
|
||||
3. Resolve the functions in IDA using the ELF symbol map.
|
||||
4. Patch the bytes, export, convert, and flash.
|
||||
4. Patch the bytes, export, convert and export.
|
||||
|
||||
| Project | Serial output | Also does | The hacks |
|
||||
| ------- | ------------- | --------- | --------- |
|
||||
@@ -65,7 +65,7 @@ The order is **dynamic first, static second**:
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Build, Flash, and Get the Symbol Map
|
||||
## Part 1: Build and Get the Symbol Map
|
||||
|
||||
### Step 1: Install the toolchain
|
||||
|
||||
@@ -77,60 +77,10 @@ The order is **dynamic first, static second**:
|
||||
|
||||
**macOS Apple Silicon**
|
||||
|
||||
```bash
|
||||
brew install cmake ninja
|
||||
```
|
||||
|
||||
- Install **IDA Pro** and complete its license activation.
|
||||
- Install the **Arm GNU Toolchain**, or let the VS Code Pico extension manage it.
|
||||
|
||||
**Linux x64**
|
||||
|
||||
```bash
|
||||
sudo apt install cmake ninja-build gcc-arm-none-eabi libnewlib-arm-none-eabi git python3 openocd minicom
|
||||
```
|
||||
|
||||
- Install **IDA Pro** and complete its license activation.
|
||||
|
||||
### Step 2: Verify your tools are the right architecture (do not skip this)
|
||||
|
||||
On **macOS Apple Silicon**, the most common failure is an Intel `x86_64` tool on your `PATH`:
|
||||
|
||||
```
|
||||
zsh: bad CPU type in executable: cmake
|
||||
```
|
||||
|
||||
You may have **two Homebrews**: the arm64 one at `/opt/homebrew` and the Intel one at `/usr/local`. If `/usr/local/bin` wins, every `brew` tool is x86_64. Check:
|
||||
|
||||
```bash
|
||||
file "$(which cmake)"
|
||||
file "$(which ninja)"
|
||||
file "$(which arm-none-eabi-gdb)"
|
||||
file "$(which arm-none-eabi-nm)"
|
||||
file "$(which openocd)"
|
||||
file "$(which telnet)"
|
||||
```
|
||||
|
||||
All must report `arm64`. If any is `x86_64`, put the Apple Silicon prefix first for the session and check again:
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
hash -r
|
||||
file "$(which cmake)"
|
||||
```
|
||||
|
||||
To make it permanent, add that `export` to `~/.zshrc`. Do not use Rosetta as a fix; OpenOCD and GDB are exactly the kind of programs where a translation layer produces failures that look like debugger bugs.
|
||||
|
||||
**`telnet` is special — and optional.** The GDB MI workflow does not need it; it is only used by the command-port fallback. macOS no longer ships `telnet`, and the Homebrew build is often the Intel one, so `telnet 127.0.0.1 4444` fails with `bad CPU type in executable`. Call the Apple Silicon Homebrew explicitly:
|
||||
|
||||
```bash
|
||||
/opt/homebrew/bin/brew install telnet
|
||||
```
|
||||
|
||||
If you would rather not install anything, macOS ships an arm64 `nc`, which can connect to the same OpenOCD port:
|
||||
|
||||
```bash
|
||||
nc 127.0.0.1 4444
|
||||
bash
|
||||
```
|
||||
|
||||
**Windows x64** and **Linux x64** do not have this problem. Skip to Step 3.
|
||||
@@ -287,77 +237,6 @@ Things in this project that have **no symbol of their own**, because the compile
|
||||
|
||||
> **`dht_pin` is a `b` symbol, not a function.** `arm-none-eabi-nm -n` lists `20000b7c b dht_pin`. That lowercase `b` is a **local data** symbol: the `dht11.c` file-scope `static uint dht_pin;` is parked in RAM `.bss` at `0x20000b7c`. It holds the GPIO number (`4`) after `dht11_init(4)` runs. There is no function there; do not `Y` it with a prototype.
|
||||
|
||||
### Step 5: Flash and confirm the output
|
||||
|
||||
A `.bin` has no headers, so OpenOCD must be told the base address `0x10000000`. From the repository root:
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```bash
|
||||
./flash.sh 0x001a_operators/build/0x001a_operators.bin
|
||||
```
|
||||
|
||||
**Windows x64 (PowerShell):**
|
||||
|
||||
```powershell
|
||||
.\flash.ps1 -Bin 0x001a_operators\build\0x001a_operators.bin
|
||||
```
|
||||
|
||||
**Or flash from the IDA console** (the console reads the repo root from the marker file, so it works with no database open):
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
bin_path = os.path.join(root, "0x001a_operators", "build", "0x001a_operators.bin")
|
||||
log = os.path.join(os.path.dirname(bin_path), "flash.log")
|
||||
subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # free the probe first
|
||||
subprocess.Popen([os.path.join(root, "flash.sh"), bin_path],
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
print("flashing in the background; log:", log)
|
||||
```
|
||||
|
||||
**Windows x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
bin_path = os.path.join(root, "0x001a_operators", "build", "0x001a_operators.bin")
|
||||
log = os.path.join(os.path.dirname(bin_path), "flash.log")
|
||||
subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # free the probe first
|
||||
subprocess.Popen(["powershell", "-ExecutionPolicy", "Bypass", "-File",
|
||||
os.path.join(root, "flash.ps1"), "-Bin", bin_path],
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT)
|
||||
print("flashing in the background; log:", log)
|
||||
```
|
||||
|
||||
Wait for `wrote 17484 bytes ...` and `** Verified OK **`. Open a serial monitor at **115200** baud:
|
||||
|
||||
- **Windows x64:** PuTTY -> Connection type **Serial**, the Pico's COM port, speed `115200`.
|
||||
- **macOS Apple Silicon:** `screen /dev/tty.usbmodem* 115200` (quit with `Ctrl-A` then `K`).
|
||||
- **Linux x64:** `minicom -D /dev/ttyACM0 -b 115200`.
|
||||
|
||||
```
|
||||
arithmetic_operator: 50
|
||||
increment_operator: 5
|
||||
relational_operator: 0
|
||||
logical_operator: 0
|
||||
bitwise_operator: 10
|
||||
assignment_operator: 10
|
||||
Humidity: 51.0%, Temperature: 23.8°C
|
||||
...
|
||||
```
|
||||
|
||||
Two of these values are *not* what a naive reading of the source predicts, and the build is correct:
|
||||
|
||||
- **`increment_operator: 5`** — `compute_arithmetic_ops` takes `x` **by value**, so the `*incr = x++;` inside the helper increments the helper's private copy. `compute_operators`' own `x` is still `5` afterwards.
|
||||
- **`bitwise_operator: 10`** and **`assignment_operator: 10`** — because `x` is still `5`, `x << 1` is `10` (not `12`), and `x += 5` is `10` (not `11`). The relational and logical operators compare `5 > 10`, so both are `0`.
|
||||
|
||||
The humidity and temperature lines come from the physical DHT11 and vary with your sensor.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Load the Raw `.bin` into IDA
|
||||
|
||||
Start from a fresh IDA state. If you already have a database for this lesson, **close it and start over**; a stale database keeps old names and patches.
|
||||
@@ -419,7 +298,7 @@ Once `main` shows code, the import is correct.
|
||||
- **Hex view:** raw bytes, used for patching.
|
||||
- **Function list:** the sidebar list of every detected function.
|
||||
|
||||
Navigation: `G` go to address, `N` rename, `Y` set type or signature, `;` add a comment. Breakpoints are set from the GUI through the GDB MI adapter — see Step 12.
|
||||
Navigation: `G` go to address, `N` rename, `Y` set type or signature, `;` add a comment.
|
||||
|
||||
> **macOS function keys:** the top-row `F` keys are usually mapped to system functions. Every step here uses menu paths that work without them.
|
||||
|
||||
@@ -427,299 +306,7 @@ Navigation: `G` go to address, `N` rename, `Y` set type or signature, `;` add a
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Dynamic — Break at the `printf` Call and Hack Live
|
||||
|
||||
### Step 9: Start OpenOCD as a live debug server
|
||||
|
||||
Make sure no other OpenOCD is running; a forgotten server holds port `3333`.
|
||||
|
||||
**macOS / Linux:**
|
||||
|
||||
```bash
|
||||
ps aux | grep -i openocd
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
Get-Process | Where-Object { $_.ProcessName -like '*openocd*' }
|
||||
```
|
||||
|
||||
Stop any leftover server gracefully:
|
||||
|
||||
**macOS / Linux:**
|
||||
|
||||
```bash
|
||||
pkill -TERM -f openocd
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
Get-Process openocd -ErrorAction SilentlyContinue | Stop-Process
|
||||
```
|
||||
|
||||
Start the server **parked at `main`**:
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```bash
|
||||
BP_ADDR=0x10000234 ./debug-server.sh
|
||||
```
|
||||
|
||||
**Windows x64 (PowerShell):**
|
||||
|
||||
```powershell
|
||||
$env:BP_ADDR="0x10000234"; .\debug-server.ps1
|
||||
```
|
||||
|
||||
**Or start it from the IDA console**, freeing the probe first and launching the server in the background so the console returns immediately:
|
||||
|
||||
**macOS Apple Silicon / Linux x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # stop any running server first
|
||||
log = os.path.join(root, "openocd.log")
|
||||
p = subprocess.Popen([os.path.join(root, "debug-server.sh")], cwd=root,
|
||||
env=dict(os.environ, BP_ADDR="0x10000234"),
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
print("OpenOCD started (pid", p.pid, "); log:", log)
|
||||
```
|
||||
|
||||
**Windows x64:**
|
||||
|
||||
```python
|
||||
import os, subprocess
|
||||
root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3)
|
||||
subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # stop any running server first
|
||||
log = os.path.join(root, "openocd.log")
|
||||
p = subprocess.Popen(["powershell", "-ExecutionPolicy", "Bypass", "-File",
|
||||
os.path.join(root, "debug-server.ps1")], cwd=root,
|
||||
env=dict(os.environ, BP_ADDR="0x10000234"),
|
||||
stdout=open(log, "w"), stderr=subprocess.STDOUT)
|
||||
print("OpenOCD started (pid", p.pid, "); log:", log)
|
||||
```
|
||||
|
||||
`Popen` returns in a few milliseconds; the server keeps running in the background. Check `openocd.log` for `Listening on port 3333`, then connect in Step 10.
|
||||
|
||||
Wait for:
|
||||
|
||||
```
|
||||
Info : [rp2350.dap.core0] Examination succeed
|
||||
Startup breakpoint at 0x10000234 (2-byte hardware execute, one-shot).
|
||||
Info : starting gdb server for rp2350.dap.core0 on 3333
|
||||
Info : Listening on port 3333 for gdb connections
|
||||
```
|
||||
|
||||
> **`BP_ADDR` parks the core at `main` before any client connects.** The script arms a 2-byte hardware breakpoint and then does the startup `reset run`, so the core runs from the vector table and stops at your address with no debugger attached yet. When IDA connects a moment later, the first thing it reads is already the truth: `Stopped at 0x10000234`. This is the whole reason the lab works cleanly — you never have to drive a reset from outside the GUI.
|
||||
>
|
||||
> Use the address you actually want to stop at:
|
||||
>
|
||||
> | What you want to stop at | Address | Command |
|
||||
> | --- | --- | --- |
|
||||
> | `main` (once per reset) | `0x10000234` | `BP_ADDR=0x10000234 ./debug-server.sh` |
|
||||
> | The **loop** — the arithmetic `printf` call, hit every iteration | `0x10000272` | `BP_ADDR=0x10000272 ./debug-server.sh` |
|
||||
>
|
||||
> ```bash
|
||||
> BP_ADDR=0x10000234 ./debug-server.sh # park at main
|
||||
> BP_ADDR=0x10000272 ./debug-server.sh # park in the loop instead
|
||||
> ```
|
||||
>
|
||||
> ```powershell
|
||||
> $env:BP_ADDR="0x10000234"; .\debug-server.ps1 # park at main
|
||||
> $env:BP_ADDR="0x10000272"; .\debug-server.ps1 # park in the loop
|
||||
> ```
|
||||
>
|
||||
> **This startup stop is single-use.** OpenOCD flushes breakpoints when a client connects, so this one is gone once IDA attaches — fine for `main`, which only runs once per reset. Every breakpoint after that is set from the IDA GUI (Step 12) and is repeatable. To stop at `main` again, restart the server with `BP_ADDR` and reconnect.
|
||||
|
||||
> **Exactly one core.** The line must say `core0` and must **not** mention `core1`. Core1 is never started by this firmware; exposing it makes IDA read core1's reset-state registers, which are not real addresses, and OpenOCD floods the log with `Failed to read memory at 0xf0000000`. The scripts already use `USE_CORE=0`; do not change it.
|
||||
|
||||
> **Windows driver note:** the Debug Probe must use the **WinUSB** driver. If OpenOCD reports `unable to open CMSIS-DAP device`, install it with [Zadig](https://zadig.akeo.ie/) (select `Debug Probe (CMSIS-DAP)` -> WinUSB).
|
||||
|
||||
### Step 10: Connect IDA to the GDB server
|
||||
|
||||
1. Make sure the image is open and analyzed (Part 2) and the server from Step 9 is running (parked at `main`).
|
||||
2. Choose `Debugger -> Connect to Remote Process`.
|
||||
3. In the **adapter** dropdown, select **GDB MI**.
|
||||
4. In the **connect** settings group, set **IP Address** to `127.0.0.1` and **Port** to `3333`.
|
||||
5. Set **Full GDB Executable Path** to the `arm-none-eabi-gdb` from the **Arm GNU Toolchain 14.2.rel1**. It ships for all three hosts, and the Raspberry Pi Pico VS Code extension installs that same 14.2.rel1 toolchain (including `arm-none-eabi-gdb`) on all of them:
|
||||
|
||||
| OS | `arm-none-eabi-gdb` path |
|
||||
| -- | ------------------------ |
|
||||
| macOS Apple Silicon | `/Applications/ArmGNUToolchain/14.2.rel1/arm-none-eabi/bin/arm-none-eabi-gdb` (or the Pico extension's `~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb`) |
|
||||
| Windows x64 | `%USERPROFILE%\.pico-sdk\toolchain\14_2_Rel1\bin\arm-none-eabi-gdb.exe` (Pico extension), or `C:\Program Files (x86)\Arm GNU Toolchain arm-none-eabi\14.2 rel1\bin\arm-none-eabi-gdb.exe` |
|
||||
| Linux x64 | `~/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gdb` (Pico extension), or the `bin/` directory of the extracted `arm-gnu-toolchain-14.2.rel1-x86_64-arm-none-eabi` tarball |
|
||||
6. Click **Accept**.
|
||||
|
||||
> **Use the GDB MI adapter.** It launches a real `arm-none-eabi-gdb --interpreter=mi2` and lets IDA drive it, so breakpoints and stepping go through real GDB — which sends the correct 2-byte breakpoint length and handles step-over itself. Verified working end to end: connect, GUI breakpoints (**Add Hardware Breakpoint...**, hardware execute), **Step Into** / **Step Over**, and register edits. Stops are reported as `Breakpoint` (not `SingleStep`).
|
||||
>
|
||||
> **Do NOT have any breakpoints set in IDA before you connect.** With the GDB MI adapter, attaching while IDA already has a breakpoint **hangs the session**. Start the server parked with `BP_ADDR` (Step 9), connect, and only add hardware breakpoints *after* the connection is up. This is a IDA bug; it is the single most common GDB MI failure.
|
||||
>
|
||||
> **The GDB executable path matters.** Use the **14.2.rel1** build on every OS (Windows, macOS, Linux). The 13.3.rel1 build did **not** connect in testing.
|
||||
>
|
||||
> **This step is temporary.** Vector35 plans to ship a GDB binary with the GDB MI adapter ([Vector35/debugger#929](https://github.com/Vector35/debugger/issues/929), milestone *Langara*). Once that lands, IDA provides GDB itself and you will not need to set **Full GDB Executable Path** at all.
|
||||
>
|
||||
> **Do not pick Corellium.** IDA's adapter dropdown also lists **Corellium**, which is for Corellium's virtual devices and expects an API token, not a local OpenOCD server. It is not the adapter for this lab. The dropdown is a combo box, so an accidental arrow-key press can land on it — always read the label back and confirm it says **GDB MI** before clicking **Accept**.
|
||||
|
||||
> **The adapter and port are not saved in the `.i64`.** Every time you relaunch IDA you must re-select **GDB MI**, re-enter port `3333`, and re-set the GDB path.
|
||||
|
||||
> **Watch for an off-screen error dialog.** When a connection fails, IDA pops a `IDA critical alert` window that can be positioned mostly outside the main window, which makes it look like nothing happened. If the connect seems to do nothing, check your other display.
|
||||
|
||||
The target keeps running. Open the **Registers** tab (bug icon) and confirm you see live values. `pc` inside `0x10003xxx` and `sp` just below `0x20082000` are healthy.
|
||||
|
||||
> **If `pc` is `0x00000088`, `0x000000ec`, or `sp` is `0xf0000000`, the session is bad.** Restart the server, then restart IDA (a server restart while attached leaves IDA in a stale session), and connect again.
|
||||
|
||||
### Step 11: Find `main` without relying on its address
|
||||
|
||||
`main` can move between programs, so we do not guess it. We follow the one fixed path to it. Press `G` and go to `0x10000000`:
|
||||
|
||||
```
|
||||
0x10000000 0x20082000 initial stack pointer (top of SRAM)
|
||||
0x10000004 0x1000015d reset vector
|
||||
```
|
||||
|
||||
Bit 0 of a vector is the Thumb bit, so `0x1000015d` means "start at `0x1000015c`". That is `_reset_handler`. Follow the reset path to `0x10000186`, `platform_entry`:
|
||||
|
||||
```asm
|
||||
10000186: ldr r1, [pc, #80] @ (100001d8 <data_cpy_table+0x38>)
|
||||
10000188: blx r1
|
||||
1000018a: ldr r1, [pc, #80] @ (100001dc <data_cpy_table+0x3c>)
|
||||
1000018c: blx r1
|
||||
1000018e: ldr r1, [pc, #80] @ (100001e0 <data_cpy_table+0x40>)
|
||||
10000190: blx r1
|
||||
10000192: bkpt 0x0000
|
||||
10000194: b.n 10000192 <platform_entry+0xc>
|
||||
```
|
||||
|
||||
**The middle `blx` at `0x1000018c` is the call to `main`.** `platform_entry` is byte-identical in every project, so `0x1000018c` catches `main` no matter where the linker placed it. The literal pool at `0x100001dc` holds `main | 1`; clearing bit 0 gives `0x10000234`.
|
||||
|
||||
### Step 12: Set a hardware breakpoint in the GUI
|
||||
|
||||
With the **GDB MI** adapter, IDA sets breakpoints through real GDB, which sends the correct 2-byte length, so you set them **in the UI**. There is no command port here.
|
||||
|
||||
> **Why older drafts used the command port.** IDA's **GDB RSP** adapter is its own minimal RSP client and sends a **1-byte** breakpoint (`Z0,<addr>,1`); the Cortex-M33 FPB comparators need 2 bytes, so OpenOCD rejected it with `only breakpoints of two bytes length supported`. The old workaround was to arm breakpoints by hand over telnet. **The GDB MI adapter does not have this problem** — it drives real `arm-none-eabi-gdb`, which sends the right length. So everything below is done in the GUI. The command port still exists as a fallback (see the end of this step), but you do not need it.
|
||||
|
||||
#### Where you can stop
|
||||
|
||||
| You want to stop at | Address | How | Repeatable? |
|
||||
| --- | --- | --- | --- |
|
||||
| **`main`** | `0x10000234` | The server starts parked there with `BP_ADDR=0x10000234` (Step 9), so IDA is already stopped at `main` when it connects. | No — `main` runs once per reset. |
|
||||
| **The arithmetic `printf` call** | `0x10000272` | Set a hardware breakpoint in the GUI, then click **Resume**. | Yes — fires on every iteration. |
|
||||
| **The second `printf` call (increment)** | `0x1000027a` | Same. | Yes. |
|
||||
| **The `dht11_read` call** | `0x100002a2` | Same. | Yes. |
|
||||
|
||||
#### Set the loop breakpoint in the GUI
|
||||
|
||||
1. Press `G`, type the loop address (`0x10000272`), and press Enter.
|
||||
2. Set a **hardware execution** breakpoint at that address, either way:
|
||||
- `Debugger -> Add Hardware Breakpoint...` — a **hardware execute** (`HE`) breakpoint. **Use this one.**
|
||||
- click the line and press `F2` (`Debugger -> Toggle Breakpoint`) — a **software** breakpoint. It will **not** work here: the code is in read-only flash, so GDB cannot install it and the core just keeps running.
|
||||
3. Click **Resume**. The core is already running the loop, so the breakpoint fires on the next iteration. IDA stops with the PC at the loop address and reports it as a **Breakpoint** — verified: `Stopped (Breakpoint) at 0x10000272`.
|
||||
|
||||
> **No breakpoints before you connect.** With GDB MI, a breakpoint set before the connection hangs the session (Step 10). Start parked with `BP_ADDR`, connect, *then* add breakpoints.
|
||||
|
||||
#### Stepping
|
||||
|
||||
With the target halted at the breakpoint, **Step Into** (`F7`) and **Step Over** (`F8`) run through real GDB and move the PC. Verified: `0x10000272 -> 0x10003524 -> 0x10003526 -> ...`.
|
||||
|
||||
> **Step Over on the raw `.bin` steps *into* calls.** The raw image has no symbol for `__wrap_printf`, so **Step Over** at the `printf` call behaves like **Step Into**. When the lab needs to execute the call and then stop, it moves the breakpoint to the return site and clicks **Resume** instead (Step 13 shows this).
|
||||
|
||||
> **Never use IDA's Restart button.** On RP2350 it resets and halts inside the boot ROM (`pc=0x88`, `sp=0xf0000000`). To reset cleanly, restart the server with `BP_ADDR` and reconnect.
|
||||
|
||||
> **If you ever need the command port.** It is still there — `nc 127.0.0.1 4444`, and `bp <addr> 2 hw` still arms a breakpoint, `rbp <addr>` / `rbp all` still remove them. It is the fallback if you switch back to the **GDB RSP** adapter, whose 1-byte breakpoints the GUI cannot set. With GDB MI you do not need it for this lab.
|
||||
|
||||
### Step 13: HACK IT LIVE — change the printed `arithmetic_operator`
|
||||
|
||||
`main` loads the constant `0x32` (50) into `r1` and calls `printf` on every iteration. We break on that call and change it live:
|
||||
|
||||
```asm
|
||||
1000026e: movs r1, #50 @ 0x32
|
||||
10000270: ldr r0, [pc, #68] @ (100002b8 <main+0x84>)
|
||||
10000272: bl 10003524 <__wrap_printf>
|
||||
10000276: movs r1, #5
|
||||
10000278: ldr r0, [pc, #64] @ (100002bc <main+0x88>)
|
||||
1000027a: bl 10003524 <__wrap_printf>
|
||||
```
|
||||
|
||||
1. Press `G`, go to `0x10000272` (the `bl __wrap_printf` for `arithmetic_operator`).
|
||||
2. Set a **hardware execute** breakpoint there: `Debugger -> Add Hardware Breakpoint...`. (Do not use `F2` — that is a software breakpoint and will not work on read-only flash.)
|
||||
3. Click **Resume** in IDA. The target is already running the loop, so the breakpoint fires on the next iteration. IDA stops with the program counter at `0x10000272`, `r0 = 0x100038e8`, and `r1 = 0x32`.
|
||||
4. Open the **Registers** widget (bug icon -> **Registers**).
|
||||
5. Find `r1`. Its value is `0x32` (50), loaded by the `movs r1, #50` at `0x1000026e`.
|
||||
6. **Set `r1` to `0x63` (99).** From IDA's Python console (`Plugins -> Python Console`):
|
||||
```python
|
||||
dbg.set_reg_value("r1", 0x63)
|
||||
```
|
||||
`dbg.set_reg_value(name, value)` writes one register (returns `True` on success). You can also right-click `r1` in the **Registers** widget, press `E` (edit), type `63`, and press Enter. The widget may not repaint the value, but the write reaches the target — you confirm it by the printed output in the next steps.
|
||||
7. **Move the breakpoint past the call.** You want `printf` to run once and then stop, so move the breakpoint from `0x10000272` to the instruction *after* the call, `0x10000276` (the `movs r1, #5` that begins the increment half of the loop): remove the breakpoint at `0x10000272` and set a hardware breakpoint at `0x10000276`. Two reasons not to just click **Step Over** here: a breakpoint left on the current PC re-traps the step, and IDA's **Step Over** steps *into* `__wrap_printf` on this raw `.bin` because the image carries no symbol for the call. Moving the breakpoint to the return site is deterministic.
|
||||
8. Click **Resume** in IDA. The core executes `bl __wrap_printf` with `r1 = 0x63`, so this iteration prints `arithmetic_operator: 99`, then stops at `0x10000276`.
|
||||
9. Look at your serial monitor — the `screen` session on the Pico's USB serial port — and at the **Target** tab in IDA:
|
||||
|
||||
```
|
||||
arithmetic_operator: 99
|
||||
```
|
||||
|
||||
You changed a running program's output without touching the binary.
|
||||
|
||||
### Step 13b: HACK THE STRING LIVE — change `arithmetic_operator:` to `hacked_operator:` (optional)
|
||||
|
||||
The text `"arithmetic_operator: %d\r\n"` lives in flash (`.rodata`) at `0x100038e8`, and flash is **read-only at runtime** — a debugger write there does not stick. So instead of overwriting the text in place, redirect the pointer: at the `printf` call, `r0` holds the string address, so point `r0` at a replacement string you place in RAM.
|
||||
|
||||
1. Arm the breakpoint at the `printf` call and hit it, exactly as in Step 13 steps 1-3. At the stop, `r0 = 0x100038e8` and `r1 = 0x32`.
|
||||
2. Put the replacement string into free RAM at `0x20080000` from IDA's **Python console** (`Plugins -> Python Console`) — no command port needed:
|
||||
```python
|
||||
dbg.write_memory(0x20080000, b"hacked_operator: %d\r\n\x00")
|
||||
```
|
||||
`dbg.write_memory(address, bytes)` is IDA's debugger memory-write API; it returns `True` on success. That writes `hacked_operator: %d\r\n\0`.
|
||||
3. Point `r0` at that string:
|
||||
```python
|
||||
dbg.set_reg_value("r0", 0x20080000)
|
||||
```
|
||||
(Or right-click `r0` in the **Registers** widget, press `E`, type `0x20080000`, and press Enter.)
|
||||
4. If you want the value hack too, set `r1` to `0x63` as in Step 13. Then move the breakpoint past the call in the GUI (remove it at `0x10000272`, set one at `0x10000276`) and click **Resume**. The core runs `printf` with `r0` pointing at your RAM string and `r1 = 0x63`, so this iteration prints:
|
||||
```
|
||||
hacked_operator: 99
|
||||
```
|
||||
then stops at `0x10000276`.
|
||||
|
||||
Like the value hack, this is **one iteration only**: the loop reloads `r0` (and `r1`) from flash on every pass, so the next line is `arithmetic_operator: 50` again. The permanent version is the static patch in Step 18c.
|
||||
|
||||
### Step 14: Why the hack reverts (and why we patch next)
|
||||
|
||||
Press **Resume**. The loop branches back to `0x1000026e`, which reloads `movs r1, #50`, so the next line is `arithmetic_operator: 50`. The live edit changed one iteration only. There is no variable in memory to change; the value is baked into the instruction. To make `arithmetic_operator: 99` permanent we must patch the instruction. That is the static pass.
|
||||
|
||||
Press **Pause** to stop the output flood.
|
||||
|
||||
### Step 15: Kill the debugger and OpenOCD
|
||||
|
||||
The live hack is done. Do this **before** the static pass.
|
||||
|
||||
1. In the **Debugger** sidebar, click the **X** (**Kill**) (or **`Debugger -> Kill`**) to disconnect IDA.
|
||||
2. **Kill does not stop the OpenOCD process** — `debug-server.sh` started it separately, and it keeps running and holding the probe. Stop it from the IDA console:
|
||||
|
||||
**macOS / Linux:**
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # stop the debug server, free the probe
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # stop the debug server, free the probe
|
||||
```
|
||||
|
||||
3. Confirm nothing is left: `pgrep -fl openocd` (macOS/Linux) prints nothing.
|
||||
|
||||
From a terminal it is the same: `pkill -TERM -f openocd`, or `Get-Process openocd | Stop-Process` on Windows.
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Static — Resolve the Functions in IDA and Patch
|
||||
## Part 3: Static — Resolve the Functions in IDA and Patch
|
||||
|
||||
### Step 16: Resolve the functions in the IDA GUI
|
||||
|
||||
@@ -1155,351 +742,9 @@ python ..\uf2conv.py 0x001a_operators-h.bin ^
|
||||
>
|
||||
> This writes `hacked.uf2` next to the `.bin`, ready to drag onto the Pico.
|
||||
|
||||
### Step 21: Flash and verify
|
||||
## Static stopping point
|
||||
|
||||
Hold **BOOTSEL**, plug in the Pico 2, and drag `hacked.uf2` onto the **`RP2350`** drive. Open the serial monitor:
|
||||
|
||||
```
|
||||
hacked_operator: 99
|
||||
increment_operator: 5
|
||||
relational_operator: 0
|
||||
logical_operator: 0
|
||||
bitwise_operator: 10
|
||||
assignment_operator: 10
|
||||
Humidity: 255.0%, Temperature: 119.0°C
|
||||
...
|
||||
```
|
||||
|
||||
- The first line now reads **`hacked_operator: 99`**.
|
||||
- The humidity/temperature decimals are scaled by **5.0** instead of **0.1**, so the readings are roughly 50x larger (exact numbers depend on your sensor).
|
||||
|
||||
**All three changes — one instruction byte, one float word, and one string — with no source code.**
|
||||
|
||||
> **Faster: flash over the Debug Probe (no BOOTSEL).** The repo's `flash.sh` writes the raw `.bin` straight into XIP flash over SWD (`program <bin> 0x10000000 verify reset exit`), so you never touch BOOTSEL or a UF2. Run it from a terminal (`./flash.sh <bin>`), or from the IDA console **without freezing it** — use `subprocess.Popen`, which returns immediately, and send OpenOCD's output to a log file. (`subprocess.run` blocks the console until the flash finishes; do not use it here.)
|
||||
>
|
||||
> ```python
|
||||
> import os, subprocess
|
||||
> root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip()
|
||||
> bin_path = os.path.join(os.path.join(root, "0x001a_operators", "build"), "0x001a_operators-h.bin")
|
||||
> log = os.path.join(os.path.join(root, "0x001a_operators", "build"), "flash.log")
|
||||
> subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # free the probe first
|
||||
> p = subprocess.Popen([os.path.join(root, "flash.sh"), bin_path],
|
||||
> stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
> print("flashing in the background; log:", log)
|
||||
> ```
|
||||
>
|
||||
> The `pkill` frees the probe first; on Windows use `subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"])`.
|
||||
>
|
||||
> The console is free the moment this returns. Check it with `print(p.poll())` (`None` = still running, `0` = done) or read `flash.log` — success ends with `** Verified OK **`.
|
||||
>
|
||||
> The same non-blocking form without the script:
|
||||
>
|
||||
> ```python
|
||||
> import os, subprocess
|
||||
> ocd = os.path.expanduser("~/.pico-sdk/openocd/0.12.0+dev")
|
||||
> bin_path = os.path.join(os.path.join(root, "0x001a_operators", "build"), "0x001a_operators-h.bin")
|
||||
> log = os.path.join(os.path.join(root, "0x001a_operators", "build"), "flash.log")
|
||||
> subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # free the probe first
|
||||
> p = subprocess.Popen([f"{ocd}/openocd", "-s", f"{ocd}/scripts",
|
||||
> "-f", "interface/cmsis-dap.cfg", "-f", "target/rp2350.cfg",
|
||||
> "-c", "adapter speed 5000",
|
||||
> "-c", f"program {bin_path} 0x10000000 verify reset exit"],
|
||||
> stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True)
|
||||
> print("flashing in the background; log:", log)
|
||||
> ```
|
||||
>
|
||||
> **The Debug Probe is single-owner.** If IDA is still attached (the `debug-server.sh` OpenOCD is running), the flash cannot grab the probe. Detach in IDA and stop that OpenOCD first:
|
||||
>
|
||||
> ```bash
|
||||
> # macOS / Linux
|
||||
> pkill -TERM -f openocd
|
||||
> ```
|
||||
> ```powershell
|
||||
> # Windows
|
||||
> Get-Process openocd -ErrorAction SilentlyContinue | Stop-Process
|
||||
> ```
|
||||
>
|
||||
> Success looks like `Programming Finished` -> `Verified OK` -> `Resetting Target`. On Windows use `flash.ps1` (`.\flash.ps1 -Bin <path>`) the same way.
|
||||
|
||||
---
|
||||
|
||||
## Cheatsheet
|
||||
|
||||
### IDA GUI actions
|
||||
|
||||
| Action | How |
|
||||
| ------ | --- |
|
||||
| Go to address | `G` |
|
||||
| Rename function/symbol | `N` |
|
||||
| Set type or signature | `Y` |
|
||||
| Add comment | `;` |
|
||||
| Open Hex view | `View -> Hex` |
|
||||
| Enable hex editing | Toggle the lock in the status bar |
|
||||
| Reanalyze after a patch | Right-click function -> `Reanalyze` |
|
||||
| Edit a register live | `dbg.set_reg_value("r1", 0x63)` in the Python console (or right-click the register, press `E`, type hex, Enter) |
|
||||
| Write a RAM string live | `dbg.write_memory(0x20080000, b"hacked_operator: %d\r\n\x00")` |
|
||||
| Set a breakpoint | `Debugger -> Add Hardware Breakpoint...` (hardware execute). Do **not** use `F2` — software breakpoints cannot be written to read-only flash. |
|
||||
| Move a breakpoint | Remove it and set it at the new address in the GUI (command-port fallback: `rbp <old addr>` then `bp <new addr> 2 hw`) |
|
||||
| Confirm what is armed | The **Breakpoints** widget lists it (command-port fallback: `mdw 0xE0002000 8`, each armed breakpoint shows as `<addr \| 1>`) |
|
||||
| Apply the ELF symbol map | Paste the Python snippet from Step 16 into the Python Console |
|
||||
| Load peripheral names (optional) | Load the RP2350 SVD — it lives at **`WEEK04/rp2350.svd`** |
|
||||
|
||||
### OpenOCD server and reset
|
||||
|
||||
The server runs with `gdb_breakpoint_override hard` so that flash-writes are never attempted. Breakpoints in this lab are set in the IDA GUI through the **GDB MI** adapter (Step 12). The command-port rows below are the fallback if you use the **GDB RSP** adapter instead.
|
||||
|
||||
| Action | Command |
|
||||
| ------ | ------- |
|
||||
| Connect to the OpenOCD prompt (fallback) | `nc 127.0.0.1 4444` (or `telnet 127.0.0.1 4444`) |
|
||||
| Reset and run (command port) | `reset run` |
|
||||
| Check core state (command port) | `targets` |
|
||||
| Set a breakpoint in the GUI | `Debugger -> Add Hardware Breakpoint...` (hardware execute; `F2` software breakpoints do not work on flash) |
|
||||
| (fallback) Add a breakpoint without the GUI | `bp <addr> 2 hw` |
|
||||
| Remove one breakpoint | `rbp <addr>` — **address only, no length, no `hw`** |
|
||||
| Remove every breakpoint | `rbp all` |
|
||||
| Start the server parked at `main` | macOS/Linux: `BP_ADDR=0x10000234 ./debug-server.sh` — Windows: `$env:BP_ADDR="0x10000234"; .\debug-server.ps1` (one-shot) |
|
||||
| Start the server parked in the loop | macOS/Linux: `BP_ADDR=0x10000272 ./debug-server.sh` — Windows: `$env:BP_ADDR="0x10000272"; .\debug-server.ps1` |
|
||||
| Break on the loop in a running target | set a hardware breakpoint in the GUI at the loop address, then **Resume** — repeatable |
|
||||
| Make IDA stepping work | `rp2350.dap.core0 configure -rtos none` (already in the scripts) |
|
||||
| Step without re-trapping | move the breakpoint off the current PC first, then **Step Into**/**Step Over** |
|
||||
| Reset without desyncing IDA | **Detach**, `reset run` on the port, reconnect — never `reset run` while attached |
|
||||
|
||||
### Every address and byte we changed
|
||||
|
||||
| Project | Address | Before | After | Effect |
|
||||
| ------- | ------- | ------ | ----- | ------ |
|
||||
| `0x001a` | `0x1000026e` | `32` | `63` | `movs r1, #50` -> `#99`, prints `arithmetic_operator: 99` |
|
||||
| `0x001a` | `0x10000438` | `cd cc cc 3d` | `00 00 a0 40` | DHT11 scale `0.1f` -> `5.0f`, readings ~50x larger |
|
||||
| `0x001a` | `0x100038e8` | `61 72 69 74 68 6d 65 74 69 63 5f 6f 70 65 72 61 74 6f 72 3a 20 25 64 0d 0a 00` | `68 61 63 6b 65 64 5f 6f 70 65 72 61 74 6f 72 3a 20 25 64 0d 0a 00 00 00 00 00` | prints `hacked_operator:` instead of `arithmetic_operator:` |
|
||||
|
||||
### The operator / DHT11 memory map
|
||||
|
||||
| Item | Address | Notes |
|
||||
| ---- | ------- | ----- |
|
||||
| `dht_pin` | `0x20000b7c` | RAM `.bss`, the `dht11.c` static holding the GPIO number (`4`) |
|
||||
| Literal-pool word — humidity/temp format | `0x100002b4` | `0x10003988` — `"Humidity: %.1f%%, Temperature: %.1f°C\r\n"` |
|
||||
| Literal-pool word — arithmetic format | `0x100002b8` | `0x100038e8` — `"arithmetic_operator: %d\r\n"` |
|
||||
| Literal-pool word — increment format | `0x100002bc` | `0x10003904` — `"increment_operator: %d\r\n"` |
|
||||
| Literal-pool word — relational format | `0x100002c0` | `0x10003920` — `"relational_operator: %d\r\n"` |
|
||||
| Literal-pool word — logical format | `0x100002c4` | `0x1000393c` — `"logical_operator: %d\r\n"` |
|
||||
| Literal-pool word — bitwise format | `0x100002c8` | `0x10003954` — `"bitwise_operator: %d\r\n"` |
|
||||
| Literal-pool word — assignment format | `0x100002cc` | `0x1000396c` — `"assignment_operator: %d\r\n"` |
|
||||
| Literal-pool word — failure string | `0x100002d0` | `0x100039b4` — `"DHT11 read failed\r\n"` |
|
||||
| `dht11_read` literal-pool word | `0x10000434` | `0x400b0000` — TIMER0 base, used by the inlined `time_us_32` |
|
||||
| DHT11 scale constant | `0x10000438` | `0x3dcccccd` — `0.1f` |
|
||||
| `dht11_read` literal-pool word | `0x1000043c` | `0x20000b7c` — `&dht_pin` |
|
||||
|
||||
### Raw image facts
|
||||
|
||||
| Item | Value |
|
||||
| ---- | ----- |
|
||||
| Build type | `Release` |
|
||||
| Load base address | `0x10000000` |
|
||||
| Project size | `17484` bytes (`0x444c`) |
|
||||
| Fixed `main` anchor | `0x1000018c` (reset handler middle `blx`) |
|
||||
| `main` | `0x10000234` |
|
||||
| `dht11_init` | `0x100002d4` |
|
||||
| `dht11_read` | `0x100002f4` |
|
||||
| `printf` call / return, `arithmetic_operator` | `0x10000272` / `0x10000276` |
|
||||
| `arithmetic_operator` format string | `0x100038e8` |
|
||||
| `increment_operator` format string | `0x10003904` |
|
||||
| `relational_operator` format string | `0x10003920` |
|
||||
| `logical_operator` format string | `0x1000393c` |
|
||||
| `bitwise_operator` format string | `0x10003954` |
|
||||
| `assignment_operator` format string | `0x1000396c` |
|
||||
| Humidity/Temperature format string | `0x10003988` |
|
||||
| `"DHT11 read failed\r\n"` string | `0x100039b4` |
|
||||
| DHT11 scale constant | `0x10000438` |
|
||||
| DHT11 GPIO pin | `4` |
|
||||
| RP2350 UF2 family ID | `0xe48bff59` |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### My operator values are 12 and 11, not 10 and 10
|
||||
|
||||
You are reading the original Week 9 text, not this build. In `0x001a_operators.c`, `compute_arithmetic_ops` receives `x` **by value**, so its `*incr = x++;` never touches `compute_operators`' `x`. That `x` stays `5`, so `x << 1` is `10` and `x += 5` is `10`. Confirm it on your own ELF in Step 4: the immediates in `main` are `#50`, `#5`, `#0`, `#0`, `#10`, `#10`.
|
||||
|
||||
### IDA hangs or crashes when you connect (macOS 27)
|
||||
|
||||
Three different causes have been seen on this setup; check them in this order.
|
||||
|
||||
- **A breakpoint set before connecting.** With the **GDB MI** adapter, if the binary view already has a breakpoint, the session hangs. Start parked with `BP_ADDR`, connect, then add breakpoints (see the next entry).
|
||||
- **The wrong GDB executable.** Point **Full GDB Executable Path** at the **14.2.rel1** toolchain (Step 10). The 13.3.rel1 build did **not** connect in testing.
|
||||
- **The LLDB adapter.** A crash report with `libdebuggercore.dylib -> std::terminate() -> abort()` and `liblldb` in the stack is the **LLDB** adapter, not GDB MI. Avoid LLDB on this setup.
|
||||
|
||||
**Use GDB MI**, with the 14.2.rel1 path above. If it still fails, fall back to plain `arm-none-eabi-gdb` against the same server — the addresses and register values are identical to the GUI steps.
|
||||
|
||||
If IDA hangs, force-quit it; the connect dialog has no working Cancel. The static steps (resolve, patch, export, flash) never touch the debugger and always work.
|
||||
|
||||
### The GUI refuses to set a breakpoint (GDB RSP adapter only)
|
||||
|
||||
If you are on the **GDB RSP** adapter, the GUI cannot set breakpoints on this target. That adapter is IDA's own minimal RSP client and sends a **1-byte** breakpoint (`Z0,<addr>,1`); the Cortex-M33 comparators need 2 bytes, so OpenOCD answers `only breakpoints of two bytes length supported`. It affects every address, both `Toggle Breakpoint` and `Add Hardware Breakpoint`, and the dialog's **Size** field is disabled. `gdb_breakpoint_override` makes no difference.
|
||||
|
||||
**Fix: use the GDB MI adapter** (Step 10). It drives real GDB, which sends the correct length, so GUI breakpoints just work. If you must stay on GDB RSP, arm breakpoints from the command port after connecting (`bp <addr> 2 hw`) — but the lab uses GDB MI and does not need that.
|
||||
|
||||
### GDB MI hangs when you connect (a breakpoint already existed)
|
||||
|
||||
With the **GDB MI** adapter, if IDA already has a breakpoint set when you connect, the session **hangs**. This is a IDA bug. The working order is:
|
||||
|
||||
1. Start the server parked, e.g. `BP_ADDR=0x10000234 ./debug-server.sh` (Windows: `$env:BP_ADDR="0x10000234"; .\debug-server.ps1`).
|
||||
2. Connect with the **GDB MI** adapter.
|
||||
3. Only *then* set hardware breakpoints in the UI.
|
||||
|
||||
Never have a breakpoint in the binary view before the GDB MI connection. If it hangs, quit IDA, restart the server with `BP_ADDR`, and connect again before adding any breakpoints.
|
||||
|
||||
### Step Into / Step Over does nothing (PC never moves)
|
||||
|
||||
Two causes have been seen on this target.
|
||||
|
||||
1. **A breakpoint on the current PC re-traps the step.** OpenOCD's step-over-breakpoint logic fails with `Duplicate Breakpoint address` and the PC stays put. Fix: move the breakpoint off the current PC (in the GUI), then step.
|
||||
2. **The `hwthread` RTOS (GDB RSP adapter only).** With the **GDB RSP** adapter, OpenOCD can log `fake step thread 0` and reply without stepping, because the RP2350 config's `-rtos hwthread` makes the current thread id 1 while IDA sends thread id 0. Fix: `rp2350.dap.core0 configure -rtos none` (the launcher scripts already pass this). **GDB MI does not hit this.**
|
||||
|
||||
To tell them apart, turn on OpenOCD logging (`log_output /tmp/ocd.log`, then `debug_level 3` on the command port) and look for `fake step` versus `Duplicate Breakpoint`.
|
||||
|
||||
### `zsh: bad CPU type in executable: cmake`
|
||||
|
||||
An Intel `x86_64` tool is on your `PATH` on Apple Silicon. Run Step 2: `export PATH="/opt/homebrew/bin:$PATH"`, then `hash -r`. Add it to `~/.zshrc` to make it permanent.
|
||||
|
||||
### My addresses do not match this guide
|
||||
|
||||
You probably built `Debug`. This lesson is a `Release` build. Re-run Step 3 with `-DCMAKE_BUILD_TYPE=Release`. A `Debug` build moves the SDK functions and keeps `print_operator_results`, `compute_operators`, and the `dht11.c` helpers as separate calls, so `main` is not at `0x10000234` and the operator results are not bare immediates.
|
||||
|
||||
### A breakpoint never fires
|
||||
|
||||
First, confirm you actually set one, and that it is a **hardware** breakpoint. With the **GDB MI** adapter, `Debugger -> Add Hardware Breakpoint...` (hardware execute) should land in the **Breakpoints** widget. If nothing lands, or the core keeps running, you probably used `F2` (`Toggle Breakpoint`) — that is a software breakpoint and cannot be written to read-only flash, so it never installs. Also check you are on **GDB MI**, not **GDB RSP** (the GDB RSP adapter cannot set breakpoints on this target at all).
|
||||
|
||||
Then check the order and the state:
|
||||
|
||||
- **Arm it only after IDA is connected.** OpenOCD flushes every breakpoint when a client attaches, so anything armed earlier is gone. This also applies to `BP_ADDR` on the startup command line.
|
||||
- **Verify it is armed:** `mdw 0xE0002000 8`. You should see your address with the low bit set (`0x10000272` -> `0x10000273`). All zeros means nothing is armed — re-read this first, because it distinguishes "not armed" from "armed but never reached".
|
||||
- **Is the core running?** `poll` on the command port should not report a halt. If it is stopped, click **Resume**.
|
||||
- **Does the address get reached again?** `main` runs once per reset, so use `BP_ADDR` at startup (Step 9) rather than `reset run` while attached. Loop addresses such as `0x10000272` fire on the next pass with no reset — arm them and click **Resume** in IDA.
|
||||
- **With GDB MI the stop is reported as `Breakpoint`** and appears in the **Breakpoints** widget, because GDB really did set it.
|
||||
|
||||
### I edit `r1` (or another register) and it reverts
|
||||
|
||||
`main` reloads the value at the top of every loop iteration — `movs r1, #50` at `0x1000026e` runs right before the `printf` at `0x10000272`. So `r1` is only `0x63` for the instant between your edit and the next pass; then it is `0x32` again. The edit sticks only if the core is **genuinely stopped** at the breakpoint and stays stopped.
|
||||
|
||||
If it keeps reverting, the core is running, which almost always means the breakpoint is not installed — usually because it is a **software** breakpoint (`F2`) that cannot be written to read-only flash. Use `Debugger -> Add Hardware Breakpoint...` (hardware execute).
|
||||
|
||||
> **The Registers widget is a snapshot, not a live view.** IDA reads the registers at each stop and shows that snapshot; it does not poll the target, and there is no "refresh registers" command. So a value changed outside IDA will not appear until the next stop.
|
||||
|
||||
### The string hack does nothing
|
||||
|
||||
The live version needs to run **before** the `printf` call, which means breaking at `0x10000272` and redirecting `r0` while stopped. If you let it resume, the loop reloads `r0` from the literal pool on the next pass and the hack is gone.
|
||||
|
||||
Also confirm you wrote a NUL-terminated string. `printf` walks bytes until `*r0 == 0`; without the trailing `\x00`, it keeps reading RAM garbage.
|
||||
|
||||
### The patched instruction still shows the old value
|
||||
|
||||
Right-click the function and choose `Reanalyze`. IDA caches the disassembly text; a byte edit does not always trigger a re-lift by itself.
|
||||
|
||||
### The arith patch corrupts the instruction
|
||||
|
||||
You patched the wrong byte. `movs r1, #imm8` is 16-bit and its immediate is the **first** byte: the instruction starts at `0x1000026e`, so the byte to change is `0x1000026e` (`0x32 -> 0x63`). Patching `0x1000026f` (the `0x21` opcode) corrupts the instruction and the core will fault.
|
||||
|
||||
### The DHT11 reading did not change
|
||||
|
||||
Confirm you patched the right word. The scale is the literal loaded by `vldr s11, [pc, #44]` at `0x1000040a`; that literal lives at `0x10000438` and is `cd cc cc 3d` (`0.1f`). Change it to `00 00 a0 40` (`5.0f`). If your sensor is not connected or not answering, `dht11_read` returns `false` and the firmware prints `DHT11 read failed` instead — the scale patch only matters when a reading succeeds.
|
||||
|
||||
### The `arithmetic_operator` string hack overran
|
||||
|
||||
The replacement must be NUL-terminated. `"hacked_operator: %d\r\n"` is 21 characters; write it plus a `\0` (22 bytes) and pad the remaining four bytes with `00`. The next format string begins at `0x10003904`, 28 bytes past `0x100038e8`, so a 26-byte write stays clear of it. A longer write overwrites `"increment_operator: …"`.
|
||||
|
||||
### The serial capture is garbage on macOS
|
||||
|
||||
Reading `/dev/cu.usbmodem*` with a bare `read()` returns garbage. Set **raw termios at 115200** first: clear canonical/echo flags, set `CLOCAL|CREAD`, and `B115200` on input and output. `screen /dev/cu.usbmodem* 115200` does all of this for you; a script must call `tcsetattr` itself. Once set, the capture reads clean `arithmetic_operator: 50` lines.
|
||||
|
||||
### It worked for a second, then stopped (IDA's view desyncs)
|
||||
|
||||
This is the most common failure, and it has one main cause: **driving the core from the OpenOCD command port while IDA is connected.**
|
||||
|
||||
- If you send `reset run` from the port while attached, the core resets, runs, and halts at your breakpoint — but IDA never receives the stop event. Its sidebar keeps showing the *previous* location, so **Step** and **Resume** act on a stale PC and appear to do nothing.
|
||||
- If the OpenOCD process dies (or you restart it) while attached, IDA keeps believing it is connected: the sidebar stays, but the menu shows **Pause** enabled and **Resume**/**Step** disabled because IDA last saw the target *running*.
|
||||
|
||||
Recovery: **Detach, then reconnect.** If Detach does nothing (the connection is already dead), restart IDA — its menu still shows a session that no longer exists.
|
||||
|
||||
Prevention:
|
||||
|
||||
- Stop at `main` with `BP_ADDR` on a fresh server start, not with `reset run` while attached.
|
||||
- For loop addresses, set the breakpoint in the GUI and click **Resume**. Let IDA be the thing that starts the core.
|
||||
- If you must reset, **Detach first**, `reset run`, then reconnect.
|
||||
- Never leave a breakpoint on the PC you are about to step or resume from.
|
||||
|
||||
### The target "blows past" `main` and stops at `0x10003648` instead
|
||||
|
||||
`0x10003648` is `stdio_uart_out_flush`, the UART transmit-FIFO drain loop inside `printf`:
|
||||
|
||||
```asm
|
||||
10003648: 4b02 ldr r3, [pc, #8] @ (10003654 <stdio_uart_out_flush+0xc>)
|
||||
1000364a: 681a ldr r2, [r3]
|
||||
1000364c: 6993 ldr r3, [r2, #24]
|
||||
1000364e: 071b lsls r3, r3, #28
|
||||
10003650: d4fc bmi.n 1000364c <stdio_uart_out_flush+0x4>
|
||||
10003652: 4770 bx lr
|
||||
```
|
||||
|
||||
That is where the core sits while the UART drains, so the core is running `main`'s loop and simply spends nearly all its time there. The breakpoint at `main` did not fire because `main`'s entry runs exactly **once per reset**. If you arm the breakpoint after the reset, or set it while the target is already running and just resume, the core is already past `main` and will never re-execute it. Either arm the breakpoint **before** resetting, or break inside the loop at `0x10000272`, which fires every iteration.
|
||||
|
||||
### The console floods with `Failed to read memory at 0xf0000000`
|
||||
|
||||
Core1 is exposed. The scripts must run with `USE_CORE=0`. Stop the server, confirm only `core0` is reported, restart, then restart IDA.
|
||||
|
||||
### `Connect to Remote Process` is greyed out and Pause does nothing
|
||||
|
||||
IDA is in a stale session, usually because the debug server restarted while attached. Quit and reopen IDA (or the `.i64`) and connect again.
|
||||
|
||||
### The decompiler still shows the old value after patching
|
||||
|
||||
Right-click the function and choose `Reanalyze`.
|
||||
|
||||
---
|
||||
|
||||
## Fallback: do the dynamic steps with GDB (macOS 27)
|
||||
|
||||
If IDA's debugger crashes on attach on macOS 27 (see Troubleshooting), you can still do the live hack with the ARM GDB from the toolchain, against the same OpenOCD server. The addresses and register values are identical to the GUI steps.
|
||||
|
||||
Start the debug server (Step 9), then in a new terminal:
|
||||
|
||||
```
|
||||
arm-none-eabi-gdb
|
||||
```
|
||||
|
||||
At the `(gdb)` prompt:
|
||||
|
||||
```
|
||||
set architecture armv8-m.main
|
||||
target extended-remote :3333
|
||||
hbreak *0x10000272
|
||||
continue
|
||||
```
|
||||
|
||||
Do **not** run `monitor reset run` before `hbreak`. `0x10000272` is inside `main`'s loop, so the breakpoint fires on the next iteration with no reset. If you reset first, the core runs `main` and you will not catch it.
|
||||
|
||||
GDB stops at the `printf` call. Confirm the value, change it, and let it run:
|
||||
|
||||
```
|
||||
info registers pc r0 r1 # pc = 0x10000272, r0 = 0x100038e8, r1 = 0x32
|
||||
set $r1 = 0x63
|
||||
stepi
|
||||
continue
|
||||
```
|
||||
|
||||
The serial monitor prints `arithmetic_operator: 99` for the iteration you changed — the same temporary live hack as editing `r1` in the IDA Registers widget. When you are done, press `Ctrl-C`, then `detach` and `quit`.
|
||||
|
||||
**If you specifically want to stop at `main` (`0x10000234`),** remember its entry runs only once per reset, so the breakpoint must be armed *before* the reset:
|
||||
|
||||
```
|
||||
monitor reset halt
|
||||
hbreak *0x10000234
|
||||
continue
|
||||
```
|
||||
|
||||
If you instead set it while the target is running and just `continue`, you will "blow past" `main` and catch the core inside `printf` — in this build at `0x10003648`, the `stdio_uart_out_flush` UART-drain loop.
|
||||
|
||||
`hbreak` sets a hardware breakpoint, which is required for read-only flash. It works from plain GDB because GDB sends the 2-byte length the Cortex-M33 comparators need. IDA's **GDB MI** adapter goes through the same GDB, so its GUI breakpoints work too; the old **GDB RSP** adapter was the one that sent a 1-byte length and could not set breakpoints here.
|
||||
The patched image has been exported and converted to UF2. This IDA guide ends at static verification; it does not connect to, flash, or debug a live target.
|
||||
|
||||
## Glossary
|
||||
|
||||
@@ -1533,4 +778,4 @@ If you instead set it while the target is running and just `continue`, you will
|
||||
|
||||
---
|
||||
|
||||
**Remember:** the ELF tells you what every address is, and the `.bin` is what you actually patch. Prove the behavior dynamically, read `r1` at the arithmetic `printf` call, resolve the names from the ELF (including the `dht11.c` symbols), then patch the bytes — the `movs` immediate (`0x32 -> 0x63`), the DHT11 float word (`0.1f -> 5.0f`), and the format string (`arithmetic_operator:` -> `hacked_operator:`) — and flash.
|
||||
**Remember:** the ELF tells you what every address is, and the `.bin` is what you actually patch. Verify the static behavior from the recovered instructions.c` symbols), then patch the bytes — the `movs` immediate (`0x32 -> 0x63`), the DHT11 float word (`0.1f -> 5.0f`), and the format string (`arithmetic_operator:` -> `hacked_operator:`) — and export the patched artifact.
|
||||
+12
-937
File diff suppressed because it is too large.
Load diff
+12
-962
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user