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:
Executable
+84
@@ -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)")
|
||||
Reference in New Issue
Block a user