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.
95 lines
2.9 KiB
Python
Executable File
95 lines
2.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Read-only live disassembler for the FIFA17 client (CardsDLL / FIFA17.exe).
|
|
|
|
ldis.py <image_va_hex> [nbytes] [--exe] disassemble
|
|
ldis.py --bytes <image_va_hex> [nbytes] hexdump
|
|
ldis.py --map show module bases
|
|
|
|
CardsDLL image base 0x180000000; FIFA17.exe image base 0x140000000.
|
|
Live address = module_base + (image_va - img_base). Sections map 1:1 for both,
|
|
but this is recomputed and printed so the offset trap stays visible.
|
|
"""
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
PID = None
|
|
CARDS_IMG = 0x180000000
|
|
EXE_IMG = 0x140000000
|
|
|
|
|
|
def pid():
|
|
global PID
|
|
if PID is None:
|
|
import glob, os
|
|
for d in glob.glob("/proc/[0-9]*"):
|
|
try:
|
|
if open(os.path.join(d, "comm")).read().strip() == "FIFA17.exe":
|
|
PID = int(os.path.basename(d))
|
|
break
|
|
except OSError:
|
|
pass
|
|
if PID is None:
|
|
raise SystemExit("FIFA17.exe not running")
|
|
return PID
|
|
|
|
|
|
def module_base(needle):
|
|
"""Base = the NAMED PE-header mapping for the module (Wine maps the rest
|
|
anonymously, so never trust the mapping that merely CONTAINS an address)."""
|
|
for l in open(f"/proc/{pid()}/maps"):
|
|
if needle.lower() in l.lower():
|
|
return int(l.split("-")[0], 16)
|
|
raise SystemExit(f"module {needle} not mapped")
|
|
|
|
|
|
def live(va, exe=False):
|
|
if exe:
|
|
return module_base("FIFA17.exe") + (va - EXE_IMG)
|
|
return module_base("CardsDLL") + (va - CARDS_IMG)
|
|
|
|
|
|
def read(va, n, exe=False):
|
|
la = live(va, exe)
|
|
with open(f"/proc/{pid()}/mem", "rb", 0) as m:
|
|
m.seek(la)
|
|
return la, m.read(n)
|
|
|
|
|
|
def main():
|
|
a = sys.argv[1:]
|
|
if not a or a[0] == "--map":
|
|
print(f" pid = {pid()}")
|
|
print(f" CardsDLL = 0x{module_base('CardsDLL'):x} (image 0x{CARDS_IMG:x})")
|
|
print(f" FIFA17.exe = 0x{module_base('FIFA17.exe'):x} (image 0x{EXE_IMG:x})")
|
|
return
|
|
hexdump = a[0] == "--bytes"
|
|
if hexdump:
|
|
a = a[1:]
|
|
exe = "--exe" in a
|
|
a = [x for x in a if x != "--exe"]
|
|
va = int(a[0], 16)
|
|
n = int(a[1]) if len(a) > 1 else 160
|
|
la, buf = read(va, n, exe)
|
|
print(f" image 0x{va:x} -> live 0x{la:x} ({len(buf)} bytes)")
|
|
if hexdump:
|
|
for i in range(0, len(buf), 16):
|
|
c = buf[i:i + 16]
|
|
print(f" 0x{va+i:x}: {' '.join(f'{b:02x}' for b in c):<47} "
|
|
+ "".join(chr(b) if 32 <= b < 127 else "." for b in c))
|
|
return
|
|
with tempfile.NamedTemporaryFile(suffix=".bin") as f:
|
|
f.write(buf)
|
|
f.flush()
|
|
out = subprocess.run(
|
|
["objdump", "-D", "-b", "binary", "-m", "i386:x86-64", "-M", "intel",
|
|
f"--adjust-vma=0x{va:x}", f.name],
|
|
capture_output=True, text=True).stdout
|
|
for line in out.splitlines():
|
|
if re.match(r"\s+[0-9a-f]+:", line):
|
|
print(" " + line.strip())
|
|
|
|
|
|
main()
|