re_match: byte-match functions with the game's compiler (Visual C++ 2003)
Runs R:/Workspace/Tools/re/match71.py for up to 50 addresses from main or an agent's worktree; configured by matcher/match_flags in [reverse].
This commit is contained in:
1 parent
584fabeb74
commit
c7e678eacb
5 files changed
+78
-3
No files matched your search
+10
-3
@@ -255,9 +255,10 @@ 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"])
|
||||
p.add_argument("action", choices=["disasm", "xrefs", "bytes", "lookup", "ghidra", "ghidra-tools", "debugger-tools", "match"])
|
||||
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")
|
||||
p.add_argument("--max-bytes", type=int, dest="max_bytes")
|
||||
p.add_argument("--count", type=int, default=16)
|
||||
p.add_argument("--as", dest="as_type", default="hex", choices=["hex", "int32", "uint32", "float", "double", "string"])
|
||||
@@ -675,8 +676,14 @@ 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")) else Reverse(harbor.project.reverse)
|
||||
if args.action == "debugger-tools":
|
||||
r = None if args.action.startswith(("ghidra", "debugger", "match")) else Reverse(harbor.project.reverse)
|
||||
if args.action == "match":
|
||||
from .reverse import match_functions
|
||||
res = match_functions(harbor.project.reverse, args.target, args.decomp)
|
||||
out(res, res["summary"] + "\n" + "\n".join(
|
||||
f"{x['address']} {x['status']:<9} {x.get('similarity', '')!s:<6} {x['file']} {x.get('note', '')}"
|
||||
for x in res["results"]))
|
||||
elif args.action == "debugger-tools":
|
||||
from .reverse import Debugger
|
||||
res = Debugger(harbor.project.reverse).tools()
|
||||
out(res, "\n".join(f"{t['name']:<22} {(t['description'] or '').splitlines()[0][:90] if t['description'] else ''}"
|
||||
|
||||
@@ -503,6 +503,21 @@ def _gh(fn):
|
||||
raise ToolError(str(error)) from None
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def re_match(targets: str, decomp: str | None = None) -> dict:
|
||||
"""Compile functions with the game's own compiler (Visual C++ 2003, cl 13.10) and compare them with the
|
||||
original bytes. targets: up to 50 addresses, e.g. '0x412F70, 0x4ADC70'; decomp: your worktree (default main).
|
||||
status 'match' means byte-identical apart from linker-filled addresses; 'arguments in registers' means the
|
||||
game passed them in registers (link-time code generation) and the rest is identical. similarity (0-1)
|
||||
compares instructions, so you can see whether a rewrite moved closer. Matching is a bonus, not a gate:
|
||||
readable, correct C comes first."""
|
||||
try:
|
||||
from .reverse import match_functions
|
||||
return match_functions(harbor().project.reverse, targets, decomp)
|
||||
except (HarborError, projects.ProjectError) as error:
|
||||
raise ToolError(str(error)) from None
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ghidra_tools() -> list:
|
||||
"""The tools of the team's shared Ghidra session (GhidrAssistMCP, read-only, the curated project with every
|
||||
|
||||
@@ -452,3 +452,32 @@ class Debugger:
|
||||
pass
|
||||
self.proc.terminate()
|
||||
self.proc = None
|
||||
|
||||
|
||||
def match_functions(config, targets, decomp=None, flags=None):
|
||||
"""Compile functions with the game's compiler (Visual C++ 2003) and compare them with the original bytes.
|
||||
targets: addresses ("0x4ADC70, 0x412F70"); decomp: the checkout or worktree whose src/ holds them."""
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
config = config or {}
|
||||
matcher = config.get("matcher")
|
||||
if not matcher:
|
||||
raise HarborError("this project has no matcher 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) > 50:
|
||||
raise HarborError("give 1 to 50 addresses")
|
||||
with tempfile.TemporaryDirectory(dir=config.get("debugger_temp")) as tmp:
|
||||
out = Path(tmp) / "match.json"
|
||||
command = [sys.executable, "-B", str(matcher), "--all", "--only", ",".join(wanted), "--json", str(out),
|
||||
"--flags", flags or config.get("match_flags", "-TC -O2")]
|
||||
if decomp:
|
||||
command += ["--decomp", str(decomp)]
|
||||
result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||
timeout=600, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||
if not out.exists():
|
||||
raise HarborError("the matcher failed: " + (result.stdout + result.stderr).strip()[-800:])
|
||||
rows = json.loads(out.read_text(encoding="utf-8"))
|
||||
errors = [line for line in result.stdout.splitlines() if "compile errors" in line]
|
||||
return {"results": rows, "summary": result.stdout.strip().splitlines()[0] if result.stdout.strip() else "",
|
||||
**({"compile": errors[0]} if errors else {})}
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Stands in for match71.py: writes one result per --only address."""
|
||||
import json
|
||||
import sys
|
||||
|
||||
args = sys.argv[1:]
|
||||
only = args[args.index("--only") + 1].split(",")
|
||||
out = args[args.index("--json") + 1]
|
||||
rows = [{"address": a, "file": a + "_F.c", "status": "match", "similarity": 1.0} for a in only]
|
||||
open(out, "w", encoding="utf-8").write(json.dumps(rows))
|
||||
print(f"cl 13.10 {args[args.index('--flags') + 1]}: {len(rows)} functions: {len(rows)} match")
|
||||
@@ -136,3 +136,17 @@ class DebuggerForwarder(unittest.TestCase):
|
||||
d.close()
|
||||
with self.assertRaises(HarborError):
|
||||
Debugger({})
|
||||
|
||||
|
||||
class MatchFunctions(unittest.TestCase):
|
||||
def test_runs_the_matcher_and_reads_its_results(self):
|
||||
from pathlib import Path
|
||||
from harbor.reverse import match_functions
|
||||
config = {"matcher": str(Path(__file__).with_name("fake_matcher.py")), "match_flags": "-TC -O2"}
|
||||
res = match_functions(config, "0x412F70, 0x404810")
|
||||
self.assertEqual([r["address"] for r in res["results"]], ["0x412F70", "0x404810"])
|
||||
self.assertIn("-TC -O2", res["summary"])
|
||||
with self.assertRaises(HarborError):
|
||||
match_functions(config, "")
|
||||
with self.assertRaises(HarborError):
|
||||
match_functions({}, "0x412F70")
|
||||
Reference in new issue
Block a user