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.
96 lines
2.9 KiB
Python
Executable File
96 lines
2.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Find IMMEDIATE stores of a constant to a struct offset, in a live module.
|
|
|
|
immstore.py <imm_dec> [disp_hex|any] [--exe]
|
|
|
|
Only `C7 /0` (mov dword [reg+disp], imm32) can INTRODUCE a constant into a
|
|
field; `89 /r` merely propagates one. Emits image VAs so they can be fed to
|
|
ldis.py. Read-only.
|
|
"""
|
|
import glob
|
|
import os
|
|
import re
|
|
import struct
|
|
import sys
|
|
|
|
CARDS_IMG = 0x180000000
|
|
EXE_IMG = 0x140000000
|
|
|
|
|
|
def 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
|
|
raise SystemExit("FIFA17.exe not running")
|
|
|
|
|
|
P = pid()
|
|
|
|
|
|
def module_base(n):
|
|
for l in open(f"/proc/{P}/maps"):
|
|
if n.lower() in l.lower():
|
|
return int(l.split("-")[0], 16)
|
|
raise SystemExit(f"{n} not mapped")
|
|
|
|
|
|
def text_spans(base):
|
|
out = []
|
|
started = False
|
|
for l in open(f"/proc/{P}/maps"):
|
|
m = re.match(r"([0-9a-f]+)-([0-9a-f]+)\s+(\S{4})\s+\S+\s+\S+\s+\S+\s*(.*)", l)
|
|
if not m:
|
|
continue
|
|
lo, hi, perms, path = int(m.group(1), 16), int(m.group(2), 16), m.group(3), m.group(4)
|
|
if lo == base:
|
|
started = True
|
|
continue
|
|
if started:
|
|
if not path.strip() and "x" in perms:
|
|
out.append((lo, hi))
|
|
elif out:
|
|
break
|
|
return out
|
|
|
|
|
|
args = [a for a in sys.argv[1:] if a != "--exe"]
|
|
exe = "--exe" in sys.argv
|
|
imm = int(args[0], 0)
|
|
want_disp = None if len(args) < 2 or args[1] == "any" else int(args[1], 16)
|
|
img = EXE_IMG if exe else CARDS_IMG
|
|
base = module_base("FIFA17.exe" if exe else "CardsDLL")
|
|
|
|
mem = open(f"/proc/{P}/mem", "rb", 0)
|
|
immb = struct.pack("<i", imm)
|
|
hits = 0
|
|
for lo, hi in text_spans(base):
|
|
mem.seek(lo)
|
|
buf = mem.read(hi - lo)
|
|
img_lo = img + (lo - base)
|
|
i = buf.find(b"\xc7", 0)
|
|
while i >= 0:
|
|
modrm = buf[i + 1] if i + 1 < len(buf) else 0
|
|
if (modrm & 0x38) == 0: # /0
|
|
mod, rm = modrm >> 6, modrm & 7
|
|
if mod == 1 and i + 7 <= len(buf): # disp8
|
|
disp, ib = buf[i + 2], i + 3
|
|
sz = 7
|
|
elif mod == 2 and i + 10 <= len(buf): # disp32
|
|
disp, ib = struct.unpack_from("<i", buf, i + 2)[0], i + 6
|
|
sz = 10
|
|
elif mod == 0 and rm not in (4, 5) and i + 6 <= len(buf):
|
|
disp, ib = 0, i + 2
|
|
sz = 6
|
|
else:
|
|
disp = None
|
|
if disp is not None and buf[ib:ib + 4] == immb:
|
|
if want_disp is None or disp == want_disp:
|
|
print(f" image 0x{img_lo+i:x} mov dword [reg+0x{disp:x}], {imm} ({sz}B)")
|
|
hits += 1
|
|
i = buf.find(b"\xc7", i + 1)
|
|
print(f" {hits} immediate store(s) of {imm}"
|
|
+ (f" at +0x{want_disp:x}" if want_disp is not None else ""))
|