Files
OpenFUT/fifa17-recon/tools/ldis.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

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()