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
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Find references to an image VA inside a live module's .text/.rdata/.data.
|
||||
|
||||
xref.py <target_image_va_hex> [--exe]
|
||||
|
||||
Reports:
|
||||
call rel32 (e8) / jmp rel32 (e9) -- direct callers
|
||||
lea rip-rel (48 8d 0x) -- address-taken
|
||||
absolute 8-byte pointer -- vtable / table slot
|
||||
|
||||
Read-only. Section ranges are recomputed from /proc/<pid>/maps every run.
|
||||
"""
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
|
||||
CARDS_IMG = 0x180000000
|
||||
EXE_IMG = 0x140000000
|
||||
|
||||
|
||||
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("FIFA17.exe not running")
|
||||
|
||||
|
||||
P = pid()
|
||||
|
||||
|
||||
def module_base(needle):
|
||||
for l in open(f"/proc/{P}/maps"):
|
||||
if needle.lower() in l.lower():
|
||||
return int(l.split("-")[0], 16)
|
||||
raise SystemExit(f"{needle} not mapped")
|
||||
|
||||
|
||||
def spans(base, limit=0x400000):
|
||||
"""Contiguous mappings belonging to this module, as (live_lo, live_hi, perms)."""
|
||||
out = []
|
||||
for l in open(f"/proc/{P}/maps"):
|
||||
m = re.match(r"([0-9a-f]+)-([0-9a-f]+)\s+(\S{4})\s+\S+\s+\S+\s+\S+\s*(.*)", l)
|
||||
if not m:
|
||||
continue
|
||||
lo, hi, perms, path = int(m.group(1), 16), int(m.group(2), 16), m.group(3), m.group(4)
|
||||
if lo == base:
|
||||
out.append((lo, hi, perms))
|
||||
continue
|
||||
if out and lo == out[-1][1] and not path.strip():
|
||||
out.append((lo, hi, perms))
|
||||
elif out and lo > out[-1][1]:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
a = [x for x in sys.argv[1:] if x != "--exe"]
|
||||
exe = "--exe" in sys.argv
|
||||
target = int(a[0], 16)
|
||||
img = EXE_IMG if exe else CARDS_IMG
|
||||
base = module_base("FIFA17.exe" if exe else "CardsDLL")
|
||||
tgt_live = base + (target - img)
|
||||
|
||||
mem = open(f"/proc/{P}/mem", "rb", 0)
|
||||
print(f" pid={P} module_base=0x{base:x} target image 0x{target:x} live 0x{tgt_live:x}")
|
||||
hits = 0
|
||||
for lo, hi, perms in spans(base):
|
||||
try:
|
||||
mem.seek(lo)
|
||||
buf = mem.read(hi - lo)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
img_lo = img + (lo - base)
|
||||
# rel32 call/jmp
|
||||
for op, name in ((0xE8, "call"), (0xE9, "jmp ")):
|
||||
i = buf.find(bytes([op]))
|
||||
while i >= 0:
|
||||
if i + 5 <= len(buf):
|
||||
rel = struct.unpack_from("<i", buf, i + 1)[0]
|
||||
if img_lo + i + 5 + rel == target:
|
||||
print(f" {name} rel32 from image 0x{img_lo+i:x} [{perms}]")
|
||||
hits += 1
|
||||
i = buf.find(bytes([op]), i + 1)
|
||||
# lea reg,[rip+rel32] (48 8d /r with mod=00 rm=101)
|
||||
i = buf.find(b"\x48\x8d")
|
||||
while i >= 0:
|
||||
if i + 7 <= len(buf):
|
||||
modrm = buf[i + 2]
|
||||
if (modrm & 0xC7) == 0x05:
|
||||
rel = struct.unpack_from("<i", buf, i + 3)[0]
|
||||
if img_lo + i + 7 + rel == target:
|
||||
print(f" lea rip-rel from image 0x{img_lo+i:x} [{perms}]")
|
||||
hits += 1
|
||||
i = buf.find(b"\x48\x8d", i + 1)
|
||||
# absolute pointer (live address stored in a table)
|
||||
pat = struct.pack("<Q", tgt_live)
|
||||
i = buf.find(pat)
|
||||
while i >= 0:
|
||||
if i % 8 == 0:
|
||||
print(f" abs ptr slot at image 0x{img_lo+i:x} [{perms}]")
|
||||
hits += 1
|
||||
i = buf.find(pat, i + 1)
|
||||
print(f" {hits} reference(s)")
|
||||
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user