Files
OpenFUT/fifa17-recon/tools/dmp.py
T
funman300 59934b4ef0 fifa17-recon: FUT squad blocker solved + userInfo delivered
The client now issues PUT /squad and the hub renders coins, record and the
squad roster. Three separate root causes, all verified live.

Squad blocker (the long-standing "client never sends PUT /squad"):
  AddPlayerToSquad, GetSquads and SelectSquadById issue ZERO network requests
  (pure local model reads/mutations, FutSquadServiceImpl vtable 0x180233ff0);
  only SaveCurrentSquad writes, and it is unguarded. The client simply needed a
  populated ACTIVE squad model, which arrives via the massinfo `squad` member.
  No response of ours was ever being rejected.

userMassInfo is NOT required to be {}:
  0x180174630 is a FLAT {userInfo, squad, settings, userData} body -- the old
  "wrapper key is user" note was wrong, and the historical freeze was the
  malformed squad member, not the envelope.

clubNameChangeAllowed must be false:
  sending true advertises a club-rename flow whose UI model is never populated;
  the client shows a naming prompt and dies confirming it (ACCESS_VIOLATION
  reading 0x0 at FIFA17.exe+0x71b8651, 4/4 runs, no CardsDLL frame and no request
  in flight). Isolated by a single-variable run; guarded by a contract check.

Endpoint/schema corrections found in live traffic, invisible to static analysis:
  * GET ut/%s/squad/list is a real endpoint and must return {"squad":[...]},
    not the active-squad object (the /list suffix is appended by the caller, so
    it never appeared in the request table)
  * PUT lands on ut/%s/squad/<id>, not a bare ut/%s/squad
  * userInfo currencies are read as name/funds/finalFunds/active -- there is no
    "value" key, so coins always rendered 0
  * squad-list elements take STRING formation/squadType, not ints
  * the CardsDLL script-API thunk<->name table was off by one (AddPlayerToSquad
    is 0x18004aa70; 0x18004aff0 is GetPotentialChemistry_Club)

FUT_MASSINFO / FUT_USERINFO ladders keep every step of the bisect reproducible.
Contract suite 311 -> 358 checks. Tooling added: PyGhidra harness (Ghidra's
Java/OSGi script path is broken on this box), minidump reader, live code grabber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
2026-08-03 20:47:16 -07:00

72 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""Minimal minidump reader: exception code/address + module list, so a FIFA 17
CrashDump can be mapped to <module>+RVA (and thence to a Ghidra address)."""
import struct, sys
p = sys.argv[1]
d = open(p, "rb").read()
assert d[:4] == b"MDMP", "not a minidump: %r" % d[:4]
ver, nstreams, dirrva = struct.unpack_from("<IiI", d, 4)[0], *struct.unpack_from("<II", d, 8)
nstreams, dirrva = struct.unpack_from("<II", d, 8)
streams = {}
for i in range(nstreams):
st, size, rva = struct.unpack_from("<III", d, dirrva + i * 12)
streams.setdefault(st, []).append((size, rva))
print("streams:", sorted(streams))
def mstring(rva):
(ln,) = struct.unpack_from("<I", d, rva)
return d[rva + 4: rva + 4 + ln].decode("utf-16-le", "replace")
mods = []
if 4 in streams:
size, rva = streams[4][0]
(n,) = struct.unpack_from("<I", d, rva)
off = rva + 4
for i in range(n):
base, sz, csum, ts, nrva = struct.unpack_from("<QIIII", d, off)
mods.append((base, sz, mstring(nrva)))
off += 108
print("modules:", len(mods))
exc_addr = None
if 6 in streams:
size, rva = streams[6][0]
tid, _pad = struct.unpack_from("<II", d, rva)
code, flags, recptr, addr, nparams = struct.unpack_from("<IIQQI", d, rva + 8)
exc_addr = addr
NAMES = {0xC0000005: "ACCESS_VIOLATION", 0xC000001D: "ILLEGAL_INSTRUCTION",
0xC0000094: "INT_DIVIDE_BY_ZERO", 0xC0000096: "PRIV_INSTRUCTION",
0x80000003: "BREAKPOINT", 0xC00000FD: "STACK_OVERFLOW",
0xC0000374: "HEAP_CORRUPTION", 0xC0000135: "DLL_NOT_FOUND"}
print("\n=== EXCEPTION ===")
print(" thread : %#x" % tid)
print(" code : %#010x %s" % (code, NAMES.get(code, "?")))
print(" address : %#018x" % addr)
params = [struct.unpack_from("<Q", d, rva + 8 + 32 + i * 8)[0] for i in range(min(nparams, 15))]
print(" params : %s" % [hex(x) for x in params])
if code == 0xC0000005 and len(params) >= 2:
print(" -> %s at %#x" % ({0: "READ from", 1: "WRITE to", 8: "EXECUTE at"}.get(params[0], "access"),
params[1]))
if exc_addr is not None:
hit = [m for m in mods if m[0] <= exc_addr < m[0] + m[1]]
print("\n=== FAULTING MODULE ===")
if hit:
base, sz, name = hit[0]
short = name.split("\\")[-1]
print(" %s base=%#x size=%#x" % (short, base, sz))
print(" RVA = %#x" % (exc_addr - base))
print(" ghidra (PE base 0x180000000) = %#x" % (0x180000000 + (exc_addr - base)))
else:
print(" address %#x is in NO loaded module (bad indirect call / corrupt ptr)" % exc_addr)
print("\n=== modules of interest ===")
for base, sz, name in mods:
s = name.split("\\")[-1].lower()
if any(k in s for k in ("cards", "fifa", "dbdata", "core", "game")):
print(" %-28s base=%#014x size=%#x" % (name.split("\\")[-1], base, sz))