Files
OpenFUT/fifa17-recon/tools/classify_calls.py
T
funman300 0701ac94e1 tool(fifa17-recon): live native-RE toolkit (disasm, xref, immediate-store, vtable)
Read-only probes for resolving FIFA17 code paths against a running client
without Ghidra, per the live-disassembly method (/proc/<pid>/mem + objdump).
All open /proc/<pid>/mem 'rb' only.

  ldis.py           image-VA disassembler/hexdump for CardsDLL and FIFA17.exe;
                    recomputes the module base from the NAMED PE-header mapping
                    every run, because Wine maps PE sections anonymously and the
                    mapping that merely CONTAINS an address is not the module.
  xref.py           references to an image VA: call/jmp rel32, rip-relative lea,
                    and absolute pointer slots. An absolute-only hit means the
                    function is virtual and reachable solely via its vtable.
  immstore.py       immediate stores (C7 /0) of a constant to a struct offset.
                    Only an immediate store can INTRODUCE a constant; a register
                    store merely propagates one. Zero hits is a real result: it
                    proves the constant arrives from a call, not a literal.
  classify_calls.py splits call sites of a constant-returning stub into STORE
                    (can assign) vs compare (predicate). Turned 81 call sites of
                    the 130000 provider into 17 assignments.
  vtab.py           dumps a vtable as image VAs and looks for sibling vtables
                    holding a different function in the same slot, which is how
                    a type/mode dispatch shows up.
  scan_mt.py        match-team records by the invariant header (11,7,0,0,76).
                    Never filters on +0x18: that word is a per-session handle
                    (-1 on 2026-08-24, 0x54001/0x54000 on 2026-08-25) and
                    filtering on it previously produced a false negative.

Workflow note: dump .text once and cache the objdump output, then query the
cached listing; a full CardsDLL .text linear disassembly is ~563k lines and
re-disassembling per question is wasteful.
2026-08-25 04:23:22 +00:00

56 lines
2.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Classify call sites of the 130000/130001 provider stubs.
A call whose result is COMPARED implements a predicate ("is this the FUT custom
club?"). Only a call whose result is STORED can assign a team id. This turns an
unreadable 81-site list into the handful that could actually introduce 130000
into a struct.
classify_calls.py <asmfile> <target_va_hex> [more_targets...]
"""
import re
import sys
asm = sys.argv[1]
targets = [t.lower().lstrip("0x") for t in sys.argv[2:]]
lines = []
for l in open(asm, errors="replace"):
m = re.match(r"\s*([0-9a-f]+):\s+((?:[0-9a-f]{2} )+)\s*(.*)", l)
if m:
lines.append((int(m.group(1), 16), m.group(3).strip()))
idx = {a: i for i, (a, _t) in enumerate(lines)}
STORE = re.compile(r"^mov\s+(?:DWORD PTR |QWORD PTR )?\[[^\]]+\],(eax|rax)\b")
CMP = re.compile(r"^(cmp|sub|test)\b.*\b(eax|rax)\b")
MOVREG = re.compile(r"^mov\s+(e[a-z]{2}|r\d+d|r[a-z]{2}),(eax|rax)\b")
for tgt in targets:
print(f"\n ===== callers of 0x{tgt} =====")
stores, cmps, other = [], [], []
for i, (a, txt) in enumerate(lines):
if not txt.startswith("call") or tgt not in txt:
continue
# look at the next few instructions for the fate of eax
window = [lines[j][1] for j in range(i + 1, min(i + 7, len(lines)))]
verdict, detail = "other", window[0] if window else ""
for w in window:
if STORE.match(w):
verdict, detail = "STORE", w
break
if CMP.match(w):
verdict, detail = "compare", w
break
if MOVREG.match(w):
verdict, detail = "movreg", w
break
rec = (a, detail)
(stores if verdict == "STORE" else cmps if verdict == "compare" else other).append(rec)
print(f" STORE (can assign) : {len(stores)}")
for a, d in stores:
print(f" 0x{a:x} {d}")
print(f" compare (predicate) : {len(cmps)}")
print(f" other/moved to reg : {len(other)}")
for a, d in other[:14]:
print(f" 0x{a:x} {d}")