diff --git a/harbor/cli.py b/harbor/cli.py index 0422f26..ec4444d 100644 --- a/harbor/cli.py +++ b/harbor/cli.py @@ -255,7 +255,7 @@ def build_parser(): p = sub.add_parser("who", help="who has worked on a scope (notes, reviews, claims, messages)") p.add_argument("scope") p = sub.add_parser("re", help="reverse-engineering lookups from the project's image and annotations") - p.add_argument("action", choices=["disasm", "xrefs", "bytes", "lookup", "ghidra", "ghidra-tools", "debugger-tools", "match", "similar", "struct-audit"]) + p.add_argument("action", choices=["disasm", "xrefs", "bytes", "lookup", "ghidra", "ghidra-tools", "debugger-tools", "match", "similar", "struct-audit", "behaviour"]) p.add_argument("target", nargs="?", default="", help="an address (0x4AD850) or a name; for ghidra, the tool") p.add_argument("arguments", nargs="?", default="{}", help="ghidra: the tool's arguments as JSON") p.add_argument("--decomp", help="match: the checkout or worktree to compile from") @@ -690,8 +690,12 @@ def main(argv=None): out(r, f"summary #{r['id']} pinned to {r['thread']}") elif cmd == "re": from .reverse import Ghidra, Reverse - r = None if args.action.startswith(("ghidra", "debugger", "match")) else Reverse(harbor.project.reverse) - if args.action in ("similar", "struct-audit"): + r = None if args.action.startswith(("ghidra", "debugger", "match", "behaviour")) else Reverse(harbor.project.reverse) + if args.action == "behaviour": + from .reverse import behaviour_check + res = behaviour_check(harbor.project.reverse, args.target, args.decomp) + out(res, "\n".join(f"{r['address']} {r.get('verdict', ''):<8} {r.get('name', '')} {r.get('reason') or ''}" for r in res)) + elif args.action in ("similar", "struct-audit"): from .reverse import run_toolkit res = (run_toolkit(harbor.project.reverse, "similar", [args.target]) if args.action == "similar" else run_toolkit(harbor.project.reverse, "struct_audit", ["--struct", args.target], json_to_file=True)) diff --git a/harbor/mcp_server.py b/harbor/mcp_server.py index c9d624d..679af1e 100644 --- a/harbor/mcp_server.py +++ b/harbor/mcp_server.py @@ -539,6 +539,20 @@ def re_match_program(targets: str = "", decomp: str | None = None) -> dict: raise ToolError(str(error)) from None +@mcp.tool() +def re_behaviour(targets: str, decomp: str | None = None) -> list: + """The emulator check: run the game's own machine code and your compiled C of each function (up to 30 + addresses; decomp = your worktree) from the same registers, stack and memory, 12 trials each, and compare the + result, every memory write, the calls made and their arguments. verdict 'same' means it behaves like the game; + 'differs' gives the first divergence in words (e.g. 'returns 1 vs 0', 'original writes 0x86BAC0=5, ours 4'); + 'weak' means no trial ran to the end in both. Use it before submitting anything that does not byte-match.""" + try: + from .reverse import behaviour_check + return behaviour_check(harbor().project.reverse, targets, decomp) + except (HarborError, projects.ProjectError) as error: + raise ToolError(str(error)) from None + + @mcp.tool() def re_similar(target: str, k: int = 10, min_score: float = 0.7) -> dict: """Functions whose instructions look like this one (normalized: addresses blanked, registers and small diff --git a/harbor/reverse.py b/harbor/reverse.py index f1a3a94..72bee46 100644 --- a/harbor/reverse.py +++ b/harbor/reverse.py @@ -614,3 +614,31 @@ def match_program(config, targets=None, decomp=None): "ret_pops", "ours_ret_pops", "relocation_problems", "note") return {"counts": counts, "results": [{k: r[k] for k in keep if k in r} for r in rows] if wanted else [{k: r[k] for k in keep if k in r} for r in rows if r.get("status") != "differs"][:200]} + + +def behaviour_check(config, targets, decomp=None, precision=24): + """Run the original machine code and our compiled C of each function in an emulator from the same inputs and + compare results, memory writes and calls (diffrun.py). targets: 1 to 30 addresses.""" + import subprocess + import tempfile + config = config or {} + command = config.get("diffrun_command") + if not command: + raise HarborError("this project has no diffrun_command in its [reverse] table") + wanted = [t.strip() for t in re.split(r"[,\s]+", str(targets)) if t.strip()] + if not wanted or len(wanted) > 30: + raise HarborError("give 1 to 30 addresses") + command = command if isinstance(command, list) else [command] + with tempfile.TemporaryDirectory(dir=config.get("debugger_temp")) as tmp: + out = Path(tmp) / "behaviour.json" + argv = [*command, *wanted, "--json", str(out), "--precision", str(int(precision)), "--jobs", "4"] + if decomp: + argv += ["--decomp", str(decomp)] + result = subprocess.run(argv, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=1500, + cwd=str(Path(command[-1]).parent), + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) + if not out.exists(): + raise HarborError("the emulator check failed: " + (result.stdout + result.stderr).strip()[-800:]) + rows = json.loads(out.read_text(encoding="utf-8")) + keep = ("address", "name", "verdict", "reason", "weak", "counts", "notes") + return [{k: r[k] for k in keep if k in r} for r in rows]