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.
This commit is contained in:
funman300
2026-08-25 04:23:22 +00:00
parent 025122ec9a
commit 0701ac94e1
6 changed files with 506 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Dump a CardsDLL vtable as image VAs, and find sibling vtables that hold a
different function in the same slot (a type/mode dispatch).
vtab.py <slot_image_va_hex> [before] [after]
"""
import glob
import os
import re
import struct
import sys
CARDS_IMG = 0x180000000
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("no FIFA17.exe")
P = pid()
BASE = [int(l.split("-")[0], 16) for l in open(f"/proc/{P}/maps") if "CardsDLL" in l][0]
def img2live(va):
return BASE + (va - CARDS_IMG)
def live2img(la):
return CARDS_IMG + (la - BASE)
slot = int(sys.argv[1], 16)
before = int(sys.argv[2]) if len(sys.argv) > 2 else 10
after = int(sys.argv[3]) if len(sys.argv) > 3 else 10
mem = open(f"/proc/{P}/mem", "rb", 0)
start = slot - before * 8
mem.seek(img2live(start))
buf = mem.read((before + after) * 8)
print(f" vtable neighbourhood of image 0x{slot:x}")
target = None
for k in range(0, len(buf) - 7, 8):
a = start + k
p = struct.unpack_from("<Q", buf, k)[0]
ivа = live2img(p) if BASE <= p < BASE + 0x400000 else None
mark = " <== the team-pair assigner" if a == slot else ""
if a == slot:
target = ivа
print(f" 0x{a:x} [{a-slot:+#5x}] -> "
+ (f"image 0x{ivа:x}" if ivа else f"raw 0x{p:x}") + mark)
# Find every other .rdata slot pointing at a DIFFERENT function but whose
# neighbours overlap this vtable -> sibling implementations of the same slot.
print("\n === sibling vtables: same neighbour, different slot function ===")
mem.seek(img2live(0x1801e5000))
rdata = mem.read(0x28a000 - 0x1e5000)
# take the two neighbours around the slot as a signature
sig_prev = struct.unpack_from("<Q", buf, (before - 1) * 8)[0]
sig_next = struct.unpack_from("<Q", buf, (before + 1) * 8)[0]
found = 0
for name, sig in (("preceding", sig_prev), ("following", sig_next)):
pat = struct.pack("<Q", sig)
i = rdata.find(pat)
while i >= 0:
if i % 8 == 0:
here = 0x1801e5000 + i
# the slot in THIS vtable at the same relative position
off = i + (8 if name == "preceding" else -8)
if 0 <= off <= len(rdata) - 8:
fn = struct.unpack_from("<Q", rdata, off)[0]
if BASE <= fn < BASE + 0x400000:
fimg = live2img(fn)
if fimg != target:
print(f" vtable @image 0x{here:x} ({name} matches) "
f"slot -> image 0x{fimg:x} DIFFERENT")
found += 1
i = rdata.find(pat, i + 1)
print(f" {found} sibling implementation(s)")