59934b4ef0
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
88 lines
2.7 KiB
Python
88 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Pseudo-backtrace from a minidump: walk the faulting thread's stack and report
|
|
every qword that points into a loaded module's code (i.e. plausible return
|
|
addresses), innermost first."""
|
|
import struct, sys
|
|
|
|
p = sys.argv[1]
|
|
d = open(p, "rb").read()
|
|
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))
|
|
|
|
|
|
def mstring(rva):
|
|
(ln,) = struct.unpack_from("<I", d, rva)
|
|
return d[rva + 4: rva + 4 + ln].decode("utf-16-le", "replace")
|
|
|
|
|
|
mods = []
|
|
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).split("\\")[-1]))
|
|
off += 108
|
|
mods.sort()
|
|
|
|
# faulting thread + exception address
|
|
size, rva = streams[6][0]
|
|
tid, _ = struct.unpack_from("<II", d, rva)
|
|
code, flags, recptr, exc_addr, nparams = struct.unpack_from("<IIQQI", d, rva + 8)
|
|
|
|
|
|
def whose(a):
|
|
for base, sz, name in mods:
|
|
if base <= a < base + sz:
|
|
return name, a - base
|
|
return None, 0
|
|
|
|
|
|
# thread list
|
|
size, rva = streams[3][0]
|
|
(nthreads,) = struct.unpack_from("<I", d, rva)
|
|
off = rva + 4
|
|
target = None
|
|
for i in range(nthreads):
|
|
t_id, susp, pcls, prio, teb, stk_start, stk_size, stk_rva, ctx_size, ctx_rva = \
|
|
struct.unpack_from("<IIIIQQIIII", d, off)
|
|
if t_id == tid:
|
|
target = (stk_start, stk_size, stk_rva, ctx_size, ctx_rva)
|
|
off += 48
|
|
|
|
print("faulting thread %#x exception at %s+%#x" % (tid, *whose(exc_addr)))
|
|
if not target:
|
|
print("no stack captured for the faulting thread"); sys.exit(0)
|
|
stk_start, stk_size, stk_rva, ctx_size, ctx_rva = target
|
|
print("stack %#x..%#x (%d bytes)\n" % (stk_start, stk_start + stk_size, stk_size))
|
|
|
|
# CONTEXT_AMD64: Rsp at offset 0x98, Rip at 0xF8 (RUNTIME layout)
|
|
if ctx_size >= 0x100:
|
|
rsp = struct.unpack_from("<Q", d, ctx_rva + 0x98)[0]
|
|
rip = struct.unpack_from("<Q", d, ctx_rva + 0xF8)[0]
|
|
print("RSP=%#x RIP=%#x (%s+%#x)\n" % (rsp, rip, *whose(rip)))
|
|
else:
|
|
rsp = stk_start
|
|
|
|
print("=== plausible return addresses (innermost first) ===")
|
|
seen, out = set(), []
|
|
start = max(rsp - stk_start, 0)
|
|
for o in range(int(start), stk_size - 8, 8):
|
|
(v,) = struct.unpack_from("<Q", d, stk_rva + o)
|
|
name, off2 = whose(v)
|
|
if name is None:
|
|
continue
|
|
key = (name, off2)
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
out.append((stk_start + o, name, off2))
|
|
for i, (sa, name, off2) in enumerate(out[:45]):
|
|
tag = ""
|
|
if "cards" in name.lower():
|
|
tag = " <-- CardsDLL ghidra %#x" % (0x180000000 + off2)
|
|
print(" [%2d] %#x %s+%#x%s" % (i, sa, name, off2, tag))
|