""" find_eor_rendersurface.py Queries the EoR Ghidra MCP HTTP API to: 1. Enumerate all callers of operator_new (FUN_005df0f5) 2. Decompile each caller 3. Filter for ones whose body contains operator_new(0x120) — the unique sizeof for RenderSurface. That's EoR's RenderSurface::Allocator. 4. For each match, capture the caller's address and report the body. Also fetches xrefs to RenderSurface::Allocator → identifies the vtable slot(s) where it appears (Allocate slot in each derived class's vtable). """ import json import re import sys import urllib.request from concurrent.futures import ThreadPoolExecutor, as_completed EOR_BASE = "http://192.168.1.98:8081" OP_NEW_ADDR = "0x005df0f5" TARGET_SIZE_HEX = "0x120" TARGET_SIZE_DEC = 288 def http_get(path, **params): qs = "&".join(f"{k}={v}" for k, v in params.items()) url = f"{EOR_BASE}{path}?{qs}" if qs else f"{EOR_BASE}{path}" with urllib.request.urlopen(url, timeout=60) as r: return r.read().decode("utf-8", errors="replace") def get_xrefs_to(addr, offset=0, limit=500): raw = http_get("/xrefs_to", address=addr, offset=offset, limit=limit) refs = [] for line in raw.splitlines(): # Format: "From 00536d73 in FUN_00536d70 [UNCONDITIONAL_CALL]" m = re.match(r"From ([0-9a-f]+) in (\S+) \[([\w_]+)\]", line) if m: refs.append({ "site": int(m.group(1), 16), "owner_name": m.group(2), "kind": m.group(3), }) return refs def decompile(addr): return http_get("/decompile_function_by_address", address=f"0x{addr:08x}") def main(): # Collect all xrefs to operator_new (paginated) all_refs = [] offset = 0 limit = 500 while True: batch = get_xrefs_to(OP_NEW_ADDR, offset=offset, limit=limit) if not batch: break all_refs.extend(batch) if len(batch) < limit: break offset += limit if offset > 20000: break print(f"total xrefs to operator_new: {len(all_refs)}") # Owners (functions that call operator_new). Dedupe by owner_name. owners = {} for r in all_refs: owners.setdefault(r["owner_name"], r["site"]) print(f"unique calling functions: {len(owners)}") # Resolve each owner's function start address by decompiling and matching. # Owner name is FUN_xxxxxxxx — the xxxx is the function start. owner_addrs = [] for name, site in owners.items(): m = re.match(r"FUN_([0-9a-f]+)", name) if m: owner_addrs.append((int(m.group(1), 16), name)) print(f"resolvable owner-function start addresses: {len(owner_addrs)}") # Decompile each, look for the 0x120 size signature def check_one(addr_name): addr, name = addr_name try: body = decompile(addr) except Exception as e: return (addr, name, None, f"error: {e}") # Look for operator_new call with size 0x120 (288). Various forms. if re.search(r"0x120\b", body) or re.search(r"\b288\b", body): # Filter to actual op_new call sites for line in body.splitlines(): if "0x120" in line or " 288" in line: return (addr, name, "MATCH", line.strip()) return (addr, name, "MATCH", "(contains 0x120 somewhere)") return (addr, name, None, None) matches = [] print(f"decompiling {len(owner_addrs)} candidate Allocators...") with ThreadPoolExecutor(max_workers=16) as ex: futures = [ex.submit(check_one, an) for an in owner_addrs] for i, fut in enumerate(as_completed(futures), 1): addr, name, status, line = fut.result() if status == "MATCH": matches.append((addr, name, line)) print(f" [{i}/{len(owner_addrs)}] MATCH at 0x{addr:08x} ({name}): {line}") print(f"\n=== {len(matches)} candidate RenderSurface::Allocator(s) ===") for addr, name, line in matches: print(f" 0x{addr:08x} {name}") body = decompile(addr) # print first 25 non-empty lines for ln in body.splitlines()[:30]: print(f" {ln}") print() if __name__ == "__main__": main()