import ghidra.app.script.GhidraScript; import ghidra.program.model.address.*; import ghidra.program.model.mem.*; import ghidra.program.model.listing.*; import ghidra.program.model.symbol.*; import ghidra.app.decompiler.*; import java.util.*; import java.util.regex.*; import java.io.*; // Pass 2 (read-only): event dispatch + main loop + WinMain chain + worker threads. public class SpineRecon2 extends GhidraScript { DecompInterface decomp; Memory mem; PrintWriter out; ReferenceManager rm; SymbolTable st; Set toDump = new LinkedHashSet(); static final String[] CLASSES = {"SETurnEndPending","SETurnEndCancelled","SETurnTimeExpired","SEProcessTurn","SEAIPrepareTurn","SEFleetArrived", "SEBuildCompleted","SETurnResults","SETurnEvents","SEResumePlaying","SELastPlaying","SNMEndTurn","SNMQueryEndTurnDone","SNMDoEncounterQuery", "SNMEncounterQueryResults","SNMHostCombat","SNMLaunchCombat","SNMEncounterResults","SNMAllCombatDone","SNMResumePlaying","SNMResumePlayingReceived", "SNMRunAI","StrategyEvent","StrategyServer","TurnCommands","AppStartup"}; static final long[] STRS = {0x00a23c08L,0x00a32698L,0x00a258a8L,0x00a261a8L,0x00a21978L}; static final String[] APIS = {"PeekMessageA","GetMessageA","DispatchMessageA","TranslateMessage","WinMain","GetStartupInfoA","ShowWindow","QueryPerformanceCounter","timeGetTime"}; static final long[] DECOMP = {0x00925501L,0x0089dd30L,0x00741cd0L,0x00732ab0L,0x00902470L,0x0091b5a0L,0x008ef040L,0x008a7230L,0x008cd820L,0x008fe730L,0x00761300L,0x00761380L,0x007861d0L,0x00786d50L,0x007825f0L,0x008559e0L,0x0079e500L}; String cstr(Address a, int max) { try { byte[] b = new byte[max]; int got = mem.getBytes(a, b); int i = 0; while (i < got && b[i] != 0 && (b[i]&0xff) >= 0x20 && (b[i]&0xff) < 0x7f) i++; if (i >= 1 && i < got && b[i] == 0) return new String(b, 0, i, "ISO-8859-1"); } catch (Exception e) {} return null; } String decompRes(Function f) { try { DecompileResults res = decomp.decompileFunction(f, 240, monitor); if (res == null || !res.decompileCompleted()) return "[decompile failed]"; String c = res.getDecompiledFunction().getC(); Matcher m = Pattern.compile("&?(DAT|PTR_s_|s_[A-Za-z0-9_]*|PTR_DAT|u_[A-Za-z0-9_]*)_([0-9a-f]{8})").matcher(c); StringBuffer sb = new StringBuffer(); while (m.find()) { Address a = toAddr(Long.parseLong(m.group(2), 16)); String s = cstr(a, 64); String rep = m.group(0); if (s != null && s.length() <= 60) rep = "\"" + s + "\""; m.appendReplacement(sb, Matcher.quoteReplacement(rep)); } m.appendTail(sb); return sb.toString(); } catch (Exception e) { return "[exception " + e.getMessage() + "]"; } } void dumpFunc(Function f) throws IOException { String n = String.format("%08x", f.getEntryPoint().getOffset()); File fl = new File("/tmp/spine/" + n + ".c"); if (fl.exists()) return; PrintWriter w = new PrintWriter(new FileWriter(fl)); w.println("// " + f.getName() + " @ " + f.getEntryPoint() + " size=" + f.getBody().getNumAddresses()); StringBuilder cs = new StringBuilder(); for (Function c : f.getCallingFunctions(monitor)) cs.append(c.getName() + "@" + c.getEntryPoint() + " "); w.println("// CALLERS: " + cs); StringBuilder ce = new StringBuilder(); for (Function c : f.getCalledFunctions(monitor)) ce.append(c.getName() + " "); w.println("// CALLEES: " + ce); w.println(decompRes(f)); w.close(); } String funcDesc(Function f) { StringBuilder cs = new StringBuilder(); int n = 0; for (Function c : f.getCallingFunctions(monitor)) { if (n++ < 6) cs.append(c.getName() + " "); } return f.getName() + "@" + f.getEntryPoint() + " sz=" + f.getBody().getNumAddresses() + " callers(" + n + "):[" + cs.toString().trim() + "]"; } void refsTo(Address a, String label, boolean dump, int maxDump) { ReferenceIterator ri = rm.getReferencesTo(a); int n = 0; StringBuilder sb = new StringBuilder(); Set fs = new LinkedHashSet(); while (ri.hasNext()) { Reference r = ri.next(); n++; Function f = getFunctionContaining(r.getFromAddress()); if (f == null) { sb.append("\n ?" + r.getFromAddress() + " " + r.getReferenceType()); continue; } sb.append("\n " + r.getFromAddress() + " " + r.getReferenceType() + " in " + funcDesc(f)); fs.add(f); } out.println(a + " " + label + " refs=" + n + sb); if (dump) { int k = 0; for (Function f : fs) { if (k++ >= maxDump) break; toDump.add(f); } } } void callersUp(Function f, int depth, String indent, Set seen) { if (depth == 0 || f == null || seen.contains(f)) return; seen.add(f); for (Function c : f.getCallingFunctions(monitor)) { out.println(indent + "<- " + funcDesc(c)); toDump.add(c); callersUp(c, depth - 1, indent + " ", seen); } } @Override public void run() throws Exception { mem = currentProgram.getMemory(); rm = currentProgram.getReferenceManager(); st = currentProgram.getSymbolTable(); decomp = new DecompInterface(); decomp.openProgram(currentProgram); new File("/tmp/spine").mkdirs(); out = new PrintWriter(new FileWriter("/tmp/spine/recon2.txt")); out.println("==== 1. STRING XREFS ===="); for (long s : STRS) { Address a = toAddr(s); refsTo(a, "\"" + cstr(a, 80) + "\"", true, 6); } out.println("\n==== 2. CLASS VFTABLE / TYPEDESC XREFS ===="); SymbolIterator si = st.getAllSymbols(true); List syms = new ArrayList(); while (si.hasNext()) { Symbol s = si.next(); String n = s.getName(true); if (n.contains("Helper")) continue; if (!(n.endsWith("::vftable") || n.endsWith("::RTTI_Type_Descriptor"))) continue; for (String c : CLASSES) if (n.equals("Game::" + c + "::vftable") || n.equals("Game::" + c + "::RTTI_Type_Descriptor") || n.equals("Mars::" + c + "::vftable") || n.equals("Mars::" + c + "::RTTI_Type_Descriptor")) { syms.add(s); break; } } for (Symbol s : syms) refsTo(s.getAddress(), s.getName(true), true, 4); out.println("\n==== 3. WIN32 API XREFS ===="); for (String api : APIS) for (Symbol s : st.getSymbols(api)) { if (s.getSymbolType() != SymbolType.FUNCTION && s.getSymbolType() != SymbolType.LABEL) continue; refsTo(s.getAddress(), api + "(" + s.getSymbolType() + ")", api.startsWith("PeekMessage") || api.startsWith("GetMessage") || api.startsWith("DispatchMessage") || api.equals("GetStartupInfoA"), 6); // follow thunks ReferenceIterator ri = rm.getReferencesTo(s.getAddress()); while (ri.hasNext()) { Reference r = ri.next(); Function f = getFunctionContaining(r.getFromAddress()); if (f != null && (f.isThunk() || f.getBody().getNumAddresses() <= 6)) refsTo(f.getEntryPoint(), " thunk " + f.getName(), api.startsWith("PeekMessage") || api.startsWith("DispatchMessage"), 6); } } out.println("\n==== 4. CALLERS UP from Init (FUN_008a0e50) and FUN_00741cd0 / FUN_00902470 ===="); for (long fa : new long[]{0x008a0e50L, 0x00741cd0L, 0x00902470L}) { Function f = getFunctionAt(toAddr(fa)); out.println("ROOT " + funcDesc(f)); callersUp(f, 5, " ", new HashSet()); } out.println("\n==== 5. APP CLASSES (vftables with 'App' / 'Application' / 'Game' in name) ===="); si = st.getAllSymbols(true); while (si.hasNext()) { Symbol s = si.next(); String n = s.getName(true); if (!n.endsWith("::vftable") || n.contains("Helper")) continue; if (!(n.contains("App") || n.contains("Application") || n.contains("StarsGame") || n.contains("GameApp") || n.contains("StrategyClient") || n.contains("StrategyGame") || n.contains("GameServer") || n.contains("IApplication"))) continue; Address a = s.getAddress(); StringBuilder sb = new StringBuilder(a + " " + n + " :"); for (int i = 0; i < 24; i++) { try { long p = mem.getInt(a.add(i*4)) & 0xffffffffL; Function f = getFunctionAt(toAddr(p)); if (f == null) break; sb.append(" [" + i + "]" + f.getName() + "(" + f.getBody().getNumAddresses() + ")"); } catch (Exception e) { break; } } out.println(sb); } // DAT_00b2d540 (app singleton) writers out.println("\n==== 6. APP SINGLETON DAT_00b2d540 refs (writes) ===="); { ReferenceIterator ri = rm.getReferencesTo(toAddr(0x00b2d540L)); int n = 0; while (ri.hasNext()) { Reference r = ri.next(); if (!r.getReferenceType().isWrite()) continue; Function f = getFunctionContaining(r.getFromAddress()); out.println(" W " + r.getFromAddress() + " in " + (f == null ? "?" : funcDesc(f))); if (f != null) toDump.add(f); if (++n > 20) break; } } out.println("\n==== 7. what is at 0x009bf127 (s_SNMEndTurn ref) ===="); { Address a = toAddr(0x009bf100L); MemoryBlock b = mem.getBlock(a); out.println("block=" + (b == null ? "none" : b.getName() + " exec=" + b.isExecute())); Function f = getFunctionContaining(a); out.println("func=" + (f == null ? "none" : f.getName())); if (getInstructionAt(a) == null) disassemble(a); Instruction ins = getInstructionAt(a); int k = 0; while (ins != null && k++ < 40) { StringBuilder refs = new StringBuilder(); for (Reference r : ins.getReferencesFrom()) { String s2 = cstr(r.getToAddress(), 40); Function tf = getFunctionAt(r.getToAddress()); refs.append(s2 != null ? " \"" + s2 + "\"" : (tf != null ? " ->" + tf.getName() : "")); } out.println(" " + ins.getAddress() + " " + ins + refs); ins = ins.getNext(); } // where do undefined-code refs come from: refs TO 0x009bf120 region refsTo(toAddr(0x009bf110L), "0x009bf110", false, 0); } for (long d : DECOMP) { Function f = getFunctionAt(toAddr(d)); if (f != null) toDump.add(f); } out.println("\n==== 8. DUMPED ===="); for (Function f : toDump) { out.println(f.getName() + " @ " + f.getEntryPoint() + " sz=" + f.getBody().getNumAddresses()); dumpFunc(f); } out.close(); decomp.dispose(); println("done"); } }