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.
85 lines
2.7 KiB
Python
Executable File
85 lines
2.7 KiB
Python
Executable File
#!/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)")
|