forked from Keelrleek/Harbor
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:
1 parent
57fb31f2d6
commit
21b7fedb6f
4 files changed
+366
-1
No files matched your search
@@ -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."""
|
||||
|
||||
Reference in new issue
Block a user