tooling(re): restore Ghidra 11.1.2 headless + pyhidra for cardsdll/powdll
This commit is contained in:
Executable
+151
@@ -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 <query.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 <query.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)
|
||||||
Executable
+70
@@ -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 - <<PY
|
||||||
|
import urllib.request
|
||||||
|
urllib.request.urlretrieve("${GHIDRA_URL}", "/tmp/ghidra.zip")
|
||||||
|
print("downloaded")
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
( cd /home/alex/ghidra && unzip -q -o /tmp/ghidra.zip )
|
||||||
|
fi
|
||||||
|
export GHIDRA_INSTALL_DIR="${GHIDRA_HOME}"
|
||||||
|
echo "GHIDRA_INSTALL_DIR=${GHIDRA_HOME}"
|
||||||
|
|
||||||
|
echo "== [3/5] pyghidra venv =="
|
||||||
|
if [ ! -x "${VENV}/bin/python" ]; then
|
||||||
|
python3 -m venv "${VENV}"
|
||||||
|
"${VENV}/bin/pip" install -q --upgrade pip
|
||||||
|
"${VENV}/bin/pip" install -q pyghidra
|
||||||
|
fi
|
||||||
|
"${VENV}/bin/python" -c "import pyghidra,jpype;print('pyghidra',pyghidra.__version__)"
|
||||||
|
|
||||||
|
echo "== [4/5] analyse cardsdll + powdll into ${PROJ_DIR}/fut.gpr =="
|
||||||
|
mkdir -p "${PROJ_DIR}"
|
||||||
|
if [ ! -f "${PROJ_DIR}/fut.gpr" ]; then
|
||||||
|
for dll in /tmp/fut/cardsdll.dll /tmp/powdll.dll; do
|
||||||
|
"${GHIDRA_HOME}/support/analyzeHeadless" "${PROJ_DIR}" fut \
|
||||||
|
-import "${dll}" -processor x86:LE:64:default -cspec windows \
|
||||||
|
-analysisTimeoutPerFile 1200
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== [5/5] done. Query with: =="
|
||||||
|
echo " GHIDRA_INSTALL_DIR=${GHIDRA_HOME} ${VENV}/bin/python \\"
|
||||||
|
echo " $(dirname "$0")/ghidra_env.py <query.py>"
|
||||||
Reference in New Issue
Block a user