re_summary: one call for a function's name, prototype, size, convention, readable status, callers and callees with their status, globals, strings, vtable slots and Mac/Xbox hints (harbor re summary); ghidra_check: confirm the shared Ghidra server serves our image (address range, byte samples, SHA-256 when the imported file is readable) (harbor re ghidra-check)

This commit is contained in:
KeelrLeek committed 2026-10-09 09:51:09 -05:00
1 parent 57fb31f2d6
commit 21b7fedb6f
4 files changed
+366 -1

No files matched your search

+99
View File
@@ -58,6 +58,105 @@ class ReverseTools(unittest.TestCase):
with self.assertRaises(HarborError):
Reverse({})
def test_summary_names_status_vtables_and_hints(self):
image = bytearray((self.d / "image.bin").read_bytes())
image[0xC0:0xC8] = struct.pack("<II", 0x401000, 0x401040) # 0x4010C0: a two-slot vtable
(self.d / "image.bin").write_bytes(bytes(image))
(self.d / "sections.json").write_text(json.dumps([
{"name": ".text", "start": 0x401000, "end": 0x401080}, {"name": ".rdata", "start": 0x4010B0,
"end": 0x401100}]), encoding="utf-8")
facts = [json.loads(l) for l in (self.d / "facts.jsonl").read_text(encoding="utf-8").splitlines()]
facts[1]["data"] = ["0x4010C0"] # the constructor installs it
facts[0]["strings"] = [".cue"]
(self.d / "facts.jsonl").write_text("\n".join(json.dumps(f) for f in facts), encoding="utf-8")
src = self.d / "src" / "game"
src.mkdir(parents=True)
(src / "401040_Helper.c").write_text("/* Helper\n * status: readable\n */\nvoid Helper(void) {}\n",
encoding="utf-8")
(self.d / "mac.csv").write_text("pc_address,demangled,confidence\n0x00401000,Level::Get(),high\n",
encoding="utf-8")
r = Reverse({"image": str(self.d / "image.bin"), "base": "0x401000", "facts": str(self.d / "facts.jsonl"),
"annotations": str(self.d / "annotations"), "src": str(self.d / "src"),
"mac_names": str(self.d / "mac.csv")})
s = r.summary("GetLevel")
self.assertEqual((s["name"], s["size"], s["status"], s["readable"]), ("GetLevel", 18, "no file", False))
self.assertIn("plain ret", s["prototype"])
self.assertEqual(s["callees"], [{"address": "0x401040", "name": "Helper", "status": "readable",
"readable": True}])
self.assertEqual(s["globals"], [{"address": "0x401080", "name": "g_root", "type": "int"}])
self.assertEqual(s["strings"], [".cue"])
self.assertEqual(s["vtable_slots"], [{"table": "0x4010C0", "slot": 0, "offset": "0x0", "kind": "vtable"}])
self.assertEqual(s["hints"]["mac_name"]["demangled"], "Level::Get()")
h = r.summary("0x401040")
self.assertTrue(h["readable"])
self.assertEqual([c["name"] for c in h["callers"]], ["GetLevel"])
self.assertEqual(h["vtable_slots"][0]["slot"], 1)
from harbor.reverse import format_summary
self.assertIn("vtable 0x4010C0 slot 1 (+0x4)", format_summary(h))
class GhidraCheck(unittest.TestCase):
"""ghidra_check compares the served program with the project image: range, SHA-256 of the file, byte samples."""
def serve(self, lo, hi, path, image):
import http.server
import threading
class Fake(http.server.BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def do_POST(self):
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
if "id" not in body:
self.send_response(202)
self.end_headers()
return
params = body.get("params", {})
name, args = params.get("name"), params.get("arguments", {})
text = ""
if name == "get_binary_info":
text = f"Program Information:\nName: image.bin\nImage Base: 00000000\nMin Address: {lo:08x}\n" \
f"Max Address: {hi:08x}\nFunction Count: 2\n"
elif name == "list_binaries":
text = f"Open Programs in Ghidra:\n\n1. image.bin [ACTIVE]\n Executable Path: /{path}\n"
elif name == "get_data_at":
a, n = int(args["address"], 16), int(args["len"])
chunk = image[a - 0x401000:a - 0x401000 + n]
rows = [f"{a + i:x} " + " ".join(f"{b:02x}" for b in chunk[i:i + 16]) + " |....|"
for i in range(0, len(chunk), 16)]
text = f"Hexdump at {a:08x} ({n} bytes):\n\n" + "\n".join(rows) + "\n"
result = {"content": [{"type": "text", "text": text}]} if body["method"] == "tools/call" else {}
data = json.dumps({"jsonrpc": "2.0", "id": body["id"], "result": result}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Fake)
threading.Thread(target=server.serve_forever, daemon=True).start()
self.addCleanup(server.server_close)
self.addCleanup(server.shutdown)
return f"http://127.0.0.1:{server.server_port}/mcp"
def test_match_and_mismatch(self):
from harbor.reverse import ghidra_check
d = temp_dir()
self.addCleanup(shutil.rmtree, d, True)
image = bytes(range(256)) * 4
(d / "image.bin").write_bytes(image)
config = {"image": str(d / "image.bin"), "base": "0x401000"}
good = ghidra_check(config | {"ghidra_url": self.serve(0x401000, 0x4013FF, (d / "image.bin").as_posix(), image)})
self.assertTrue(good["ok"], good)
self.assertEqual([c["ok"] for c in good["checks"]], [True, True, True])
other = d / "other.bin"
other.write_bytes(image[:-1] + b"\x00")
bad = ghidra_check(config | {"ghidra_url": self.serve(0x401000, 0x4013FE, other.as_posix(),
image[:512] + b"\0" * 512)})
self.assertFalse(bad["ok"])
self.assertEqual([c["ok"] for c in bad["checks"]], [False, False, False])
class GhidraForwarder(unittest.TestCase):
"""The ghidra() tool against a fake streamable-HTTP MCP server: a session, a polled async task, a refusal."""