0701ac94e1
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.
56 lines
2.1 KiB
Python
Executable File
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}")
|