From 23b9f628ef14aa789804d1c2ef9aa7c7c81344f7 Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Sun, 11 Oct 2026 17:43:45 -0400 Subject: [PATCH] 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 --- .opencode/skill/embedded-c-standard/SKILL.md | 54 + .../embedded-c-standard/audit_c_standard.py | 989 +++++++++ 0x0011a_cb/src/aes.c | 139 +- 0x0011a_cb/src/gps.c | 7 + 0x0011a_cb/src/lcd.c | 63 + 0x0011a_cb/src/lora.c | 18 + 0x0011a_cb/src/main.c | 70 +- 0x0014a_cb/include/crypto.h | 5 + 0x0014a_cb/include/gpio_ctrl.h | 29 + 0x0014a_cb/include/lora.h | 18 + 0x0014a_cb/include/safe_arm.h | 11 + 0x0014a_cb/src/gpio_ctrl.c | 10 + 0x0014a_cb/src/lora.c | 38 + 0x0014a_cb/src/main.c | 10 + 0x0014a_cb/src/safe_arm.c | 42 + 0x001aa_cb/0x001aa_cb.bin | Bin 23272 -> 35152 bytes 0x001aa_cb/0x001aa_cb.uf2 | Bin 47104 -> 71168 bytes 0x001aa_cb/0x001aa_cb_patched.bin | Bin 0 -> 35152 bytes 0x001aa_cb/0x001aa_cb_patched.uf2 | Bin 0 -> 70656 bytes 0x001aa_cb/CLASSIFIED-BRIEF-0x03.md | 144 +- 0x001aa_cb/CLASSIFIED-BRIEF-0x03.pdf | Bin 104573 -> 186754 bytes 0x001aa_cb/CMakeLists.txt | 4 +- 0x001aa_cb/include/crypto.h | 26 + 0x001aa_cb/include/dht11.h | 21 +- 0x001aa_cb/include/gpio_ctrl.h | 63 + 0x001aa_cb/include/logic.h | 23 +- 0x001aa_cb/include/lora.h | 64 + 0x001aa_cb/scripts/lora_console.py | 195 ++ 0x001aa_cb/scripts/patch_binary.py | 109 + 0x001aa_cb/scripts/uf2conv.py | 861 ++++++++ 0x001aa_cb/scripts/uf2families.json | 22 + 0x001aa_cb/src/crypto.c | 27 + 0x001aa_cb/src/dht11.c | 139 +- 0x001aa_cb/src/gpio_ctrl.c | 78 + 0x001aa_cb/src/logic.c | 192 +- 0x001aa_cb/src/lora.c | 151 ++ 0x001aa_cb/src/main.c | 27 +- 0x001da_cb/include/pwm_ctrl.h | 18 + 0x001da_cb/include/state_machine.h | 6 + 0x001da_cb/src/main.c | 4 +- 0x0020a_cb/include/dazzler.h | 12 + 0x0020a_cb/include/ir_rx.h | 6 + 0x0020a_cb/src/dazzler.c | 60 +- 0x0020a_cb/src/main.c | 4 +- WEEK04/WEEK04-IDA.md | 1762 +-------------- WEEK05/WEEK05-IDA.md | 1934 +---------------- WEEK06/WEEK06-IDA.md | 733 +------ WEEK07/WEEK07-IDA.md | 756 +------ WEEK09/WEEK09-IDA.md | 781 +------ WEEK10/WEEK10-IDA.md | 949 +------- WEEK11/WEEK11-IDA.md | 974 +-------- 51 files changed, 3744 insertions(+), 7904 deletions(-) create mode 100644 .opencode/skill/embedded-c-standard/SKILL.md create mode 100644 .opencode/skill/embedded-c-standard/audit_c_standard.py create mode 100644 0x001aa_cb/0x001aa_cb_patched.bin create mode 100644 0x001aa_cb/0x001aa_cb_patched.uf2 create mode 100644 0x001aa_cb/include/crypto.h create mode 100644 0x001aa_cb/include/gpio_ctrl.h create mode 100644 0x001aa_cb/include/lora.h create mode 100755 0x001aa_cb/scripts/lora_console.py create mode 100644 0x001aa_cb/scripts/patch_binary.py create mode 100644 0x001aa_cb/scripts/uf2conv.py create mode 100644 0x001aa_cb/scripts/uf2families.json create mode 100644 0x001aa_cb/src/crypto.c create mode 100644 0x001aa_cb/src/gpio_ctrl.c create mode 100644 0x001aa_cb/src/lora.c diff --git a/.opencode/skill/embedded-c-standard/SKILL.md b/.opencode/skill/embedded-c-standard/SKILL.md new file mode 100644 index 0000000..bdfaa96 --- /dev/null +++ b/.opencode/skill/embedded-c-standard/SKILL.md @@ -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 _H` / `#define + _H` / `#endif // _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/.c` with `include/.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/ .c + include/ .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 +``` diff --git a/.opencode/skill/embedded-c-standard/audit_c_standard.py b/.opencode/skill/embedded-c-standard/audit_c_standard.py new file mode 100644 index 0000000..c003b71 --- /dev/null +++ b/.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()) diff --git a/0x0011a_cb/src/aes.c b/0x0011a_cb/src/aes.c index b1c604e..570e408 100644 --- a/0x0011a_cb/src/aes.c +++ b/0x0011a_cb/src/aes.c @@ -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); diff --git a/0x0011a_cb/src/gps.c b/0x0011a_cb/src/gps.c index dbb14f4..60280b8 100644 --- a/0x0011a_cb/src/gps.c +++ b/0x0011a_cb/src/gps.c @@ -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); diff --git a/0x0011a_cb/src/lcd.c b/0x0011a_cb/src/lcd.c index b22c9ca..57883c6 100644 --- a/0x0011a_cb/src/lcd.c +++ b/0x0011a_cb/src/lcd.c @@ -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; diff --git a/0x0011a_cb/src/lora.c b/0x0011a_cb/src/lora.c index 6ef5aff..e58b95e 100644 --- a/0x0011a_cb/src/lora.c +++ b/0x0011a_cb/src/lora.c @@ -33,6 +33,12 @@ #include #include +/** + * @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); diff --git a/0x0011a_cb/src/main.c b/0x0011a_cb/src/main.c index f52bc90..c1aa89e 100644 --- a/0x0011a_cb/src/main.c +++ b/0x0011a_cb/src/main.c @@ -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; diff --git a/0x0014a_cb/include/crypto.h b/0x0014a_cb/include/crypto.h index 2960cb1..4d7c825 100644 --- a/0x0014a_cb/include/crypto.h +++ b/0x0014a_cb/include/crypto.h @@ -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 */ diff --git a/0x0014a_cb/include/gpio_ctrl.h b/0x0014a_cb/include/gpio_ctrl.h index 55ca92d..2589467 100644 --- a/0x0014a_cb/include/gpio_ctrl.h +++ b/0x0014a_cb/include/gpio_ctrl.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 */ diff --git a/0x0014a_cb/include/lora.h b/0x0014a_cb/include/lora.h index 379ca2d..a18737f 100644 --- a/0x0014a_cb/include/lora.h +++ b/0x0014a_cb/include/lora.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 diff --git a/0x0014a_cb/include/safe_arm.h b/0x0014a_cb/include/safe_arm.h index 975089d..f54d971 100644 --- a/0x0014a_cb/include/safe_arm.h +++ b/0x0014a_cb/include/safe_arm.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 */ diff --git a/0x0014a_cb/src/gpio_ctrl.c b/0x0014a_cb/src/gpio_ctrl.c index 537a8bc..d5a8661 100644 --- a/0x0014a_cb/src/gpio_ctrl.c +++ b/0x0014a_cb/src/gpio_ctrl.c @@ -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); diff --git a/0x0014a_cb/src/lora.c b/0x0014a_cb/src/lora.c index 2da0fdf..82928e9 100644 --- a/0x0014a_cb/src/lora.c +++ b/0x0014a_cb/src/lora.c @@ -33,18 +33,33 @@ #include #include +/** + * @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'; diff --git a/0x0014a_cb/src/main.c b/0x0014a_cb/src/main.c index f105963..bbe8449 100644 --- a/0x0014a_cb/src/main.c +++ b/0x0014a_cb/src/main.c @@ -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(); diff --git a/0x0014a_cb/src/safe_arm.c b/0x0014a_cb/src/safe_arm.c index 5c54f11..eba79ac 100644 --- a/0x0014a_cb/src/safe_arm.c +++ b/0x0014a_cb/src/safe_arm.c @@ -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(); diff --git a/0x001aa_cb/0x001aa_cb.bin b/0x001aa_cb/0x001aa_cb.bin index c1a571527e0a2a64868fd217a38bcba8cc59b426..88cef35930f4d54e78e439585086ab3d780807c6 100755 GIT binary patch delta 16576 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 diff --git a/0x001aa_cb/0x001aa_cb.uf2 b/0x001aa_cb/0x001aa_cb.uf2 index 99be41594e19f9a396361bd205bde8c8240215d2..aefe135a2b7f14f8804a9026ddbe6d272735d7bc 100644 GIT binary patch delta 20627 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 delta 7594 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 diff --git a/0x001aa_cb/0x001aa_cb_patched.bin b/0x001aa_cb/0x001aa_cb_patched.bin new file mode 100644 index 0000000000000000000000000000000000000000..810f82efa492e2120afeb40758c05f24a694cdcb GIT binary patch literal 35152 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 YTrNtJdXLA`EPCr9SPMzn~p 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 literal 0 HcmV?d00001 diff --git a/0x001aa_cb/CLASSIFIED-BRIEF-0x03.md b/0x001aa_cb/CLASSIFIED-BRIEF-0x03.md index 4296156..1711fc7 100644 --- a/0x001aa_cb/CLASSIFIED-BRIEF-0x03.md +++ b/0x001aa_cb/CLASSIFIED-BRIEF-0x03.md @@ -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. diff --git a/0x001aa_cb/CLASSIFIED-BRIEF-0x03.pdf b/0x001aa_cb/CLASSIFIED-BRIEF-0x03.pdf index f9224daf97646e453a98666f89bddde3fe87780b..ac927e4fd312601f7738e9a12a471f02c736f7cd 100644 GIT binary patch delta 131744 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 ~jEV%BA12QG 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 diff --git a/0x001aa_cb/CMakeLists.txt b/0x001aa_cb/CMakeLists.txt index 25fa88b..9d82362 100644 --- a/0x001aa_cb/CMakeLists.txt +++ b/0x001aa_cb/CMakeLists.txt @@ -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 diff --git a/0x001aa_cb/include/crypto.h b/0x001aa_cb/include/crypto.h new file mode 100644 index 0000000..eb85cae --- /dev/null +++ b/0x001aa_cb/include/crypto.h @@ -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 +#include + +#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 */ diff --git a/0x001aa_cb/include/dht11.h b/0x001aa_cb/include/dht11.h index c950a74..fabefbc 100644 --- a/0x001aa_cb/include/dht11.h +++ b/0x001aa_cb/include/dht11.h @@ -10,14 +10,33 @@ #ifndef DHT11_H #define DHT11_H + #include +#include + +#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 diff --git a/0x001aa_cb/include/gpio_ctrl.h b/0x001aa_cb/include/gpio_ctrl.h new file mode 100644 index 0000000..3c44b23 --- /dev/null +++ b/0x001aa_cb/include/gpio_ctrl.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 + +#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 diff --git a/0x001aa_cb/include/logic.h b/0x001aa_cb/include/logic.h index 374e329..5857263 100644 --- a/0x001aa_cb/include/logic.h +++ b/0x001aa_cb/include/logic.h @@ -10,5 +10,26 @@ #ifndef LOGIC_H #define LOGIC_H + +#include +#include + +#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 diff --git a/0x001aa_cb/include/lora.h b/0x001aa_cb/include/lora.h new file mode 100644 index 0000000..a18737f --- /dev/null +++ b/0x001aa_cb/include/lora.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 +#include +#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 diff --git a/0x001aa_cb/scripts/lora_console.py b/0x001aa_cb/scripts/lora_console.py new file mode 100755 index 0000000..24310f9 --- /dev/null +++ b/0x001aa_cb/scripts/lora_console.py @@ -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() diff --git a/0x001aa_cb/scripts/patch_binary.py b/0x001aa_cb/scripts/patch_binary.py new file mode 100644 index 0000000..ca22844 --- /dev/null +++ b/0x001aa_cb/scripts/patch_binary.py @@ -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() diff --git a/0x001aa_cb/scripts/uf2conv.py b/0x001aa_cb/scripts/uf2conv.py new file mode 100644 index 0000000..c5a832f --- /dev/null +++ b/0x001aa_cb/scripts/uf2conv.py @@ -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("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(" 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(" 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} ") + + +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()) diff --git a/0x001aa_cb/scripts/uf2families.json b/0x001aa_cb/scripts/uf2families.json new file mode 100644 index 0000000..39373c4 --- /dev/null +++ b/0x001aa_cb/scripts/uf2families.json @@ -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" + } +] \ No newline at end of file diff --git a/0x001aa_cb/src/crypto.c b/0x001aa_cb/src/crypto.c new file mode 100644 index 0000000..fe73ef9 --- /dev/null +++ b/0x001aa_cb/src/crypto.c @@ -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); + } +} diff --git a/0x001aa_cb/src/dht11.c b/0x001aa_cb/src/dht11.c index a6cd1d1..4189076 100644 --- a/0x001aa_cb/src/dht11.c +++ b/0x001aa_cb/src/dht11.c @@ -9,19 +9,140 @@ */ #include "dht11.h" -#include +#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; } diff --git a/0x001aa_cb/src/gpio_ctrl.c b/0x001aa_cb/src/gpio_ctrl.c new file mode 100644 index 0000000..4f7fb49 --- /dev/null +++ b/0x001aa_cb/src/gpio_ctrl.c @@ -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); +} diff --git a/0x001aa_cb/src/logic.c b/0x001aa_cb/src/logic.c index e46150b..b86d685 100644 --- a/0x001aa_cb/src/logic.c +++ b/0x001aa_cb/src/logic.c @@ -10,27 +10,185 @@ #include "logic.h" #include "dht11.h" +#include "crypto.h" +#include "gpio_ctrl.h" +#include "lora.h" #include -#include "pico/stdlib.h" -#include "hardware/gpio.h" +#include -#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(); +} diff --git a/0x001aa_cb/src/lora.c b/0x001aa_cb/src/lora.c new file mode 100644 index 0000000..2d8af37 --- /dev/null +++ b/0x001aa_cb/src/lora.c @@ -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 +#include + +/** + * @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; +} diff --git a/0x001aa_cb/src/main.c b/0x001aa_cb/src/main.c index 5e66b3b..e89f983 100644 --- a/0x001aa_cb/src/main.c +++ b/0x001aa_cb/src/main.c @@ -10,19 +10,36 @@ #include #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; } diff --git a/0x001da_cb/include/pwm_ctrl.h b/0x001da_cb/include/pwm_ctrl.h index b035a89..8aac3ca 100644 --- a/0x001da_cb/include/pwm_ctrl.h +++ b/0x001da_cb/include/pwm_ctrl.h @@ -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 diff --git a/0x001da_cb/include/state_machine.h b/0x001da_cb/include/state_machine.h index a819862..67be7af 100644 --- a/0x001da_cb/include/state_machine.h +++ b/0x001da_cb/include/state_machine.h @@ -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 diff --git a/0x001da_cb/src/main.c b/0x001da_cb/src/main.c index 4181d0c..94660a4 100644 --- a/0x001da_cb/src/main.c +++ b/0x001da_cb/src/main.c @@ -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); diff --git a/0x0020a_cb/include/dazzler.h b/0x0020a_cb/include/dazzler.h index b8e61e0..5cd2fae 100644 --- a/0x0020a_cb/include/dazzler.h +++ b/0x0020a_cb/include/dazzler.h @@ -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 diff --git a/0x0020a_cb/include/ir_rx.h b/0x0020a_cb/include/ir_rx.h index ac2d2b2..018435a 100644 --- a/0x0020a_cb/include/ir_rx.h +++ b/0x0020a_cb/include/ir_rx.h @@ -11,5 +11,11 @@ #ifndef IR_RX_H #define IR_RX_H #include + +/** + * @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 diff --git a/0x0020a_cb/src/dazzler.c b/0x0020a_cb/src/dazzler.c index 8671540..f358546 100644 --- a/0x0020a_cb/src/dazzler.c +++ b/0x0020a_cb/src/dazzler.c @@ -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()); +} diff --git a/0x0020a_cb/src/main.c b/0x0020a_cb/src/main.c index 37133bf..30f8788 100644 --- a/0x0020a_cb/src/main.c +++ b/0x0020a_cb/src/main.c @@ -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); diff --git a/WEEK04/WEEK04-IDA.md b/WEEK04/WEEK04-IDA.md index eb67652..8b3f2eb 100644 --- a/WEEK04/WEEK04-IDA.md +++ b/WEEK04/WEEK04-IDA.md @@ -22,10 +22,8 @@ By using this repository and course, you acknowledge and agree that: - Build the two lesson projects with `Release` and get both an `.elf` and a raw `.bin` - Dump the **ELF symbol map** with `arm-none-eabi-nm` and use it as ground truth - Load the raw `.bin` into IDA at `0x10000000` -- **Break at `main`** on live silicon, even though `main` can move between programs -- **Hack a running target live** by editing a register in IDA's Registers widget - **Resolve the functions in the IDA GUI** using the ELF symbol map -- **Patch** the bytes that control the behavior, export the image, and flash it +- **Patch** the bytes that control the behavior, export the image, and export it --- @@ -42,12 +40,12 @@ The `.bin` is built **from** the `.elf`, so the ELF tells you exactly what is at > **Build `Release`, not `Debug`.** Every address in this guide matches the Week 4 lesson, and the Week 4 lesson is a `Release` build. `Release` optimizes the code the same way the original lesson was built: it folds `age = 42` away in Project 1 and inlines `blink_and_print` into `main` in Project 2. If you build `Debug`, the SDK function addresses move and Project 2 keeps a separate `blink_and_print`, so nothing lines up. Always build `Release` for this lesson. -The order is **dynamic first, static second**, twice — once per project: +The workflow is static-only: import the raw image, recover the code, decompile it, and patch the artifact: -1. Break on the live target and prove what the code does. -2. Hack it live in the debugger and watch the behavior 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 hack | | ------- | ------ | --------- | -------- | @@ -58,7 +56,7 @@ The order is **dynamic first, static second**, twice — once per project: --- -## Part 1: Build, Flash, and Get the Symbol Map +## Part 1: Build and Get the Symbol Map ### Step 1: Install the toolchain @@ -70,1753 +68,9 @@ The order is **dynamic first, static second**, twice — once per project: **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 -``` - -**Windows x64** and **Linux x64** do not have this problem. Skip to Step 3. - -### Step 3: Build the two projects with `Release` - -Run this once inside `0x0005_intro-to-variables/` and once inside `0x0008_uninitialized-variables/`: - -```bash -cmake -B build -G Ninja -DPICO_BOARD=pico2 -DPICO_PLATFORM=rp2350 -DCMAKE_BUILD_TYPE=Release -cmake --build build -``` - -**Point IDA at this repository (once).** Every console snippet below reads the repo root from `~/.embedded-hacking-repo`, so IDA never needs a database open and nothing is hardcoded. From the repo root, run once: - -**macOS / Linux:** - -```bash -pwd > ~/.embedded-hacking-repo -``` - -**Windows (PowerShell):** - -```powershell -(Get-Location).Path | Set-Content "$env:USERPROFILE\.embedded-hacking-repo" -``` - -**Then build from the IDA console**, so the whole build -> patch -> flash loop stays inside IDA. The console inherits a minimal `PATH` — on macOS just `/usr/bin:/bin:/usr/sbin:/sbin` — so it does not see Homebrew; add your package manager's `bin` first, then run plain `cmake`. - -**macOS Apple Silicon:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() -os.environ["PATH"] = "/opt/homebrew/bin:" + os.environ["PATH"] # the console's PATH omits Homebrew -for name in ("0x0005_intro-to-variables", "0x0008_uninitialized-variables"): - proj = os.path.join(root, name) - subprocess.run(["cmake", "-B", "build", "-G", "Ninja", "-DPICO_BOARD=pico2", - "-DPICO_PLATFORM=rp2350", "-DCMAKE_BUILD_TYPE=Release"], cwd=proj) - subprocess.run(["cmake", "--build", "build"], cwd=proj) -``` - -**Linux x64:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() -for name in ("0x0005_intro-to-variables", "0x0008_uninitialized-variables"): - proj = os.path.join(root, name) - subprocess.run(["cmake", "-B", "build", "-G", "Ninja", "-DPICO_BOARD=pico2", - "-DPICO_PLATFORM=rp2350", "-DCMAKE_BUILD_TYPE=Release"], cwd=proj) - subprocess.run(["cmake", "--build", "build"], cwd=proj) -``` - -**Windows x64:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() -for name in ("0x0005_intro-to-variables", "0x0008_uninitialized-variables"): - proj = os.path.join(root, name) - subprocess.run(["cmake", "-B", "build", "-G", "Ninja", "-DPICO_BOARD=pico2", - "-DPICO_PLATFORM=rp2350", "-DCMAKE_BUILD_TYPE=Release"], cwd=proj) - subprocess.run(["cmake", "--build", "build"], cwd=proj) -``` - -Each build directory now contains the pair we need: - -- `0x0005_intro-to-variables/build/0x0005_intro-to-variables.elf` and `.bin` — `.bin` is **15292** bytes -- `0x0008_uninitialized-variables/build/0x0008_uninitialized-variables.elf` and `.bin` — `.bin` is **15668** bytes - -If the ARM toolchain is not on your `PATH`, add `-DPICO_TOOLCHAIN_PATH=...`: - -| OS | Typical toolchain path | -| -- | ---------------------- | -| Windows x64 | `C:/Program Files/Arm GNU Toolchain arm-none-eabi/14.2 rel1/bin` | -| macOS Apple Silicon | `/Applications/ArmGNUToolchain/14.2.rel1/arm-none-eabi/bin` | -| Linux x64 | `/usr` | - -### Step 4: Dump the ELF symbol map - -This is the ground truth for the whole lesson. Run `arm-none-eabi-nm` on each ELF and keep the output in a terminal or a text file: - -**macOS Apple Silicon / Linux x64:** - -```bash -arm-none-eabi-nm -n --defined-only build/0x0005_intro-to-variables.elf | grep -E ' [Tt] ' -arm-none-eabi-nm -n --defined-only build/0x0008_uninitialized-variables.elf | grep -E ' [Tt] ' -``` - -**Windows x64:** - -```powershell -arm-none-eabi-nm -n --defined-only build\0x0005_intro-to-variables.elf | Select-String ' [Tt] ' -``` - -Each line is `address type name`. The `T`/`t` type is a function. Here are the functions this lesson uses. - -**Project 1 — `0x0005_intro-to-variables`:** - -| Address | ELF symbol | Signature | Role | -| ------- | ---------- | --------- | ---- | -| `0x1000015c` | `_reset_handler` | `void _reset_handler(void)` | reset entry | -| `0x10000186` | `platform_entry` | `void platform_entry(void)` | calls `runtime_init`, `main`, `exit` | -| `0x1000019a` | `data_cpy` | `void data_cpy(void*, void*, void*)` | copies `.data` from flash to SRAM | -| `0x100001e4` | `_init` | `void _init(void)` | runs `.init_array` | -| `0x10000210` | `frame_dummy` | `void frame_dummy(void)` | C runtime boilerplate | -| `0x10000234` | `main` | `int main(void)` | the lesson function | -| `0x10000248` | `gpio_set_function` | `void gpio_set_function(uint, gpio_function_t)` | SDK GPIO helper | -| `0x10002cfc` | `exit` | `void exit(int)` | C runtime exit | -| `0x10002d04` | `runtime_init` | `void runtime_init(void)` | SDK runtime init | -| `0x10002f54` | `stdio_init_all` | `bool stdio_init_all(void)` | SDK serial init | -| `0x100030e4` | `__wrap_printf` | `int __wrap_printf(const char*, ...)` | the `printf` wrapper | - -These signatures come from the ELF's DWARF debug info, so they are exact. You apply them in IDA in Step 16 (`G` -> address, then `Y` -> Change Type). - -**Project 2 — `0x0008_uninitialized-variables`:** - -| Address | ELF symbol | Signature | Role | -| ------- | ---------- | --------- | ---- | -| `0x1000015c` | `_reset_handler` | `void _reset_handler(void)` | reset entry | -| `0x10000186` | `platform_entry` | `void platform_entry(void)` | calls `runtime_init`, `main`, `exit` | -| `0x1000019a` | `data_cpy` | `void data_cpy(void*, void*, void*)` | copies `.data` from flash to SRAM | -| `0x100001e4` | `_init` | `void _init(void)` | runs `.init_array` | -| `0x10000210` | `frame_dummy` | `void frame_dummy(void)` | C runtime boilerplate | -| `0x10000234` | `main` | `int main(void)` | the lesson function (`blink_and_print` inlined) | -| `0x10000278` | `gpio_set_function` | `void gpio_set_function(uint, gpio_function_t)` | SDK GPIO helper | -| `0x100002b4` | `gpio_init` | `void gpio_init(uint)` | SDK GPIO init | -| `0x10000d10` | `sleep_ms` | `void sleep_ms(uint32_t)` | SDK delay | -| `0x10002e74` | `exit` | `void exit(int)` | C runtime exit | -| `0x10002e7c` | `runtime_init` | `void runtime_init(void)` | SDK runtime init | -| `0x100030cc` | `stdio_init_all` | `bool stdio_init_all(void)` | SDK serial init | -| `0x1000325c` | `__wrap_printf` | `int __wrap_printf(const char*, ...)` | the `printf` wrapper | - -> **`main` is `0x10000234` in both projects.** In Project 2 the `static void blink_and_print` helper is inlined into `main` by the `Release` optimizer, so it does not appear as a separate symbol. That is why both projects put `main` at the same address. In a `Debug` build it stays separate and `main` moves — another reason to build `Release`. - -### Step 5: Flash Project 1 and confirm `age: 43` - -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 0x0005_intro-to-variables/build/0x0005_intro-to-variables.bin -``` - -**Windows x64 (PowerShell):** - -```powershell -.\flash.ps1 -Bin 0x0005_intro-to-variables\build\0x0005_intro-to-variables.bin -``` - -**Or flash from the IDA console** (with a database open, so the repo root is taken from it — otherwise use the terminal form above): - -**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, "0x0005_intro-to-variables", "build", "0x0005_intro-to-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, "0x0005_intro-to-variables", "build", "0x0005_intro-to-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 15292 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`. - -``` -age: 43 -age: 43 -age: 43 -... -``` - -### Step 6: Flash Project 2 and confirm `age: 0` + red LED - -```bash -# macOS / Linux -./flash.sh 0x0008_uninitialized-variables/build/0x0008_uninitialized-variables.bin -``` -```powershell -# Windows -.\flash.ps1 -Bin 0x0008_uninitialized-variables\build\0x0008_uninitialized-variables.bin -``` - -**Or flash from the IDA console** (same form as Step 5, pointing at the Project 2 `.bin`): - -**macOS / Linux:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) -bin_path = os.path.join(root, "0x0008_uninitialized-variables", "build", "0x0008_uninitialized-variables.bin") -log = os.path.join(os.path.dirname(bin_path), "flash.log") -subprocess.run(["pkill", "-TERM", "-f", "openocd"]) -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:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) -bin_path = os.path.join(root, "0x0008_uninitialized-variables", "build", "0x0008_uninitialized-variables.bin") -log = os.path.join(os.path.dirname(bin_path), "flash.log") -subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) -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 15668 bytes ...`. The serial monitor shows: - -``` -age: 0 -age: 0 -age: 0 -... -``` - -and the **red LED on GPIO 16** blinks once per second. - ---- - -## 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. - -### Step 7: Bring the raw `.bin` into IDA - -A raw `.bin` has no headers, so IDA cannot know where it belongs or what architecture it is. You must supply both. If you just double-click the `.bin`, IDA may load it with a guessed architecture, and every address in this lesson will be wrong. - -#### 1.1 Open the file -1. **File ▸ Open…** (Ctrl/Cmd+O). -2. Select the `.bin` file and click **Open**. - -IDA opens the **"Load a new file"** dialog because a raw `.bin` has no header. - -#### 1.2 Set the format and processor -In the *Load a new file* dialog: -1. **Format**: leave it as **Binary file** (IDA auto-detects a headerless image). -2. **Processor type**: click the **…** button next to the processor field. In the *Processor type* dialog: - - Family: **ARM** - - Type: **ARM Little-endian** (the 32-bit ARM module). - - Click **OK**. - -> If IDA asks **"Do you want to disassemble it as 64-bit code?"** — click **No**. -> The RP2350 is 32-bit; answering *Yes* loads AArch64 and you get `X` registers / data. (This is the single most common cause of the `DCB` wall.) - -#### 1.3 Configure the ARM architecture and Thumb mode -Still in the *Load a new file* dialog, open the processor options (the **Processor options** / **Set…** button, which opens the dialog titled **ARM architecture options**): -- **ARM architecture**: choose **ARMv8-M** (this is the Cortex-M33 / RP2350 core). -- **Thumb instructions**: select **Thumb-2** (ARMv8-M is Thumb-only, so this is the correct mode). -- Leave **automatic ARM-THUMB switch** off; we want Thumb throughout. - -#### 1.4 Set the loading address -- **Loading address**: enter **`10000000`** (hex; the RP2350 XIP flash base). - IDA rejects addresses that don't look like ROM/RAM with *"The loading address should belong to RAM or ROM"* — `0x10000000` is the correct flash window. - -Leave **Manual load** and **Create segments** at their defaults. Click **OK**. - -#### 1.5 Force Thumb, then seed the code from the vector table -A raw `.bin` has no header, so IDA's loader has **no entry point** and defaults the segment to **ARM mode** (which cannot decode Thumb). Two required steps — in this order: - -**A. Set the segment to Thumb (do this first).** -- **Edit ▸ Segments ▸ Set default segment register value…** → register **`T`** = **`1`** (or press **`Alt+G`**). IDA reanalyzes the segment as Thumb automatically. - -**B. Seed the code from the vector table.** -1. **G** → `10000000`. The first two words are the initial stack pointer and the **reset vector**. -2. **Read the 4-byte word at `0x10000004`** (do **not** assume its value — it differs per build). -3. **Clear bit 0** (the ARM **Thumb** flag) → the reset handler address. -4. **G** to that address → press **C** (**MakeCode**). IDA disassembles the reset handler and follows its `bl`s into `main`. - -Once `main` shows code, the import is correct. - -**Why IDA needs this and others don't:** IDA's generic raw-binary ARM loader treats the image as flat data and waits for you to set Thumb and mark the entry point. It's an IDA quirk, not a mistake on your part. - -### Step 8: Save it as a IDA database (`.i64`) - -IDA never writes back into the `.bin`. Your names, comments, types, and patches live in a separate **`.i64`** database. Save one now, before you make any changes: - -1. Choose `File -> Save As...`. -2. Save it next to the image as `0x0005_intro-to-variables.i64`. -3. From now on, save with `File -> Save` (`Cmd+S` on macOS, `Ctrl+S` on Windows/Linux) whenever you rename or patch. - -The two files have different roles: - -| File | Role | -| ---- | ---- | -| `0x0005_intro-to-variables.bin` | the raw firmware image; IDA never modifies it | -| `0x0005_intro-to-variables.i64` | your analysis database: names, types, comments, and patches | - -When you come back later, **open the `.i64`**, not the `.bin`; that restores all your work. If a database gets messy, delete the `.i64` and re-import the `.bin` from Step 7 — the firmware is never at risk. You export the patched image out of this view later, in Step 19. - -### Step 9: The views you will use - -- **Linear view:** the disassembly listing. You navigate, read, and patch here. -- **Graph view:** the control-flow graph of the current function. -- **Decompiler (HLIL):** the pseudo-C decompilation. -- **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 OpenOCD command port, not from the GUI — see Step 13. - -> **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 (Project 1) - -### Step 10: 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 11. - -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 | Project 1 `0x0005` | Project 2 `0x0008` | Command | -> | --- | --- | --- | --- | -> | `main` (once per reset) | `0x10000234` | `0x10000234` | `BP_ADDR=0x10000234 ./debug-server.sh` | -> | The **loop** — the `printf` call, hit every iteration | `0x1000023e` | `0x1000024e` | `BP_ADDR=0x1000023e ./debug-server.sh` | -> -> ```bash -> BP_ADDR=0x10000234 ./debug-server.sh # park at main -> BP_ADDR=0x1000023e ./debug-server.sh # park in the loop instead -> ``` -> -> ```powershell -> $env:BP_ADDR="0x10000234"; .\debug-server.ps1 # park at main -> $env:BP_ADDR="0x1000023e"; .\debug-server.ps1 # park in the loop -> ``` -> -> **Note the loop address is not the same in both projects.** Project 2 does more setup before the loop, so its `bl __wrap_printf` sits at `0x1000024e`, not `0x1000023e`. Both were verified against the Release `.elf` with `arm-none-eabi-objdump` and confirmed live on hardware. -> -> **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 13) 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 11: Connect IDA to the GDB server - -1. Make sure the image is open and analyzed (Part 2) and the server from Step 10 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 10), 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 — which is what `/opt/homebrew/bin/arm-none-eabi-gdb` symlinks to — 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 (seen at `2430,331` with the main window at `2560,30`), 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 12: 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 : -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 -``` - -**The middle `blx` at `0x1000018c` is the call to `main`.** `platform_entry` is byte-identical in both projects, so `0x1000018c` catches `main` no matter where the linker placed it. The literal pool at `0x100001dc` holds `main | 1`, and clearing bit 0 gives `0x10000234`. - -### Step 13: 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,,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 10), so IDA is already stopped at `main` when it connects. | No — `main` runs once per reset. | -| **The loop** (`printf` call) | Project 1 `0x1000023e`, Project 2 `0x1000024e` | 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 (`0x1000023e` for Project 1, `0x1000024e` for Project 2), 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 0x1000023e`. - -> **No breakpoints before you connect.** With GDB MI, a breakpoint set before the connection hangs the session (Step 11). 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: `0x1000023e -> 0x100030e4 -> 0x100030e6 -> ...`. - -> **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 14 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 2 hw` still arms a breakpoint, `rbp ` / `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 14: HACK IT LIVE — change the printed value - -`main` loads the constant `0x2b` (43) into `r1` and calls `printf` on every iteration. We break on that call in the GUI and change it live. - -1. Press `G`, go to `0x1000023e` (the `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 `0x1000023e` and `r1 = 0x2b`. -4. Open the **Registers** widget (bug icon -> **Registers**). -5. Find `r1`. Its value is `0x2b`. -6. **Set `r1` to `0x46`.** From IDA's Python console (`Plugins -> Python Console`): - ```python - dbg.set_reg_value("r1", 0x46) - ``` - `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 `46`, 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 `0x1000023e` to the instruction *after* the call, `0x10000242` (the `b.n` that closes the loop): remove the breakpoint at `0x1000023e` and set a hardware breakpoint at `0x10000242`. 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 = 0x46`, so this iteration prints `age: 70`, then stops at `0x10000242`. -9. Look at your serial monitor — the `screen` session on the Pico's USB serial port — and at the **Target** tab in IDA: - - ``` - age: 70 - ``` - -You changed a running program's output without touching the binary. - -### Step 14b: HACK THE STRING LIVE — change `age:` to `foo:` - -The text `"age: %d\r\n"` lives in flash (`.rodata`) at `0x100034a0`, and flash is **read-only at runtime** — a debugger write there does not stick (verified: writing `0x66` to `0x100034a0` read back unchanged). So you cannot overwrite the text in place. Instead you redirect the pointer: at the `printf` call, `r0` holds the string address, so you 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 14 steps 1-3. At the stop, `r0 = 0x100034a0` and `r1 = 0x2b`. -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"foo: %d\r\n\x00") - ``` - `dbg.write_memory(address, bytes)` is IDA's debugger memory-write API; it returns `True` on success. That writes the bytes `66 6f 6f 3a 20 25 64 0d 0a 00` = `"foo: %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. Move the breakpoint past the call in the GUI (remove it at `0x1000023e`, set one at `0x10000242`) and click **Resume**. The core runs `printf` with `r0` pointing at your RAM string and `r1 = 0x2b`, so this iteration prints: - ``` - foo: 43 - ``` - then stops at `0x10000242`. - -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 `age: 43` again. The permanent version is the static patch in Step 19b. - -### Step 15: Why the hack reverts (and why we patch next) - -Press **Resume**. The loop branches back to `0x1000023a`, which reloads `movs r1, #43`, so the next line is `age: 43`. The live edit changed one iteration only. There is no variable in memory to change; the value is baked into the instruction. To make `age: 70` permanent we must patch the instruction. That is the static pass. - -Press **Pause** to stop the output flood. - -### Step 15b: 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 (Project 1) - -### Step 16: Resolve the functions in the IDA GUI - -We now name the functions in IDA using the ELF symbol map from Step 4. IDA loaded the raw `.bin` with **no symbols**, so every function shows as `sub_` — resolving means giving each one its real name and signature. - -Three keys do all the work: - -| Key | IDA action | Use it for | -| --- | --- | --- | -| `G` | Go to address | Jump to a function's address | -| `Y` | **Change Type** | Set the function's signature. The dialog shows the full prototype, so this sets the name *and* the type in one step. | -| `N` | Rename | Rename only, when you just want the name and not the type | - -For each function below: `G` to its address, then **`Y` (Change Type)** and type the prototype from the table. - -#### How to resolve a function in IDA (`Y`) - -`Y` is the **Change Type** key, and it is what actually resolves the function — it turns `void sub_10002f54()` into `bool stdio_init_all(void)`. The Change Type dialog shows the full declaration (name and type), so typing the prototype sets both: - -1. `G` to the function's address. The cursor lands on the function. -2. Press **`Y`**. In the Change Type dialog, type the prototype from the table exactly — for example `bool stdio_init_all(void)` — and press Enter. - -The decompiler header then shows the real prototype, and calls to the function read cleanly instead of `sub_()`. `N` is only for renaming without touching the type; `Y` alone sets both the name and the type. - -If `Y` seems to do nothing, confirm the cursor is on the function, or right-click it and pick **Change Type...**. IDA parses what you type and silently keeps the old type if it does not parse, so glance at the header after each `Y`. - -#### Worked example: `main` - -1. Press `G`, type `0x10000234`, press Enter. The view jumps there; the cursor lands on `sub_10000234`. -2. Press **`Y`** (Change Type), type `int main(void)`, press Enter. That sets the name to `main` and the type to `int(void)`. - -> **IDA shows `int32_t` where Ghidra shows `int`.** After you set `int main(void)`, the decompiler header may read `int32_t main(void)`. That is the same type — on this platform `int` is 32 bits and IDA's parser normalises it to `int32_t`. Do not fight it; it is not an error. - -#### Worked example: `stdio_init_all` - -1. `G` -> `0x10002f54`. -2. `Y` -> `bool stdio_init_all(void)`. - -It returns **`bool`**, not `void` — the ELF says `_Bool stdio_init_all(void)`. Our `main` ignores the return value, so the decompiler still reads cleanly. - -#### Worked example: `uart_init` - -1. `G` -> `0x10000e10`. -2. `Y` -> `uint uart_init(uart_inst_t *uart, uint baudrate)`. - -#### Worked example: `__wrap_printf` - -1. `G` -> `0x100030e4`. -2. `Y` -> `int __wrap_printf(const char *fmt, ...)`. Keep the `...` — `printf` is variadic. - -> **`printf` in our source is `__wrap_printf` in the binary.** The SDK links our `printf` calls to its `__wrap_printf` wrapper. - -The rest of the chain is the same two keystrokes per function (`G`, then `Y`). This is **our code plus the library functions it actually calls** — not the whole SDK. `main` only calls `stdio_init_all` and `printf`, so we follow that chain down: `stdio_init_all` pulls in the stdio/UART setup, and `printf` lands in the SDK's `__wrap_printf`. - -The call chain for this project: - -``` -main -├── stdio_init_all -│ ├── stdio_uart_init ── gpio_set_function, uart_init -│ ├── stdio_set_driver_enabled -│ ├── stdio_out_chars_crlf -│ ├── stdio_put_string ── strlen -│ └── time_us_64 -└── __wrap_printf ── __wrap_vprintf -``` - -**Project 1 — resolve every function in that chain:** - -| Address | Rename to (`N`) | Signature (`Y`) | -| ------- | --------------- | --------------- | -| `0x1000015c` | `_reset_handler` | `void _reset_handler(void)` | -| `0x10000186` | `platform_entry` | `void platform_entry(void)` | -| `0x1000019a` | `data_cpy` | `void data_cpy(void*, void*, void*)` | -| `0x100001e4` | `_init` | `void _init(void)` | -| `0x10000210` | `frame_dummy` | `void frame_dummy(void)` | -| **`0x10000234`** | **`main`** | **`int main(void)`** | -| `0x10002cfc` | `exit` | `void exit(int)` | -| `0x10002d04` | `runtime_init` | `void runtime_init(void)` | -| `0x10002f54` | `stdio_init_all` | `bool stdio_init_all(void)` | -| `0x100032a0` | `stdio_uart_init` | `void stdio_uart_init(void)` | -| `0x10002f2c` | `stdio_set_driver_enabled` | `void stdio_set_driver_enabled(stdio_driver_t*, bool)` | -| `0x10002d30` | `stdio_out_chars_crlf` | `void stdio_out_chars_crlf(stdio_driver_t*, const char*, int)` | -| `0x10002e40` | `stdio_put_string` | `int stdio_put_string(const char*, int, bool, bool)` | -| `0x100030e4` | `__wrap_printf` | `int __wrap_printf(const char*, ...)` | -| `0x10003020` | `__wrap_vprintf` | `int __wrap_vprintf(const char*, va_list)` | -| `0x10000248` | `gpio_set_function` | `void gpio_set_function(uint, gpio_function_t)` | -| `0x10000e10` | `uart_init` | `uint uart_init(uart_inst_t*, uint)` | -| `0x10000da0` | `time_us_64` | `uint64_t time_us_64(void)` | -| `0x100033e0` | `strlen` | `size_t strlen(const char*)` | - -> **A `void` return type may not stick — here is the fix.** IDA treats `void` as low-confidence, and its analysis can override it with an inferred type — most often `int32_t` on this 32-bit target. It is most visible on `_reset_handler` (a hand-written assembly entry that never returns normally), but it can happen to **any** function whose return type IDA thinks it can infer. -> -> Setting the full signature with `Y` reproduces the unwanted `int32_t`, and `fn.return_type = ...` fails too. What works is the **return-value** setter: -> -> ```python -> from binaryninja import ReturnValue, Type -> fn = bv.get_function_at(0x1000015c) -> if fn is not None: -> fn.return_value = ReturnValue(Type.void()) -> ``` -> -> That holds `_reset_handler` at `void` even after reanalysis (verified live). Setting the signature and the return type both failing while `return_value` succeeds looks like a bug or inconsistency in this build (BN 6.0.10601). If it still will not stick, leave it — it does not affect the rest of the lesson. - -> **`__wrap_printf` is the real symbol.** `printf` in our source compiles to the SDK's `__wrap_printf` (which forwards to `__wrap_vprintf`). Rename it `printf` if you prefer the lesson's shorthand, but `__wrap_printf` is what the ELF says. -> -> **`stdio_init_all` returns `bool`, not `void`** — `_Bool stdio_init_all(void)` in the ELF. The `main` source ignores the return value, so the decompiler still reads fine. - -> **Shortcut — resolves name *and* type for every function.** Instead of doing `N` + `Y` by hand, paste this into IDA's Python console (`Plugins -> Python Console`). It sets each function's name and signature programmatically: -> -> ```python -> from binaryninja import Symbol, SymbolType -> # The raw .bin has no headers, so these SDK types don't exist. set_user_type() -> # re-parses each signature as C, so an undefined name raises -> # "SyntaxError: unknown type name '...'". Define them first. -> sdk = bv.parse_types_from_string(""" -> typedef unsigned int uint; -> typedef char* va_list; -> struct stdio_driver; -> typedef struct stdio_driver stdio_driver_t; -> struct uart_inst; -> typedef struct uart_inst uart_inst_t; -> enum gpio_function { -> GPIO_FUNC_XIP = 0, GPIO_FUNC_SPI = 1, GPIO_FUNC_UART = 2, GPIO_FUNC_I2C = 3, -> GPIO_FUNC_PWM = 4, GPIO_FUNC_SIO = 5, GPIO_FUNC_PIO0 = 6, GPIO_FUNC_PIO1 = 7, -> GPIO_FUNC_GPCK = 8, GPIO_FUNC_USB = 9, GPIO_FUNC_NULL = 0x1f, -> }; -> typedef enum gpio_function gpio_function_t; -> """) -> for name, ty in sdk.types.items(): -> bv.define_user_type(name, ty) -> -> # address: (name, signature); None means "leave the type alone" -> funcs = { -> 0x1000015c: ("_reset_handler", "void _reset_handler(void)"), -> 0x10000186: ("platform_entry", "void platform_entry(void)"), -> 0x1000019a: ("data_cpy", "void data_cpy(void*, void*, void*)"), -> 0x100001e4: ("_init", "void _init(void)"), -> 0x10000210: ("frame_dummy", "void frame_dummy(void)"), -> 0x10000234: ("main", "int main(void)"), -> 0x10002cfc: ("exit", "void exit(int)"), -> 0x10002d04: ("runtime_init", "void runtime_init(void)"), -> 0x10002f54: ("stdio_init_all", "bool stdio_init_all(void)"), -> 0x100032a0: ("stdio_uart_init", "void stdio_uart_init(void)"), -> 0x10002f2c: ("stdio_set_driver_enabled", "void stdio_set_driver_enabled(stdio_driver_t*, bool)"), -> 0x10002d30: ("stdio_out_chars_crlf", "void stdio_out_chars_crlf(stdio_driver_t*, const char*, int)"), -> 0x10002e40: ("stdio_put_string", "int stdio_put_string(const char*, int, bool, bool)"), -> 0x100030e4: ("__wrap_printf", "int __wrap_printf(const char*, ...)"), -> 0x10003020: ("__wrap_vprintf", "int __wrap_vprintf(const char*, va_list)"), -> 0x10000248: ("gpio_set_function", "void gpio_set_function(uint, gpio_function_t)"), -> 0x10000e10: ("uart_init", "uint uart_init(uart_inst_t*, uint)"), -> 0x10000da0: ("time_us_64", "uint64_t time_us_64(void)"), -> 0x100033e0: ("strlen", "size_t strlen(const char*)"), -> } -> for addr, (name, sig) in funcs.items(): -> bv.define_user_symbol(Symbol(SymbolType.FunctionSymbol, addr, name)) -> if sig: -> f = bv.get_function_at(addr) -> if f is not None: -> f.set_user_type(sig) -> ``` -> -> SDK type names (`stdio_driver_t`, `gpio_function_t`, `uart_inst_t`, plus `uint` and `va_list`) are **not** in the raw `.bin`. `set_user_type` re-parses each signature as C, so an undefined name raises `SyntaxError: unknown type name '...'` and stops the loop — it is not harmless. The `sdk` block above defines them first (an opaque `struct`/`enum`/`typedef` is enough to parse). If you add a function that uses another SDK type, add a definition for it to that block too. - -### Step 17: Read `main` in the decompiler - -Open the **Decompiler** view on `main`. It reads: - -```c -int32_t main(void) -{ - stdio_init_all(); - do - { - printf("age: %d\r\n", 0x2b); - } while (true); -} -``` - -The `0x2b` is the value we edited live. Now make it permanent. - -### Step 18: Patch `0x2b` to `0x46` in the GUI - -Go to `0x1000023a`: - -```asm -1000023a: 212b movs r1, #43 ; 0x2b -``` - -The halfword is `0x212b`, stored little-endian as `2b 21`. The immediate is the low byte, so the byte at the instruction's own address is `0x2b`. Change it to `0x46` (70). - -**Option A — Hex view:** - -1. Switch to the **Hex** view (`View -> Hex`). -2. Toggle the lock off so editing is enabled. -3. Go to `0x1000023a` and change the byte `2B` to `46`. -4. Return to the linear view, right-click the function -> `Reanalyze`. - -**Option B — Python console:** - -```python -bv.write(0x1000023a, b"\x46") -print(hex(bv.read(0x1000023a, 1)[0])) # -> 0x46 -``` - -After reanalysis the instruction reads `movs r1, #70`. - -### Step 18b: Patch the string `age:` to `foo:` in the GUI - -The format string `"age: %d\r\n"` starts at `0x100034a0`. Its first three bytes are `61 67 65` (`age`). Change them to `66 6f 6f` (`foo`), leaving the `: %d\r\n` tail untouched, so the line prints `foo: 70`. - -**Option A — Hex view:** - -1. Switch to the **Hex** view (`View -> Hex`). -2. Go to `0x100034a0` and change the three bytes `61 67 65` to `66 6f 6f`. -3. Return to the linear view and reanalyze. - -**Option B — Python console:** - -```python -bv.write(0x100034a0, b"foo") -print(bv.read(0x100034a0, 10)) # -> b'foo: %d\r\n\x00' -``` - -Keep the replacement exactly three bytes. If you use a shorter string you must pad it, or `%d` shifts and `printf` reads the wrong argument. A longer string would overwrite the `: %d` tail. - -### Step 19: Export the patched `.bin` - -```python -import os -seg = next(s for s in bv.segments if s.data_length) # the loadable image segment -data = bv.read(seg.start, seg.data_length) # base + size come from the view itself -out = os.path.join(os.path.join(root, "0x0005_intro-to-variables", "build"), "0x0005_intro-to-variables-h.bin") -open(out, "wb").write(data) -print(len(data), out) # -> 15292 /.../build/0x0005_intro-to-variables-h.bin -``` - -Where the two numbers come from — nothing is hardcoded: - -- **`seg.start`** is the image base IDA loaded the `.bin` at (`0x10000000`), the same value you pass to `uf2conv --base`. -- **`seg.data_length`** is the segment's size in the file (`0x3bbc` = 15292). Exactly one segment carries data (the image); every peripheral and synthetic segment has `data_length == 0`, so `next(...)` picks the image. -- Reading `seg.start` for `seg.data_length` bytes therefore grabs exactly the image. - -Two gotchas this avoids: - -- **No relative path.** IDA's Python console runs with a read-only working directory (inside the app bundle), so `open("0x0005_intro-to-variables-h.bin", "wb")` fails with `OSError: [Errno 30] Read-only file system`. `root` (from `~/.embedded-hacking-repo`, Step 3) is the repo, so the file is written into the project's `build/` — no machine-specific path and no database needed. -- **Read the image, not the whole view.** `bv.read(bv.start, bv.length)` spans the entire mapped range (`0x10000000`..`0xe008000c`), which is not the image. The segment's `data_length` is the image size. - -A different size means you exported a partial view. - -### Step 20: Convert to UF2 - -Run from the project directory: - -**macOS Apple Silicon / Linux x64:** - -```bash -python3 ../uf2conv.py 0x0005_intro-to-variables-h.bin \ - --base 0x10000000 --family 0xe48bff59 --output hacked.uf2 -``` - -**Windows x64:** - -```cmd -python ..\uf2conv.py 0x0005_intro-to-variables-h.bin ^ - --base 0x10000000 --family 0xe48bff59 --output hacked.uf2 -``` - -> **Or convert from the IDA console** — it is a normal Python interpreter, so you never have to leave the app. `chdir` to a writable directory first (the default one is read-only), then run the script: -> -> ```python -> import os, sys, runpy -> os.chdir(os.path.join(root, "0x0005_intro-to-variables", "build")) # the project build dir (writable) -> sys.argv = ["uf2conv.py", "0x0005_intro-to-variables-h.bin", -> "--base", "0x10000000", "--family", "0xe48bff59", "--output", "hacked.uf2"] -> runpy.run_path("../../uf2conv.py", run_name="__main__") # path to your uf2conv.py -> ``` -> -> This writes `hacked.uf2` next to the `.bin`, ready to drag onto the Pico. - -### Step 21: Flash and verify `age: 70` - -Hold **BOOTSEL**, plug in the Pico 2, and drag `hacked.uf2` onto the **`RP2350`** drive. Open the serial monitor: - -``` -age: 70 -age: 70 -age: 70 -... -``` - -**43 became 70, permanently, with one byte changed and 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 0x10000000 verify reset exit`), so you never touch BOOTSEL or a UF2. Run it from a terminal (`./flash.sh `), 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, "0x0005_intro-to-variables", "build"), "0x0005_intro-to-variables-h.bin") -> log = os.path.join(os.path.join(root, "0x0005_intro-to-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 **`. (Verified: `Popen` returns in ~1 ms; the flash itself takes ~2 s.) -> -> 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, "0x0005_intro-to-variables", "build"), "0x0005_intro-to-variables-h.bin") -> log = os.path.join(os.path.join(root, "0x0005_intro-to-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 `) the same way. - ---- - -## Part 5: Dynamic — Break at `main` and Hack Live (Project 2) - -### Step 22: Reflash Project 2 and reload IDA - -Part 4 left the Pico running the patched Project 1 image. Put the original Project 2 back and start a fresh session. - -1. Stop any running debug server so the flash script can use the probe: - - ```bash - # macOS / Linux - pkill -TERM -f openocd - ``` - ```powershell - # Windows - Get-Process openocd -ErrorAction SilentlyContinue | Stop-Process - ``` - -2. Flash the original Project 2 image: - - ```bash - # macOS / Linux - ./flash.sh 0x0008_uninitialized-variables/build/0x0008_uninitialized-variables.bin - ``` - ```powershell - # Windows - .\flash.ps1 -Bin 0x0008_uninitialized-variables\build\0x0008_uninitialized-variables.bin - ``` - - **Or do steps 1–2 from the IDA console** (the active view is still Project 1, so take the repo root from it and point at the Project 2 `.bin`): - - **macOS / Linux:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) - bin_path = os.path.join(root, "0x0008_uninitialized-variables", "build", "0x0008_uninitialized-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 Project 2 in the background; log:", log) - ``` - - **Windows:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) - bin_path = os.path.join(root, "0x0008_uninitialized-variables", "build", "0x0008_uninitialized-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 Project 2 in the background; log:", log) - ``` - -3. Start the debug server again (Step 10) and wait for `Listening on port 3333`. -4. Load Project 2 and save its database — see Step 22b. -5. Connect IDA again (Step 11): adapter **GDB MI**, IP `127.0.0.1`, port `3333`. - -Confirm the Pico prints `age: 0` and blinks the red LED. - -### Step 22b: Load Project 2 into IDA and save the database - -Exactly like Steps 7–8, but for Project 2. **Use `File -> Open with Options...`** (not plain `File -> Open`), select `0x0008_uninitialized-variables/build/0x0008_uninitialized-variables.bin`, and set: - -- **Architecture:** `thumb2` -- **Platform:** `thumb2` -- **Base Address:** `0x10000000` - -Click **Open**. Then press `G`, type `0x10000000`, and confirm the first two words: - -``` -0x10000000 0x20082000 initial stack pointer -0x10000004 0x1000015d reset vector (bit 0 = Thumb) -``` - -If you see data at `0x00000000`, close the tab and redo it with `Open with Options`. - -Save it with `File -> Save As...` as `0x0008_uninitialized-variables.i64` (next to the `.bin`). From now on open the `.i64`, not the `.bin`; save with `Cmd+S` / `Ctrl+S` after every rename or patch. - -> **Console equivalent:** -> ```python -> load("0x0008_uninitialized-variables/build/0x0008_uninitialized-variables.bin", -> options={"loader.imageBase": 0x10000000, "loader.platform": "thumb2"}) -> ``` - -Then resolve the functions for Project 2 the same way as Project 1 — Step 26. - -### Step 23: Break at `main` - -`main` is at `0x10000234` in this project too. The GUI sets breakpoints fine (Step 13); the only caution is not to drive `reset run` from the port while IDA is attached (it desyncs IDA's view). Use `BP_ADDR`, which arms `main` before IDA connects: - -1. Stop the server (Ctrl-C), then start it parked at `main`: - ```bash - # macOS / Linux - BP_ADDR=0x10000234 ./debug-server.sh - ``` - ```powershell - # Windows - $env:BP_ADDR="0x10000234"; .\debug-server.ps1 - ``` - - **Or restart it from the IDA console** — kill any running server, then start it parked at `main`: - - **macOS / Linux:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() - log = os.path.join(root, "openocd.log") - subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # kill any running server first - 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 restarted parked at main; log:", log) - ``` - - **Windows:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() - log = os.path.join(root, "openocd.log") - subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # kill any running server first - 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 restarted parked at main; log:", log) - ``` -2. Connect IDA (Step 11): adapter **GDB MI**, IP `127.0.0.1`, port `3333`. - -The target is already halted at `main` when IDA connects, and the sidebar reads `Stopped at 0x10000234`. - -> If you instead want to reach `main` on an already-connected session, you must Detach, send `reset run` from the port, then reconnect. Arming `main` and resetting while attached leaves the sidebar showing a stale address. - -`main` sets up GPIO 16 and then loops: print `age`, turn the LED on, sleep, turn it off, sleep. The whole loop is one function because `blink_and_print` was inlined: - -```asm -10000234
: -10000234: b538 push {r3, r4, r5, lr} -10000236: f002 ff49 bl 0x100030cc ; stdio_init_all -1000023a: 2010 movs r0, #16 ; LED_PIN -1000023c: f000 f83a bl 0x100002b4 ; gpio_init -10000240: f04f 0501 mov.w r5, #1 -10000244: 2310 movs r3, #16 ; LED_PIN -10000246: ec45 3044 mcrr 0, 4, r3, r5, cr4 ; gpio_set_dir(16, OUT) -1000024a: 2100 movs r1, #0 ; age -1000024c: 4809 ldr r0, [pc, #36] ; @ 0x10000274 -> "age: %d\r\n" -1000024e: f003 f805 bl 0x1000325c ; __wrap_printf -10000252: 2410 movs r4, #16 ; LED_PIN -10000254: ec45 4040 mcrr 0, 4, r4, r5, cr0 ; gpio_put(16, 1) -10000258: f44f 70fa mov.w r0, #500 -1000025c: f000 fd58 bl 0x10000d10 ; sleep_ms -10000260: f04f 0300 mov.w r3, #0 -10000264: ec43 4040 mcrr 0, 4, r4, r3, cr0 ; gpio_put(16, 0) -10000268: f44f 70fa mov.w r0, #500 -1000026c: f000 fd50 bl 0x10000d10 ; sleep_ms -10000270: e7eb b.n 0x1000024a -10000272: bf00 nop -10000274: 10003618 .word 0x10003618 -``` - -Look at the **Registers** widget at `0x1000024e`: `r1` is `0`, which is why the Pico prints `age: 0`. - -### Step 24: Inspect the GPIO registers live - -The GPIO hardware is memory-mapped. Go to each address and watch it change as you step: - -| Address | Block | Role | -| ------- | ----- | ---- | -| `0x40028000` | `IO_BANK0` | pin function select and status | -| `0x40038000` | `PADS_BANK0` | pad configuration | -| `0xd0000000` | `SIO` | single-cycle GPIO block driven by `mcrr` | - -Step Over through `0x10000254` (`mcrr 0, 4, r4, r5, cr0`) and watch the SIO output register change: this is `gpio_put(16, 1)` turning the red LED on at the hardware level. - -### Step 25: HACK IT LIVE — change the printed value - -1. Press `G`, go to `0x1000024e` (the `bl __wrap_printf`). -2. Set a **hardware execute** breakpoint at `0x1000024e` in the GUI (`Debugger -> Add Hardware Breakpoint...`; not `F2`). Note `0x1000024e` — Project 2's loop sits at a different address than Project 1's. -3. Click **Resume** in IDA. The target is already looping, so the breakpoint fires on the next pass. IDA stops with `r1 = 0`. -4. Set `r1` to `0x42` (66): - ```python - dbg.set_reg_value("r1", 0x42) - ``` - (Or right-click `r1` in the **Registers** widget, press `E`, type `42`, and press Enter.) -5. **Move the breakpoint past the call.** `0x10000252` is the instruction right after the `bl __wrap_printf`. Remove the breakpoint at `0x1000024e` and set a hardware breakpoint at `0x10000252`, then click **Resume**. The core runs `printf` with `r1 = 0x42` and stops at `0x10000252`. (Not **Step Over** — it steps into the call on this symbol-less `.bin`, and a breakpoint left on the current PC re-traps the step; Step 13 explains both.) -6. Look at your serial monitor and the **Target** tab: - - ``` - age: 66 - ``` - -Press **Resume** and the next iteration prints `age: 0` again, because the loop reloads `movs r1, #0` each pass. The live hack is temporary; the static patch makes it permanent. - -### Step 25b: HACK THE STRING LIVE — change `age:` to `foo:` - -Same idea as Project 1, different addresses. Here the format string is at `0x10003618` and the `printf` call is at `0x1000024e`. - -1. Hit the breakpoint at `0x1000024e` as in Step 25. At the stop, `r0 = 0x10003618` and `r1 = 0`. -2. Write the replacement string to free RAM at `0x20080000` from the **Python console** (`dbg.write_memory` — no command port needed): - ```python - dbg.write_memory(0x20080000, b"foo: %d\r\n\x00") - ``` - Bytes `66 6f 6f 3a 20 25 64 0d 0a 00` = `"foo: %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. Move the breakpoint past the call in the GUI (remove it at `0x1000024e`, set one at `0x10000252`) and click **Resume**. This iteration prints: - ``` - foo: 0 - ``` - then stops at `0x10000252`. One iteration only — the loop reloads `r0` each pass. The permanent version is the static patch in Step 28b. - -### Step 25c: Kill the debugger and OpenOCD - -Same as Step 15b: click the **X** (**Kill**) in the **Debugger** sidebar (or **`Debugger -> Kill`**), then stop OpenOCD 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 -``` - ---- - -## Part 6: Static — Resolve the Functions and Patch (Project 2) - -### Step 26: Resolve the functions in the IDA GUI - -Same two keys as Step 16 — `G` to the address, then `Y` (Change Type) to set the prototype — using the Project 2 ELF symbol map from Step 4. - -The mechanics are identical to Step 16, so here are the worked examples for the functions that are specific to this project. - -#### `main` - -1. `G` -> `0x10000234`. -2. `Y` -> `int main(void)` (IDA shows `int32_t main(void)` — the same 32-bit `int`). - -#### `gpio_init` - -1. `G` -> `0x100002b4`. -2. `Y` -> `void gpio_init(uint gpio)`. - -#### `sleep_ms` - -1. `G` -> `0x10000d10`. -2. `Y` -> `void sleep_ms(uint32_t ms)`. - -#### `stdio_init_all` and `__wrap_printf` - -Same as Project 1, different addresses: `stdio_init_all` at `0x100030cc` (`bool stdio_init_all(void)`), and `__wrap_printf` at `0x1000325c` (`int __wrap_printf(const char *fmt, ...)`). - -Then work down the table the same way. - -Same idea as Project 1: **our code plus what it calls**, not the whole SDK. The call chain here is one function longer because `main` also drives the GPIO and sleeps: - -``` -main -├── stdio_init_all -│ ├── stdio_uart_init ── gpio_set_function, uart_init -│ ├── stdio_set_driver_enabled -│ ├── stdio_out_chars_crlf -│ ├── stdio_put_string ── strlen -│ └── time_us_64 -├── gpio_init -├── __wrap_printf ── __wrap_vprintf -└── sleep_ms -``` - -Two things in this project have **no symbol of their own**, because the compiler inlined them into `main`: - -- `blink_and_print` — the `static` helper in our own source is inlined, so there is no `blink_and_print` address to rename. You see its body directly inside `main`. -- `gpio_set_dir` and `gpio_put` — these are `static inline` in the SDK headers, so they compile to the `mcrr`/SIO writes you see in `main` rather than to calls. - -**Project 2 — resolve every function in that chain:** - -| Address | Rename to (`N`) | Signature (`Y`) | -| ------- | --------------- | --------------- | -| `0x1000015c` | `_reset_handler` | `void _reset_handler(void)` | -| `0x10000186` | `platform_entry` | `void platform_entry(void)` | -| `0x1000019a` | `data_cpy` | `void data_cpy(void*, void*, void*)` | -| `0x100001e4` | `_init` | `void _init(void)` | -| `0x10000210` | `frame_dummy` | `void frame_dummy(void)` | -| **`0x10000234`** | **`main`** | **`int main(void)`** | -| `0x100002b4` | `gpio_init` | `void gpio_init(uint)` | -| `0x10000278` | `gpio_set_function` | `void gpio_set_function(uint, gpio_function_t)` | -| `0x10000d10` | `sleep_ms` | `void sleep_ms(uint32_t)` | -| `0x10002e74` | `exit` | `void exit(int)` | -| `0x10002e7c` | `runtime_init` | `void runtime_init(void)` | -| `0x100030cc` | `stdio_init_all` | `bool stdio_init_all(void)` | -| `0x10003418` | `stdio_uart_init` | `void stdio_uart_init(void)` | -| `0x100030a4` | `stdio_set_driver_enabled` | `void stdio_set_driver_enabled(stdio_driver_t*, bool)` | -| `0x10002ea8` | `stdio_out_chars_crlf` | `void stdio_out_chars_crlf(stdio_driver_t*, const char*, int)` | -| `0x10002fb8` | `stdio_put_string` | `int stdio_put_string(const char*, int, bool, bool)` | -| `0x1000325c` | `__wrap_printf` | `int __wrap_printf(const char*, ...)` | -| `0x10003198` | `__wrap_vprintf` | `int __wrap_vprintf(const char*, va_list)` | -| `0x10000f88` | `uart_init` | `uint uart_init(uart_inst_t*, uint)` | -| `0x10000ef4` | `time_us_64` | `uint64_t time_us_64(void)` | -| `0x10003558` | `strlen` | `size_t strlen(const char*)` | - -Python console shortcut (resolves name **and** type): - -```python -from binaryninja import Symbol, SymbolType -# The raw .bin has no headers, so these SDK types don't exist. set_user_type() -# re-parses each signature as C, so an undefined name raises -# "SyntaxError: unknown type name '...'". Define them first. -sdk = bv.parse_types_from_string(""" -typedef unsigned int uint; -typedef char* va_list; -struct stdio_driver; -typedef struct stdio_driver stdio_driver_t; -struct uart_inst; -typedef struct uart_inst uart_inst_t; -enum gpio_function { - GPIO_FUNC_XIP = 0, GPIO_FUNC_SPI = 1, GPIO_FUNC_UART = 2, GPIO_FUNC_I2C = 3, - GPIO_FUNC_PWM = 4, GPIO_FUNC_SIO = 5, GPIO_FUNC_PIO0 = 6, GPIO_FUNC_PIO1 = 7, - GPIO_FUNC_GPCK = 8, GPIO_FUNC_USB = 9, GPIO_FUNC_NULL = 0x1f, -}; -typedef enum gpio_function gpio_function_t; -""") -for name, ty in sdk.types.items(): - bv.define_user_type(name, ty) - -# address: (name, signature); None means "leave the type alone" -funcs = { - 0x1000015c: ("_reset_handler", "void _reset_handler(void)"), - 0x10000186: ("platform_entry", "void platform_entry(void)"), - 0x1000019a: ("data_cpy", "void data_cpy(void*, void*, void*)"), - 0x100001e4: ("_init", "void _init(void)"), - 0x10000210: ("frame_dummy", "void frame_dummy(void)"), - 0x10000234: ("main", "int main(void)"), - 0x100002b4: ("gpio_init", "void gpio_init(uint)"), - 0x10000278: ("gpio_set_function", "void gpio_set_function(uint, gpio_function_t)"), - 0x10000d10: ("sleep_ms", "void sleep_ms(uint32_t)"), - 0x10002e74: ("exit", "void exit(int)"), - 0x10002e7c: ("runtime_init", "void runtime_init(void)"), - 0x100030cc: ("stdio_init_all", "bool stdio_init_all(void)"), - 0x10003418: ("stdio_uart_init", "void stdio_uart_init(void)"), - 0x100030a4: ("stdio_set_driver_enabled", "void stdio_set_driver_enabled(stdio_driver_t*, bool)"), - 0x10002ea8: ("stdio_out_chars_crlf", "void stdio_out_chars_crlf(stdio_driver_t*, const char*, int)"), - 0x10002fb8: ("stdio_put_string", "int stdio_put_string(const char*, int, bool, bool)"), - 0x1000325c: ("__wrap_printf", "int __wrap_printf(const char*, ...)"), - 0x10003198: ("__wrap_vprintf", "int __wrap_vprintf(const char*, va_list)"), - 0x10000f88: ("uart_init", "uint uart_init(uart_inst_t*, uint)"), - 0x10000ef4: ("time_us_64", "uint64_t time_us_64(void)"), - 0x10003558: ("strlen", "size_t strlen(const char*)"), -} -for addr, (name, sig) in funcs.items(): - bv.define_user_symbol(Symbol(SymbolType.FunctionSymbol, addr, name)) - if sig: - f = bv.get_function_at(addr) - if f is not None: - f.set_user_type(sig) -``` - -The decompiler now shows `main` initializing GPIO 16 and looping. We make two changes: - -- **Move the LED from GPIO 16 to GPIO 17** by patching three `0x10` immediates. -- **Change the printed value from 0 to 66** by patching one `0x00` immediate. - -### Step 27: Patch 1 — move the LED from GPIO 16 to GPIO 17 - -GPIO 16 is the red LED; GPIO 17 is the green LED. The pin number appears in three instructions. Change the low byte of each from `10` to `11`: - -| Address | Instruction | Bytes before | Bytes after | Role | -| ------- | ----------- | ------------ | ----------- | ---- | -| `0x1000023a` | `movs r0, #16` | `10 20` | `11 20` | pin passed to `gpio_init` | -| `0x10000244` | `movs r3, #16` | `10 23` | `11 23` | pin used by `gpio_set_dir` | -| `0x10000252` | `movs r4, #16` | `10 24` | `11 24` | pin used by `gpio_put` in the blink loop | - -In the **Hex** view (lock off), change each byte and reanalyze. Or in the Python console: - -```python -for addr in (0x1000023a, 0x10000244, 0x10000252): - bv.write(addr, b"\x11") -``` - -> **All three are required.** If you patch only the `gpio_set_dir` site, pin 17's output driver is enabled but `gpio_put` still drives pin 16, whose driver was never enabled. Nothing lights up. This is the most common mistake in this lesson. - -### Step 28: Patch 2 — change the printed value from 0 to 66 - -`main` loads `age = 0` with `movs r1, #0` at `0x1000024a`. Change the immediate byte from `00` to `42` (`0x42` = 66): - -```python -bv.write(0x1000024a, b"\x42") -``` - -Verify all four patches: - -```python -for addr in (0x1000023a, 0x10000244, 0x10000252, 0x1000024a): - print(hex(addr), hex(bv.read(addr, 1)[0])) -# -> 0x1000023a 0x11 -# -> 0x10000244 0x11 -# -> 0x10000252 0x11 -# -> 0x1000024a 0x42 -``` - -### Step 28b: Patch the string `age:` to `foo:` - -The format string starts at `0x10003618`; change its first three bytes `61 67 65` (`age`) to `66 6f 6f` (`foo`): - -```python -bv.write(0x10003618, b"foo") -print(bv.read(0x10003618, 10)) # -> b'foo: %d\r\n\x00' -``` - -Exactly three bytes, same rule as Project 1: a shorter string must be padded, a longer one overwrites the `: %d` tail. - -### Step 29: Export, convert, and flash - -```python -import os -seg = next(s for s in bv.segments if s.data_length) # the loadable image segment -data = bv.read(seg.start, seg.data_length) # base + size from the view itself -out = os.path.join(os.path.join(root, "0x0008_uninitialized-variables", "build"), "0x0008_uninitialized-variables-h.bin") -open(out, "wb").write(data) -print(len(data), out) # -> 15668 /.../build/0x0008_uninitialized-variables-h.bin -``` - -Same as Project 1: `seg.start` is the load base and `seg.data_length` is the image size (here `0x3d34` = 15668) — both read from the view, and no relative path (the console's CWD is read-only). - -**macOS Apple Silicon / Linux x64:** - -```bash -python3 ../uf2conv.py 0x0008_uninitialized-variables-h.bin \ - --base 0x10000000 --family 0xe48bff59 --output hacked.uf2 -``` - -**Windows x64:** - -```cmd -python ..\uf2conv.py 0x0008_uninitialized-variables-h.bin ^ - --base 0x10000000 --family 0xe48bff59 --output hacked.uf2 -``` - -Or run the conversion from the IDA console, exactly as in Step 20 (`os.chdir` to the build dir, then `runpy.run_path("../../uf2conv.py", run_name="__main__")` with `sys.argv` set to the arguments above). - -Hold **BOOTSEL**, plug in the Pico 2, drag `hacked.uf2` onto the **`RP2350`** drive. Or flash the `.bin` over the Debug Probe with SWD — no BOOTSEL — from the console, exactly as in Step 21 (stop any running OpenOCD first, and use `Popen`, not `run`, so the console is not blocked): - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() -bin_path = os.path.join(os.path.join(root, "0x0008_uninitialized-variables", "build"), "0x0008_uninitialized-variables-h.bin") -log = os.path.join(os.path.join(root, "0x0008_uninitialized-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) -``` - -### Step 30: Verify - -Open the serial monitor: - -``` -age: 66 -age: 66 -age: 66 -... -``` - -The **green LED on GPIO 17** now blinks instead of the red one. - -**We changed the printed value and moved the LED, with four bytes and no source code.** - ---- - -## 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", 0x46)` in the Python console (or right-click the register, press `E`, type hex, Enter) | -| 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 ` then `bp 2 hw`) | -| Confirm what is armed | The **Breakpoints** widget lists it (command-port fallback: `mdw 0xE0002000 8`, each armed breakpoint shows as ``) | -| Apply the ELF symbol map | Paste the Python snippet from Step 16 / 26 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 13). 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 2 hw` | -| Remove one breakpoint | `rbp ` — **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=0x1000023e ./debug-server.sh` — Windows: `$env:BP_ADDR="0x1000023e"; .\debug-server.ps1` (`0x1000024e` for Project 2) | -| 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 | -| ------- | ------- | ------ | ----- | ------ | -| `0x0005` | `0x1000023a` | `2b` | `46` | prints `age: 70` | -| `0x0008` | `0x1000023a` | `10` | `11` | `gpio_init` configures GPIO 17 | -| `0x0008` | `0x10000244` | `10` | `11` | `gpio_set_dir` enables GPIO 17 | -| `0x0008` | `0x10000252` | `10` | `11` | `gpio_put` drives GPIO 17 | -| `0x0008` | `0x1000024a` | `00` | `42` | prints `age: 66` | -| `0x0005` | `0x100034a0` | `61 67 65` | `66 6f 6f` | string prints `foo:` instead of `age:` | -| `0x0008` | `0x10003618` | `61 67 65` | `66 6f 6f` | string prints `foo:` instead of `age:` | - -### Raw image facts - -| Item | Value | -| ---- | ----- | -| Build type | `Release` | -| Load base address | `0x10000000` | -| Project 1 size | `15292` bytes | -| Project 2 size | `15668` bytes | -| Fixed `main` anchor (both projects) | `0x1000018c` (reset handler middle `blx`) | -| `main` (both projects) | `0x10000234` | -| `printf` call, Project 1 | `0x1000023e` | -| `printf` call, Project 2 | `0x1000024e` | -| 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 11). The 13.3.rel1 build (what `/opt/homebrew/bin/arm-none-eabi-gdb` symlinks to) 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,,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 11). 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 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 `blink_and_print` separate, so Project 2's `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 (`breakpoint_remove_all_internal` -> `Delete all breakpoints`), so anything armed earlier is gone. This also applies to `BP_ADDR` on the startup command line, and to `hbreak` followed by `detach` in GDB. -- **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 10) rather than `reset run` while attached. Loop addresses such as `0x1000023e` 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. (On the old **GDB RSP** workaround the stop showed as `SingleStep` with an empty widget, because the breakpoint was armed behind IDA's back.) - -### I edit `r1` (or another register) and it reverts - -`main` reloads the value at the top of every loop iteration — `movs r1, #43` at `0x1000023a` runs right before the `printf` at `0x1000023e`. So `r1` is only `0x46` for the instant between your edit and the next pass; then it is `0x2b` 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). Confirm with `mdw 0xE0002000 4` on the command port: a hardware breakpoint shows as `0x1000023f`; all zeros means nothing is armed. - -> **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 (only "Force Update Memory Cache", which is for memory). So a value changed outside IDA will not appear until the next stop. - -### 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 `age: 43` 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. Verified: target at `0x1000023e` while the sidebar still read `Stopped at 0x10003020`. -- 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 (see the stepping section above). - -> **If the stop is at `0x1000320c` rather than your breakpoint,** you stopped inside `stdio_uart_out_flush`, not at `main`. See the next section. - -### The target "blows past" `main` and stops at `0x1000320c` instead - -`0x1000320c` is inside `stdio_uart_out_flush`: - -```asm -1000320c: 6993 ldr r3, [r2, #24] -1000320e: 071b lsls r3, r3, #28 -10003210: d4fc bmi.n 0x1000320c -``` - -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 (`0x10000234`) 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 `0x10000234` and will never re-execute it. Either arm the breakpoint **before** resetting, or break inside the loop at `0x1000023e`, which fires every iteration. - -**`0x1000320c` is not a function.** It is one instruction inside `stdio_uart_out_flush`, which starts at `0x10003208`: - -```asm -10003208 : -10003208: 4b02 ldr r3, [pc, #8] ; @ 0x10003214 -1000320a: 681a ldr r2, [r3] -1000320c: 6993 ldr r3, [r2, #24] ; the core sits here while the UART drains -1000320e: 071b lsls r3, r3, #28 -10003210: d4fc bmi.n 0x1000320c -10003212: 4770 bx lr -10003214: 20000850 .word 0x20000850 -``` - -If IDA has created a function at `0x1000320c` (for example because the debugger stopped at that PC), the decompiler shows garbage: registers named `entry_r4`/`entry_r5`, and stores to invented constants like `0x3a` and `0xfffffff6`. 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 `0x10003208`. - -### 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`. - -### Project 2's LED does not light at all after patching - -You patched only some of the three GPIO 16 sites. All three of `0x1000023a`, `0x10000244`, and `0x10000252` must change. - ---- - -## 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 10), then in a new terminal: - -``` -arm-none-eabi-gdb -``` - -At the `(gdb)` prompt: - -``` -set architecture armv8-m.main -target extended-remote :3333 -hbreak *0x1000023e -continue -``` - -Do **not** run `monitor reset run` before `hbreak`. `0x1000023e` 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 = 0x1000023e, r1 = 0x2b -set $r1 = 0x46 -stepi -continue -``` - -The serial monitor prints `age: 70` 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 `0x1000320c`, the `stdio_uart_out_flush` UART-drain loop. - -Project 2 is the same with the other call site and value: - -``` -hbreak *0x1000024e -continue -info registers pc r1 # pc = 0x1000024e, r1 = 0 -set $r1 = 0x42 -stepi -``` - -`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. - ## Glossary | Term | Definition | @@ -1826,7 +80,7 @@ stepi | **`.elf`** | Linked image with the symbol table; the ground truth for addresses and names | | **`.rodata`** | Read-only section for constants and string literals; stays in flash | | **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 `blink_and_print` disappears in `Release` | | **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 | @@ -1837,4 +91,4 @@ stepi --- -**Remember:** the ELF tells you what every address is, and the `.bin` is what you actually patch. Prove the behavior dynamically, resolve the names from the ELF, then patch the bytes 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. diff --git a/WEEK05/WEEK05-IDA.md b/WEEK05/WEEK05-IDA.md index 3c1e925..b090047 100644 --- a/WEEK05/WEEK05-IDA.md +++ b/WEEK05/WEEK05-IDA.md @@ -23,10 +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 `float`/`double` constant is materialized from the compiler's literal pool -- **Break at `main`** on live silicon, even though `main` moves between these two programs + these two programs - Reconstruct a 64-bit `double` from the ABI register pair `r2:r3` and hack it live - **Resolve the functions in the IDA GUI** using the ELF symbol map, including the `pico_double` formatting helpers `printf` pulls in -- **Patch** the constant bytes, export the image, convert to UF2, and flash it +- **Patch** the constant bytes, export the image, convert to UF2, and export it - Prove why `42.5 -> 99.0` is a one-word patch but `42.52525 -> 99.99` needs two --- @@ -44,12 +44,12 @@ The `.bin` is built **from** the `.elf`, so the ELF tells you exactly what is at > **Build `Release`, not `Debug`.** Every address in this guide matches the Week 5 lesson, and the Week 5 lesson is a `Release` build. `Release` optimizes the code the same way the original lesson was built: it folds the `float`/`double` initializer into the literal pool and links the `pico_double` formatting helpers straight into the `printf` path. If you build `Debug`, the SDK function addresses move and the float/double helpers are laid out differently, so nothing lines up. Always build `Release` for this lesson. -The order is **dynamic first, static second**, twice — once per project: +The workflow is static-only: import the raw image, recover the code, decompile it, and patch the artifact: -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 | The hack | | ------- | ------ | -------- | @@ -62,7 +62,7 @@ The order is **dynamic first, static second**, twice — once per project: --- -## Part 1: Build, Flash, and Get the Symbol Map +## Part 1: Build and Get the Symbol Map ### Step 1: Install the toolchain @@ -74,1924 +74,10 @@ The order is **dynamic first, static second**, twice — once per project: **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 -``` - -**Windows x64** and **Linux x64** do not have this problem. Skip to Step 3. - -### Step 3: Build the two projects with `Release` - -Run this once inside `0x000e_floating-point-data-type/` and once inside `0x0011_double-floating-point-data-type/`: - -```bash -cmake -B build -G Ninja -DPICO_BOARD=pico2 -DPICO_PLATFORM=rp2350 -DCMAKE_BUILD_TYPE=Release -cmake --build build -``` - -**Point IDA at this repository (once).** Every console snippet below reads the repo root from `~/.embedded-hacking-repo`, so IDA never needs a database open and nothing is hardcoded. From the repo root, run once: - -**macOS / Linux:** - -```bash -pwd > ~/.embedded-hacking-repo -``` - -**Windows (PowerShell):** - -```powershell -(Get-Location).Path | Set-Content "$env:USERPROFILE\.embedded-hacking-repo" -``` - -**Then build from the IDA console**, so the whole build -> patch -> flash loop stays inside IDA. The console inherits a minimal `PATH` — on macOS just `/usr/bin:/bin:/usr/sbin:/sbin` — so it does not see Homebrew; add your package manager's `bin` first, then run plain `cmake`. - -**macOS Apple Silicon:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() -os.environ["PATH"] = "/opt/homebrew/bin:" + os.environ["PATH"] # the console's PATH omits Homebrew -for name in ("0x000e_floating-point-data-type", "0x0011_double-floating-point-data-type"): - proj = os.path.join(root, name) - subprocess.run(["cmake", "-B", "build", "-G", "Ninja", "-DPICO_BOARD=pico2", - "-DPICO_PLATFORM=rp2350", "-DCMAKE_BUILD_TYPE=Release"], cwd=proj) - subprocess.run(["cmake", "--build", "build"], cwd=proj) -``` - -**Linux x64:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() -for name in ("0x000e_floating-point-data-type", "0x0011_double-floating-point-data-type"): - proj = os.path.join(root, name) - subprocess.run(["cmake", "-B", "build", "-G", "Ninja", "-DPICO_BOARD=pico2", - "-DPICO_PLATFORM=rp2350", "-DCMAKE_BUILD_TYPE=Release"], cwd=proj) - subprocess.run(["cmake", "--build", "build"], cwd=proj) -``` - -**Windows x64:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() -for name in ("0x000e_floating-point-data-type", "0x0011_double-floating-point-data-type"): - proj = os.path.join(root, name) - subprocess.run(["cmake", "-B", "build", "-G", "Ninja", "-DPICO_BOARD=pico2", - "-DPICO_PLATFORM=rp2350", "-DCMAKE_BUILD_TYPE=Release"], cwd=proj) - subprocess.run(["cmake", "--build", "build"], cwd=proj) -``` - -Each build directory now contains the pair we need: - -- `0x000e_floating-point-data-type/build/0x000e_floating-point-data-type.elf` and `.bin` — `.bin` is **15308** bytes (`0x3bcc`) -- `0x0011_double-floating-point-data-type/build/0x0011_double-floating-point-data-type.elf` and `.bin` — `.bin` is **15324** bytes (`0x3bdc`) - -If the ARM toolchain is not on your `PATH`, add `-DPICO_TOOLCHAIN_PATH=...`: - -| OS | Typical toolchain path | -| -- | ---------------------- | -| Windows x64 | `C:/Program Files/Arm GNU Toolchain arm-none-eabi/14.2 rel1/bin` | -| macOS Apple Silicon | `~/.pico-sdk/toolchain/14_2_Rel1/bin` | -| Linux x64 | `/usr` | - -> **This guide's toolchain lives at `~/.pico-sdk/toolchain/14_2_Rel1/bin`.** All of `arm-none-eabi-nm`, `arm-none-eabi-objdump`, and `arm-none-eabi-gdb` resolved in this document come from there. If your install is elsewhere, `which arm-none-eabi-nm` tells you where to point. - -### Step 4: Dump the ELF symbol map - -This is the ground truth for the whole lesson. Run `arm-none-eabi-nm` on each ELF and keep the output in a terminal or a text file: - -**macOS Apple Silicon / Linux x64:** - -```bash -arm-none-eabi-nm -n --defined-only build/0x000e_floating-point-data-type.elf | grep -E ' [Tt] ' -arm-none-eabi-nm -n --defined-only build/0x0011_double-floating-point-data-type.elf | grep -E ' [Tt] ' -``` - -**Windows x64:** - -```powershell -arm-none-eabi-nm -n --defined-only build\0x000e_floating-point-data-type.elf | Select-String ' [Tt] ' -arm-none-eabi-nm -n --defined-only build\0x0011_double-floating-point-data-type.elf | Select-String ' [Tt] ' -``` - -Each line is `address type name`. The `T`/`t` type is a function. Here are the functions this lesson uses. The signatures come from the ELF's DWARF debug info queried with `arm-none-eabi-gdb -batch -ex "ptype "`, so they are exact. - -**Project 1 — `0x000e_floating-point-data-type`:** - -| Address | ELF symbol | Signature | Role | -| ------- | ---------- | --------- | ---- | -| `0x1000015c` | `_reset_handler` | `void _reset_handler(void)` | reset entry | -| `0x10000186` | `platform_entry` | `void platform_entry(void)` | calls `runtime_init`, `main`, `exit` | -| `0x1000019a` | `data_cpy` | `void data_cpy(void*, void*, void*)` | copies `.data` from flash to SRAM | -| `0x100001e4` | `_init` | `void _init(void)` | runs `.init_array` | -| `0x10000210` | `frame_dummy` | `void frame_dummy(void)` | C runtime boilerplate | -| `0x10000234` | `main` | `int main(void)` | the lesson function | -| `0x10000254` | `gpio_set_function` | `void gpio_set_function(uint, gpio_function_t)` | SDK GPIO helper | -| `0x10000da8` | `time_us_64` | `uint64_t time_us_64(void)` | SDK microsecond clock | -| `0x10000e18` | `uart_init` | `uint uart_init(uart_inst_t*, uint)` | SDK UART init | -| `0x10002ca8` | `vfctprintf` | `int vfctprintf(void (*)(char, void*), void*, const char*, va_list)` | printf format engine | -| `0x10002d04` | `exit` | `void exit(int)` | C runtime exit | -| `0x10002d0c` | `runtime_init` | `void runtime_init(void)` | SDK runtime init | -| `0x10002d38` | `stdio_out_chars_crlf` | `void stdio_out_chars_crlf(stdio_driver_t*, const char*, int)` | CRLF output driver | -| `0x10002e48` | `stdio_put_string` | `int stdio_put_string(const char*, int, bool, bool)` | buffered string output | -| `0x10002f34` | `stdio_set_driver_enabled` | `void stdio_set_driver_enabled(stdio_driver_t*, bool)` | enable the UART driver | -| `0x10002f5c` | `stdio_init_all` | `bool stdio_init_all(void)` | SDK serial init | -| `0x10003028` | `__wrap_vprintf` | `int __wrap_vprintf(const char*, va_list)` | printf core | -| `0x100030ec` | `__wrap_printf` | `int __wrap_printf(const char*, ...)` | the `printf` wrapper | -| `0x100032a8` | `stdio_uart_init` | `void stdio_uart_init(void)` | SDK UART stdio init | -| `0x100033e8` | `strlen` | `size_t strlen(const char*)` | C runtime string length | - -Project 1 also links in the `pico_double` formatting helpers that `printf`'s `%f` path calls. These are reachable from `main` through `__wrap_printf`: - -| Address | ELF symbol | Signature | Role | -| ------- | ---------- | --------- | ---- | -| `0x10001384` | `__wrap___aeabi_dadd` | `double __wrap___aeabi_dadd(double, double)` | double add | -| `0x100013ac` | `__wrap___aeabi_dsub` | `double __wrap___aeabi_dsub(double, double)` | double subtract | -| `0x100013d4` | `__wrap___aeabi_dmul` | `double __wrap___aeabi_dmul(double, double)` | double multiply | -| `0x10001420` | `__wrap___aeabi_ddiv` | `double __wrap___aeabi_ddiv(double, double)` | double divide | -| `0x100014bc` | `__wrap___aeabi_i2d` | `double __wrap___aeabi_i2d(int)` | int -> double | -| `0x100014e0` | `__wrap___aeabi_ui2d` | `double __wrap___aeabi_ui2d(unsigned)` | unsigned -> double | -| `0x10001504` | `__wrap___aeabi_d2iz` | `int __wrap___aeabi_d2iz(double)` | double -> int | -| `0x10001528` | `__wrap___aeabi_d2uiz` | `unsigned __wrap___aeabi_d2uiz(double)` | double -> unsigned | -| `0x1000154c` | `__wrap___aeabi_dcmpun` | `int __wrap___aeabi_dcmpun(double, double)` | unordered compare | -| `0x10001570` | `__wrap___aeabi_dcmplt` | `int __wrap___aeabi_dcmplt(double, double)` | less-than compare | -| `0x10001598` | `__wrap___aeabi_dcmple` | `int __wrap___aeabi_dcmple(double, double)` | less-or-equal compare | -| `0x100015c0` | `__wrap___aeabi_dcmpge` | `int __wrap___aeabi_dcmpge(double, double)` | greater-or-equal compare | -| `0x100015e8` | `__wrap___aeabi_dcmpgt` | `int __wrap___aeabi_dcmpgt(double, double)` | greater-than compare | -| `0x1000160c` | `_out_rev` | `unsigned _out_rev(out_fct_type, char*, size_t, size_t, const char*, size_t, unsigned, unsigned)` | reversed-digit output | -| `0x100016a8` | `_ntoa_format` | `unsigned _ntoa_format(out_fct_type, char*, size_t, size_t, char*, size_t, bool, unsigned, unsigned, unsigned, unsigned)` | number formatter | -| `0x1000187c` | `_out_char` | `void _out_char(char, void*, size_t, size_t)` | single-char sink | -| `0x10001890` | `_ftoa` | `unsigned int _ftoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)` | fixed-point float formatter | -| `0x10001d50` | `_etoa` | `unsigned int _etoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)` | exponential float formatter | -| `0x100022c4` | `_vsnprintf` | `int _vsnprintf(out_fct_type, char*, size_t, const char*, va_list)` | the format dispatcher | -| `0x10000dbc` | `busy_wait_us` | `void busy_wait_us(uint64_t)` | UART timing loop | -| `0x10000fec` | `clock_get_hz` | `unsigned long clock_get_hz(clock_handle_t)` | UART clock lookup | - -**Project 2 — `0x0011_double-floating-point-data-type`:** - -| Address | ELF symbol | Signature | Role | -| ------- | ---------- | --------- | ---- | -| `0x1000015c` | `_reset_handler` | `void _reset_handler(void)` | reset entry | -| `0x10000186` | `platform_entry` | `void platform_entry(void)` | calls `runtime_init`, `main`, `exit` | -| `0x1000019a` | `data_cpy` | `void data_cpy(void*, void*, void*)` | copies `.data` from flash to SRAM | -| `0x100001e4` | `_init` | `void _init(void)` | runs `.init_array` | -| `0x10000210` | `frame_dummy` | `void frame_dummy(void)` | C runtime boilerplate | -| `0x10000238` | `main` | `int main(void)` | the lesson function | -| `0x1000025c` | `gpio_set_function` | `void gpio_set_function(uint, gpio_function_t)` | SDK GPIO helper | -| `0x10000db0` | `time_us_64` | `uint64_t time_us_64(void)` | SDK microsecond clock | -| `0x10000e20` | `uart_init` | `uint uart_init(uart_inst_t*, uint)` | SDK UART init | -| `0x10002cb0` | `vfctprintf` | `int vfctprintf(void (*)(char, void*), void*, const char*, va_list)` | printf format engine | -| `0x10002d0c` | `exit` | `void exit(int)` | C runtime exit | -| `0x10002d14` | `runtime_init` | `void runtime_init(void)` | SDK runtime init | -| `0x10002d40` | `stdio_out_chars_crlf` | `void stdio_out_chars_crlf(stdio_driver_t*, const char*, int)` | CRLF output driver | -| `0x10002e50` | `stdio_put_string` | `int stdio_put_string(const char*, int, bool, bool)` | buffered string output | -| `0x10002f3c` | `stdio_set_driver_enabled` | `void stdio_set_driver_enabled(stdio_driver_t*, bool)` | enable the UART driver | -| `0x10002f64` | `stdio_init_all` | `bool stdio_init_all(void)` | SDK serial init | -| `0x10003030` | `__wrap_vprintf` | `int __wrap_vprintf(const char*, va_list)` | printf core | -| `0x100030f4` | `__wrap_printf` | `int __wrap_printf(const char*, ...)` | the `printf` wrapper | -| `0x100032b0` | `stdio_uart_init` | `void stdio_uart_init(void)` | SDK UART stdio init | -| `0x100033f0` | `strlen` | `size_t strlen(const char*)` | C runtime string length | - -Project 2 uses the same `pico_double` formatting helpers as Project 1, shifted by eight bytes because `main` moved: - -| Address | ELF symbol | Signature | Role | -| ------- | ---------- | --------- | ---- | -| `0x1000138c` | `__wrap___aeabi_dadd` | `double __wrap___aeabi_dadd(double, double)` | double add | -| `0x100013b4` | `__wrap___aeabi_dsub` | `double __wrap___aeabi_dsub(double, double)` | double subtract | -| `0x100013dc` | `__wrap___aeabi_dmul` | `double __wrap___aeabi_dmul(double, double)` | double multiply | -| `0x10001428` | `__wrap___aeabi_ddiv` | `double __wrap___aeabi_ddiv(double, double)` | double divide | -| `0x100014c4` | `__wrap___aeabi_i2d` | `double __wrap___aeabi_i2d(int)` | int -> double | -| `0x100014e8` | `__wrap___aeabi_ui2d` | `double __wrap___aeabi_ui2d(unsigned)` | unsigned -> double | -| `0x1000150c` | `__wrap___aeabi_d2iz` | `int __wrap___aeabi_d2iz(double)` | double -> int | -| `0x10001530` | `__wrap___aeabi_d2uiz` | `unsigned __wrap___aeabi_d2uiz(double)` | double -> unsigned | -| `0x10001554` | `__wrap___aeabi_dcmpun` | `int __wrap___aeabi_dcmpun(double, double)` | unordered compare | -| `0x10001578` | `__wrap___aeabi_dcmplt` | `int __wrap___aeabi_dcmplt(double, double)` | less-than compare | -| `0x100015a0` | `__wrap___aeabi_dcmple` | `int __wrap___aeabi_dcmple(double, double)` | less-or-equal compare | -| `0x100015c8` | `__wrap___aeabi_dcmpge` | `int __wrap___aeabi_dcmpge(double, double)` | greater-or-equal compare | -| `0x100015f0` | `__wrap___aeabi_dcmpgt` | `int __wrap___aeabi_dcmpgt(double, double)` | greater-than compare | -| `0x10001614` | `_out_rev` | `unsigned _out_rev(out_fct_type, char*, size_t, size_t, const char*, size_t, unsigned, unsigned)` | reversed-digit output | -| `0x100016b0` | `_ntoa_format` | `unsigned _ntoa_format(out_fct_type, char*, size_t, size_t, char*, size_t, bool, unsigned, unsigned, unsigned, unsigned)` | number formatter | -| `0x10001884` | `_out_char` | `void _out_char(char, void*, size_t, size_t)` | single-char sink | -| `0x10001898` | `_ftoa` | `unsigned int _ftoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)` | fixed-point float formatter | -| `0x10001d58` | `_etoa` | `unsigned int _etoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)` | exponential float formatter | -| `0x100022cc` | `_vsnprintf` | `int _vsnprintf(out_fct_type, char*, size_t, const char*, va_list)` | the format dispatcher | -| `0x10000dc4` | `busy_wait_us` | `void busy_wait_us(uint64_t)` | UART timing loop | -| `0x10000ff4` | `clock_get_hz` | `unsigned long clock_get_hz(clock_handle_t)` | UART clock lookup | - -> **A literal pool is still a pool.** The `float`/`double` initializer does not survive as a C variable, but the compiler still has to place the IEEE-754 bit pattern somewhere. It parks the 32-bit word (or word pair) right after `main`'s code and reaches it with a PC-relative `ldr`/`ldrd`. That is why the value you patch is a `.word` in the image, not a stack store. - -### Step 5: Flash Project 1 and confirm `fav_num: 42.500000` - -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 0x000e_floating-point-data-type/build/0x000e_floating-point-data-type.bin -``` - -**Windows x64 (PowerShell):** - -```powershell -.\flash.ps1 -Bin 0x000e_floating-point-data-type\build\0x000e_floating-point-data-type.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, "0x000e_floating-point-data-type", "build", "0x000e_floating-point-data-type.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, "0x000e_floating-point-data-type", "build", "0x000e_floating-point-data-type.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 15308 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.500000 -fav_num: 42.500000 -fav_num: 42.500000 -... -``` - -The program prints `42.500000` because `printf` with `%f` defaults to six decimal places. - -### Step 6: Flash Project 2 and confirm `fav_num: 42.525250` - -```bash -# macOS / Linux -./flash.sh 0x0011_double-floating-point-data-type/build/0x0011_double-floating-point-data-type.bin -``` -```powershell -# Windows -.\flash.ps1 -Bin 0x0011_double-floating-point-data-type\build\0x0011_double-floating-point-data-type.bin -``` - -**Or flash from the IDA console** (same form as Step 5, pointing at the Project 2 `.bin`): - -**macOS / Linux:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) -bin_path = os.path.join(root, "0x0011_double-floating-point-data-type", "build", "0x0011_double-floating-point-data-type.bin") -log = os.path.join(os.path.dirname(bin_path), "flash.log") -subprocess.run(["pkill", "-TERM", "-f", "openocd"]) -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:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) -bin_path = os.path.join(root, "0x0011_double-floating-point-data-type", "build", "0x0011_double-floating-point-data-type.bin") -log = os.path.join(os.path.dirname(bin_path), "flash.log") -subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) -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 15324 bytes ...`. The serial monitor shows: - -``` -fav_num: 42.525250 -fav_num: 42.525250 -fav_num: 42.525250 -... -``` - -`42.52525` has a repeating binary fraction, so its 52 mantissa bits are not all zero. Remember that: it is why this value needs two words patched, and `42.5` needs only one. ---- - -## 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. - -### Step 7: Bring the raw `.bin` into IDA - -A raw `.bin` has no headers, so IDA cannot know where it belongs or what architecture it is. You must supply both. If you just double-click the `.bin`, IDA may load it with a guessed architecture, and every address in this lesson will be wrong. - -#### 1.1 Open the file -1. **File ▸ Open…** (Ctrl/Cmd+O). -2. Select the `.bin` file and click **Open**. - -IDA opens the **"Load a new file"** dialog because a raw `.bin` has no header. - -#### 1.2 Set the format and processor -In the *Load a new file* dialog: -1. **Format**: leave it as **Binary file** (IDA auto-detects a headerless image). -2. **Processor type**: click the **…** button next to the processor field. In the *Processor type* dialog: - - Family: **ARM** - - Type: **ARM Little-endian** (the 32-bit ARM module). - - Click **OK**. - -> If IDA asks **"Do you want to disassemble it as 64-bit code?"** — click **No**. -> The RP2350 is 32-bit; answering *Yes* loads AArch64 and you get `X` registers / data. (This is the single most common cause of the `DCB` wall.) - -#### 1.3 Configure the ARM architecture and Thumb mode -Still in the *Load a new file* dialog, open the processor options (the **Processor options** / **Set…** button, which opens the dialog titled **ARM architecture options**): -- **ARM architecture**: choose **ARMv8-M** (this is the Cortex-M33 / RP2350 core). -- **Thumb instructions**: select **Thumb-2** (ARMv8-M is Thumb-only, so this is the correct mode). -- Leave **automatic ARM-THUMB switch** off; we want Thumb throughout. - -#### 1.4 Set the loading address -- **Loading address**: enter **`10000000`** (hex; the RP2350 XIP flash base). - IDA rejects addresses that don't look like ROM/RAM with *"The loading address should belong to RAM or ROM"* — `0x10000000` is the correct flash window. - -Leave **Manual load** and **Create segments** at their defaults. Click **OK**. - -#### 1.5 Force Thumb, then seed the code from the vector table -A raw `.bin` has no header, so IDA's loader has **no entry point** and defaults the segment to **ARM mode** (which cannot decode Thumb). Two required steps — in this order: - -**A. Set the segment to Thumb (do this first).** -- **Edit ▸ Segments ▸ Set default segment register value…** → register **`T`** = **`1`** (or press **`Alt+G`**). IDA reanalyzes the segment as Thumb automatically. - -**B. Seed the code from the vector table.** -1. **G** → `10000000`. The first two words are the initial stack pointer and the **reset vector**. -2. **Read the 4-byte word at `0x10000004`** (do **not** assume its value — it differs per build). -3. **Clear bit 0** (the ARM **Thumb** flag) → the reset handler address. -4. **G** to that address → press **C** (**MakeCode**). IDA disassembles the reset handler and follows its `bl`s into `main`. - -Once `main` shows code, the import is correct. - -**Why IDA needs this and others don't:** IDA's generic raw-binary ARM loader treats the image as flat data and waits for you to set Thumb and mark the entry point. It's an IDA quirk, not a mistake on your part. - -### Step 8: Save it as a IDA database (`.i64`) - -IDA never writes back into the `.bin`. Your names, comments, types, and patches live in a separate **`.i64`** database. Save one now, before you make any changes: - -1. Choose `File -> Save As...`. -2. Save it next to the image as `0x000e_floating-point-data-type.i64`. -3. From now on, save with `File -> Save` (`Cmd+S` on macOS, `Ctrl+S` on Windows/Linux) whenever you rename or patch. - -The two files have different roles: - -| File | Role | -| ---- | ---- | -| `0x000e_floating-point-data-type.bin` | the raw firmware image; IDA never modifies it | -| `0x000e_floating-point-data-type.i64` | your analysis database: names, types, comments, and patches | - -When you come back later, **open the `.i64`**, not the `.bin`; that restores all your work. If a database gets messy, delete the `.i64` and re-import the `.bin` from Step 7 — the firmware is never at risk. You export the patched image out of this view later, in Step 19. - -### Step 9: The views you will use - -- **Linear view:** the disassembly listing. You navigate, read, and patch here. -- **Graph view:** the control-flow graph of the current function. -- **Decompiler (HLIL):** the pseudo-C decompilation. -- **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 13. - -> **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 (Project 1) - -### Step 10: 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 11. - -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 | Project 1 `0x000e` | Project 2 `0x0011` | Command | -> | --- | --- | --- | --- | -> | `main` (once per reset) | `0x10000234` | `0x10000238` | `BP_ADDR=0x10000234 ./debug-server.sh` | -> | The **loop** — the `printf` call, hit every iteration | `0x10000244` | `0x1000024a` | `BP_ADDR=0x10000244 ./debug-server.sh` | -> -> ```bash -> BP_ADDR=0x10000234 ./debug-server.sh # park at main -> BP_ADDR=0x10000244 ./debug-server.sh # park in the loop instead -> ``` -> -> ```powershell -> $env:BP_ADDR="0x10000234"; .\debug-server.ps1 # park at main -> $env:BP_ADDR="0x10000244"; .\debug-server.ps1 # park in the loop -> ``` -> -> **Note the loop address differs from Week 4 and between the two projects.** In Project 1 the `bl __wrap_printf` sits at `0x10000244`; in Project 2 it sits at `0x1000024a`, because Project 2 loads the pair with `ldrd` first. Both were verified against the Release `.elf` with `arm-none-eabi-objdump` and confirmed live on hardware. -> -> **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 13) 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 11: Connect IDA to the GDB server - -1. Make sure the image is open and analyzed (Part 2) and the server from Step 10 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 10), 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 12: Find `main` without relying on its address - -`main` moves between these programs (`0x10000234` vs `0x10000238`), 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 -``` - -**The middle `blx` at `0x1000018c` is the call to `main`.** `platform_entry` is byte-identical in both projects, so `0x1000018c` catches `main` no matter where the linker placed it. The literal pool at `0x100001dc` holds `main | 1`; clearing bit 0 gives `0x10000234` for Project 1 and `0x10000238` for Project 2. - -### Step 13: 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,,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`** | Project 1 `0x10000234`, Project 2 `0x10000238` | The server starts parked there with `BP_ADDR` (Step 10), so IDA is already stopped at `main` when it connects. | No — `main` runs once per reset. | -| **The loop** (`printf` call) | Project 1 `0x10000244`, Project 2 `0x1000024a` | 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 (`0x10000244` for Project 1, `0x1000024a` for Project 2), 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 0x10000244`. - -> **No breakpoints before you connect.** With GDB MI, a breakpoint set before the connection hangs the session (Step 11). 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: `0x10000244 -> 0x100030ec -> 0x100030ee -> ...`. - -> **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 14 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 2 hw` still arms a breakpoint, `rbp ` / `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 14: HACK IT LIVE — change the printed double - -Project 1's `main` sets `r4 = 0`, loads the high word of the double into `r5`, and on every iteration copies them into the `r2:r3` argument pair before calling `printf`. We break on that call and change the value live: - -1. Press `G`, go to `0x10000244` (the `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 `0x10000244`, `r2 = 0x00000000`, and `r3 = 0x40454000`. -4. Open the **Registers** widget (bug icon -> **Registers**). -5. The ABI passes the promoted `double` in `r2:r3` — `r2` is the low 32 bits, `r3` the high 32 bits. Together they are `0x40454000_00000000`, the IEEE-754 encoding of `42.5`. -6. **Set `r3` to `0x4058C000`** — the high word of `99.0` — from IDA's Python console (`Plugins -> Python Console`): - ```python - dbg.set_reg_value("r2", 0x00000000) # low word of the 99.0 double (unchanged) - dbg.set_reg_value("r3", 0x4058C000) # high word of the 99.0 double - ``` - `dbg.set_reg_value(name, value)` writes one register (returns `True` on success). You can also right-click a register in the **Registers** widget, press `E` (edit), type the hex value, and press Enter. The widget may not repaint, 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 `0x10000244` to the instruction *after* the call, `0x10000248` (the `b.n` that closes the loop): remove the breakpoint at `0x10000244` and set a hardware breakpoint at `0x10000248`. 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 `r2:r3 = 0x4058C000_00000000`, so this iteration prints `fav_num: 99.000000`, then stops at `0x10000248`. -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: 99.000000 - ``` - -You changed a running program's output without touching the binary. - -### Step 14b: HACK THE STRING LIVE — change `fav_num:` to `myvalue:` - -The text `"fav_num: %f\r\n"` lives in flash (`.rodata`) at `0x100034a8`, 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 14 steps 1-3. At the stop, `r0 = 0x100034a8`, `r2 = 0x00000000`, and `r3 = 0x40454000`. -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"myvalue: %f\r\n\x00") - ``` - `dbg.write_memory(address, bytes)` is IDA's debugger memory-write API; it returns `True` on success. That writes the bytes `6d 79 76 61 6c 75 65 3a 20 25 66 0d 0a 00` = `"myvalue: %f\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 `r3` to `0x4058C000` as in Step 14. Then move the breakpoint past the call in the GUI (remove it at `0x10000244`, set one at `0x10000248`) and click **Resume**. The core runs `printf` with `r0` pointing at your RAM string and `r2:r3` holding `99.0`, so this iteration prints: - ``` - myvalue: 99.000000 - ``` - then stops at `0x10000248`. - -Like the value hack, this is **one iteration only**: the loop reloads `r0` (and `r2`/`r3`) from flash/literals on every pass, so the next line is `fav_num: 42.500000` again. The permanent version is the static patch in Step 18b. - -### Step 15: Why the hack reverts (and why we patch next) - -Press **Resume**. The loop branches back to `0x1000023e`, which copies `r4` and `r5` into `r2` and `r3` again. `r4` and `r5` were set once before the loop (`movs r4, #0`, `ldr r5, [pc, #12]`), so your `r3` edit is overwritten and the next line is `fav_num: 42.500000`. The live edit changed one iteration only. There is no variable in memory to change; the value is baked into the instruction/literal pool. To make `99.0` permanent we must patch the constant. That is the static pass. - -Press **Pause** to stop the output flood. - -### Step 15b: 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 (Project 1) - -### Step 16: Resolve the functions in the IDA GUI - -We now name the functions in IDA using the ELF symbol map from Step 4. IDA loaded the raw `.bin` with **no symbols**, so every function shows as `sub_` — resolving means giving each one its real name and signature. - -Three keys do all the work: - -| Key | IDA action | Use it for | -| --- | --- | --- | -| `G` | Go to address | Jump to a function's address | -| `Y` | **Change Type** | Set the function's signature. The dialog shows the full prototype, so this sets the name *and* the type in one step. | -| `N` | Rename | Rename only, when you just want the name and not the type | - -For each function below: `G` to its address, then **`Y` (Change Type)** and type the prototype from the table. - -#### How to resolve a function in IDA (`Y`) - -`Y` is the **Change Type** key, and it is what actually resolves the function — it turns `void sub_10002f5c()` into `bool stdio_init_all(void)`. The Change Type dialog shows the full declaration (name and type), so typing the prototype sets both: - -1. `G` to the function's address. The cursor lands on the function. -2. Press **`Y`**. In the Change Type dialog, type the prototype from the table exactly — for example `bool stdio_init_all(void)` — and press Enter. - -The decompiler header then shows the real prototype, and calls to the function read cleanly instead of `sub_()`. `N` is only for renaming without touching the type; `Y` alone sets both the name and the type. - -If `Y` seems to do nothing, confirm the cursor is on the function, or right-click it and pick **Change Type...**. IDA parses what you type and silently keeps the old type if it does not parse, so glance at the header after each `Y`. - -#### Worked example: `main` - -1. Press `G`, type `0x10000234`, press Enter. The view jumps there; the cursor lands on `sub_10000234`. -2. Press **`Y`** (Change Type), type `int main(void)`, press Enter. That sets the name to `main` and the type to `int(void)`. - -> **IDA shows `int32_t` where Ghidra shows `int`.** After you set `int main(void)`, the decompiler header may read `int32_t main(void)`. That is the same type — on this platform `int` is 32 bits and IDA's parser normalises it to `int32_t`. Do not fight it; it is not an error. - -#### Worked example: `stdio_init_all` - -1. `G` -> `0x10002f5c`. -2. `Y` -> `bool stdio_init_all(void)`. - -It returns **`bool`**, not `void` — the ELF says `_Bool stdio_init_all(void)`. Our `main` ignores the return value, so the decompiler still reads cleanly. - -#### Worked example: `uart_init` - -1. `G` -> `0x10000e18`. -2. `Y` -> `uint uart_init(uart_inst_t *uart, uint baudrate)`. - -#### Worked example: `__wrap_printf` - -1. `G` -> `0x100030ec`. -2. `Y` -> `int __wrap_printf(const char *fmt, ...)`. Keep the `...` — `printf` is variadic. - -> **`printf` in our source is `__wrap_printf` in the binary.** The SDK links our `printf` calls to its `__wrap_printf` wrapper. - -#### Worked example: `_ftoa` (the double formatter) - -1. `G` -> `0x10001890`. -2. `Y` -> `unsigned int _ftoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)`. - -This is the function that actually turns the double into the `42.500000` text. It is why the `pico_double` `__aeabi_*` helpers are in the image at all. - -The rest of the chain is the same two keystrokes per function (`G`, then `Y`). This is **our code plus the library functions it actually calls** — not the whole SDK. `main` only calls `stdio_init_all` and `printf`, so we follow that chain down: `stdio_init_all` pulls in the stdio/UART setup, and `printf` lands in the SDK's `__wrap_printf`, which reaches the `pico_double` formatter. - -The call chain for this project: - -``` -main -├── stdio_init_all -│ └── stdio_uart_init ── gpio_set_function, uart_init, stdio_set_driver_enabled -│ └── uart_init ── clock_get_hz, busy_wait_us -└── __wrap_printf ── __wrap_vprintf - ├── vfctprintf ── _vsnprintf - │ ├── _ftoa / _etoa ── __wrap___aeabi_* (the pico_double helpers) - │ └── _ntoa_format / _out_rev - ├── stdio_out_chars_crlf - └── time_us_64 -``` - -**Project 1 — resolve every function in that chain:** - -| Address | Rename to (`N`) | Signature (`Y`) | -| ------- | --------------- | --------------- | -| `0x1000015c` | `_reset_handler` | `void _reset_handler(void)` | -| `0x10000186` | `platform_entry` | `void platform_entry(void)` | -| `0x1000019a` | `data_cpy` | `void data_cpy(void*, void*, void*)` | -| `0x100001e4` | `_init` | `void _init(void)` | -| `0x10000210` | `frame_dummy` | `void frame_dummy(void)` | -| **`0x10000234`** | **`main`** | **`int main(void)`** | -| `0x10002ca8` | `vfctprintf` | `int vfctprintf(void (*)(char, void*), void*, const char*, va_list)` | -| `0x10002d04` | `exit` | `void exit(int)` | -| `0x10002d0c` | `runtime_init` | `void runtime_init(void)` | -| `0x10002d38` | `stdio_out_chars_crlf` | `void stdio_out_chars_crlf(stdio_driver_t*, const char*, int)` | -| `0x10002e48` | `stdio_put_string` | `int stdio_put_string(const char*, int, bool, bool)` | -| `0x10002f34` | `stdio_set_driver_enabled` | `void stdio_set_driver_enabled(stdio_driver_t*, bool)` | -| `0x10002f5c` | `stdio_init_all` | `bool stdio_init_all(void)` | -| `0x10003028` | `__wrap_vprintf` | `int __wrap_vprintf(const char*, va_list)` | -| `0x100030ec` | `__wrap_printf` | `int __wrap_printf(const char*, ...)` | -| `0x100032a8` | `stdio_uart_init` | `void stdio_uart_init(void)` | -| `0x100033e8` | `strlen` | `size_t strlen(const char*)` | -| `0x10000254` | `gpio_set_function` | `void gpio_set_function(uint, gpio_function_t)` | -| `0x10000e18` | `uart_init` | `uint uart_init(uart_inst_t*, uint)` | -| `0x10000da8` | `time_us_64` | `uint64_t time_us_64(void)` | - -**Project 1 — resolve the `pico_double` formatting helpers `printf` reaches:** - -| Address | Rename to (`N`) | Signature (`Y`) | -| ------- | --------------- | --------------- | -| `0x10001384` | `__wrap___aeabi_dadd` | `double __wrap___aeabi_dadd(double, double)` | -| `0x100013ac` | `__wrap___aeabi_dsub` | `double __wrap___aeabi_dsub(double, double)` | -| `0x100013d4` | `__wrap___aeabi_dmul` | `double __wrap___aeabi_dmul(double, double)` | -| `0x10001420` | `__wrap___aeabi_ddiv` | `double __wrap___aeabi_ddiv(double, double)` | -| `0x100014bc` | `__wrap___aeabi_i2d` | `double __wrap___aeabi_i2d(int)` | -| `0x100014e0` | `__wrap___aeabi_ui2d` | `double __wrap___aeabi_ui2d(unsigned)` | -| `0x10001504` | `__wrap___aeabi_d2iz` | `int __wrap___aeabi_d2iz(double)` | -| `0x10001528` | `__wrap___aeabi_d2uiz` | `unsigned __wrap___aeabi_d2uiz(double)` | -| `0x1000154c` | `__wrap___aeabi_dcmpun` | `int __wrap___aeabi_dcmpun(double, double)` | -| `0x10001570` | `__wrap___aeabi_dcmplt` | `int __wrap___aeabi_dcmplt(double, double)` | -| `0x10001598` | `__wrap___aeabi_dcmple` | `int __wrap___aeabi_dcmple(double, double)` | -| `0x100015c0` | `__wrap___aeabi_dcmpge` | `int __wrap___aeabi_dcmpge(double, double)` | -| `0x100015e8` | `__wrap___aeabi_dcmpgt` | `int __wrap___aeabi_dcmpgt(double, double)` | -| `0x1000160c` | `_out_rev` | `unsigned _out_rev(out_fct_type, char*, size_t, size_t, const char*, size_t, unsigned, unsigned)` | -| `0x100016a8` | `_ntoa_format` | `unsigned _ntoa_format(out_fct_type, char*, size_t, size_t, char*, size_t, bool, unsigned, unsigned, unsigned, unsigned)` | -| `0x1000187c` | `_out_char` | `void _out_char(char, void*, size_t, size_t)` | -| `0x10001890` | `_ftoa` | `unsigned int _ftoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)` | -| `0x10001d50` | `_etoa` | `unsigned int _etoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)` | -| `0x100022c4` | `_vsnprintf` | `int _vsnprintf(out_fct_type, char*, size_t, const char*, va_list)` | -| `0x10000dbc` | `busy_wait_us` | `void busy_wait_us(uint64_t)` | -| `0x10000fec` | `clock_get_hz` | `unsigned long clock_get_hz(clock_handle_t)` | - -> **A `void` return type may not stick — here is the fix.** IDA treats `void` as low-confidence, and its analysis can override it with an inferred type — most often `int32_t` on this 32-bit target. It is most visible on `_reset_handler` (a hand-written assembly entry that never returns normally), but it can happen to **any** function whose return type IDA thinks it can infer. -> -> Setting the full signature with `Y` reproduces the unwanted `int32_t`, and `fn.return_type = ...` fails too. What works is the **return-value** setter: -> -> ```python -> from binaryninja import ReturnValue, Type -> fn = bv.get_function_at(0x1000015c) -> if fn is not None: -> fn.return_value = ReturnValue(Type.void()) -> ``` -> -> That holds `_reset_handler` at `void` even after reanalysis. If it still will not stick, leave it — it does not affect the rest of the lesson. - -> **`__wrap_printf` is the real symbol.** `printf` in our source compiles to the SDK's `__wrap_printf` (which forwards to `__wrap_vprintf`). Rename it `printf` if you prefer the lesson's shorthand, but `__wrap_printf` is what the ELF says. -> -> **`stdio_init_all` returns `bool`, not `void`** — `_Bool stdio_init_all(void)` in the ELF. - -> **Shortcut — resolves name *and* type for every function.** Instead of doing `N` + `Y` by hand, paste this into IDA's Python console (`Plugins -> Python Console`). It sets each function's name and signature programmatically: -> -> ```python -> from binaryninja import Symbol, SymbolType -> # The raw .bin has no headers, so these SDK types don't exist. set_user_type() -> # re-parses each signature as C, so an undefined name raises -> # "SyntaxError: unknown type name '...'". Define them first. -> sdk = bv.parse_types_from_string(""" -> typedef unsigned int uint; -> typedef char* va_list; -> typedef unsigned long clock_handle_t; -> typedef void (*out_fct_type)(char, void*, size_t, size_t); -> struct stdio_driver; -> typedef struct stdio_driver stdio_driver_t; -> struct uart_inst; -> typedef struct uart_inst uart_inst_t; -> enum gpio_function { -> GPIO_FUNC_XIP = 0, GPIO_FUNC_SPI = 1, GPIO_FUNC_UART = 2, GPIO_FUNC_I2C = 3, -> GPIO_FUNC_PWM = 4, GPIO_FUNC_SIO = 5, GPIO_FUNC_PIO0 = 6, GPIO_FUNC_PIO1 = 7, -> GPIO_FUNC_GPCK = 8, GPIO_FUNC_USB = 9, GPIO_FUNC_NULL = 0x1f, -> }; -> typedef enum gpio_function gpio_function_t; -> """) -> for name, ty in sdk.types.items(): -> bv.define_user_type(name, ty) -> -> # address: (name, signature) -> funcs = { -> 0x1000015c: ("_reset_handler", "void _reset_handler(void)"), -> 0x10000186: ("platform_entry", "void platform_entry(void)"), -> 0x1000019a: ("data_cpy", "void data_cpy(void*, void*, void*)"), -> 0x100001e4: ("_init", "void _init(void)"), -> 0x10000210: ("frame_dummy", "void frame_dummy(void)"), -> 0x10000234: ("main", "int main(void)"), -> 0x10002ca8: ("vfctprintf", "int vfctprintf(void (*)(char, void*), void*, const char*, va_list)"), -> 0x10002d04: ("exit", "void exit(int)"), -> 0x10002d0c: ("runtime_init", "void runtime_init(void)"), -> 0x10002d38: ("stdio_out_chars_crlf", "void stdio_out_chars_crlf(stdio_driver_t*, const char*, int)"), -> 0x10002e48: ("stdio_put_string", "int stdio_put_string(const char*, int, bool, bool)"), -> 0x10002f34: ("stdio_set_driver_enabled", "void stdio_set_driver_enabled(stdio_driver_t*, bool)"), -> 0x10002f5c: ("stdio_init_all", "bool stdio_init_all(void)"), -> 0x10003028: ("__wrap_vprintf", "int __wrap_vprintf(const char*, va_list)"), -> 0x100030ec: ("__wrap_printf", "int __wrap_printf(const char*, ...)"), -> 0x100032a8: ("stdio_uart_init", "void stdio_uart_init(void)"), -> 0x100033e8: ("strlen", "size_t strlen(const char*)"), -> 0x10000254: ("gpio_set_function", "void gpio_set_function(uint, gpio_function_t)"), -> 0x10000e18: ("uart_init", "uint uart_init(uart_inst_t*, uint)"), -> 0x10000da8: ("time_us_64", "uint64_t time_us_64(void)"), -> 0x10001384: ("__wrap___aeabi_dadd", "double __wrap___aeabi_dadd(double, double)"), -> 0x100013ac: ("__wrap___aeabi_dsub", "double __wrap___aeabi_dsub(double, double)"), -> 0x100013d4: ("__wrap___aeabi_dmul", "double __wrap___aeabi_dmul(double, double)"), -> 0x10001420: ("__wrap___aeabi_ddiv", "double __wrap___aeabi_ddiv(double, double)"), -> 0x100014bc: ("__wrap___aeabi_i2d", "double __wrap___aeabi_i2d(int)"), -> 0x100014e0: ("__wrap___aeabi_ui2d", "double __wrap___aeabi_ui2d(unsigned)"), -> 0x10001504: ("__wrap___aeabi_d2iz", "int __wrap___aeabi_d2iz(double)"), -> 0x10001528: ("__wrap___aeabi_d2uiz", "unsigned __wrap___aeabi_d2uiz(double)"), -> 0x1000154c: ("__wrap___aeabi_dcmpun", "int __wrap___aeabi_dcmpun(double, double)"), -> 0x10001570: ("__wrap___aeabi_dcmplt", "int __wrap___aeabi_dcmplt(double, double)"), -> 0x10001598: ("__wrap___aeabi_dcmple", "int __wrap___aeabi_dcmple(double, double)"), -> 0x100015c0: ("__wrap___aeabi_dcmpge", "int __wrap___aeabi_dcmpge(double, double)"), -> 0x100015e8: ("__wrap___aeabi_dcmpgt", "int __wrap___aeabi_dcmpgt(double, double)"), -> 0x1000160c: ("_out_rev", "unsigned _out_rev(out_fct_type, char*, size_t, size_t, const char*, size_t, unsigned, unsigned)"), -> 0x100016a8: ("_ntoa_format", "unsigned _ntoa_format(out_fct_type, char*, size_t, size_t, char*, size_t, bool, unsigned, unsigned, unsigned, unsigned)"), -> 0x1000187c: ("_out_char", "void _out_char(char, void*, size_t, size_t)"), -> 0x10001890: ("_ftoa", "unsigned int _ftoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)"), -> 0x10001d50: ("_etoa", "unsigned int _etoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)"), -> 0x100022c4: ("_vsnprintf", "int _vsnprintf(out_fct_type, char*, size_t, const char*, va_list)"), -> 0x10000dbc: ("busy_wait_us", "void busy_wait_us(uint64_t)"), -> 0x10000fec: ("clock_get_hz", "unsigned long clock_get_hz(clock_handle_t)"), -> } -> for addr, (name, sig) in funcs.items(): -> bv.define_user_symbol(Symbol(SymbolType.FunctionSymbol, addr, name)) -> f = bv.get_function_at(addr) -> if f is not None: -> f.set_user_type(sig) -> ``` -> -> SDK type names (`stdio_driver_t`, `gpio_function_t`, `uart_inst_t`, plus `uint`, `va_list`, `out_fct_type`, and `clock_handle_t`) are **not** in the raw `.bin`. `set_user_type` re-parses each signature as C, so an undefined name raises `SyntaxError: unknown type name '...'` and stops the loop — it is not harmless. The `sdk` block above defines them first (an opaque `struct`/`enum`/`typedef` is enough to parse). If you add a function that uses another SDK type, add a definition for it to that block too. - -### Step 17: Read `main` in the decompiler - -Open the **Decompiler** view on `main`. It reads: - -```c -int32_t main(void) -{ - stdio_init_all(); - do - { - __wrap_printf("fav_num: %f\r\n", 0, 0x40454000); - } while (true); -} -``` - -The trailing pair is the promoted `double`: `r2 = 0`, `r3 = 0x40454000`. IDA already knows the calling convention, so once `__wrap_printf` is typed `int __wrap_printf(const char*, ...)`, the pair is shown as data. Now make the hack permanent. - -### Step 18: Patch `0x40454000` to `0x4058C000` in the GUI - -Go to `0x1000024c`: - -```asm -1000024c: 40454000 .word 0x40454000 -``` - -That word is the high half of the `double` `42.5`. Its bytes, little-endian, are `00 40 45 40`. Change them to `00 c0 58 40`, the high half of `99.0`: - -| Project | Address | Before | After | Effect | -| ------- | ------- | ------ | ----- | ------ | -| `0x000e` | `0x1000024c` | `00 40 45 40` | `00 c0 58 40` | high word of `42.5` -> `99.0` | - -**Option A — Hex view:** - -1. Switch to the **Hex** view (`View -> Hex`). -2. Toggle the lock off so editing is enabled. -3. Go to `0x1000024c` and change `00 40 45 40` to `00 c0 58 40`. -4. Return to the linear view, right-click the function -> `Reanalyze`. - -**Option B — Python console:** - -```python -bv.write(0x1000024c, bytes.fromhex("00c05840")) -print(bv.read(0x1000024c, 4)) # -> b'\x00\xc0X@' -``` - -After reanalysis the literal reads `0x4058C000`, and the decompiler shows `__wrap_printf("fav_num: %f\r\n", 0, 0x4058c000)`. - -### Step 18b: Patch the string `fav_num:` to `myvalue:` in the GUI - -The format string `"fav_num: %f\r\n"` starts at `0x100034a8`. Its first eight bytes are `66 61 76 5f 6e 75 6d 3a` (`fav_num:`). Change them to `6d 79 76 61 6c 75 65 3a` (`myvalue:`), leaving the ` %f\r\n` tail untouched, so the line prints `myvalue: 99.000000`. - -| Project | Address | Before | After | Effect | -| ------- | ------- | ------ | ----- | ------ | -| `0x000e` | `0x100034a8` | `66 61 76 5f 6e 75 6d 3a` | `6d 79 76 61 6c 75 65 3a` | `fav_num:` -> `myvalue:` | - -**Option A — Hex view:** - -1. Switch to the **Hex** view (`View -> Hex`). -2. Go to `0x100034a8` and change the eight bytes `66 61 76 5f 6e 75 6d 3a` to `6d 79 76 61 6c 75 65 3a`. -3. Return to the linear view and reanalyze. - -**Option B — Python console:** - -```python -bv.write(0x100034a8, b"myvalue:") -print(bv.read(0x100034a8, 15)) # -> b'myvalue: %f\r\n\x00' -``` - -Keep the replacement exactly eight bytes. If you use a shorter string you must pad it, or `%f` shifts and `printf` reads the wrong argument. A longer string would overwrite the ` %f` tail. - -### Step 19: Export the patched `.bin` - -```python -import os -seg = next(s for s in bv.segments if s.data_length) # the loadable image segment -data = bv.read(seg.start, seg.data_length) # base + size come from the view itself -out = os.path.join(os.path.join(root, "0x000e_floating-point-data-type", "build"), "0x000e_floating-point-data-type-h.bin") -open(out, "wb").write(data) -print(len(data), out) # -> 15308 /.../build/0x000e_floating-point-data-type-h.bin -``` - -Where the two numbers come from — nothing is hardcoded: - -- **`seg.start`** is the image base IDA loaded the `.bin` at (`0x10000000`), the same value you pass to `uf2conv --base`. -- **`seg.data_length`** is the segment's size in the file (`0x3bcc` = 15308). Exactly one segment carries data (the image); every peripheral and synthetic segment has `data_length == 0`, so `next(...)` picks the image. -- Reading `seg.start` for `seg.data_length` bytes therefore grabs exactly the image. - -Two gotchas this avoids: - -- **No relative path.** IDA's Python console runs with a read-only working directory (inside the app bundle), so `open("0x000e_floating-point-data-type-h.bin", "wb")` fails with `OSError: [Errno 30] Read-only file system`. `root` (from `~/.embedded-hacking-repo`, Step 3) is the repo, so the file is written into the project's `build/` — no machine-specific path and no database needed. -- **Read the image, not the whole view.** `bv.read(bv.start, bv.length)` spans the entire mapped range, which is not the image. The segment's `data_length` is the image size. - -A different size means you exported a partial view. - -### Step 20: Convert to UF2 - -Run from the project directory: - -**macOS Apple Silicon / Linux x64:** - -```bash -python3 ../uf2conv.py 0x000e_floating-point-data-type-h.bin \ - --base 0x10000000 --family 0xe48bff59 --output hacked.uf2 -``` - -**Windows x64:** - -```cmd -python ..\uf2conv.py 0x000e_floating-point-data-type-h.bin ^ - --base 0x10000000 --family 0xe48bff59 --output hacked.uf2 -``` - -> **Or convert from the IDA console** — it is a normal Python interpreter, so you never have to leave the app. `chdir` to a writable directory first (the default one is read-only), then run the script: -> -> ```python -> import os, sys, runpy -> os.chdir(os.path.join(root, "0x000e_floating-point-data-type", "build")) # the project build dir (writable) -> sys.argv = ["uf2conv.py", "0x000e_floating-point-data-type-h.bin", -> "--base", "0x10000000", "--family", "0xe48bff59", "--output", "hacked.uf2"] -> runpy.run_path("../../uf2conv.py", run_name="__main__") # path to your uf2conv.py -> ``` -> -> This writes `hacked.uf2` next to the `.bin`, ready to drag onto the Pico. - -### Step 21: Flash and verify `fav_num: 99.000000` - -Hold **BOOTSEL**, plug in the Pico 2, and drag `hacked.uf2` onto the **`RP2350`** drive. Open the serial monitor: - -``` -myvalue: 99.000000 -myvalue: 99.000000 -myvalue: 99.000000 -... -``` - -**42.5 became 99.0, permanently, with one 32-bit word changed and 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 0x10000000 verify reset exit`), so you never touch BOOTSEL or a UF2. Run it from a terminal (`./flash.sh `), 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, "0x000e_floating-point-data-type", "build"), "0x000e_floating-point-data-type-h.bin") -> log = os.path.join(os.path.join(root, "0x000e_floating-point-data-type", "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, "0x000e_floating-point-data-type", "build"), "0x000e_floating-point-data-type-h.bin") -> log = os.path.join(os.path.join(root, "0x000e_floating-point-data-type", "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 `) the same way. ---- - -## Part 5: Dynamic — Break at `main` and Hack Live (Project 2) - -### Step 22: Reflash Project 2 and reload IDA - -Part 4 left the Pico running the patched Project 1 image. Put the original Project 2 back and start a fresh session. - -1. Stop any running debug server so the flash script can use the probe: - - ```bash - # macOS / Linux - pkill -TERM -f openocd - ``` - ```powershell - # Windows - Get-Process openocd -ErrorAction SilentlyContinue | Stop-Process - ``` - -2. Flash the original Project 2 image: - - ```bash - # macOS / Linux - ./flash.sh 0x0011_double-floating-point-data-type/build/0x0011_double-floating-point-data-type.bin - ``` - ```powershell - # Windows - .\flash.ps1 -Bin 0x0011_double-floating-point-data-type\build\0x0011_double-floating-point-data-type.bin - ``` - - **Or do steps 1-2 from the IDA console** (the active view is still Project 1, so take the repo root from the marker file and point at the Project 2 `.bin`): - - **macOS / Linux:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) - bin_path = os.path.join(root, "0x0011_double-floating-point-data-type", "build", "0x0011_double-floating-point-data-type.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 Project 2 in the background; log:", log) - ``` - - **Windows:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) - bin_path = os.path.join(root, "0x0011_double-floating-point-data-type", "build", "0x0011_double-floating-point-data-type.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 Project 2 in the background; log:", log) - ``` - -3. Start the debug server again (Step 10) and wait for `Listening on port 3333`. -4. Load Project 2 and save its database — see Step 22b. -5. Connect IDA again (Step 11): adapter **GDB MI**, IP `127.0.0.1`, port `3333`. - -Confirm the Pico prints `fav_num: 42.525250`. - -### Step 22b: Load Project 2 into IDA and save the database - -Exactly like Steps 7-8, but for Project 2. **Use `File -> Open with Options...`** (not plain `File -> Open`), select `0x0011_double-floating-point-data-type/build/0x0011_double-floating-point-data-type.bin`, and set: - -- **Architecture:** `thumb2` -- **Platform:** `thumb2` -- **Base Address:** `0x10000000` - -Click **Open**. Then press `G`, type `0x10000000`, and confirm the first two words: - -``` -0x10000000 0x20082000 initial stack pointer -0x10000004 0x1000015d reset vector (bit 0 = Thumb) -``` - -If you see data at `0x00000000`, close the tab and redo it with `Open with Options`. - -Save it with `File -> Save As...` as `0x0011_double-floating-point-data-type.i64` (next to the `.bin`). From now on open the `.i64`, not the `.bin`; save with `Cmd+S` / `Ctrl+S` after every rename or patch. - -> **Console equivalent:** -> ```python -> load("0x0011_double-floating-point-data-type/build/0x0011_double-floating-point-data-type.bin", -> options={"loader.imageBase": 0x10000000, "loader.platform": "thumb2"}) -> ``` - -Then resolve the functions for Project 2 the same way as Project 1 — Step 26. - -### Step 23: Break at `main` - -`main` is at `0x10000238` in this project. The GUI sets breakpoints fine (Step 13); the only caution is not to drive `reset run` from the port while IDA is attached (it desyncs IDA's view). Use `BP_ADDR`, which arms `main` before IDA connects: - -1. Stop the server (Ctrl-C), then start it parked at `main`: - ```bash - # macOS / Linux - BP_ADDR=0x10000238 ./debug-server.sh - ``` - ```powershell - # Windows - $env:BP_ADDR="0x10000238"; .\debug-server.ps1 - ``` - - **Or restart it from the IDA console** — kill any running server, then start it parked at `main`: - - **macOS / Linux:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() - log = os.path.join(root, "openocd.log") - subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # kill any running server first - subprocess.Popen([os.path.join(root, "debug-server.sh")], cwd=root, - env=dict(os.environ, BP_ADDR="0x10000238"), - stdout=open(log, "w"), stderr=subprocess.STDOUT, start_new_session=True) - print("OpenOCD restarted parked at main; log:", log) - ``` - - **Windows:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() - log = os.path.join(root, "openocd.log") - subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # kill any running server first - subprocess.Popen(["powershell", "-ExecutionPolicy", "Bypass", "-File", - os.path.join(root, "debug-server.ps1")], cwd=root, - env=dict(os.environ, BP_ADDR="0x10000238"), - stdout=open(log, "w"), stderr=subprocess.STDOUT) - print("OpenOCD restarted parked at main; log:", log) - ``` -2. Connect IDA (Step 11): adapter **GDB MI**, IP `127.0.0.1`, port `3333`. - -The target is already halted at `main` when IDA connects, and the sidebar reads `Stopped at 0x10000238`. - -> If you instead want to reach `main` on an already-connected session, you must Detach, send `reset run` from the port, then reconnect. Arming `main` and resetting while attached leaves the sidebar showing a stale address. - -`main` loads the whole 64-bit `double` from a single literal-pool address, then loops: copy the pair into `r2:r3` and call `printf`. The whole thing is one function: - -```asm -10000238: b538 push {r3, r4, r5, lr} -1000023a: a506 add r5, pc, #24 ; adr r5, 0x10000254 -1000023c: e9d5 4500 ldrd r4, r5, [r5] ; r4 = low word, r5 = high word -10000240: f002 fe90 bl 0x10002f64 ; stdio_init_all -10000244: 4622 mov r2, r4 -10000246: 462b mov r3, r5 -10000248: 4801 ldr r0, [pc, #4] ; -> 0x10000250 = 0x100034b0 (format string) -1000024a: f002 ff53 bl 0x100030f4 ; __wrap_printf -1000024e: e7f9 b.n 0x10000244 -10000250: 100034b0 .word 0x100034b0 -10000254: 645a1cac .word 0x645a1cac -10000258: 4045433b .word 0x4045433b -``` - -Notice the `ldrd r4, r5, [r5]` — a single 64-bit load from the literal pool at `0x10000254`, which fills **both** halves of the double at once. That is the structural difference from Project 1, where the low half was a register zero (`movs r4, #0`) and only the high half lived in the pool. - -Look at the **Registers** widget at `0x1000024a`: `r2 = 0x645A1CAC` (low) and `r3 = 0x4045433B` (high). Together that is `0x4045433B_645A1CAC`, the IEEE-754 encoding of `42.52525`. - -### Step 24: Inspect the double argument live - -The `double` crosses the ABI in two registers. At the `printf` call the pair is exactly the literal-pool word pair: - -| Register | Value | Role | -| -------- | ----- | ---- | -| `r2` | `0x645A1CAC` | low 32 bits of `42.52525` | -| `r3` | `0x4045433B` | high 32 bits of `42.52525` | - -Step Over through `0x10000244` (`mov r2, r4`) and `0x10000246` (`mov r3, r5`) and watch `r2`/`r3` populate from `r4`/`r5`. This is C's variadic rule in action: `printf`'s `%lf` receives a 64-bit `double`, and on this target a `double` argument travels in `r2:r3`. - -> **Why `r2:r3`, not `r0:r1`?** The first variadic argument goes after the named format pointer, so `printf(fmt, value)` puts `fmt` in `r0` and the promoted `double` in `r2:r3`. That leaves `r1` unused here, which is why the format string pointer is `r0` and the number is `r2:r3`. - -### Step 25: HACK IT LIVE — change the printed double - -1. Press `G`, go to `0x1000024a` (the `bl __wrap_printf`). -2. Set a **hardware execute** breakpoint at `0x1000024a` in the GUI (`Debugger -> Add Hardware Breakpoint...`; not `F2`). Note `0x1000024a` — Project 2's loop sits at a different address than Project 1's. -3. Click **Resume** in IDA. The target is already looping, so the breakpoint fires on the next pass. IDA stops with `r2 = 0x645A1CAC`, `r3 = 0x4045433B`. -4. Overwrite both halves with the encoding of `99.99` (`0x4058FF5C_28F5C28F`): - ```python - dbg.set_reg_value("r2", 0x28F5C28F) # low word of the 99.99 double - dbg.set_reg_value("r3", 0x4058FF5C) # high word of the 99.99 double - ``` - (Or right-click each register in the **Registers** widget, press `E`, type the hex value, and press Enter.) -5. **Move the breakpoint past the call.** `0x1000024e` is the instruction right after the `bl __wrap_printf`. Remove the breakpoint at `0x1000024a` and set a hardware breakpoint at `0x1000024e`, then click **Resume**. The core runs `printf` with `r2:r3 = 0x4058FF5C_28F5C28F` and stops at `0x1000024e`. (Not **Step Over** — it steps into the call on this symbol-less `.bin`, and a breakpoint left on the current PC re-traps the step; Step 13 explains both.) -6. Look at your serial monitor and the **Target** tab: - - ``` - fav_num: 99.990000 - ``` - -Press **Resume** and the next iteration prints `fav_num: 42.525250` again, because the loop reloads `r2`/`r3` from `r4`/`r5` each pass. The live hack is temporary; the static patch makes it permanent. - -### Step 25b: HACK THE STRING LIVE — change `fav_num:` to `myvalue:` - -Same idea as Project 1, different addresses. Here the format string is at `0x100034b0` and the `printf` call is at `0x1000024a`. - -1. Hit the breakpoint at `0x1000024a` as in Step 25. At the stop, `r0 = 0x100034b0`, `r2 = 0x645A1CAC`, `r3 = 0x4045433B`. -2. Write the replacement string to free RAM at `0x20080000` from the **Python console** (`dbg.write_memory` — no command port needed): - ```python - dbg.write_memory(0x20080000, b"myvalue: %lf\r\n\x00") - ``` - Bytes `6d 79 76 61 6c 75 65 3a 20 25 6c 66 0d 0a 00` = `"myvalue: %lf\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 `r2 = 0x28F5C28F` and `r3 = 0x4058FF5C` as in Step 25. Then move the breakpoint past the call in the GUI (remove it at `0x1000024a`, set one at `0x1000024e`) and click **Resume**. This iteration prints: - ``` - myvalue: 99.990000 - ``` - then stops at `0x1000024e`. One iteration only — the loop reloads `r0` (and `r2`/`r3`) each pass. The permanent version is the static patch in Step 28b. - -### Step 25c: Kill the debugger and OpenOCD - -Same as Step 15b: click the **X** (**Kill**) in the **Debugger** sidebar (or **`Debugger -> Kill`**), then stop OpenOCD 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 -``` ---- - -## Part 6: Static — Resolve the Functions and Patch (Project 2) - -### Step 26: Resolve the functions in the IDA GUI - -Same two keys as Step 16 — `G` to the address, then `Y` (Change Type) to set the prototype — using the Project 2 ELF symbol map from Step 4. - -The mechanics are identical to Step 16, so here are the worked examples for the functions that are specific to this project. - -#### `main` - -1. `G` -> `0x10000238`. -2. `Y` -> `int main(void)` (IDA shows `int32_t main(void)` — the same 32-bit `int`). - -#### `stdio_uart_init` - -1. `G` -> `0x100032b0`. -2. `Y` -> `void stdio_uart_init(void)`. - -#### `_ftoa` and `_etoa` - -Same formatters as Project 1, eight bytes higher: `_ftoa` at `0x10001898` (`unsigned int _ftoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)`) and `_etoa` at `0x10001d58` (same prototype). - -#### `stdio_init_all` and `__wrap_printf` - -Same as Project 1, different addresses: `stdio_init_all` at `0x10002f64` (`bool stdio_init_all(void)`), and `__wrap_printf` at `0x100030f4` (`int __wrap_printf(const char *fmt, ...)`). - -Then work down the table the same way. - -Same idea as Project 1: **our code plus what it calls**, not the whole SDK. The call chain here is identical to Project 1 — because `%lf` and `%f` both route into the same `pico_double` formatter: - -``` -main -├── stdio_init_all -│ └── stdio_uart_init ── gpio_set_function, uart_init, stdio_set_driver_enabled -│ └── uart_init ── clock_get_hz, busy_wait_us -└── __wrap_printf ── __wrap_vprintf - ├── vfctprintf ── _vsnprintf - │ ├── _ftoa / _etoa ── __wrap___aeabi_* (the pico_double helpers) - │ └── _ntoa_format / _out_rev - ├── stdio_out_chars_crlf - └── time_us_64 -``` - -One thing in this project has **no separate call**, because the compiler emitted it as a single instruction: the 64-bit literal load is the `ldrd r4, r5, [r5]` you see inside `main`. There is no helper function to rename for it — it is two `.word`s in the pool at `0x10000254`. - -**Project 2 — resolve every function in that chain:** - -| Address | Rename to (`N`) | Signature (`Y`) | -| ------- | --------------- | --------------- | -| `0x1000015c` | `_reset_handler` | `void _reset_handler(void)` | -| `0x10000186` | `platform_entry` | `void platform_entry(void)` | -| `0x1000019a` | `data_cpy` | `void data_cpy(void*, void*, void*)` | -| `0x100001e4` | `_init` | `void _init(void)` | -| `0x10000210` | `frame_dummy` | `void frame_dummy(void)` | -| **`0x10000238`** | **`main`** | **`int main(void)`** | -| `0x10002cb0` | `vfctprintf` | `int vfctprintf(void (*)(char, void*), void*, const char*, va_list)` | -| `0x10002d0c` | `exit` | `void exit(int)` | -| `0x10002d14` | `runtime_init` | `void runtime_init(void)` | -| `0x10002d40` | `stdio_out_chars_crlf` | `void stdio_out_chars_crlf(stdio_driver_t*, const char*, int)` | -| `0x10002e50` | `stdio_put_string` | `int stdio_put_string(const char*, int, bool, bool)` | -| `0x10002f3c` | `stdio_set_driver_enabled` | `void stdio_set_driver_enabled(stdio_driver_t*, bool)` | -| `0x10002f64` | `stdio_init_all` | `bool stdio_init_all(void)` | -| `0x10003030` | `__wrap_vprintf` | `int __wrap_vprintf(const char*, va_list)` | -| `0x100030f4` | `__wrap_printf` | `int __wrap_printf(const char*, ...)` | -| `0x100032b0` | `stdio_uart_init` | `void stdio_uart_init(void)` | -| `0x100033f0` | `strlen` | `size_t strlen(const char*)` | -| `0x1000025c` | `gpio_set_function` | `void gpio_set_function(uint, gpio_function_t)` | -| `0x10000e20` | `uart_init` | `uint uart_init(uart_inst_t*, uint)` | -| `0x10000db0` | `time_us_64` | `uint64_t time_us_64(void)` | - -**Project 2 — resolve the `pico_double` formatting helpers `printf` reaches:** - -| Address | Rename to (`N`) | Signature (`Y`) | -| ------- | --------------- | --------------- | -| `0x1000138c` | `__wrap___aeabi_dadd` | `double __wrap___aeabi_dadd(double, double)` | -| `0x100013b4` | `__wrap___aeabi_dsub` | `double __wrap___aeabi_dsub(double, double)` | -| `0x100013dc` | `__wrap___aeabi_dmul` | `double __wrap___aeabi_dmul(double, double)` | -| `0x10001428` | `__wrap___aeabi_ddiv` | `double __wrap___aeabi_ddiv(double, double)` | -| `0x100014c4` | `__wrap___aeabi_i2d` | `double __wrap___aeabi_i2d(int)` | -| `0x100014e8` | `__wrap___aeabi_ui2d` | `double __wrap___aeabi_ui2d(unsigned)` | -| `0x1000150c` | `__wrap___aeabi_d2iz` | `int __wrap___aeabi_d2iz(double)` | -| `0x10001530` | `__wrap___aeabi_d2uiz` | `unsigned __wrap___aeabi_d2uiz(double)` | -| `0x10001554` | `__wrap___aeabi_dcmpun` | `int __wrap___aeabi_dcmpun(double, double)` | -| `0x10001578` | `__wrap___aeabi_dcmplt` | `int __wrap___aeabi_dcmplt(double, double)` | -| `0x100015a0` | `__wrap___aeabi_dcmple` | `int __wrap___aeabi_dcmple(double, double)` | -| `0x100015c8` | `__wrap___aeabi_dcmpge` | `int __wrap___aeabi_dcmpge(double, double)` | -| `0x100015f0` | `__wrap___aeabi_dcmpgt` | `int __wrap___aeabi_dcmpgt(double, double)` | -| `0x10001614` | `_out_rev` | `unsigned _out_rev(out_fct_type, char*, size_t, size_t, const char*, size_t, unsigned, unsigned)` | -| `0x100016b0` | `_ntoa_format` | `unsigned _ntoa_format(out_fct_type, char*, size_t, size_t, char*, size_t, bool, unsigned, unsigned, unsigned, unsigned)` | -| `0x10001884` | `_out_char` | `void _out_char(char, void*, size_t, size_t)` | -| `0x10001898` | `_ftoa` | `unsigned int _ftoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)` | -| `0x10001d58` | `_etoa` | `unsigned int _etoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)` | -| `0x100022cc` | `_vsnprintf` | `int _vsnprintf(out_fct_type, char*, size_t, const char*, va_list)` | -| `0x10000dc4` | `busy_wait_us` | `void busy_wait_us(uint64_t)` | -| `0x10000ff4` | `clock_get_hz` | `unsigned long clock_get_hz(clock_handle_t)` | - -Python console shortcut (resolves name **and** type): - -```python -from binaryninja import Symbol, SymbolType -# The raw .bin has no headers, so these SDK types don't exist. set_user_type() -# re-parses each signature as C, so an undefined name raises -# "SyntaxError: unknown type name '...'". Define them first. -sdk = bv.parse_types_from_string(""" -typedef unsigned int uint; -typedef char* va_list; -typedef unsigned long clock_handle_t; -typedef void (*out_fct_type)(char, void*, size_t, size_t); -struct stdio_driver; -typedef struct stdio_driver stdio_driver_t; -struct uart_inst; -typedef struct uart_inst uart_inst_t; -enum gpio_function { - GPIO_FUNC_XIP = 0, GPIO_FUNC_SPI = 1, GPIO_FUNC_UART = 2, GPIO_FUNC_I2C = 3, - GPIO_FUNC_PWM = 4, GPIO_FUNC_SIO = 5, GPIO_FUNC_PIO0 = 6, GPIO_FUNC_PIO1 = 7, - GPIO_FUNC_GPCK = 8, GPIO_FUNC_USB = 9, GPIO_FUNC_NULL = 0x1f, -}; -typedef enum gpio_function gpio_function_t; -""") -for name, ty in sdk.types.items(): - bv.define_user_type(name, ty) - -# address: (name, signature) -funcs = { - 0x1000015c: ("_reset_handler", "void _reset_handler(void)"), - 0x10000186: ("platform_entry", "void platform_entry(void)"), - 0x1000019a: ("data_cpy", "void data_cpy(void*, void*, void*)"), - 0x100001e4: ("_init", "void _init(void)"), - 0x10000210: ("frame_dummy", "void frame_dummy(void)"), - 0x10000238: ("main", "int main(void)"), - 0x10002cb0: ("vfctprintf", "int vfctprintf(void (*)(char, void*), void*, const char*, va_list)"), - 0x10002d0c: ("exit", "void exit(int)"), - 0x10002d14: ("runtime_init", "void runtime_init(void)"), - 0x10002d40: ("stdio_out_chars_crlf", "void stdio_out_chars_crlf(stdio_driver_t*, const char*, int)"), - 0x10002e50: ("stdio_put_string", "int stdio_put_string(const char*, int, bool, bool)"), - 0x10002f3c: ("stdio_set_driver_enabled", "void stdio_set_driver_enabled(stdio_driver_t*, bool)"), - 0x10002f64: ("stdio_init_all", "bool stdio_init_all(void)"), - 0x10003030: ("__wrap_vprintf", "int __wrap_vprintf(const char*, va_list)"), - 0x100030f4: ("__wrap_printf", "int __wrap_printf(const char*, ...)"), - 0x100032b0: ("stdio_uart_init", "void stdio_uart_init(void)"), - 0x100033f0: ("strlen", "size_t strlen(const char*)"), - 0x1000025c: ("gpio_set_function", "void gpio_set_function(uint, gpio_function_t)"), - 0x10000e20: ("uart_init", "uint uart_init(uart_inst_t*, uint)"), - 0x10000db0: ("time_us_64", "uint64_t time_us_64(void)"), - 0x1000138c: ("__wrap___aeabi_dadd", "double __wrap___aeabi_dadd(double, double)"), - 0x100013b4: ("__wrap___aeabi_dsub", "double __wrap___aeabi_dsub(double, double)"), - 0x100013dc: ("__wrap___aeabi_dmul", "double __wrap___aeabi_dmul(double, double)"), - 0x10001428: ("__wrap___aeabi_ddiv", "double __wrap___aeabi_ddiv(double, double)"), - 0x100014c4: ("__wrap___aeabi_i2d", "double __wrap___aeabi_i2d(int)"), - 0x100014e8: ("__wrap___aeabi_ui2d", "double __wrap___aeabi_ui2d(unsigned)"), - 0x1000150c: ("__wrap___aeabi_d2iz", "int __wrap___aeabi_d2iz(double)"), - 0x10001530: ("__wrap___aeabi_d2uiz", "unsigned __wrap___aeabi_d2uiz(double)"), - 0x10001554: ("__wrap___aeabi_dcmpun", "int __wrap___aeabi_dcmpun(double, double)"), - 0x10001578: ("__wrap___aeabi_dcmplt", "int __wrap___aeabi_dcmplt(double, double)"), - 0x100015a0: ("__wrap___aeabi_dcmple", "int __wrap___aeabi_dcmple(double, double)"), - 0x100015c8: ("__wrap___aeabi_dcmpge", "int __wrap___aeabi_dcmpge(double, double)"), - 0x100015f0: ("__wrap___aeabi_dcmpgt", "int __wrap___aeabi_dcmpgt(double, double)"), - 0x10001614: ("_out_rev", "unsigned _out_rev(out_fct_type, char*, size_t, size_t, const char*, size_t, unsigned, unsigned)"), - 0x100016b0: ("_ntoa_format", "unsigned _ntoa_format(out_fct_type, char*, size_t, size_t, char*, size_t, bool, unsigned, unsigned, unsigned, unsigned)"), - 0x10001884: ("_out_char", "void _out_char(char, void*, size_t, size_t)"), - 0x10001898: ("_ftoa", "unsigned int _ftoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)"), - 0x10001d58: ("_etoa", "unsigned int _etoa(out_fct_type, char*, size_t, size_t, double, unsigned int, unsigned int, unsigned int)"), - 0x100022cc: ("_vsnprintf", "int _vsnprintf(out_fct_type, char*, size_t, const char*, va_list)"), - 0x10000dc4: ("busy_wait_us", "void busy_wait_us(uint64_t)"), - 0x10000ff4: ("clock_get_hz", "unsigned long clock_get_hz(clock_handle_t)"), -} -for addr, (name, sig) in funcs.items(): - bv.define_user_symbol(Symbol(SymbolType.FunctionSymbol, addr, name)) - f = bv.get_function_at(addr) - if f is not None: - f.set_user_type(sig) -``` - -The decompiler now shows `main` loading the `double` pair and looping. We make two changes: - -- **Change the printed value from `42.52525` to `99.99`** by patching **both** literal words. -- **Rename the label** `fav_num:` to `myvalue:` by patching the format string. - -### Step 27: Patch 1 — the low word `0x645A1CAC` to `0x28F5C28F` - -`99.99` is `0x4058FF5C_28F5C28F`, so the low word changes from `0x645A1CAC` to `0x28F5C28F`. At `0x10000254` the stored bytes are `ac 1c 5a 64`; change them to `8f c2 f5 28`: - -| Project | Address | Before | After | Effect | -| ------- | ------- | ------ | ----- | ------ | -| `0x0011` | `0x10000254` | `ac 1c 5a 64` | `8f c2 f5 28` | low word of `42.52525` -> `99.99` | - -**Hex view:** lock off, go to `0x10000254`, change `ac 1c 5a 64` to `8f c2 f5 28`, reanalyze. **Or the Python console:** - -```python -bv.write(0x10000254, bytes.fromhex("8fc2f528")) -print(bv.read(0x10000254, 4)) # -> b'\x8f\xc2\xf5(' -``` - -### Step 28: Patch 2 — the high word `0x4045433B` to `0x4058FF5C` - -At `0x10000258` the stored bytes are `3b 43 45 40`; change them to `5c ff 58 40`: - -| Project | Address | Before | After | Effect | -| ------- | ------- | ------ | ----- | ------ | -| `0x0011` | `0x10000258` | `3b 43 45 40` | `5c ff 58 40` | high word of `42.52525` -> `99.99` | - -**Hex view:** go to `0x10000258`, change `3b 43 45 40` to `5c ff 58 40`, reanalyze. **Or the Python console:** - -```python -bv.write(0x10000258, bytes.fromhex("5cff5840")) -for addr in (0x10000254, 0x10000258): - print(hex(addr), bv.read(addr, 4).hex()) -# -> 0x10000254 8fc2f528 -# -> 0x10000258 5cff5840 -``` - -> **Both words are required.** `42.52525` has a repeating binary fraction, so its low word is non-zero (`0x645A1CAC`). Patching only the high word leaves the low 20 mantissa bits from `0.52525`, and `printf` prints a wrong hybrid. Compare Project 1, where `42.5` is exact and the low word was already `0x00000000`, so one word sufficed. - -### Step 28b: Patch the string `fav_num:` to `myvalue:` - -The format string starts at `0x100034b0`; change its first eight bytes `66 61 76 5f 6e 75 6d 3a` (`fav_num:`) to `6d 79 76 61 6c 75 65 3a` (`myvalue:`): - -| Project | Address | Before | After | Effect | -| ------- | ------- | ------ | ----- | ------ | -| `0x0011` | `0x100034b0` | `66 61 76 5f 6e 75 6d 3a` | `6d 79 76 61 6c 75 65 3a` | `fav_num:` -> `myvalue:` | - -```python -bv.write(0x100034b0, b"myvalue:") -print(bv.read(0x100034b0, 15)) # -> b'myvalue: %lf\r\n\x00' -``` - -Exactly eight bytes, same rule as Project 1: a shorter string must be padded, a longer one overwrites the ` %lf` tail. - -### Step 29: Export, convert, and flash - -```python -import os -seg = next(s for s in bv.segments if s.data_length) # the loadable image segment -data = bv.read(seg.start, seg.data_length) # base + size from the view itself -out = os.path.join(os.path.join(root, "0x0011_double-floating-point-data-type", "build"), "0x0011_double-floating-point-data-type-h.bin") -open(out, "wb").write(data) -print(len(data), out) # -> 15324 /.../build/0x0011_double-floating-point-data-type-h.bin -``` - -Same as Project 1: `seg.start` is the load base and `seg.data_length` is the image size (here `0x3bdc` = 15324) — both read from the view, and no relative path (the console's CWD is read-only). - -**macOS Apple Silicon / Linux x64:** - -```bash -python3 ../uf2conv.py 0x0011_double-floating-point-data-type-h.bin \ - --base 0x10000000 --family 0xe48bff59 --output hacked.uf2 -``` - -**Windows x64:** - -```cmd -python ..\uf2conv.py 0x0011_double-floating-point-data-type-h.bin ^ - --base 0x10000000 --family 0xe48bff59 --output hacked.uf2 -``` - -Or run the conversion from the IDA console, exactly as in Step 20 (`os.chdir` to the build dir, then `runpy.run_path("../../uf2conv.py", run_name="__main__")` with `sys.argv` set to the arguments above). - -Hold **BOOTSEL**, plug in the Pico 2, drag `hacked.uf2` onto the **`RP2350`** drive. Or flash the `.bin` over the Debug Probe with SWD — no BOOTSEL — from the console, exactly as in Step 21 (stop any running OpenOCD first, and use `Popen`, not `run`, so the console is not blocked): - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() -bin_path = os.path.join(os.path.join(root, "0x0011_double-floating-point-data-type", "build"), "0x0011_double-floating-point-data-type-h.bin") -log = os.path.join(os.path.join(root, "0x0011_double-floating-point-data-type", "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) -``` - -### Step 30: Verify - -Open the serial monitor: - -``` -myvalue: 99.990000 -myvalue: 99.990000 -myvalue: 99.990000 -... -``` - -**We changed the printed value and relabeled the line, with ten bytes and no source code.** `42.52525` became `99.99` because both halves of the double moved together. ---- - -## 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("r3", 0x4058C000)` in the Python console (or right-click the register, press `E`, type hex, Enter) | -| Write a RAM string live | `dbg.write_memory(0x20080000, b"myvalue: %f\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 ` then `bp 2 hw`) | -| Confirm what is armed | The **Breakpoints** widget lists it (command-port fallback: `mdw 0xE0002000 8`, each armed breakpoint shows as ``) | -| Apply the ELF symbol map | Paste the Python snippet from Step 16 / 26 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 13). 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 2 hw` | -| Remove one breakpoint | `rbp ` — **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` (Project 2: `0x10000238`) — Windows: `$env:BP_ADDR="0x10000234"; .\debug-server.ps1` (one-shot) | -| Start the server parked in the loop | macOS/Linux: `BP_ADDR=0x10000244 ./debug-server.sh` — Windows: `$env:BP_ADDR="0x10000244"; .\debug-server.ps1` (`0x1000024a` for Project 2) | -| 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 | -| ------- | ------- | ------ | ----- | ------ | -| `0x000e` | `0x1000024c` | `00 40 45 40` | `00 c0 58 40` | high word of the double: `42.5` -> `99.0` | -| `0x0011` | `0x10000254` | `ac 1c 5a 64` | `8f c2 f5 28` | low word of the double: `42.52525` -> `99.99` | -| `0x0011` | `0x10000258` | `3b 43 45 40` | `5c ff 58 40` | high word of the double: `42.52525` -> `99.99` | -| `0x000e` | `0x100034a8` | `66 61 76 5f 6e 75 6d 3a` | `6d 79 76 61 6c 75 65 3a` | string prints `myvalue:` instead of `fav_num:` | -| `0x0011` | `0x100034b0` | `66 61 76 5f 6e 75 6d 3a` | `6d 79 76 61 6c 75 65 3a` | string prints `myvalue:` instead of `fav_num:` | - -### IEEE 754 Quick Reference for the Values in This Lesson - -| Value | Double Hex | High Word (`r3`) | Low Word (`r2`) | -| ----- | ---------- | ---------------- | --------------- | -| `42.5` | `0x4045400000000000` | `0x40454000` | `0x00000000` | -| `42.52525` | `0x4045433B645A1CAC` | `0x4045433B` | `0x645A1CAC` | -| `99.0` | `0x4058C00000000000` | `0x4058C000` | `0x00000000` | -| `99.99` | `0x4058FF5C28F5C28F` | `0x4058FF5C` | `0x28F5C28F` | - -### Raw image facts - -| Item | Value | -| ---- | ----- | -| Build type | `Release` | -| Load base address | `0x10000000` | -| Project 1 size | `15308` bytes (`0x3bcc`) | -| Project 2 size | `15324` bytes (`0x3bdc`) | -| Fixed `main` anchor (both projects) | `0x1000018c` (reset handler middle `blx`) | -| `main`, Project 1 | `0x10000234` | -| `main`, Project 2 | `0x10000238` | -| `printf` call / return, Project 1 | `0x10000244` / `0x10000248` | -| `printf` call / return, Project 2 | `0x1000024a` / `0x1000024e` | -| `double` argument registers | `r2` (low) : `r3` (high) | -| 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 11). 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,,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 11). 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 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 lays the `pico_double` helpers out differently, so Project 2's `main` is not at `0x10000238`. - -### 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 10) rather than `reset run` while attached. Loop addresses such as `0x10000244` 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 `r2`/`r3` (or another register) and it reverts - -Both `main`s reload the argument pair at the top of every loop iteration — `mov r2, r4` / `mov r3, r5` run right before the `printf` call. So your edit is only live for the instant between the write and the next pass; then the pair is reloaded from `r4`/`r5` (Project 1) or from the literal pool via `ldrd` (Project 2). 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 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.500000` 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 `0x10003214` instead - -`0x10003214` is inside `stdio_uart_out_flush`: - -```asm -10003210: 4b02 ldr r3, [pc, #8] ; @ 0x1000321c -10003212: 681a ldr r2, [r3] -10003214: 6993 ldr r3, [r2, #24] ; the core sits here while the UART drains -10003216: 071b lsls r3, r3, #28 -10003218: d4fc bmi.n 0x10003214 -1000321a: 4770 bx lr -1000321c: 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 the `printf` call, which fires every iteration. - -**`0x10003214` is not a function.** It is one instruction inside `stdio_uart_out_flush`, which starts at `0x10003210`. If IDA has created a function at `0x10003214` (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 `0x10003210`. (In Project 2 the same drain loop is at `0x1000321c`.) - -### 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`. - -### Project 2 prints a wrong, hybrid number after patching - -You patched only one of the two literal words. `42.52525` has a non-zero low word, so `0x10000254` **and** `0x10000258` must both change. Project 1's `42.5` is the opposite case: its low word is `0x00000000`, so only `0x1000024c` changes. - -### The double does not print as `99.99` after patching - -Confirm you wrote the bytes little-endian. `0x28F5C28F` is stored `8f c2 f5 28`, and `0x4058FF5C` is stored `5c ff 58 40`. If you typed the words in big-endian order the value is nonsense. The Python form `bytes.fromhex("8fc2f528")` is already in file order. - ---- - -## 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 10), then in a new terminal: - -``` -arm-none-eabi-gdb -``` - -At the `(gdb)` prompt: - -``` -set architecture armv8-m.main -target extended-remote :3333 -hbreak *0x10000244 -continue -``` - -Do **not** run `monitor reset run` before `hbreak`. `0x10000244` 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 pair, change it, and let it run: - -``` -info registers pc r2 r3 # pc = 0x10000244, r2 = 0x00000000, r3 = 0x40454000 -set $r3 = 0x4058C000 -stepi -continue -``` - -The serial monitor prints `fav_num: 99.000000` for the iteration you changed — the same temporary live hack as editing `r3` 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 `0x10003214`, the `stdio_uart_out_flush` UART-drain loop. - -Project 2 is the same with the other call site and pair: - -``` -hbreak *0x1000024a -continue -info registers pc r2 r3 # pc = 0x1000024a, r2 = 0x645A1CAC, r3 = 0x4045433B -set $r2 = 0x28F5C28F -set $r3 = 0x4058FF5C -stepi -``` - -`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. +bash ## Glossary @@ -2005,7 +91,7 @@ stepi | **Double** | 64-bit IEEE 754 floating-point type (1 sign, 11 exponent, 52 mantissa) | | **Float** | 32-bit IEEE 754 floating-point type (1 sign, 8 exponent, 23 mantissa) | | **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 | | **IEEE 754** | The standard that defines binary floating-point encoding | | **Inlining** | The optimizer replacing a function call with the function body | | **Literal pool** | A block of 32-bit constants that Thumb-2 code reaches with PC-relative `ldr`/`ldrd` | @@ -2020,4 +106,4 @@ stepi --- -**Remember:** the ELF tells you what every address is, and the `.bin` is what you actually patch. Prove the behavior dynamically by reading `r2:r3`, resolve the names from the ELF, then patch the constant bytes — one word for a clean fraction like `42.5`, two words for a repeating one like `42.52525` — 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.5`, two words for a repeating one like `42.52525` — and export the patched artifact. diff --git a/WEEK06/WEEK06-IDA.md b/WEEK06/WEEK06-IDA.md index b6b207b..14183af 100644 --- a/WEEK06/WEEK06-IDA.md +++ b/WEEK06/WEEK06-IDA.md @@ -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,,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 2 hw` still arms a breakpoint, `rbp ` / `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 0x10000000 verify reset exit`), so you never touch BOOTSEL or a UF2. Run it from a terminal (`./flash.sh `), 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 `) 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 ` then `bp 2 hw`) | -| Confirm what is armed | The **Breakpoints** widget lists it (command-port fallback: `mdw 0xE0002000 8`, each armed breakpoint shows as ``) | -| 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 2 hw` | -| Remove one breakpoint | `rbp ` — **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,,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 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. diff --git a/WEEK07/WEEK07-IDA.md b/WEEK07/WEEK07-IDA.md index c55f816..ec8848d 100644 --- a/WEEK07/WEEK07-IDA.md +++ b/WEEK07/WEEK07-IDA.md @@ -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 ) -10000188: blx r1 -1000018a: ldr r1, [pc, #80] @ (100001dc ) -1000018c: blx r1 -1000018e: ldr r1, [pc, #80] @ (100001e0 ) -10000190: blx r1 -10000192: bkpt 0x0000 -10000194: b.n 10000192 @ -``` - -**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,,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 2 hw` still arms a breakpoint, `rbp ` / `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 ) -10000292: bl 1000398c @ <__wrap_printf> -10000296: movw r1, #1337 @ 0x539 -1000029a: ldr r0, [pc, #28] @ (100002b8 ) -1000029c: bl 1000398c @ <__wrap_printf> -100002a0: b.n 1000028e @ -``` - -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 0x10000000 verify reset exit`), so you never touch BOOTSEL or a UF2. Run it from a terminal (`./flash.sh `), 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 `) 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 ` then `bp 2 hw`) | -| Confirm what is armed | The **Breakpoints** widget lists it (command-port fallback: `mdw 0xE0002000 8`, each armed breakpoint shows as ``) | -| 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 2 hw` | -| Remove one breakpoint | `rbp ` — **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,,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 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 ) -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 -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. diff --git a/WEEK09/WEEK09-IDA.md b/WEEK09/WEEK09-IDA.md index 4ed65ce..41994f8 100644 --- a/WEEK09/WEEK09-IDA.md +++ b/WEEK09/WEEK09-IDA.md @@ -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 ) -10000188: blx r1 -1000018a: ldr r1, [pc, #80] @ (100001dc ) -1000018c: blx r1 -1000018e: ldr r1, [pc, #80] @ (100001e0 ) -10000190: blx r1 -10000192: bkpt 0x0000 -10000194: b.n 10000192 -``` - -**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,,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 2 hw` still arms a breakpoint, `rbp ` / `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 ) -10000272: bl 10003524 <__wrap_printf> -10000276: movs r1, #5 -10000278: ldr r0, [pc, #64] @ (100002bc ) -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 0x10000000 verify reset exit`), so you never touch BOOTSEL or a UF2. Run it from a terminal (`./flash.sh `), 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 `) 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 ` then `bp 2 hw`) | -| Confirm what is armed | The **Breakpoints** widget lists it (command-port fallback: `mdw 0xE0002000 8`, each armed breakpoint shows as ``) | -| 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 2 hw` | -| Remove one breakpoint | `rbp ` — **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,,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 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 ) -1000364a: 681a ldr r2, [r3] -1000364c: 6993 ldr r3, [r2, #24] -1000364e: 071b lsls r3, r3, #28 -10003650: d4fc bmi.n 1000364c -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. diff --git a/WEEK10/WEEK10-IDA.md b/WEEK10/WEEK10-IDA.md index 7d4200d..095540b 100644 --- a/WEEK10/WEEK10-IDA.md +++ b/WEEK10/WEEK10-IDA.md @@ -22,10 +22,8 @@ By using this repository and course, you acknowledge and agree that: - Build both lesson projects with `Release` and get an `.elf` and a raw `.bin` for each - Dump the **ELF symbol map** with `arm-none-eabi-nm` and use it as ground truth - Load each raw `.bin` into IDA at `0x10000000` -- **Break at `main`** on live silicon, even though `main` can move between programs -- **Hack each running target live** from IDA's Registers widget and Python console - **Resolve the functions in the IDA GUI** using the ELF symbol map -- **Patch** the bytes that control behavior — strings, an IEEE-754 float, an immediate delay, and two `beq` targets — export, convert, and flash +- **Patch** the bytes that control behavior — strings, an IEEE-754 float, an immediate delay, and two `beq` targets — export, convert and export - Understand how a **static** conditional is optimized away while a **dynamic** conditional must keep its `cmp`/`beq`/`bne` branches --- @@ -43,12 +41,12 @@ The `.bin` is built **from** the `.elf`, so the ELF tells you exactly what is at > **Build `Release`, not `Debug`.** Every address in this guide matches the current `Release` builds. `Release` folds the `static` helpers (`print_if_else`, `print_switch`, `sweep_servo`, `eval_if_else`, `process_servo_command`) into `main` and keeps the code layout stable. If you build `Debug`, the SDK function addresses move and the helpers stay separate, so nothing lines up. Always build `Release` for this lesson. -The order is **dynamic first, static second**, twice — once per project: +The workflow is static-only: import the raw image, recover the code, decompile it, and patch the artifact: -1. Break on the live target and prove what the code does. -2. Hack it live in the debugger and watch the behavior 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 hack | | ------- | ------ | --------- | -------- | @@ -67,7 +65,7 @@ A servo wants a **50 Hz** signal (a 20 ms frame). The RP2350 system clock is **1 --- -## Part 1: Build, Flash, and Get the Symbol Map +## Part 1: Build and Get the Symbol Map ### Step 1: Install the toolchain @@ -79,38 +77,6 @@ A servo wants a **50 Hz** signal (a 20 ms frame). The RP2350 system clock is **1 **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)" -``` All must report `arm64`. If any is `x86_64`, put the Apple Silicon prefix first for the session and check again: @@ -120,7 +86,6 @@ 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. **Windows x64** and **Linux x64** do not have this problem. Skip to Step 3. @@ -293,117 +258,6 @@ Each line is `address type name`. The `T`/`t` type is a function. The signatures > **`printf` in our source is `__wrap_printf` in the binary.** The SDK links our `printf` calls to its `__wrap_printf` wrapper, which forwards to `__wrap_vprintf`. Project 1 has no `printf` at all: the compiler replaced every `printf("...")` with a `__wrap_puts` because the strings have no format specifiers. -### Step 5: Flash Project 1 and confirm `1` / `one` + servo sweep - -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 0x001d_static-conditionals/build/0x001d_static-conditionals.bin -``` - -**Windows x64 (PowerShell):** - -```powershell -.\flash.ps1 -Bin 0x001d_static-conditionals\build\0x001d_static-conditionals.bin -``` - -**Or flash from the IDA console:** - -**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, "0x001d_static-conditionals", "build", "0x001d_static-conditionals.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, "0x001d_static-conditionals", "build", "0x001d_static-conditionals.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 8084 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`. - -``` -1 -one -1 -one -1 -one -... -``` - -The servo sweeps **0° → 180° → 0°** once per second, and because `choice` is hard-coded the same two lines repeat forever. - -### Step 6: Flash Project 2 and confirm the dynamic behavior - -```bash -# macOS / Linux -./flash.sh 0x0020_dynamic-conditionals/build/0x0020_dynamic-conditionals.bin -``` -```powershell -# Windows -.\flash.ps1 -Bin 0x0020_dynamic-conditionals\build\0x0020_dynamic-conditionals.bin -``` - -**Or flash from the IDA console** (same form, pointing at the Project 2 `.bin`): - -**macOS / Linux:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) -bin_path = os.path.join(root, "0x0020_dynamic-conditionals", "build", "0x0020_dynamic-conditionals.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:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) -bin_path = os.path.join(root, "0x0020_dynamic-conditionals", "build", "0x0020_dynamic-conditionals.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 16188 bytes ...`. Nothing prints until you type. In the serial monitor: - -- type `1` → the Pico prints `1` then `one`, and the servo sweeps **0° → 180°**; -- type `2` → it prints `2` then `two`, and the servo sweeps **180° → 0°**; -- type anything else → it prints `??` twice and waits for another key. - ---- - ## 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. @@ -480,263 +334,13 @@ When you come back later, **open the `.i64`**, not the `.bin`; that restores all - **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 13. +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 (Project 1) - -### Step 10: 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 11. - -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 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 13) 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 11: Connect IDA to the GDB server - -1. Make sure the image is open and analyzed (Part 2) and the server from Step 10 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. Verified working end to end: connect, GUI breakpoints (**Add Hardware Breakpoint...**, hardware execute), **Step Into** / **Step Over**, and register edits. - -> **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 10), 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. 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. - -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 12: 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 : -10000186: ldr r1, [pc, #80] -10000188: blx r1 -1000018a: ldr r1, [pc, #80] -1000018c: blx r1 -1000018e: ldr r1, [pc, #80] -10000190: blx r1 -10000192: bkpt 0x0000 -``` - -**The middle `blx` at `0x1000018c` is the call to `main`.** `platform_entry` is byte-identical in both projects, 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 13: 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. - -#### Where you can stop - -| You want to stop at | Project 1 address | How | Repeatable? | -| --- | --- | --- | --- | -| **`main`** | `0x10000234` | The server starts parked there with `BP_ADDR=0x10000234` (Step 10), so IDA is already stopped at `main` when it connects. | No — `main` runs once per reset. | -| **First `puts` (`"1\r\n"`)** | `0x10000246` | Set a hardware breakpoint in the GUI, then click **Resume**. | Yes — fires on every iteration. | -| **Second `puts` (`"one\r"`)** | `0x1000024c` | Same. | Yes. | -| **`servo_set_angle(0.0f)`** | `0x10000252` | Same. | Yes. | -| **`servo_set_angle(180.0f)`** | `0x10000260` | Same — this is the angle we hack. | Yes. | - -#### Set a breakpoint in the GUI - -1. Press `G`, type the address (for example `0x10000260`), 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 address and reports it as a **Breakpoint**. - -> **No breakpoints before you connect.** With GDB MI, a breakpoint set before the connection hangs the session (Step 11). Start parked with `BP_ADDR`, connect, *then* add breakpoints. - -> **Step Over on the raw `.bin` steps *into* calls.** The raw image has no symbol for `__wrap_puts` or `servo_set_angle`, so **Step Over** at a `bl` 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 14 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. - -### Step 14: HACK IT LIVE — change the servo angle from 180° to 30° - -`main` loads the constant `0x43340000` (180.0f) into `r4` once, before the loop, then copies it into `r0` at `0x1000025e` right before the `servo_set_angle` call at `0x10000260`. We break on that call and change the angle live. - -1. Press `G`, go to `0x10000260` (the `bl servo_set_angle` for 180.0°). -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 pass. IDA stops with the program counter at `0x10000260` and `r0 = 0x43340000` — the `mov r0, r4` at `0x1000025e` just loaded the 180.0f literal into `r0`. -4. Open the **Registers** widget (bug icon → **Registers**). -5. Find `r0`. Its value is `0x43340000`. -6. **Set `r0` to `0x41f00000` (30.0f).** From IDA's Python console (`Plugins -> Python Console`): - ```python - dbg.set_reg_value("r0", 0x41f00000) # 30.0f - ``` - `dbg.set_reg_value(name, value)` writes one register (returns `True` on success). You can also right-click `r0` in the **Registers** widget, press `E` (edit), type `41f00000`, and press Enter. The widget may not repaint the value, but the write reaches the target. -7. **Move the breakpoint past the call.** Remove the breakpoint at `0x10000260` and set a hardware breakpoint at `0x10000264` (the `mov.w r0, #500` right after the call). Two reasons not to just click **Step Over**: a breakpoint left on the current PC re-traps the step, and IDA's **Step Over** steps *into* `servo_set_angle` on this raw `.bin`. -8. Click **Resume**. The core executes `bl servo_set_angle` with `r0 = 0x41f00000`, so this sweep ends at **30°** instead of 180°, then stops at `0x10000264`. Watch the servo arm. - -> **`r4` is the real source — and it never reloads inside the loop.** `r4` is loaded once at `0x10000242` (before the loop starts at `0x10000244`) from the literal at `0x10000270`, so if you set `r4 = 0x41f00000` instead of `r0`, *every* pass uses 30° until the next reset. Editing `r0` changes only the current sweep because the next pass reloads `r0` from `r4`. Both edits are useful: `r0` shows a one-shot live change; `r4` shows a sticky one. - -### Step 14b: HACK THE STRING LIVE — change `one` to `fun` - -The text `"one\r"` lives in flash (`.rodata`) at `0x10001c64`, and flash is **read-only at runtime** — a debugger write there does not stick. So you cannot overwrite the text in place. Instead you redirect the pointer: at the `puts` call, `r0` holds the string address, so you point `r0` at a replacement string you place in RAM. - -1. Press `G`, go to `0x1000024c` (the second `bl __wrap_puts`) and set a **hardware execute** breakpoint. Resume; the loop hits it next pass. At the stop, `r0 = 0x10001c64` — the `ldr r0, [pc, #44]` at `0x1000024a` just loaded the `"one\r"` pointer from the literal at `0x10000278`. -2. Put the replacement string into free RAM at `0x20080000` from IDA's **Python console**: - ```python - dbg.write_memory(0x20080000, b"fun\r\x00") # puts appends the newline - ``` - `dbg.write_memory(address, bytes)` is IDA's debugger memory-write API; it returns `True` on success. The bytes are `66 75 6e 0d 00` = `"fun\r\0"`. We keep the `\r` and let `puts` add the `\n`, exactly as the compiler does for the original `"one\r"`. -3. Point `r0` at that string: - ```python - dbg.set_reg_value("r0", 0x20080000) - ``` - (Or right-click `r0` in the **Registers** widget, press `E`, type `20080000`, and press Enter.) -4. Move the breakpoint past the call (remove it at `0x1000024c`, set one at `0x10000250`) and click **Resume**. The core runs `puts` with `r0` pointing at your RAM string, so this iteration prints: - ``` - fun - ``` - then stops at `0x10000250`. - -Like the angle hack, this is **one iteration only**: the loop reloads `r0` from the literal pool on every pass, so the next line is `one` again. The permanent version is the static patch in Step 18. - -### Step 15: Why the hack reverts (and why we patch next) - -Press **Resume**. The loop branches back to `0x10000244`, which reloads `r0` from `0x10001c64` and `0x1000025e` reloads `r0` from `r4`, so the next line is `one` and the next sweep ends at 180° again. The live edits changed one iteration only; nothing in RAM controls these values. To make the changes permanent we must patch the bytes — the static pass. - -Press **Pause** to stop the output flood. - -### Step 15b: 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. - ---- - -## Part 4: Static — Resolve the Functions in IDA and Patch (Project 1) +## Part 3: Static — Resolve the Functions in IDA and Patch (Project 1) ### Step 16: Resolve the functions in the IDA GUI @@ -1096,311 +700,7 @@ python ..\uf2conv.py 0x001d_static-conditionals-h.bin ^ > runpy.run_path("../../uf2conv.py", run_name="__main__") > ``` -### Step 21: Flash and verify - -Hold **BOOTSEL**, plug in the Pico 2, and drag `hacked.uf2` onto the **`RP2350`** drive. Or flash the `.bin` over the Debug Probe with SWD — no BOOTSEL — from the console (stop any running OpenOCD first, and use `Popen`, not `run`, so the console is not blocked): - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() -bin_path = os.path.join(os.path.join(root, "0x001d_static-conditionals", "build"), "0x001d_static-conditionals-h.bin") -log = os.path.join(os.path.join(root, "0x001d_static-conditionals", "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) -``` - -Open the serial monitor: - -``` -2 -fun -2 -fun -2 -fun -... -``` - -The servo now sweeps **0° → 30°** and does it **5× faster**. **Five bytes changed, no source code.** - ---- - -## Part 5: Reflash Project 2 and Load It into IDA - -### Step 22: Reflash Project 2 and restart the session - -Part 4 left the Pico running the patched Project 1 image. Put the original Project 2 back and start a fresh session. - -1. Stop any running debug server so the flash script can use the probe: - - ```bash - # macOS / Linux - pkill -TERM -f openocd - ``` - ```powershell - # Windows - Get-Process openocd -ErrorAction SilentlyContinue | Stop-Process - ``` - -2. Flash the original Project 2 image: - - ```bash - # macOS / Linux - ./flash.sh 0x0020_dynamic-conditionals/build/0x0020_dynamic-conditionals.bin - ``` - ```powershell - # Windows - .\flash.ps1 -Bin 0x0020_dynamic-conditionals\build\0x0020_dynamic-conditionals.bin - ``` - - **Or do steps 1–2 from the IDA console:** - - **macOS / Linux:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) - bin_path = os.path.join(root, "0x0020_dynamic-conditionals", "build", "0x0020_dynamic-conditionals.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 Project 2 in the background; log:", log) - ``` - - **Windows:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) - bin_path = os.path.join(root, "0x0020_dynamic-conditionals", "build", "0x0020_dynamic-conditionals.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 Project 2 in the background; log:", log) - ``` - -3. Load Project 2 and save its database — see Step 22b. - -Confirm the Pico responds to `1` / `2` again. - -### Step 22b: Load Project 2 into IDA and save the database - -Exactly like Steps 7–8, but for Project 2. **Use `File -> Open with Options...`** (not plain `File -> Open`), select `0x0020_dynamic-conditionals/build/0x0020_dynamic-conditionals.bin`, and set: - -- **Architecture:** `thumb2` -- **Platform:** `thumb2` -- **Base Address:** `0x10000000` - -Click **Open**. Then press `G`, type `0x10000000`, and confirm the first two words: - -``` -0x10000000 0x20082000 initial stack pointer -0x10000004 0x1000015d reset vector (bit 0 = Thumb) -``` - -If you see data at `0x00000000`, close the tab and redo it with `Open with Options`. - -Save it with `File -> Save As...` as `0x0020_dynamic-conditionals.i64` (next to the `.bin`). From now on open the `.i64`, not the `.bin`; save with `Cmd+S` / `Ctrl+S` after every rename or patch. - -> **Console equivalent:** -> ```python -> load("0x0020_dynamic-conditionals/build/0x0020_dynamic-conditionals.bin", -> options={"loader.imageBase": 0x10000000, "loader.platform": "thumb2"}) -> ``` - ---- - -## Part 6: Dynamic — Break at `main` and Hack Live (Project 2) - -### Step 23: Break at `main` - -`main` is at `0x10000234` in this project too. Start the server parked at `main` (Step 10 form) and connect with the **GDB MI** adapter (Step 11): - -1. Restart the server parked at `main`: - - **macOS / Linux:** - - ```bash - BP_ADDR=0x10000234 ./debug-server.sh - ``` - ```powershell - # Windows - $env:BP_ADDR="0x10000234"; .\debug-server.ps1 - ``` - - **Or restart it from the IDA console:** - - **macOS / Linux:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() - log = os.path.join(root, "openocd.log") - subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # kill any running server first - 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 restarted parked at main; log:", log) - ``` - - **Windows:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() - log = os.path.join(root, "openocd.log") - subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # kill any running server first - 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 restarted parked at main; log:", log) - ``` -2. Connect IDA (Step 11): adapter **GDB MI**, IP `127.0.0.1`, port `3333`. - -The target is already halted at `main` when IDA connects, and the sidebar reads `Stopped at 0x10000234`. - -### Step 24: Read `main` and find the dynamic conditional - -The whole loop is one function because both helpers were inlined. Notice the `cmp`/`beq`/`bne` chain the compiler had to keep this time: - -```asm -10000234
: -10000234: push {r3, r4, r5, lr} -10000236: bl 10003244 -1000023a: movs r0, #6 -1000023c: bl 100002dc -10000240: movs r4, #0 -10000242: ldr r5, [pc, #124] -10000244: bl 10003250 <__wrap_getchar> -10000248: uxtb r0, r0 -1000024a: cmp r0, #49 -1000024c: beq.n 10000268 -1000024e: cmp r0, #50 -10000250: beq.n 10000294 -10000252: ldr r0, [pc, #112] -10000254: bl 10003344 <__wrap_puts> -10000258: ldr r0, [pc, #104] -1000025a: bl 10003344 <__wrap_puts> -1000025e: bl 10003250 <__wrap_getchar> -10000262: uxtb r0, r0 -10000264: cmp r0, #49 -10000266: bne.n 1000024e -10000268: ldr r0, [pc, #92] -1000026a: bl 10003344 <__wrap_puts> -1000026e: ldr r1, [pc, #92] -10000270: ldr r0, [pc, #92] -10000272: bl 10003444 <__wrap_printf> -10000276: mov r0, r4 -10000278: bl 10000378 -1000027c: mov.w r0, #500 -10000280: bl 10000e88 -10000284: mov r0, r5 -10000286: bl 10000378 -1000028a: mov.w r0, #500 -1000028e: bl 10000e88 -10000292: b.n 10000244 -10000294: ldr r0, [pc, #60] -10000296: bl 10003344 <__wrap_puts> -1000029a: ldr r1, [pc, #60] -1000029c: ldr r0, [pc, #48] -1000029e: bl 10003444 <__wrap_printf> -100002a2: mov r0, r5 -100002a4: bl 10000378 -100002a8: mov.w r0, #500 -100002ac: bl 10000e88 -100002b0: mov r0, r4 -100002b2: bl 10000378 -100002b6: mov.w r0, #500 -100002ba: bl 10000e88 -100002be: b.n 10000244 -100002c0: .word 0x43340000 -100002c4: .word 0x1000381c -100002c8: .word 0x10003800 -100002cc: .word 0x10003808 -100002d0: .word 0x1000380c -100002d4: .word 0x10003814 -100002d8: .word 0x10003818 -``` - -Because `choice` is now `getchar()` and can be anything, the compiler cannot fold the condition. It emits the comparisons at `0x1000024a` (`cmp r0, #49` = `0x31` = `'1'`) and `0x1000024e` (`cmp r0, #50` = `0x32` = `'2'`), each followed by a `beq.n`. That is the dynamic conditional. - -The string map, read straight from `.rodata`: - -| Literal | Points at | String | -| ------- | --------- | ------ | -| `0x100002c4` | `0x1000381c` | `"??\r"` (the `else` / `default` prints) | -| `0x100002c8` | `0x10003800` | `"1\r\n"` (the `'1'` print) | -| `0x100002cc` | `0x10003808` | `"one"` (the `printf` argument) | -| `0x100002d0` | `0x1000380c` | `"%s\r\n"` (the `printf` format) | -| `0x100002d4` | `0x10003814` | `"2\r\n"` (the `'2'` print) | -| `0x100002d8` | `0x10003818` | `"two"` (the `printf` argument) | - -> **Why the `else` path has two `puts` and a second `getchar`.** The compiler inlined both `eval_if_else` and `process_servo_command`; both have a "default" that prints `"??\r\n"`, and the shared string is emitted twice (`0x10000252` and `0x10000258`). The second `getchar` at `0x1000025e` is the compiler's rotated loop back-edge. The observable behavior is what matters: `1` → `1`+`one`, `2` → `2`+`two`, anything else → `??` twice. - -### Step 25: HACK IT LIVE — drive the branch with `r0` - -`getchar` blocks until you press a key, so this breakpoint fires exactly when a key arrives. We stop right after the read and overwrite the value so the program takes whichever branch we want. - -1. Press `G`, go to `0x1000024a` (the first `cmp r0, #49`). Set a **hardware execute** breakpoint: `Debugger -> Add Hardware Breakpoint...`. -2. Click **Resume** and type any key in the serial monitor — for example `a`. `getchar` returns, the `uxtb` at `0x10000248` runs, and the breakpoint fires at `0x1000024a` with `r0 = 0x61` (`'a'`). -3. **Set `r0` to `0x31` (`'1'`)** from the Python console: - ```python - dbg.set_reg_value("r0", 0x31) # force the '1' branch - ``` - (Or right-click `r0` in the **Registers** widget, press `E`, type `31`, and press Enter.) -4. Remove the breakpoint at `0x1000024a` and click **Resume**. The core runs `cmp r0, #49`, sees the forced `0x31`, and takes the `'1'` branch — so even though you typed `a`, the Pico prints: - ``` - 1 - one - ``` - and sweeps the servo **0° → 180°**. - -> **Change the branch, not the register, if you prefer.** Setting `r0 = 0x32` instead forces the `'2'` path (`2`, `two`, servo 180° → 0°). Setting `r0` to anything else drops into the `??` path. One live register write steers the whole control-flow chain. - -### Step 25b: HACK THE STRING LIVE — change `1` to `7` - -The `'1'` print uses the string at `0x10003800` (`"1\r\n"`). Exactly like Project 1, redirect `r0` to a RAM string at the `puts` call. - -1. Press `G`, go to `0x10000268` (the `bl __wrap_puts` on the `'1'` path) and set a hardware execute breakpoint. Resume and type `1`. At the stop, `r0 = 0x10003800` — the `ldr r0, [pc, #92]` at `0x10000268` loaded the `"1\r\n"` pointer. -2. Write the replacement to RAM and repoint `r0`: - ```python - dbg.write_memory(0x20080000, b"7\r\x00") # puts appends the newline - dbg.set_reg_value("r0", 0x20080000) - ``` -3. Remove the breakpoint at `0x10000268`, set one at `0x1000026a`, and click **Resume**. This iteration prints: - ``` - 7 - one - ``` - One iteration only — the loop reloads `r0` from the literal pool each pass. The permanent version is the static patch in Step 27b. - -### Step 25c: Kill the debugger and OpenOCD - -Same as Step 15b: click the **X** (**Kill**) in the **Debugger** sidebar (or **`Debugger -> Kill`**), then stop OpenOCD 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 -``` - ---- - -## Part 7: Static — Resolve the Functions and Patch (Project 2) +## Part 5: Static — Resolve the Functions and Patch (Project 2) ### Step 26: Resolve the functions in the IDA GUI @@ -1606,7 +906,7 @@ bv.write(0x10000250, bytes.fromhex("27d0")) # beq.n -> 0x100002a2 > **How the encoding was chosen.** A 16-bit conditional branch is `1101 cond imm8`; the target is `PC + 4 + (imm8 << 1)`. For `0x1000024c` → `0x10000276`: `(0x276 - 0x250) / 2 = 0x13`. For `0x10000250` → `0x100002a2`: `(0x2a2 - 0x254) / 2 = 0x27`. Both were verified by patching a copy of the raw `.bin` and disassembling it with `arm-none-eabi-objdump`. This patch is optional; the key rewrite in Step 27b works without it (it just still prints). -### Step 28: Export, convert, and flash +### Step 28: Export, convert and export ```python import os @@ -1637,231 +937,6 @@ Or run the conversion from the IDA console, exactly as in Step 20 (`os.chdir` to Hold **BOOTSEL**, plug in the Pico 2, drag `hacked.uf2` onto the **`RP2350`** drive. Or flash the `.bin` over the Debug Probe with SWD — no BOOTSEL — from the console, exactly as in Step 21. -### Step 29: Verify - -Open the serial monitor: - -- type `x` → with Step 27c applied, **no output** and the servo sweeps silently; without it, `x` prints `x`… actually it prints the original strings `1` / `one` because only the comparison changed (the print strings are untouched). With Step 27c applied the prints are skipped entirely — a **stealth command**. -- type `y` → likewise silent (Step 27c), and the servo sweeps the other way. -- the servo's maximum angle is now **30°**, not 180°. -- the original `1` and `2` keys no longer match the comparisons. - -**We changed the servo angle and hid two secret keys, with a handful of bytes and no source code.** - ---- - -## 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("r0", 0x41f00000)` in the Python console (or right-click the register, press `E`, type hex, Enter) | -| Write debugger memory | `dbg.write_memory(0x20080000, b"fun\r\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 | -| Confirm what is armed | The **Breakpoints** widget lists it | -| Apply the ELF symbol map | Paste the Python snippet from Step 16 / 26 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 13). - -| Action | Command | -| ------ | ------- | -| Start the server parked at `main` | macOS/Linux: `BP_ADDR=0x10000234 ./debug-server.sh` — Windows: `$env:BP_ADDR="0x10000234"; .\debug-server.ps1` (one-shot) | -| Break on a loop address in a running target | set a hardware breakpoint in the GUI, then **Resume** — repeatable | -| Kill the debugger | click **X** in the **Debugger** sidebar, or `Debugger -> Kill` | -| Stop OpenOCD | macOS/Linux: `pkill -TERM -f openocd` — Windows: `taskkill /F /IM openocd.exe` | -| 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`, reconnect — never `reset run` while attached | - -### Where you can stop - -| Stop at | Project 1 `0x001d` | Project 2 `0x0020` | -| ------- | ------------------ | ------------------ | -| `main` (once per reset) | `0x10000234` | `0x10000234` | -| First `puts` | `0x10000246` (`"1\r\n"`) | `0x10000268` (`"1\r\n"`) | -| Second `puts` | `0x1000024c` (`"one\r"`) | — | -| `getchar` return (dynamic key) | — | `0x1000024a` (`cmp r0, #0x31`) | -| `servo_set_angle(0.0f)` | `0x10000252` | `0x10000276` / `0x100002a2` | -| `servo_set_angle(180.0f)` | `0x10000260` | `0x10000284` / `0x100002b0` | - -### Every address and byte we changed - -| Project | Address | Before | After | Effect | -| ------- | ------- | ------ | ----- | ------ | -| `0x001d` | `0x10001c5c` | `31` | `32` | prints `2` instead of `1` | -| `0x001d` | `0x10001c64` | `6f 6e 65` | `66 75 6e` | prints `fun` instead of `one` | -| `0x001d` | `0x10000270` | `00 00 34 43` | `00 00 f0 41` | servo max angle `180.0f` → `30.0f` | -| `0x001d` | `0x10000256` | `4f f4 fa 70` | `4f f0 64 00` | first `sleep_ms` `500` → `100` | -| `0x001d` | `0x10000264` | `4f f4 fa 70` | `4f f0 64 00` | second `sleep_ms` `500` → `100` | -| `0x0020` | `0x1000024a` | `31` | `78` | compare `'1'` → `'x'` | -| `0x0020` | `0x1000024e` | `32` | `79` | compare `'2'` → `'y'` | -| `0x0020` | `0x100002c0` | `00 00 34 43` | `00 00 f0 41` | servo max angle `180.0f` → `30.0f` | -| `0x0020` | `0x1000024c` | `0c` | `13` | `beq.n` skips the `1`/`one` prints (optional) | -| `0x0020` | `0x10000250` | `20` | `27` | `beq.n` skips the `2`/`two` prints (optional) | - -### Raw image facts - -| Item | Value | -| ---- | ----- | -| Build type | `Release` | -| Load base address | `0x10000000` | -| Project 1 size | `8084` bytes | -| Project 2 size | `16188` bytes | -| Initial stack pointer (both) | `0x20082000` | -| Reset vector (both) | `0x1000015d` | -| Fixed `main` anchor (both) | `0x1000018c` | -| `main` (both) | `0x10000234` | -| `servo_set_angle`, Project 1 | `0x10000318` | -| `servo_set_angle`, Project 2 | `0x10000378` | -| Project 1 `180.0f` literal | `0x10000270` | -| Project 2 `180.0f` literal | `0x100002c0` | -| Project 1 `one` string | `0x10001c64` | -| Project 2 `one` string | `0x10003808` | -| 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. -- **The wrong GDB executable.** Point **Full GDB Executable Path** at the **14.2.rel1** toolchain (Step 11). 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. - -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. - -### 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**. 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. - -### 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. Fix: `rp2350.dap.core0 configure -rtos none`. **GDB MI does not hit this.** - -### `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`. - -### 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 the `static` helpers separate. - -### A breakpoint never fires - -First, confirm you actually set one, and that it is a **hardware** breakpoint. `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`) — a software breakpoint cannot be written to read-only flash. - -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. -- **Is the core running?** If it is stopped, click **Resume**. -- **Does the address get reached again?** `main` runs once per reset, so use `BP_ADDR` at startup rather than `reset run` while attached. Loop addresses such as `0x10000260` fire on the next pass with no reset. For Project 2's `0x1000024a` you must also press a key, because it sits right after the blocking `getchar`. - -### I edit `r0` (or another register) and it reverts - -For the Project 1 angle, `mov r0, r4` at `0x1000025e` reloads `r0` on every pass, so the edit is visible for one sweep unless you stop the core again. Editing `r4` instead makes it stick, because `r4` is loaded once before the loop. For Project 2, `getchar` reloads `r0` on every key press. The edit sticks only while the core is **genuinely stopped** at the breakpoint. - -> **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. A value changed outside IDA will not appear until the next stop. - -### The string hack does nothing (or prints garbage) - -Pick a RAM address that is free — `0x20080000` is safe here (well above the `.data`/`.bss` end at about `0x2000062c`). Write a NUL-terminated string, and remember `__wrap_puts` appends its own `\n`, so keep the `\r` but not the `\n` (write `b"fun\r\x00"`). Then set `r0`, not `r1`. - -### The patched string shifted `printf` output - -In Project 1 both prints are `puts`, so only whole-string replacement matters. Keep `"one"` → `"fun"` exactly three bytes; a longer string would overwrite the `\r` terminator and a shorter one would leave a stray character. - -### `mov.w r0, #100` corrupts the instruction - -Use the four bytes `4f f0 64 00`, not the first two bytes of the `500` encoding. `500` needs the `4f f4 …` form; `100` uses the `4f f0 …` form. Both are 4 bytes. Verify with `arm-none-eabi-objdump` (Step 20/21 context) or by re-reading the bytes in IDA. - -### Project 2's `x`/`y` still print - -Step 27b only changes the *comparison* values. To make the keys silent you must also apply the optional `beq` redirects in Step 27c. If the servo moves but the terminal still shows `1`/`one`, you applied 27b but not 27c. - -### The optional `beq` redirect sends execution somewhere wrong - -Recompute from the ELF: `target = PC + 4 + (imm8 << 1)`. For `0x1000024c` the servo code is at `0x10000276` (imm8 `0x13`); for `0x10000250` it is at `0x100002a2` (imm8 `0x27`). Confirm the byte pair you write is little-endian (`13 d0`, `27 d0`). - -### The serial capture is garbage on macOS - -Reading `/dev/cu.usbmodem*` with a bare `read()` returns garbage. Set **raw termios at 115200** first, or just use `screen /dev/cu.usbmodem* 115200`, which does it for you. - -### It worked for a second, then stopped (IDA's view desyncs) - -The main cause is **driving the core from the OpenOCD command port while IDA is connected**. If you must reset, **Detach first**, reset, then reconnect. Never leave a breakpoint on the PC you are about to step or resume from. - -### 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. - -### 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, you can still do the live hacks 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 10), then in a new terminal: - -``` -arm-none-eabi-gdb -``` - -At the `(gdb)` prompt: - -``` -set architecture armv8-m.main -target extended-remote :3333 -hbreak *0x10000260 -continue -``` - -Do **not** run `monitor reset run` before `hbreak`. `0x10000260` is inside `main`'s loop, so the breakpoint fires on the next iteration with no reset. GDB stops at the `servo_set_angle` call: - -``` -info registers pc r0 # pc = 0x10000260, r0 = 0x43340000 -set $r0 = 0x41f00000 -continue -``` - -The servo's next sweep ends at 30° — the same temporary live hack as editing `r0` in the IDA Registers widget. For the string hack, break at `0x1000024c`, then `set {char[5]}0x20080000 = "fun\r"` and `set $r0 = 0x20080000`. - -Project 2 is the same with the other call site and value: - -``` -hbreak *0x1000024a -continue -info registers pc r0 # r0 holds the key you typed -set $r0 = 0x31 -continue -``` - -`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. - ## Glossary | Term | Definition | @@ -1873,7 +948,7 @@ continue | **Dynamic conditional** | A condition whose value is only known at run time (e.g. `getchar()`), so the compiler must emit the comparisons and branches | | **`.elf`** | Linked image with the symbol table; the ground truth for addresses and names | | **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 the `static` helpers disappear from `main` in `Release` | | **Literal pool** | A block of 32-bit constants that Thumb-2 code reaches with PC-relative `ldr` | | **PWM** | Pulse Width Modulation — a variable pulse-width signal; 50 Hz for the SG90 | @@ -1887,4 +962,4 @@ continue --- -**Remember:** the ELF tells you what every address is, and the `.bin` is what you actually patch. Prove the behavior dynamically, resolve the names from the ELF, then patch the bytes 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. diff --git a/WEEK11/WEEK11-IDA.md b/WEEK11/WEEK11-IDA.md index 2e82c20..9331136 100644 --- a/WEEK11/WEEK11-IDA.md +++ b/WEEK11/WEEK11-IDA.md @@ -22,11 +22,9 @@ By using this repository and course, you acknowledge and agree that: - Build the two lesson projects with `Release` and get an `.elf` and a raw `.bin` for each - Dump the **ELF symbol map** with `arm-none-eabi-nm` and use it as ground truth - Load each raw `.bin` into IDA at `0x10000000` -- **Break at `main`** on live silicon, even though `main` can move between programs -- **Hack each running target live** by editing a register and redirecting a string in IDA - **Resolve the functions in the IDA GUI** using the ELF symbol map - See how the compiler **flattens a C struct into hard-coded immediates** and **inlines every helper function** -- **Patch** the LED pin immediates and the NEC format string, export, convert, and flash +- **Patch** the LED pin immediates and the NEC format string, export, convert and export - Understand the security lesson: **the log says one thing while the hardware does another** --- @@ -44,12 +42,12 @@ The `.bin` is built **from** the `.elf`, so the ELF tells you exactly what is at > **Build `Release`, not `Debug`.** Every address in this guide matches the current `Release` builds. `Release` flattens the LED struct into plain immediates and inlines every `static` helper (`make_default_leds`, `init_led_gpios`, `process_ir_key`, `poll_ir`, and in Project 2 `ir_to_led_number`, `get_led_pin`, `leds_all_off`, `blink_led`, `process_ir_led_command`, `handle_ir_key`, `poll_and_handle_ir`) into `main`. If you build `Debug`, the SDK function addresses move and the helpers stay separate, so nothing lines up. Always build `Release` for this lesson. -The order is **dynamic first, static second**, twice — once per project: +The workflow is static-only: import the raw image, recover the code, decompile it, and patch the artifact: -1. Break on the live target and prove what the code does. -2. Hack it live in the debugger and watch the behavior 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 | | ------- | ------ | --------- | --------- | @@ -68,7 +66,7 @@ An IR receiver on **GPIO 5** demodulates a 38 kHz carrier and presents the NEC f --- -## Part 1: Build, Flash, and Get the Symbol Map +## Part 1: Build and Get the Symbol Map ### Step 1: Install the toolchain @@ -80,38 +78,6 @@ An IR receiver on **GPIO 5** demodulates a 38 kHz carrier and presents the NEC f **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)" -``` All must report `arm64`. If any is `x86_64`, put the Apple Silicon prefix first for the session and check again: @@ -121,7 +87,6 @@ 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. **Windows x64** and **Linux x64** do not have this problem. Skip to Step 3. @@ -298,122 +263,6 @@ Each line is `address type name`. The `T`/`t` type is a function. The signatures > **`printf` in our source is `__wrap_printf` in the binary.** The SDK links our `printf` calls to its `__wrap_printf` wrapper, which forwards to `__wrap_vprintf`. The `__wrap_puts` symbol exists in the image (the stdio family always does), but our `printf` path does not call it. -### Step 5: Flash Project 1 and confirm the NEC/LED behavior - -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 0x0023_structures/build/0x0023_structures.bin -``` - -**Windows x64 (PowerShell):** - -```powershell -.\flash.ps1 -Bin 0x0023_structures\build\0x0023_structures.bin -``` - -**Or flash from the IDA console:** - -**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, "0x0023_structures", "build", "0x0023_structures.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, "0x0023_structures", "build", "0x0023_structures.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 16372 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`. - -On reset it prints the banner once, then a line for every button press: - -``` -IR receiver on GPIO 5 ready -NEC command: 0x0C <- button "1" -> red LED (GPIO 16) -NEC command: 0x18 <- button "2" -> green LED (GPIO 17) -NEC command: 0x5E <- button "3" -> yellow LED (GPIO 18) -``` - -### Step 6: Flash Project 2 and confirm the blink behavior - -```bash -# macOS / Linux -./flash.sh 0x0026_functions/build/0x0026_functions.bin -``` -```powershell -# Windows -.\flash.ps1 -Bin 0x0026_functions\build\0x0026_functions.bin -``` - -**Or flash from the IDA console** (same form, pointing at the Project 2 `.bin`): - -**macOS / Linux:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) -bin_path = os.path.join(root, "0x0026_functions", "build", "0x0026_functions.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:** - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) -bin_path = os.path.join(root, "0x0026_functions", "build", "0x0026_functions.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 16476 bytes ...`. The serial monitor shows the banner, then for each button: - -``` -IR receiver on GPIO 5 ready -NEC command: 0x0C -LED 1 activated on GPIO 16 <- red LED blinks 3x, then holds on -NEC command: 0x18 -LED 2 activated on GPIO 17 <- green LED blinks 3x, then holds on -NEC command: 0x5E -LED 3 activated on GPIO 18 <- yellow LED blinks 3x, then holds on -``` - -> **Watch the two lines together.** The `LED N activated on GPIO P` line is built from the struct's *pin constants*, which the compiler hard-coded. Later we change the pin the loop actually drives but leave the print constant alone — that is the **log desynchronization** this week is about. - ---- - ## 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. @@ -490,265 +339,13 @@ When you come back later, **open the `.i64`**, not the `.bin`; that restores all - **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 13. +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 (Project 1) - -### Step 10: 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 11. - -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 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 13) 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 11: Connect IDA to the GDB server - -1. Make sure the image is open and analyzed (Part 2) and the server from Step 10 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. Verified working end to end: connect, GUI breakpoints (**Add Hardware Breakpoint...**, hardware execute), **Step Into** / **Step Over**, and register edits. - -> **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 10), 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. 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. - -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 12: 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 : -10000186: 4914 ldr r1, [pc, #80] -10000188: 4788 blx r1 -1000018a: 4914 ldr r1, [pc, #80] -1000018c: 4788 blx r1 -1000018e: 4914 ldr r1, [pc, #80] -10000190: 4788 blx r1 -10000192: be00 bkpt 0x0000 -``` - -**The middle `blx` at `0x1000018c` is the call to `main`.** `platform_entry` is byte-identical in both projects, 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 13: 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. - -#### Where you can stop - -| You want to stop at | Project 1 address | How | Repeatable? | -| --- | --- | --- | --- | -| **`main`** | `0x10000234` | The server starts parked there with `BP_ADDR=0x10000234` (Step 10), so IDA is already stopped at `main` when it connects. | No — `main` runs once per reset. | -| **First `printf` (`IR receiver...`)** | `0x1000026c` | Set a hardware breakpoint in the GUI, then click **Resume**. | No — runs once before the loop. | -| **Loop `printf` (`NEC command...`)** | `0x1000027c` | Same. | Yes — fires on every key press. | -| **First `gpio_put` (`mcrr`)** | `0x1000028a` | Same — this is where LED1's pin is written. | Yes — fires on every key press. | -| **`ir_getkey` return** | `0x10000274` | Same. | Yes. | - -#### Set a breakpoint in the GUI - -1. Press `G`, type the address (for example `0x1000028a`), 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 key press. IDA stops with the PC at the address and reports it as a **Breakpoint**. - -> **No breakpoints before you connect.** With GDB MI, a breakpoint set before the connection hangs the session (Step 11). Start parked with `BP_ADDR`, connect, *then* add breakpoints. - -> **Step Over on the raw `.bin` steps *into* calls.** The raw image has no symbol for `__wrap_printf`, `ir_getkey`, or `sleep_ms`, so **Step Over** at a `bl` 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 14 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. - -### Step 14: HACK IT LIVE — move LED1 from GPIO 16 to GPIO 18 - -`main` loads the constant `16` into `r5` once at `0x10000240`, before the loop, and the loop's first `mcrr` at `0x1000028a` writes the LED1 state to the pin in `r5`. Because `r5` is **never reloaded inside the loop**, changing it once is sticky for every later key press. We break on that `mcrr` and change it live. - -1. Press `G`, go to `0x1000028a` (the first `mcrr`, `gpio_put(r5, ...)` for LED1). -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, then press **"1"** on the IR remote. The `ir_getkey` call returns, the `printf` at `0x1000027c` prints `NEC command: 0x0C`, and the breakpoint fires at `0x1000028a`. -4. Open the **Registers** widget (bug icon -> **Registers**). Find `r5`. Its value is `16` (`0x10`) — LED1's pin. -5. **Set `r5` to `18` (`0x12`).** From IDA's Python console (`Plugins -> Python Console`): - ```python - dbg.set_reg_value("r5", 0x12) # LED1 now drives GPIO 18 - ``` - `dbg.set_reg_value(name, value)` writes one register (returns `True` on success). You can also right-click `r5` in the **Registers** widget, press `E` (edit), type `12`, and press Enter. The widget may not repaint the value, but the write reaches the target. -6. Remove the breakpoint at `0x1000028a` and click **Resume**. The core executes the `mcrr` with `r5 = 18`, so pressing **"1"** now lights the **yellow** LED on GPIO 18 instead of the red LED on GPIO 16. - -Because `r5` is set once and never reloaded, the change sticks for every subsequent key press until you reset. Press **"1"** again: the yellow LED lights again, while the terminal still says `NEC command: 0x0C`. - -> **`r5` is the sticky register here.** If you instead edit `r3` (the state, computed by the `clz` trick) or `r2`, the next iteration recomputes them, so the edit lasts one pass. `r5` is the pin, loaded once, so it is the one worth moving. - -### Step 14b: HACK THE STRING LIVE — change `NEC` to `HACKED` - -The text `"NEC command: 0x%02X\n"` lives in flash (`.rodata`) at `0x100038d0`, and flash is **read-only at runtime** — a debugger write there does not stick. So you cannot overwrite the text in place. Instead you redirect the pointer: at the loop `printf` call, `r0` holds the format-string address, so you point `r0` at a replacement string you place in RAM. - -1. Press `G`, go to `0x1000027c` (the loop `bl __wrap_printf`) and set a **hardware execute** breakpoint. Resume and press **"1"** on the remote. At the stop, `r0 = 0x100038d0` (the `ldr r0, [pc, #76]` at `0x1000027a` just loaded the `"NEC command: 0x%02X\n"` pointer) and `r1 = 0x0C`. -2. Put the replacement string into free RAM at `0x20080000` from the **Python console**: - ```python - dbg.write_memory(0x20080000, b"HACKED: 0x%02X\n\x00") # one %02X, same argument - ``` - `dbg.write_memory(address, bytes)` is IDA's debugger memory-write API; it returns `True` on success. Keep exactly one `%02X` so `printf` still consumes the key in `r1`. -3. Point `r0` at that string: - ```python - dbg.set_reg_value("r0", 0x20080000) - ``` - (Or right-click `r0` in the **Registers** widget, press `E`, type `20080000`, and press Enter.) -4. Move the breakpoint past the call in the GUI (remove it at `0x1000027c`, set one at `0x10000280`) and click **Resume**. The core runs `printf` with `r0` pointing at your RAM string and `r1 = 0x0C`, so this iteration prints: - ``` - HACKED: 0x0C - ``` - then stops at `0x10000280`. - -Like the pin hack, this is **one iteration only**: the loop reloads `r0` from the literal pool on every pass, so the next key prints `NEC command: ...` again. The permanent version is the static patch in Step 18b. - -### Step 15: Why the hack reverts (and why we patch next) - -Press **Resume**. The loop branches back to `0x10000270`, reloads `r0` from `0x100038d0` at `0x1000027a`, and `r5` stays at `18` only until the next reset (it is loaded once at `0x10000240`). The string edit was one iteration; the pin edit was sticky but lives only in a register. To make the behavior permanent we must patch the bytes — the static pass. - -Press **Pause** to stop the output flood. - -### Step 15b: 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 (Project 1) +## Part 3: Static — Resolve the Functions in IDA and Patch (Project 1) ### Step 16: Resolve the functions in the IDA GUI @@ -1112,327 +709,7 @@ python ..\uf2conv.py 0x0023_structures-h.bin ^ > runpy.run_path("../../uf2conv.py", run_name="__main__") # path to your uf2conv.py > ``` -### Step 21: Flash and verify the swapped LEDs - -Hold **BOOTSEL**, plug in the Pico 2, and drag `hacked.uf2` onto the **`RP2350`** drive. Or flash the `.bin` over the Debug Probe with SWD — no BOOTSEL — from the console (stop any running OpenOCD first, and use `Popen`, not `run`, so the console is not blocked): - -```python -import os, subprocess -root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() -bin_path = os.path.join(os.path.join(root, "0x0023_structures", "build"), "0x0023_structures-h.bin") -log = os.path.join(os.path.join(root, "0x0023_structures", "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) -``` - -Open the serial monitor and press the buttons: - -``` -PWN command: 0x0C <- button "1" now lights the YELLOW LED (GPIO 18) -PWN command: 0x18 <- button "2" still lights the green LED (GPIO 17) -PWN command: 0x5E <- button "3" now lights the RED LED (GPIO 16) -``` - -**Two bytes swapped the LEDs and three bytes renamed the log — no source code.** - ---- - -## Part 5: Reflash Project 2 and Load It into IDA - -### Step 22: Reflash Project 2 and restart the session - -Part 4 left the Pico running the patched Project 1 image. Put the original Project 2 back and start a fresh session. - -1. Stop any running debug server so the flash script can use the probe: - - ```bash - # macOS / Linux - pkill -TERM -f openocd - ``` - ```powershell - # Windows - Get-Process openocd -ErrorAction SilentlyContinue | Stop-Process - ``` - -2. Flash the original Project 2 image: - - ```bash - # macOS / Linux - ./flash.sh 0x0026_functions/build/0x0026_functions.bin - ``` - ```powershell - # Windows - .\flash.ps1 -Bin 0x0026_functions\build\0x0026_functions.bin - ``` - - **Or do steps 1–2 from the IDA console:** - - **macOS / Linux:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) - bin_path = os.path.join(root, "0x0026_functions", "build", "0x0026_functions.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 Project 2 in the background; log:", log) - ``` - - **Windows:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() # set once (Step 3) - bin_path = os.path.join(root, "0x0026_functions", "build", "0x0026_functions.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 Project 2 in the background; log:", log) - ``` - -3. Load Project 2 and save its database — see Step 22b. - -Confirm the Pico responds to `1` / `2` / `3` again. - -### Step 22b: Load Project 2 into IDA and save the database - -Exactly like Steps 7–8, but for Project 2. **Use `File -> Open with Options...`** (not plain `File -> Open`), select `0x0026_functions/build/0x0026_functions.bin`, and set: - -- **Architecture:** `thumb2` -- **Platform:** `thumb2` -- **Base Address:** `0x10000000` - -Click **Open**. Then press `G`, type `0x10000000`, and confirm the first two words: - -``` -0x10000000 0x20082000 initial stack pointer -0x10000004 0x1000015d reset vector (bit 0 = Thumb) -``` - -If you see data at `0x00000000`, close the tab and redo it with `Open with Options`. - -Save it with `File -> Save As...` as `0x0026_functions.i64` (next to the `.bin`). From now on open the `.i64`, not the `.bin`; save with `Cmd+S` / `Ctrl+S` after every rename or patch. - -> **Console equivalent:** -> ```python -> load("0x0026_functions/build/0x0026_functions.bin", -> options={"loader.imageBase": 0x10000000, "loader.platform": "thumb2"}) -> ``` - ---- - -## Part 6: Dynamic — Break at `main` and Hack Live (Project 2) - -### Step 23: Break at `main` - -`main` is at `0x10000234` in this project too. Start the server parked at `main` (Step 10 form) and connect with the **GDB MI** adapter (Step 11): - -1. Restart the server parked at `main`: - - **macOS / Linux:** - - ```bash - BP_ADDR=0x10000234 ./debug-server.sh - ``` - ```powershell - # Windows - $env:BP_ADDR="0x10000234"; .\debug-server.ps1 - ``` - - **Or restart it from the IDA console:** - - **macOS / Linux:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() - log = os.path.join(root, "openocd.log") - subprocess.run(["pkill", "-TERM", "-f", "openocd"]) # kill any running server first - 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 restarted parked at main; log:", log) - ``` - - **Windows:** - - ```python - import os, subprocess - root = open(os.path.expanduser("~/.embedded-hacking-repo")).read().strip() - log = os.path.join(root, "openocd.log") - subprocess.run(["taskkill", "/F", "/IM", "openocd.exe"]) # kill any running server first - 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 restarted parked at main; log:", log) - ``` -2. Connect IDA (Step 11): adapter **GDB MI**, IP `127.0.0.1`, port `3333`. - -The target is already halted at `main` when IDA connects, and the sidebar reads `Stopped at 0x10000234`. - -### Step 24: Read `main` and find the inlined function bodies - -The whole loop is one function because every helper was inlined. The key difference from Project 1 is the extra `printf` and the blink loop that carries the LED pin in `r5`: - -```asm -10000234
: -10000234: b580 push {r7, lr} -10000236: f003 f8b9 bl 100033ac -1000023a: 2010 movs r0, #16 -1000023c: f000 f9ac bl 10000598 -10000240: f04f 0701 mov.w r7, #1 -10000244: 2310 movs r3, #16 -10000246: ec47 3044 mcrr 0, 4, r3, r7, cr4 -1000024a: 2011 movs r0, #17 -1000024c: f000 f9a4 bl 10000598 -10000250: 2311 movs r3, #17 -10000252: ec47 3044 mcrr 0, 4, r3, r7, cr4 -10000256: 2012 movs r0, #18 -10000258: f000 f99e bl 10000598 -1000025c: 2312 movs r3, #18 -1000025e: ec47 3044 mcrr 0, 4, r3, r7, cr4 -10000262: 2005 movs r0, #5 -10000264: f000 f858 bl 10000318 -10000268: 2105 movs r1, #5 -1000026a: 4828 ldr r0, [pc, #160] -1000026c: f003 f966 bl 1000353c <__wrap_printf> -10000270: f04f 0600 mov.w r6, #0 -10000274: f000 f864 bl 10000340 -10000278: 1e04 subs r4, r0, #0 -1000027a: db19 blt.n 100002b0 -1000027c: 4621 mov r1, r4 -1000027e: 4824 ldr r0, [pc, #144] -10000280: f003 f95c bl 1000353c <__wrap_printf> -10000284: 2510 movs r5, #16 -10000286: ec46 5040 mcrr 0, 4, r5, r6, cr0 -1000028a: 2311 movs r3, #17 -1000028c: ec46 3040 mcrr 0, 4, r3, r6, cr0 -10000290: 2212 movs r2, #18 -10000292: ec46 2040 mcrr 0, 4, r2, r6, cr0 -10000296: 2c0c cmp r4, #12 -10000298: d00e beq.n 100002b8 -1000029a: 2c18 cmp r4, #24 -1000029c: d02e beq.n 100002fc -1000029e: 2c5e cmp r4, #94 -100002a0: d030 beq.n 10000304 -100002a2: 200a movs r0, #10 -100002a4: f000 fea4 bl 10000ff0 -100002a8: f000 f84a bl 10000340 -100002ac: 1e04 subs r4, r0, #0 -100002ae: dae5 bge.n 1000027c -100002b0: 2001 movs r0, #1 -100002b2: f000 fe9d bl 10000ff0 -100002b6: e7dd b.n 10000274 -100002b8: f04f 0801 mov.w r8, #1 -100002bc: 2403 movs r4, #3 -100002be: ec47 5040 mcrr 0, 4, r5, r7, cr0 -100002c2: 2032 movs r0, #50 -100002c4: f000 fe94 bl 10000ff0 -100002c8: ec46 5040 mcrr 0, 4, r5, r6, cr0 -100002cc: 2032 movs r0, #50 -100002ce: f000 fe8f bl 10000ff0 -100002d2: 1e63 subs r3, r4, #1 -100002d4: f013 04ff ands.w r4, r3, #255 -100002d8: d1f1 bne.n 100002be -100002da: ec47 5040 mcrr 0, 4, r5, r7, cr0 -100002de: f1b8 0f01 cmp.w r8, #1 -100002e2: d009 beq.n 100002f8 -100002e4: f1b8 0f02 cmp.w r8, #2 -100002e8: bf14 ite ne -100002ea: 2212 movne r2, #18 -100002ec: 2211 moveq r2, #17 -100002ee: 4641 mov r1, r8 -100002f0: 4808 ldr r0, [pc, #32] -100002f2: f003 f923 bl 1000353c <__wrap_printf> -100002f6: e7d4 b.n 100002a2 -100002f8: 2210 movs r2, #16 -100002fa: e7f8 b.n 100002ee -100002fc: 461d mov r5, r3 -100002fe: f04f 0802 mov.w r8, #2 -10000302: e7db b.n 100002bc -10000304: 4615 mov r5, r2 -10000306: f04f 0803 mov.w r8, #3 -1000030a: e7d7 b.n 100002bc -1000030c: 100038f8 .word 0x100038f8 -10000310: 10003918 .word 0x10003918 -10000314: 10003930 .word 0x10003930 -``` - -What the inlining produced: - -- **`leds_all_off(&leds)`** is the three `mcrr` writes at `0x10000286`, `0x1000028c`, `0x10000292`, all using `r6 = 0` (`mov.w r6, #0` at `0x10000270`). -- **`ir_to_led_number`** is the `cmp`/`beq` chain at `0x10000296`–`0x100002a0` (`12`, `24`, `94`). -- **`get_led_pin`** is the `mov r5, r3` at `0x100002fc` (key 24 -> pin 17) and the `mov r5, r2` at `0x10000304` (key 94 -> pin 18); for key 12, `r5` keeps the `16` loaded at `0x10000284`. -- **`blink_led`** is the loop at `0x100002be`–`0x100002d8`; `r4` counts 3 down to 0, `r5` is the pin, `r7 = 1` and `r6 = 0` drive it on/off. -- **`get_led_pin` in the final print** is `movs r2, #16` at `0x100002f8` (key 1), `moveq r2, #17` at `0x100002ec` (key 2), and `movne r2, #18` at `0x100002ea` (key 3). These are the constants that will **lie** after we patch the loop pins. - -The string map, read straight from `.rodata`: - -| Literal | Points at | String | -| ------- | --------- | ------ | -| `0x1000030c` | `0x100038f8` | `"IR receiver on GPIO %d ready\n"` | -| `0x10000310` | `0x10003918` | `"NEC command: 0x%02X\n"` | -| `0x10000314` | `0x10003930` | `"LED %d activated on GPIO %d\n"` | - -### Step 25: HACK IT LIVE — forge the decoded NEC key - -`ir_getkey` returns the command byte in `r0`; `main` copies it into `r4` at `0x10000278`. We stop right after the read and overwrite `r4` so the program takes a different button's path — even though the operator pressed a different button. - -1. Press `G`, go to `0x1000027c` (the `mov r1, r4` right after the `blt.n`, inside the `key >= 0` block). Set a **hardware execute** breakpoint: `Debugger -> Add Hardware Breakpoint...`. -2. Click **Resume** and press **"1"** on the IR remote. `ir_getkey` returns, `subs r4, r0, #0` at `0x10000278` runs, and the breakpoint fires at `0x1000027c` with `r4 = 0x0C` (12). -3. **Set `r4` to `0x5E` (94)** from the Python console: - ```python - dbg.set_reg_value("r4", 0x5E) # pretend button "3" was pressed - ``` - (Or right-click `r4` in the **Registers** widget, press `E`, type `5e`, and press Enter.) -4. Remove the breakpoint at `0x1000027c` and click **Resume**. The core runs `mov r1, r4`, so the `printf` prints `NEC command: 0x5E`, the `cmp` chain takes the key-94 branch at `0x10000304`, and the Pico blinks the **yellow** LED on GPIO 18 — although you pressed **"1"**. - -> **The pin variant.** If you prefer to move the pin instead of the key, break at `0x10000286` (the first `mcrr`, after `movs r5, #16` at `0x10000284`) and set `r5 = 0x12`. LED1's blink then drives GPIO 18, but `r5` is reloaded at `0x10000284` on the next key, so it is a one-key change. The `r4` edit above is the same idea one step earlier in the pipeline. - -### Step 25b: HACK THE STRING LIVE — change `NEC` to `HACKED` - -The `"NEC command: 0x%02X\n"` format is at `0x10003918`; redirect `r0` to a RAM string at the `printf` call. - -1. Press `G`, go to `0x10000280` (the `bl __wrap_printf` on the key path) and set a **hardware execute** breakpoint. Resume and press **"1"**. At the stop, `r0 = 0x10003918` — the `ldr r0, [pc, #144]` at `0x1000027e` loaded the pointer — and `r1 = 0x0C`. -2. Write the replacement to RAM and repoint `r0`: - ```python - dbg.write_memory(0x20080000, b"HACKED: 0x%02X\n\x00") # keep one %02X - dbg.set_reg_value("r0", 0x20080000) - ``` -3. Remove the breakpoint at `0x10000280`, set one at `0x10000284`, and click **Resume**. This iteration prints: - ``` - HACKED: 0x0C - ``` - One iteration only — the loop reloads `r0` from the literal pool each pass. The permanent version is the static patch in Step 27b. - -### Step 25c: Kill the debugger and OpenOCD - -Same as Step 15b: click the **X** (**Kill**) in the **Debugger** sidebar (or **`Debugger -> Kill`**), then stop OpenOCD 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 -``` - ---- - -## Part 7: Static — Resolve the Functions and Patch (Project 2) +## Part 5: Static — Resolve the Functions and Patch (Project 2) ### Step 26: Resolve the functions in the IDA GUI @@ -1617,7 +894,7 @@ print(bv.read(0x10003918, 20)) # -> b'PWN command: 0x%02X\n\x00' Exactly three bytes, same rule as Project 1: a shorter string must be padded, a longer one overwrites the ` command:` tail. -### Step 28: Export, convert, and flash +### Step 28: Export, convert and export ```python import os @@ -1648,233 +925,6 @@ Or run the conversion from the IDA console, exactly as in Step 20 (`os.chdir` to Hold **BOOTSEL**, plug in the Pico 2, drag `hacked.uf2` onto the **`RP2350`** drive. Or flash the `.bin` over the Debug Probe with SWD — no BOOTSEL — from the console, exactly as in Step 21. -### Step 29: Verify - -Open the serial monitor: - -- press **"1"** -> the **YELLOW** LED on GPIO 18 blinks (it used to be the red LED on GPIO 16), and the terminal still prints `LED 1 activated on GPIO 16` — **wrong**, it is actually GPIO 18; -- press **"3"** -> the **RED** LED on GPIO 16 blinks (it used to be the yellow LED on GPIO 18), and the terminal still prints `LED 3 activated on GPIO 18` — **wrong**, it is actually GPIO 16; -- press **"2"** -> the green LED on GPIO 17 is unchanged; -- every `NEC command:` line now reads `PWN command:`. - -**The log says one thing, the hardware does another — with two bytes and no source code.** - ---- - -## 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("r4", 0x5E)` in the Python console (or right-click the register, press `E`, type hex, Enter) | -| Write debugger memory | `dbg.write_memory(0x20080000, b"HACKED: 0x%02X\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 ` then `bp 2 hw`) | -| Confirm what is armed | The **Breakpoints** widget lists it (command-port fallback: `mdw 0xE0002000 8`, each armed breakpoint shows as ``) | -| Apply the ELF symbol map | Paste the Python snippet from Step 16 / 26 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 13). 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 2 hw` | -| Remove one breakpoint | `rbp ` — **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) | -| 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 | -| Kill the debugger | click **X** in the **Debugger** sidebar, or `Debugger -> Kill` | -| Stop OpenOCD | macOS/Linux: `pkill -TERM -f openocd` — Windows: `taskkill /F /IM openocd.exe` | - -### Where you can stop - -| Stop at | Project 1 `0x0023` | Project 2 `0x0026` | -| ------- | ------------------ | ------------------ | -| `main` (once per reset) | `0x10000234` | `0x10000234` | -| `ir_getkey` return | `0x10000274` | `0x10000274` | -| Loop `printf` (`NEC command`) | `0x1000027c` | `0x10000280` | -| First `gpio_put` (`mcrr`, LED1 pin) | `0x1000028a` | `0x10000286` | -| LED3 `gpio_put` (`mcrr`) | `0x100002aa` | `0x10000292` | - -### Every address and byte we changed - -| Project | Address | Before | After | Effect | -| ------- | ------- | ------ | ----- | ------ | -| `0x0023` | `0x10000240` | `10` | `12` | LED1 pin 16 -> 18 | -| `0x0023` | `0x100002a6` | `12` | `10` | LED3 pin 18 -> 16 | -| `0x0023` | `0x100038d0` | `4e 45 43` | `50 57 4e` | prints `PWN` instead of `NEC` | -| `0x0026` | `0x10000284` | `10` | `12` | LED1 pin 16 -> 18 | -| `0x0026` | `0x10000290` | `12` | `10` | LED3 pin 18 -> 16 | -| `0x0026` | `0x100002f8` | `10` | `12` | optional: key-1 print says GPIO 18 | -| `0x0026` | `0x10003918` | `4e 45 43` | `50 57 4e` | prints `PWN` instead of `NEC` | - -### Raw image facts - -| Item | Value | -| ---- | ----- | -| Build type | `Release` | -| Load base address | `0x10000000` | -| Project 1 size | `16372` bytes (`0x3ff4`) | -| Project 2 size | `16476` bytes (`0x405c`) | -| Initial stack pointer (both) | `0x20082000` | -| Reset vector (both) | `0x1000015d` | -| Fixed `main` anchor (both) | `0x1000018c` | -| `main` (both) | `0x10000234` | -| `ir_init`, Project 1 | `0x100002cc` | -| `ir_init`, Project 2 | `0x10000318` | -| `ir_getkey`, Project 1 | `0x100002f4` | -| `ir_getkey`, Project 2 | `0x10000340` | -| Project 1 `IR receiver` string | `0x100038b0` | -| Project 1 `NEC command` string | `0x100038d0` | -| Project 2 `IR receiver` string | `0x100038f8` | -| Project 2 `NEC command` string | `0x10003918` | -| Project 2 `LED activated` string | `0x10003930` | -| 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. -- **The wrong GDB executable.** Point **Full GDB Executable Path** at the **14.2.rel1** toolchain (Step 11). 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. - -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. - -### 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**. 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. - -### 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. Fix: `rp2350.dap.core0 configure -rtos none`. **GDB MI does not hit this.** - -### `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 the `static` helpers separate, so `main` is not at `0x10000234`. - -### A breakpoint never fires - -First, confirm you actually set one, and that it is a **hardware** breakpoint. `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`) — a software breakpoint cannot be written to read-only flash. 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. -- **Is the core running?** If it is stopped, click **Resume**. -- **Does the address get reached again?** `main` runs once per reset, so use `BP_ADDR` at startup rather than `reset run` while attached. Loop addresses such as `0x1000028a` (P1) and `0x10000286` (P2) fire on the next key press with no reset. For the `NEC` print addresses (`0x1000027c` / `0x10000280`) you must also press a remote button, because they sit inside the `if (key >= 0)` block. - -### I edit `r4` / `r5` and it reverts - -For Project 2's key forgery, `subs r4, r0, #0` reloads `r4` from `ir_getkey` on every key press, so the edit is visible for one key. For Project 1's `r5` pin edit, `r5` is loaded once at `0x10000240` and never reloaded, so it sticks until reset. For Project 2's `r5` pin edit, `movs r5, #16` at `0x10000284` reloads it on every key. The edit sticks only while the core is **genuinely stopped** at the breakpoint. - -> **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. A value changed outside IDA will not appear until the next stop. - -### The string hack does nothing (or prints garbage) - -Pick a RAM address that is free — `0x20080000` is safe here (well above the `.data`/`.bss` end at about `0x20000810`). Write a NUL-terminated string, and keep exactly the format specifiers the call consumes: the NEC format has one `%02X`, so the replacement must keep one `%02X`. Then set `r0`, not `r1`. - -### The patched string shifted `printf` output - -The `NEC command: 0x%02X\n` format has one `%02X`. Keep the replacement exactly three bytes (`NEC` -> `PWN`); a longer string would overwrite the ` command:` tail and a shorter one would leave a stray character. In the live hack you write a whole new NUL-terminated string to RAM, so any length is fine as long as it keeps one `%02X`. - -### The struct is not in memory — where is it? - -It is not. `Release` proved `simple_led_ctrl_t` never escapes `main`, so the compiler **flattened** it: the three `uint8_t` pins became the immediates `16`, `17`, `18` and the three `bool` states became register values. There is no `sub sp` for the struct and no memory address to inspect. You patch the immediates instead. If you need to see the struct in memory, build `Debug`, but then none of the addresses in this guide apply. - -### Project 2's `LED N activated on GPIO P` line is wrong after the patch - -That is the point. Step 27 swaps the pins the loop *drives* but leaves the print constants (`0x100002f8`, `0x100002ea`, `0x100002ec`) untouched, so the log shows the old mapping. If you want the print to match, apply the optional consistency patch in Step 27. - -### The serial capture is garbage on macOS - -Reading `/dev/cu.usbmodem*` with a bare `read()` returns garbage. Set **raw termios at 115200** first, or just use `screen /dev/cu.usbmodem* 115200`, which does it for you. - -### It worked for a second, then stopped (IDA's view desyncs) - -The main cause is **driving the core from the OpenOCD command port while IDA is connected**. If you must reset, **Detach first**, reset, then reconnect. Never leave a breakpoint on the PC you are about to step or resume from. - -### 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. - -### 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, you can still do the live hacks 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 10), then in a new terminal: - -``` -arm-none-eabi-gdb -``` - -At the `(gdb)` prompt for Project 1: - -``` -set architecture armv8-m.main -target extended-remote :3333 -hbreak *0x1000028a -continue -``` - -Do **not** run `monitor reset run` before `hbreak`. `0x1000028a` is inside `main`'s loop, so the breakpoint fires on the next key press with no reset. Press **"1"** on the remote, then: - -``` -info registers pc r5 # pc = 0x1000028a, r5 = 0x10 -set $r5 = 0x12 -continue -``` - -The next LED1 write drives GPIO 18 — the same temporary live hack as editing `r5` in the IDA Registers widget. For the string hack, break at `0x1000027c`, then `set {char[19]}0x20080000 = "HACKED: 0x%02X\n"` and `set $r0 = 0x20080000`. - -Project 2 is the same with the other call site and value: - -``` -hbreak *0x1000027c -continue -info registers pc r4 # r4 holds the decoded key you pressed -set $r4 = 0x5E -continue -``` - -`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. - ## Glossary | Term | Definition | @@ -1884,7 +934,7 @@ continue | **`.elf`** | Linked image with the symbol table; the ground truth for addresses and names | | **Flattening** | The optimizer replacing struct member accesses with the member's constant value; why `simple_led_ctrl_t` disappears | | **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 every `static` helper disappears from `main` | | **Literal pool** | A block of 32-bit constants that Thumb-2 code reaches with PC-relative `ldr` | | **`mcrr`** | Move to coprocessor from two registers — how the SIO GPIO writes are encoded | @@ -1897,4 +947,4 @@ continue --- -**Remember:** the ELF tells you what every address is, and the `.bin` is what you actually patch. Prove the behavior dynamically, resolve the names from the ELF, then patch the bytes and flash. \ No newline at end of file +**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. \ No newline at end of file