sots-re/ghidra/scripts/SpineRecon.java

190 lines
11 KiB
Java

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 1 (read-only): anchors for the turn/main-loop spine.
// A. strings containing SE*/SNM*/AppStartup/etc. -> xrefs -> containing functions (+ their callers)
// B. symbols (RTTI/vftables) containing those names
// C. entry point + callees (WinMain hunt), CreateThread sites + thread procs
// D. std::string layout check (FUN_008b9d70)
public class SpineRecon extends GhidraScript {
DecompInterface decomp; Memory mem; PrintWriter out; ReferenceManager rm; SymbolTable st;
static final String[] NEEDLES = {
"SETurnEndPending","SNMEndTurn","SEProcessTurn","SNMDoEncounterQuery","SNMAllCombatDone","SNMResumePlaying",
"SEAIPrepareTurn","SNMRunAI","SEFleetArrived","SEBuildCompleted","SETurnResults","SETurnEvents","AppStartup",
"SETurnTimeExpired","SEResumePlaying","SNMEncounterQueryResults","SNMHostCombat","SNMLaunchCombat","SNMEncounterResults",
"SETurnEndCancelled","SNMQueryEndTurnDone","SELastPlaying","PROCESSING_TITLE","Initializing Game","LOBBYSTATUS_STRATEGY_ROUND",
"SENewTurn","SEStartTurn","SEEndTurn","StrategyServer","TurnCommands","ProcessTurn","EndTurn","MainLoop","Tick","AppMain"
};
static final String[] DECOMP = {"00902350","00736e30","008a0e50","008b9d70","008b9d90"};
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, 180, 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, String tag) 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() + " tag=" + tag);
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++ < 8) cs.append(c.getName() + " "); }
return f.getName() + "@" + f.getEntryPoint() + " sz=" + f.getBody().getNumAddresses() + " callers(" + n + "):[" + cs.toString().trim() + "]";
}
@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/recon.txt"));
Set<Function> toDump = new LinkedHashSet<Function>();
// A. strings
out.println("==== A. STRING ANCHORS ====");
for (MemoryBlock blk : mem.getBlocks()) {
if (!blk.isInitialized() || blk.isExecute()) continue;
if (!blk.getName().equals(".rdata") && !blk.getName().equals(".data")) continue;
int len = (int) blk.getSize(); byte[] buf = new byte[len]; blk.getBytes(blk.getStart(), buf);
int start = 0;
for (int i = 0; i < len; i++) {
if (buf[i] != 0) continue;
int slen = i - start;
if (slen >= 4 && slen <= 120) {
boolean ascii = true;
for (int k = start; k < i; k++) { int c = buf[k] & 0xff; if (c < 0x20 || c > 0x7e) { ascii = false; break; } }
if (ascii) {
String s = new String(buf, start, slen, "ISO-8859-1");
for (String nd : NEEDLES) {
if (!s.contains(nd)) continue;
Address a = blk.getStart().add(start);
StringBuilder sb = new StringBuilder();
ReferenceIterator ri = rm.getReferencesTo(a); int n = 0;
while (ri.hasNext()) {
Reference r = ri.next(); n++;
Function f = getFunctionContaining(r.getFromAddress());
if (f == null) { sb.append("\n ?" + r.getFromAddress()); continue; }
sb.append("\n " + r.getFromAddress() + " in " + funcDesc(f));
if (nd.startsWith("SE") || nd.startsWith("SNM") || nd.equals("AppStartup") || nd.equals("PROCESSING_TITLE") || nd.equals("Initializing Game")) toDump.add(f);
}
out.println(a + " \"" + s + "\" [" + nd + "] refs=" + n + sb);
break;
}
}
}
start = i + 1;
}
}
// B. symbols
out.println("\n==== B. SYMBOLS ====");
SymbolIterator si = st.getAllSymbols(true);
while (si.hasNext()) {
Symbol s = si.next(); String n = s.getName(true);
boolean hit = false;
for (String nd : NEEDLES) if (n.contains(nd)) { hit = true; break; }
if (!hit) continue;
if (n.contains("StreamableHelper") || n.contains("VectorHelper")) continue;
out.println(s.getAddress() + " " + n + " [" + s.getSymbolType() + "]");
if (n.endsWith("::vftable")) {
Address a = s.getAddress();
for (int i = 0; i < 12; i++) {
try {
long p = mem.getInt(a.add(i*4)) & 0xffffffffL; Address fa = toAddr(p);
Function f = getFunctionAt(fa);
if (f == null) { if (i > 0) break; out.println(" [" + i + "] " + fa + " (nofunc)"); continue; }
out.println(" [" + i + "] " + funcDesc(f));
if (f.getBody().getNumAddresses() > 40) toDump.add(f);
} catch (Exception e) { break; }
}
}
}
// C. entry + CreateThread
out.println("\n==== C. ENTRY / THREADS ====");
for (Symbol s : st.getSymbols("entry")) {
Function f = getFunctionAt(s.getAddress());
out.println("entry sym @ " + s.getAddress() + " func=" + (f == null ? "null" : funcDesc(f)));
if (f != null) {
toDump.add(f);
for (Function c : f.getCalledFunctions(monitor)) {
out.println(" entry callee: " + funcDesc(c) + " params=" + c.getParameterCount());
if (c.getBody().getNumAddresses() > 200 && !c.getName().startsWith("_")) toDump.add(c);
}
}
}
for (Symbol s : st.getSymbols("CreateThread")) {
out.println("CreateThread sym @ " + s.getAddress() + " type=" + s.getSymbolType());
ReferenceIterator ri = rm.getReferencesTo(s.getAddress());
while (ri.hasNext()) {
Reference r = ri.next(); Function f = getFunctionContaining(r.getFromAddress());
out.println(" ref " + r.getFromAddress() + " " + r.getReferenceType() + " in " + (f == null ? "?" : funcDesc(f)));
if (f != null) toDump.add(f);
// thunk? follow refs to the thunk
if (f != null && f.isThunk() || (f != null && f.getBody().getNumAddresses() < 12)) {
ReferenceIterator ri2 = rm.getReferencesTo(f.getEntryPoint());
while (ri2.hasNext()) { Reference r2 = ri2.next(); Function f2 = getFunctionContaining(r2.getFromAddress());
out.println(" thunk-ref " + r2.getFromAddress() + " in " + (f2 == null ? "?" : funcDesc(f2))); if (f2 != null) toDump.add(f2); }
}
}
}
// thread procs: look at the CreateThread call sites listed in prior recon; find the pushed function pointer
long[] sites = {0x0090242dL, 0x008a14efL, 0x00736e84L};
for (long sa : sites) {
Address a = toAddr(sa);
out.println("site " + a + ":");
Instruction ins = getInstructionAt(a);
int back = 0;
Instruction p = ins;
while (p != null && back < 12) {
p = p.getPrevious(); back++;
if (p == null) break;
StringBuilder refs = new StringBuilder();
for (Reference r : p.getReferencesFrom()) {
Function tf = getFunctionAt(r.getToAddress());
if (tf != null) { refs.append(" -> FUNC " + funcDesc(tf)); toDump.add(tf); }
}
out.println(" " + p.getAddress() + " " + p + refs);
}
}
for (String d : DECOMP) { Function f = getFunctionAt(toAddr(Long.parseLong(d, 16))); if (f != null) toDump.add(f); }
// D. dump
out.println("\n==== D. DUMPED ====");
for (Function f : toDump) { out.println(f.getName() + " @ " + f.getEntryPoint()); dumpFunc(f, "spine"); }
out.close(); decomp.dispose(); println("done");
}
}