{"id":51462612,"url":"https://github.com/davidclawson/ripcord","last_synced_at":"2026-07-06T07:01:20.280Z","repository":{"id":361469447,"uuid":"1201704771","full_name":"DavidClawson/ripcord","owner":"DavidClawson","description":"Firmware reverse-engineering pipeline: opaque binary in, queryable fact warehouse out. Ghidra + DuckDB + execution-verified analysis, driven by Claude Code skills. Primary target: FNIRSI 2C53T oscilloscope (AT32F403A + opaque Gowin FPGA).","archived":false,"fork":false,"pushed_at":"2026-05-30T18:51:46.000Z","size":1117,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-30T20:18:16.198Z","etag":null,"topics":["binary-analysis","claude","claude-code","cortex-m","duckdb","embedded","firmware","ghidra","llm","oscilloscope","reverse-engineering","unicorn"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DavidClawson.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-05T03:26:23.000Z","updated_at":"2026-05-30T18:51:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/DavidClawson/ripcord","commit_stats":null,"previous_names":["davidclawson/ripcord"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/DavidClawson/ripcord","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidClawson%2Fripcord","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidClawson%2Fripcord/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidClawson%2Fripcord/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidClawson%2Fripcord/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DavidClawson","download_url":"https://codeload.github.com/DavidClawson/ripcord/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidClawson%2Fripcord/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35180933,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-06T02:00:07.184Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["binary-analysis","claude","claude-code","cortex-m","duckdb","embedded","firmware","ghidra","llm","oscilloscope","reverse-engineering","unicorn"],"created_at":"2026-07-06T07:01:19.319Z","updated_at":"2026-07-06T07:01:20.266Z","avatar_url":"https://github.com/DavidClawson.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ripcord\n\n**Opaque firmware binary in, queryable fact database out — one command.**\n\nripcord is a research pipeline for reverse engineering embedded firmware.\nIt takes a binary with no symbols, no source, and an undocumented hardware\nperipheral, and expands it into a structured warehouse of facts —\nfunctions, call graph, MMIO access patterns, decompiled C, behavioral\ntraces — that deterministic analyzers, formal methods, and LLM agents can\nall query without ever re-reading the raw bytes.\n\n\u003e Pull the ripcord on a parachute and a carefully packed structure tumbles\n\u003e out and inflates into something functional. Same operation, applied to\n\u003e firmware.\n\nThe driving target is the **FNIRSI 2C53T oscilloscope** (AT32F403A MCU + an\nopaque Gowin FPGA). The hardest, most valuable part of that firmware is the\nFPGA acquisition path — timing-critical code talking to a chip with *no\npublic documentation and no source*. The only way to know what the FPGA\ndoes is to watch the MCU talk to it. ripcord is built to capture that\nconversation and turn it into an execution-verified protocol spec.\n\n---\n\n## The idea in one picture\n\n```\n   firmware.bin\n        │\n        ▼\n┌───────────────────┐   deterministic, runs in minutes, no human judgment\n│  IDENTIFY         │   ISA · load address · chip family  (scripts/identify.py)\n├───────────────────┤\n│  EXTRACT (Ghidra) │   functions · calls · blocks · xrefs · strings\n│                   │   pcode · decompiled C            (PyGhidra headless)\n├───────────────────┤\n│  RECOVER          │   vector tables, func-ptr dispatch, veneers, registrars\n│                   │   → closes the call-graph reachability gap\n├───────────────────┤\n│  CLASSIFY         │   SVD-resolved peripheral register access · fingerprint\n│                   │   match library code across compilers\n├───────────────────┤\n│  TRACE (Renode)   │   boot the binary, capture MMIO transcript = ground truth\n└─────────┬─────────┘\n          ▼\n   ┌─────────────────────────────────────────┐\n   │   THE WAREHOUSE                          │   per-target Parquet tables,\n   │   build/\u003ctarget\u003e/tables/*.parquet        │   queried with DuckDB.\n   │   (no database file — Parquet is truth)  │   THIS is the artifact.\n   └─────────┬───────────────────────────────┘\n             │\n     ┌───────┴────────┬─────────────────┬──────────────────┐\n     ▼                ▼                 ▼                  ▼\n  scripts/query   LLM agent swarm   Unicorn / Renode    Claude Code\n  (SQL / DuckDB)  (bulk labeling)   (VERIFY by          (skills + CLI:\n                                     execution)          drives it all)\n```\n\nTwo principles do the heavy lifting:\n\n1. **Execution is the verification oracle — not the compiler.** A claim about\n   what a function does is confirmed by *running* it (Unicorn) or *tracing*\n   it (Renode) and diffing register/memory/MMIO deltas against the original.\n   Compilers catch type errors; execution catches logic errors. No claim\n   becomes canonical until execution backs it. This is the part most RE\n   tooling skips (see [Related work](#related-work)).\n\n2. **The database is the artifact — not clean source code.** The deliverable\n   is a queryable set of facts about the binary. Rendered C, if it exists at\n   all, is a late-stage *view* over the database, never the goal. (Why:\n   [`notes/goal-and-approach.md`](./notes/goal-and-approach.md).)\n\nLLM budget is spent only on the *residue* deterministic tools can't resolve.\nEverything mechanical — Ghidra extraction, library identification, call\nrecovery, trace capture — runs unattended in minutes.\n\n---\n\n## Quick start\n\n```bash\n# Identify ISA / load address / chip before committing to a full run\nscripts/identify.py firmware.bin\n\n# One command: identify → extract → ingest → recover calls → classify → summarize\nscripts/ripcord.py firmware.elf                                   # ELF: flags inferred\nscripts/ripcord.py firmware.bin --chip AT32F403A --base-addr 0x08004000  # raw binary\n\n# Ask questions over the warehouse + decompiled C + an LLM\nscripts/analyze --target stock_v120 \"what writes to USART2_DR?\"\n\n# Full bottom-up comprehension: smoke-test every function, name them,\n# decompose monsters, synthesize subsystem → architecture narratives\nuv run python scripts/agents/deep_analysis.py --target stock_v120\n\n# Render a self-contained HTML report\nscripts/render/report.py stock_v120\n\n# (optional) Expose the warehouse over MCP for a client without shell access.\n# The primary path is Claude Code running the tools + skills above directly.\nuv run python scripts/mcp_server.py --build-dir ./build\n```\n\nSee [`SETUP.md`](./SETUP.md) for toolchain prerequisites (Ghidra 11.2+ with\nPyGhidra, Python 3.11+, `uv`, Snakemake, DuckDB; optionally Renode and a\ncross-toolchain to build the test corpus).\n\n---\n\n## Why the harness is the point\n\nMost \"LLM + Ghidra\" tools feed a single decompiled function to a model and\nask \"what does this do?\" — a fragment with no surrounding context. That\nstarves the model exactly where embedded RE is hardest.\n\nripcord inverts it. The deterministic pipeline builds a rich, *queryable*\ncontext first; then **Claude Code drives** — running the CLI tools and\nskills (`.claude/skills/`) directly to pull precisely the tables, decompiled\nbodies, peripheral maps, and execution traces it needs, iteratively, while\nreasoning about the binary as a whole *and building new tools mid-task* when\na target demands them. Reusable procedures harden into skills\n(`firmware-bringup`, `execution-verify`); execution-verified conclusions land\nin the contract ledger (`scripts/contracts/ledger.py`), which is the durable\nproduct. The single-shot API paths (`scripts/analyze`, the agent swarm) stay\nfor cheap, scoped, *measurable* sub-tasks — fingerprint matching, bulk\nfunction labeling — where a fragment genuinely is enough. An\n[MCP server](./scripts/mcp_server.py) remains as optional interop for a\nclient that can't run the shell; it isn't the primary surface, because\nripcord's data is local Parquet the driver already reads directly.\nComprehension lives in the harness, not the access protocol.\n\n---\n\n## What's in the warehouse\n\nA `snakemake --cores 4 --resources ghidra=1` run produces typed Parquet\ntables per target under `build/\u003ctarget\u003e/tables/`. Agent and validation\nstages add more. Highlights:\n\n| table                  | grain                                                        |\n|------------------------|--------------------------------------------------------------|\n| `functions`            | one row per Ghidra-discovered function (incl. `body_hash`)   |\n| `calls` / `xrefs`      | call sites; non-call references (reads, writes, jumps, data) |\n| `basic_blocks`         | one row per CodeBlock, with containing function              |\n| `strings`              | defined strings in loaded memory                             |\n| `decompiled`           | Ghidra decompiled pseudo-C, one row per function             |\n| `pcode_features`       | per-function P-Code opcode histogram + sequence hash         |\n| `recovered_calls`      | recovered indirect call edges (vector table, func ptr, …)    |\n| `peripheral_xrefs`     | SVD-resolved peripheral register accesses                    |\n| `mmio_events`          | MemoryIORead/Write from a Renode trace, joinable by PC       |\n| `unicorn_smoke`        | per-function executability (catches code-vs-data misdecode)  |\n| `ground_truth_functions` | `nm -S` symbols, the regression signal                     |\n\nAll tables are auto-discovered as DuckDB views by `scripts/query`. The\n[`notes/queries/`](./notes/queries/) directory holds committed SQL that\ndoubles as executable documentation and regression tests.\n\n---\n\n## Current state (2026-05)\n\nPhase 0 complete; Phase 1 library-ID validated end-to-end including blind\nrecovery on a stripped binary; Phase 3 agent swarm validated end-to-end.\nRenode trace capture and Datalog (Souffle) derivations are wired into the\nSnakemake DAG. Deep hierarchical analysis, context enrichment, and Unicorn\nexecution-validation are built on top.\n\n**Fifteen targets across four build ecosystems** live in the warehouse:\n5 Raspberry Pi Pico (Cortex-M0+), 2 Zephyr (Cortex-M3), 1 stripped blind-\nrecovery target, 3 AT32F403A reference builds (GCC + LLVM, the cross-compiler\ncorpus), and 4 stock FNIRSI 2C53T firmware versions (V1.0.3–V1.2.0) — the\nprimary target *and* its own differential ground truth.\n\nA few empirical results that fell out (full list and provenance in\n[`CLAUDE.md`](./CLAUDE.md) → \"Key empirical findings\"):\n\n- **Blind recovery on a stripped binary: 86.6% recall, 94.9% precision** —\n  171/197 functions re-identified with zero symbols.\n- **Computed-call recovery closes the reachability gap from 70% unreachable\n  to 12%** via five recovery mechanisms at ~95% blended precision.\n- **Constant-based fingerprinting: 100% precision, cross-compiler.**\n- **Execution catches what static analysis can't** — the Unicorn smoke test\n  flags Ghidra decoding data as code, the #1 failure mode for raw imports.\n- **The FNIRSI V1.0.3→V1.0.7 transition was a full architectural rewrite of\n  the FPGA acquisition path** (USART2-only → DMA/SPI3), confirmed by\n  byte-identical FreeRTOS port code against a GCC reference build.\n\n---\n\n## Related work\n\nripcord's individual ingredients all exist in the wild; the combination —\na structured fact warehouse **plus** an execution-as-verification oracle\n**plus** a skills-driven Claude Code harness with a provenance-tracked\ncontract ledger, pointed at *comprehending* an opaque binary — is the part I\nhaven't found assembled elsewhere. Honest positioning:\n\n- **LLM + disassembler tools** (Gepetto, G-3PO, aiDAPal, DeGPT) mostly send a\n  decompiled snippet to a model and write back a rename/comment. ripcord\n  builds queryable context *first*, so the model never reasons from a\n  context-free fragment.\n- **Persistent structured state is no longer a differentiator.**\n  [GhidrAssist](https://github.com/jtang613/GhidrAssist) (open source, a\n  SQLite+graph knowledge DB with a 5-level hierarchy) and **Binary Ninja\n  Sidekick** (commercial, with provenance and a background validation agent)\n  both build it. ripcord's separation is that **their validation is static**\n  — re-analysis and cross-reference queries — whereas ripcord gates every\n  canonical claim on *execution*.\n- **MCP-over-a-disassembler is table stakes — and not where ripcord's value\n  is.** [GhidraMCP](https://github.com/LaurieWired/GhidraMCP) (9k+ stars) and\n  [IDA Pro MCP](https://github.com/mrexodia/ida-pro-mcp) are mature; they\n  expose *live tool calls* over a protocol. ripcord keeps an MCP surface only\n  as optional interop — the driver (Claude Code) reads the local warehouse\n  directly via the CLI, so the access protocol is incidental. What's behind\n  the surface — a *warehouse of execution-verified facts* and the skills that\n  produce it — is the interesting part.\n- **Binary-analysis-as-a-database predates ripcord** —\n  [ddisasm/GTIRB](https://github.com/GrammaTech/ddisasm) (which shares\n  ripcord's Souffle/Datalog layer) and CodeQL. ripcord *uses* that technique;\n  it didn't invent it.\n- **Firmware rehosting** (PRETENDER, P2IM, DICE, Fuzzware) already infers\n  MMIO peripheral models from traces — but the deliverable is \"enough model\n  to fuzz,\" **not** a legible, falsifiable MCU↔peripheral protocol spec.\n  Same input class, different output. ripcord aims at the legible boundary\n  contract those tools leave on the table.\n- **Matched-source decomp** (decomp.me, the N64/PSX projects) verifies by\n  byte-identical recompilation — a *stricter* oracle than ripcord's\n  behavioral execution diff, but aimed at perfect source recovery, which\n  ripcord explicitly is **not** trying to produce.\n- **Closest precedent to the core thesis:** Patrick Hulin's\n  [SimTower reimplementation](https://phulin.me/blog/simtower) put an LLM in\n  a closed loop against a Unicorn emulator as ground truth — the same\n  execution-as-oracle idea, as a one-off project rather than a general\n  pipeline.\n\n---\n\n## Where to go deeper\n\n- [`CLAUDE.md`](./CLAUDE.md) — the dense, authoritative project orientation:\n  every script, every table, every committed query, current findings.\n- [`notes/`](./notes/) — the design log and the FNIRSI target dossier. Start\n  with [`notes/README.md`](./notes/README.md). Key files:\n  [`design-decisions.md`](./notes/design-decisions.md) (why each choice was\n  made), [`pipeline-architecture.md`](./notes/pipeline-architecture.md),\n  [`scope_acquisition_spec.md`](./notes/scope_acquisition_spec.md) (the\n  MCU↔FPGA protocol), and\n  [`renode-at32-bringup.md`](./notes/renode-at32-bringup.md) (the FPGA\n  emulation oracle in action).\n\n---\n\n## Scope, honesty, and the FPGA caveat\n\nripcord is deliberately **generic**. The scope firmware is the proving\nground, not a license to hard-code 2C53T specifics into the core pipeline —\ntarget knowledge lives in `notes/` and in queries, never in the extractors.\n\nThe FPGA timing code has *no external ground truth*. ripcord tags every\nclaim with a provenance level and never presents inferred FPGA behavior as\nestablished fact: an internal dispatch/selector code is not a wire-level\nhardware transaction, and a value the firmware *wrote* is observed while a\nreply a stub *invented* is unverified until a hardware trace confirms it.\nThat discipline is the whole reason the execution oracle exists.\n\n---\n\n## License\n\n[MIT](./LICENSE). Firmware binaries analyzed by the pipeline are **not**\nincluded in this repository; their licensing belongs to their original\nauthors. The test corpus is built from open SDKs (Pico SDK, Zephyr, the\nAT32 SDK) or supplied by the user.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidclawson%2Fripcord","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavidclawson%2Fripcord","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidclawson%2Fripcord/lists"}