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.
68 lines
2.2 KiB
Python
Executable File
68 lines
2.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Scan for FIFA17 match-team records by the invariant header prefix.
|
|
|
|
Anchors ONLY on (11,7,0,0,76) at +0x00..+0x10. Never filter on +0x18: it is a
|
|
per-record marker whose value varies between sessions (-1 on 2026-08-24,
|
|
344065/344064 on 2026-08-25), and filtering on it produced a false negative.
|
|
|
|
scan_mt.py [pid]
|
|
"""
|
|
import glob
|
|
import os
|
|
import re
|
|
import struct
|
|
import sys
|
|
|
|
PAT = struct.pack("<5i", 11, 7, 0, 0, 76)
|
|
|
|
|
|
def find_pid():
|
|
for d in glob.glob("/proc/[0-9]*"):
|
|
try:
|
|
if open(os.path.join(d, "comm")).read().strip() == "FIFA17.exe":
|
|
return int(os.path.basename(d))
|
|
except OSError:
|
|
pass
|
|
return None
|
|
|
|
|
|
pid = int(sys.argv[1]) if len(sys.argv) > 1 else find_pid()
|
|
if not pid:
|
|
print(" no FIFA17.exe")
|
|
raise SystemExit(2)
|
|
|
|
mem = open(f"/proc/{pid}/mem", "rb", 0)
|
|
found = []
|
|
for line in open(f"/proc/{pid}/maps"):
|
|
m = re.match(r"([0-9a-f]+)-([0-9a-f]+)\s+(\S{4})\s+\S+\s+\S+\s+\S+\s*(.*)", line)
|
|
if not m or m.group(3)[0] != "r":
|
|
continue
|
|
lo, hi, path = int(m.group(1), 16), int(m.group(2), 16), m.group(4)
|
|
if path.startswith(("/dev", "/memfd")) or hi - lo > 512 * 1024 * 1024:
|
|
continue
|
|
try:
|
|
mem.seek(lo)
|
|
buf = mem.read(hi - lo)
|
|
except (OSError, ValueError, OverflowError):
|
|
continue
|
|
i = buf.find(PAT)
|
|
while i >= 0:
|
|
rec = buf[i:i + 0x80]
|
|
if len(rec) >= 0x80:
|
|
tid = struct.unpack_from("<i", rec, 0x14)[0]
|
|
m18 = struct.unpack_from("<i", rec, 0x18)[0]
|
|
m1c = struct.unpack_from("<i", rec, 0x1c)[0]
|
|
xi = list(struct.unpack_from("<11i", rec, 0x20))
|
|
subs = list(struct.unpack_from("<12i", rec, 0x4c))
|
|
found.append((lo + i, tid, m18, m1c, xi, subs))
|
|
i = buf.find(PAT, i + 4)
|
|
|
|
print(f" pid={pid} {len(found)} match-team record(s)")
|
|
for addr, tid, m18, m1c, xi, subs in found:
|
|
print(f"\n @0x{addr:x}")
|
|
print(f" +0x14 teamId = {tid}")
|
|
print(f" +0x18 marker = {m18} +0x1c marker = {m1c}")
|
|
print(f" XI = {xi}")
|
|
print(f" subs = {subs}")
|
|
print(f"\n distinct teamIds: {sorted({t for _a, t, *_r in found})}")
|