fifa17-recon: offline FUT squad-shell working + full card-system RE
Milestone: FIFA 17 Ultimate Team boots end-to-end on our offline backend
past every EA gate into the hub and a live Squads editor (correct 4-4-2,
5-star squad, no freezes).
Key findings this session:
- userMassInfo MUST stay {} (any content desyncs the massinfo parser
0x180174630 -> tokenizer busy-loop freeze). Deliver the squad via
GET /squad/0 (fetched on Squads-tab entry) instead.
- Player cards render generic because the card view-model (0x1800d7920)
reads identity/rating/face from a resolved record at item+0x10, filled
by a lookup (0x18011cca0) in the FUT item-definition std::map at
CardsDb+0x160c0 -- which is EMPTY offline -> default blank record.
- Version advertising (itemDbVersion/checkServerDbVersion) is proven inert
(JSON fields routed to the skip handler). Owned items don't auto-trigger
a definition fetch. In-place map overwrite is dead (map stays empty).
- Definition-serving endpoints (item/resource, defid, item?idList) built +
ready; the fetch trigger lives in the packed FIFA17.exe.
New: docs/CARD_SYSTEM.md (findings + ordered next-steps plan for real
player cards: patch-POC, dbdata extractor, drive FIFA17.exe fetch, or
live-memory store injection). Plus tools: fut_seed.py (squad ladder +
definition serving), fifadrive.sh, vgamepad.py, and the login-RE toolset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
This commit is contained in:
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dump the DECRYPTED FIFA17 login machinery from live /proc/PID/mem.
|
||||
|
||||
FIFA17.exe's .data-region code is packed/encrypted on disk (objdump of the file is
|
||||
garbage); the real instructions only exist decrypted in memory at runtime. This grabs
|
||||
generous windows around the known login-path VAs (from prior live recon) plus the
|
||||
resolved OriginMgr / session objects, then disassembles each window at its true VA so a
|
||||
follow-up reversing pass works on real code.
|
||||
|
||||
Run while FIFA17 is running (ptrace_scope=0). Output -> ./login_dump/ + manifest.txt.
|
||||
"""
|
||||
import glob, os, subprocess, struct, sys
|
||||
|
||||
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "login_dump")
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
# (name, VA, bytes_before, bytes_after) — code windows around the login machinery.
|
||||
CODE = [
|
||||
("dispatch_case2", 0x146f1e080, 0x120, 0x180), # event dispatcher; case-2 sets m_isLoggedIn
|
||||
("event_matcher", 0x147102880, 0x40, 0x400), # sender/element matcher
|
||||
("login_parser", 0x147138660, 0x40, 0x400), # <Login> element parser (reads IsLoggedIn)
|
||||
("loginstate_pclogin", 0x1471b58e0, 0x40, 0x600), # LoginStatePCLogin entry
|
||||
("txt_not_login_ebisu", 0x1471b5b00, 0x40, 0x400), # TXT_NOT_LOGIN_TO_EBISU write site(s)
|
||||
("pclogin_callsite", 0x1471b6780, 0x60, 0x120), # session-object call: ff 50 60 (vtbl+0x60)
|
||||
]
|
||||
|
||||
# Data pointers to resolve (name, ptr_VA, deref_chain_offsets, dump_span).
|
||||
# We read *[ptr_VA], then optionally add offsets, then dump `span` bytes there.
|
||||
DATA = [
|
||||
("originmgr", 0x1448acf50, [], 0x80), # OriginMgr; m_isLoggedIn @+0x13, loginError @+0x14
|
||||
("online_flags", 0x1448a3ac0, [], 0x40), # "internet reachable" byte lives here
|
||||
("auth_block", 0x1448a3b20, [0x4e98], 0x60), # auth slots +0x08/+0x10
|
||||
("session_obj", 0x144b86bf8, [], 0x80), # LoginStatePCLogin session object (vtbl @+0)
|
||||
]
|
||||
|
||||
def find_pid():
|
||||
for d in glob.glob('/proc/[0-9]*'):
|
||||
try:
|
||||
if open(d + '/comm').read().strip() == 'FIFA17.exe':
|
||||
return int(os.path.basename(d))
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def read(f, va, n):
|
||||
f.seek(va); return f.read(n)
|
||||
|
||||
def rd_u64(f, va):
|
||||
b = read(f, va, 8)
|
||||
return struct.unpack('<Q', b)[0] if len(b) == 8 else 0
|
||||
|
||||
def disasm(path, va):
|
||||
asm = path + ".asm"
|
||||
with open(asm, "w") as out:
|
||||
subprocess.run(["objdump", "-D", "-b", "binary", "-m", "i386:x86-64",
|
||||
"-M", "intel", "--adjust-vma=%#x" % va, path],
|
||||
stdout=out, stderr=subprocess.DEVNULL)
|
||||
return asm
|
||||
|
||||
def main():
|
||||
pid = find_pid()
|
||||
if not pid:
|
||||
print("FIFA17.exe not running — launch it first."); sys.exit(1)
|
||||
man = open(os.path.join(OUT, "manifest.txt"), "w")
|
||||
man.write("FIFA17 login-machinery dump pid=%d\n\n" % pid)
|
||||
with open(f"/proc/{pid}/mem", "rb") as f:
|
||||
for name, va, before, after in CODE:
|
||||
start = va - before
|
||||
data = read(f, start, before + after)
|
||||
p = os.path.join(OUT, f"{name}_{start:x}.bin")
|
||||
open(p, "wb").write(data)
|
||||
disasm(p, start)
|
||||
line = f"CODE {name:22s} window {start:#x}..{start+len(data):#x} ({len(data)}B) -> {os.path.basename(p)}[.asm]"
|
||||
print(line); man.write(line + "\n")
|
||||
man.write("\n")
|
||||
for name, ptr, chain, span in DATA:
|
||||
base = rd_u64(f, ptr)
|
||||
addr = base
|
||||
trail = f"*[{ptr:#x}]={base:#x}"
|
||||
for off in chain:
|
||||
nxt = rd_u64(f, addr + off) if off and base else base
|
||||
# for a single deref-with-offset we dump AT base+off, not deref again:
|
||||
addr = base
|
||||
target = base + (chain[0] if chain else 0)
|
||||
data = read(f, target, span) if base else b""
|
||||
p = os.path.join(OUT, f"{name}_{target:x}.bin")
|
||||
open(p, "wb").write(data)
|
||||
# hex preview
|
||||
hexp = " ".join("%02x" % x for x in data[:0x40])
|
||||
line = f"DATA {name:22s} {trail} dump@{target:#x} ({len(data)}B) -> {os.path.basename(p)}\n first64: {hexp}"
|
||||
print(line); man.write(line + "\n")
|
||||
man.close()
|
||||
print("\nWrote dumps + manifest to", OUT)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user