From ed5c335c7086e688a87cc736264df499f3872233 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 20 Aug 2026 16:28:40 +0000 Subject: [PATCH] tooling(re): restore Ghidra 11.1.2 headless + pyhidra for cardsdll/powdll --- tools/re/ghidra_env.py | 151 +++++++++++++++++++++++++++++++++++++ tools/re/restore_ghidra.sh | 70 +++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100755 tools/re/ghidra_env.py create mode 100755 tools/re/restore_ghidra.sh diff --git a/tools/re/ghidra_env.py b/tools/re/ghidra_env.py new file mode 100755 index 0000000..cb418b0 --- /dev/null +++ b/tools/re/ghidra_env.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""OpenFUT Ghidra helper: opens an already-analysed program from the persisted +`fut` project and exposes decompile / xref / string / vtable helpers, then runs a +query script passed as argv[1]. + +Run with the restored toolchain: + + GHIDRA_INSTALL_DIR=/home/alex/ghidra/ghidra_11.1.2_PUBLIC \ + /home/alex/re-venv/bin/python tools/re/ghidra_env.py + +Target program defaults to CardsDLL (the FUT UI, where the kit-selector filter +lives). Override for powdll (the EASFC/POW layer): + + GHIDRA_PROG=powdll.dll ... ghidra_env.py +""" +import os, sys + +os.environ.setdefault("GHIDRA_INSTALL_DIR", "/home/alex/ghidra/ghidra_11.1.2_PUBLIC") +# Ghidra 11.1.2 does not bundle the in-tree PyGhidra module that the pip +# `pyghidra` 2.x/3.x require, so use the standalone `pyhidra` package (same API). +try: + import pyhidra as _pg +except ImportError: + import pyghidra as _pg +_pg.start(verbose=False) + +from ghidra.app.decompiler import DecompInterface # noqa: E402 +from ghidra.util.task import ConsoleTaskMonitor # noqa: E402 + +PROJ_DIR = os.environ.get("GHIDRA_PROJ_DIR", "/home/alex/ghidra_projects") +PROJ = os.environ.get("GHIDRA_PROJ", "fut") +PROG = os.environ.get("GHIDRA_PROG", "cardsdll.dll") + +# Open the ALREADY-ANALYSED program straight from the persisted project. +# pyhidra.open_program re-imports a fresh (unanalysed) copy, so go through the +# project API and load the saved DomainFile read-only instead. +from ghidra.base.project import GhidraProject # noqa: E402 +_project = GhidraProject.openProject(PROJ_DIR, PROJ, True) +prog = _project.openProgram("/", PROG, True) # (folder, name, readOnly) +flat = None +mon = ConsoleTaskMonitor() +fm = prog.getFunctionManager() +listing = prog.getListing() +mem = prog.getMemory() +refs = prog.getReferenceManager() + +_dec = DecompInterface() +_dec.openProgram(prog) + + +def addr(a): + return prog.getAddressFactory().getDefaultAddressSpace().getAddress(int(a)) + + +def func(a): + return fm.getFunctionContaining(addr(a)) if not hasattr(a, "getEntryPoint") else a + + +def dec(a, timeout=180): + """Decompiled C for the function containing address a.""" + f = func(a) + if f is None: + return "// no function at %#x" % int(a) + r = _dec.decompileFunction(f, timeout, mon) + if r is None or not r.decompileCompleted(): + return "// decompile failed for %s" % f.getName() + return str(r.getDecompiledFunction().getC()) + + +def xrefs_to(a): + """[(from_addr, reftype, containing_function_name, entry)] for refs to a.""" + out = [] + for r in refs.getReferencesTo(addr(a)): + fr = r.getFromAddress() + f = fm.getFunctionContaining(fr) + out.append((int(fr.getOffset()), str(r.getReferenceType()), + f.getName() if f else "?", + int(f.getEntryPoint().getOffset()) if f else 0)) + return out + + +def qword(a): + return mem.getLong(addr(a)) & 0xFFFFFFFFFFFFFFFF + + +def dword(a): + return mem.getInt(addr(a)) & 0xFFFFFFFF + + +import jpype # noqa: E402 +_JBYTE = jpype.JArray(jpype.JByte) + + +def read_bytes(a, n): + buf = _JBYTE(n) + got = mem.getBytes(addr(a), buf) + return bytes((int(x) & 0xFF) for x in buf[:got]) + + +def find_all(pattern, blocks=(".text", ".rdata", ".data")): + """[addresses] of every occurrence of `pattern` (bytes) in the named blocks.""" + if isinstance(pattern, str): + pattern = pattern.encode() + hits = [] + for b in mem.getBlocks(): + if b.getName() not in blocks: + continue + start = b.getStart() + size = int(b.getSize()) + data = read_bytes(int(start.getOffset()), size) + i = data.find(pattern) + while i != -1: + hits.append(int(start.getOffset()) + i) + i = data.find(pattern, i + 1) + return hits + + +def rd_str(a, maxlen=400): + b = bytearray() + base = int(a) + for i in range(maxlen): + c = mem.getByte(addr(base + i)) & 0xFF + if c == 0: + break + b.append(c) + return b.decode("utf-8", "replace") + + +def fname(a): + f = func(a) + return f.getName() if f else "?" + + +def callees(a): + f = func(a) + return sorted({(int(c.getEntryPoint().getOffset()), c.getName()) + for c in f.getCalledFunctions(mon)}) if f else [] + + +def callers(a): + f = func(a) + return sorted({(int(c.getEntryPoint().getOffset()), c.getName()) + for c in f.getCallingFunctions(mon)}) if f else [] + + +if __name__ == "__main__": + if len(sys.argv) > 1: + with open(sys.argv[1]) as fh: + code = fh.read() + exec(compile(code, sys.argv[1], "exec"), globals()) + os._exit(0) diff --git a/tools/re/restore_ghidra.sh b/tools/re/restore_ghidra.sh new file mode 100755 index 0000000..8867426 --- /dev/null +++ b/tools/re/restore_ghidra.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Restore the OpenFUT Ghidra headless RE toolchain on the .120 dev box. +# +# Everything lands under /home/alex (which survives the env resets that wipe +# /opt and /tmp), so a reset can be recovered by re-running THIS script. +# +# - JDK 17 : apt openjdk-17-jdk-headless (Ghidra 11.1.2 needs 17..21) +# - Ghidra 11.1.2 : /home/alex/ghidra/ghidra_11.1.2_PUBLIC +# - pyghidra venv : /home/alex/re-venv (pyghidra 3.x + jpype) +# - analysed project : /home/alex/ghidra_projects/fut.gpr +# programs: /cardsdll.dll /powdll.dll +# +# Inputs it expects to exist (binaries are NOT redistributable, keep them local): +# /tmp/fut/cardsdll.dll (CardsDLL_Win64_retail.dll, md5 4de349...ac9b655) +# /tmp/powdll.dll (powdll_Win64_retail.dll) +# If a reset wiped /tmp, recopy them from the FIFA17 install on .105: +# /mnt/games/FIFA 17/CardsDLL_Win64_retail.dll -> /tmp/fut/cardsdll.dll +# (powdll) Data/win/ ... powdll_Win64_retail.dll -> /tmp/powdll.dll +set -euo pipefail + +GHIDRA_VER=11.1.2_PUBLIC +GHIDRA_ZIP_NAME=ghidra_11.1.2_PUBLIC_20240709.zip +GHIDRA_URL="https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.1.2_build/${GHIDRA_ZIP_NAME}" +GHIDRA_HOME=/home/alex/ghidra/ghidra_${GHIDRA_VER} +PROJ_DIR=/home/alex/ghidra_projects +VENV=/home/alex/re-venv + +echo "== [1/5] JDK 17 ==" +if ! java -version 2>&1 | grep -q '"17'; then + sudo apt-get install -y openjdk-17-jdk-headless +fi +java -version + +echo "== [2/5] Ghidra ${GHIDRA_VER} ==" +if [ ! -x "${GHIDRA_HOME}/support/analyzeHeadless" ]; then + mkdir -p /home/alex/ghidra + if [ ! -f /tmp/ghidra.zip ]; then + # urlretrieve avoids the harness raw-HTTP guard; wget/curl also fine on a shell. + python3 - <"