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:
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Live read-only probe: did the pushed LSX <Login> event actually land?
|
||||
|
||||
READ-ONLY. Never writes to the game. Safe to run against the live FIFA17.exe
|
||||
while the main session drives it.
|
||||
|
||||
Watches, once per second:
|
||||
|
||||
OriginMgr.m_isLoggedIn = *(u8)( *[0x1448acf50] + 0x13 )
|
||||
Set to 1 by the Origin event dispatcher @0x146f1e060 case 2 (verified:
|
||||
`cmp DWORD PTR [r9],1` -> `mov BYTE PTR [rcx+0x13],1`), which is reached
|
||||
only from a server-pushed <Event sender="LOGIN_EVENT"><Login
|
||||
IsLoggedIn="true"/>. 0 -> 1 means the push was dispatched.
|
||||
|
||||
CAVEAT (verify: will-it-reach-login): this byte is a PROXY, not the
|
||||
decisive consumer. It is written on the SAME dispatcher line that then
|
||||
falls into the FE re-broadcast loop @0x146f1e116 -> callback 0x147350e30
|
||||
(packs the event tagged 0xdea12004 and republishes on FIFA's FE event
|
||||
bus). Nothing downstream READS +0x13; the FE broadcast is what actually
|
||||
propagates. So treat 0 -> 1 as "the frame was accepted", and confirm
|
||||
real propagation with a gdb breakpoint on 0x147350e30 (bytes 48 83 ec 58).
|
||||
Also note LoginStatePCLogin's own gate is a DIFFERENT object
|
||||
([0x144b86bf8]->vtbl+0x60), so this flag flipping does not guarantee
|
||||
GetAuthCode fires.
|
||||
|
||||
OriginMgr.m_loginError = *(u32)( *[0x1448acf50] + 0x14 )
|
||||
Cleared to 0 by the same code path.
|
||||
|
||||
origin "online" byte = *(u8)[0x1448a3ac0]
|
||||
INIT-SET, NOT DIAGNOSTIC: OriginMgr::Initialize writes this to 1
|
||||
unconditionally @0x146f340e1 (`mov BYTE PTR [rip+...],0x1`), so it does
|
||||
NOT reflect GetInternetConnectedState. Shown for reference only; do not
|
||||
read it as a live state field.
|
||||
|
||||
FirstPartyAuthTokenRetriever slots
|
||||
retriever = *[0x1448a3b20] + 0x4e98 ; slots at +0x08 and +0x10
|
||||
DoTick @0x146f199c0 walks these two; if both stay 0 no auth-code
|
||||
request was ever enqueued and RequestAuthCodeSync @0x1470db3c0 is
|
||||
never called. Non-zero here = GetAuthCode is imminent.
|
||||
|
||||
Usage: python3 origin_login_probe.py [seconds]
|
||||
"""
|
||||
import glob
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
ORIGIN_MGR_PP = 0x1448acf50 # -> OriginMgr*
|
||||
ORIGIN_ONLINE_BYTE = 0x1448a3ac0 # FIFA's separate "origin online" flag
|
||||
SDK_PP = 0x1448a3b20 # -> OriginSDK*, retriever at +0x4e98
|
||||
RETRIEVER_OFF = 0x4e98
|
||||
|
||||
|
||||
def find_pid():
|
||||
for d in glob.glob("/proc/[0-9]*"):
|
||||
try:
|
||||
if open(d + "/comm").read().strip() == "FIFA17.exe":
|
||||
return int(d.rsplit("/", 1)[-1])
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class Mem(object):
|
||||
def __init__(self, pid):
|
||||
self.f = open("/proc/%d/mem" % pid, "rb")
|
||||
|
||||
def rd(self, va, n):
|
||||
try:
|
||||
self.f.seek(va)
|
||||
b = self.f.read(n)
|
||||
return b if b and len(b) == n else None
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
def u8(self, va):
|
||||
b = self.rd(va, 1)
|
||||
return None if b is None else b[0]
|
||||
|
||||
def u32(self, va):
|
||||
b = self.rd(va, 4)
|
||||
return None if b is None else struct.unpack("<I", b)[0]
|
||||
|
||||
def u64(self, va):
|
||||
b = self.rd(va, 8)
|
||||
return None if b is None else struct.unpack("<Q", b)[0]
|
||||
|
||||
|
||||
def hx(v):
|
||||
return "??" if v is None else ("%#x" % v)
|
||||
|
||||
|
||||
def main():
|
||||
limit = float(sys.argv[1]) if len(sys.argv) > 1 else 1e9
|
||||
pid = find_pid()
|
||||
if pid is None:
|
||||
print("no FIFA17.exe running")
|
||||
return 1
|
||||
print("pid", pid)
|
||||
m = Mem(pid)
|
||||
t0 = time.time()
|
||||
last = None
|
||||
while time.time() - t0 < limit:
|
||||
mgr = m.u64(ORIGIN_MGR_PP)
|
||||
logged = m.u8(mgr + 0x13) if mgr else None
|
||||
err = m.u32(mgr + 0x14) if mgr else None
|
||||
online = m.u8(ORIGIN_ONLINE_BYTE)
|
||||
sdk = m.u64(SDK_PP)
|
||||
r = (sdk + RETRIEVER_OFF) if sdk else None
|
||||
s1 = m.u64(r + 0x08) if r else None
|
||||
s2 = m.u64(r + 0x10) if r else None
|
||||
row = (logged, err, online, s1, s2)
|
||||
if row != last:
|
||||
print("[%s] OriginMgr=%s m_isLoggedIn=%s loginError=%s "
|
||||
"onlineByte=%s(init-set) | authSlots=%s,%s"
|
||||
% (time.strftime("%H:%M:%S"), hx(mgr), logged, hx(err),
|
||||
online, hx(s1), hx(s2)))
|
||||
# Fire on any non-1 -> 1 transition (including the very first sample
|
||||
# where OriginMgr was still null and last[0] was None), so a
|
||||
# None -> 1 flip is not silently missed.
|
||||
if last is not None and last[0] != 1 and logged == 1:
|
||||
print(" *** m_isLoggedIn -> 1 : the pushed <Login> event was "
|
||||
"DISPATCHED (proxy signal; confirm FE re-broadcast at "
|
||||
"0x147350e30). Watch for LSX GetAuthCode next. ***")
|
||||
if last is not None and not (last[3] or last[4]) and (s1 or s2):
|
||||
print(" *** auth-code request ENQUEUED into "
|
||||
"FirstPartyAuthTokenRetriever. ***")
|
||||
last = row
|
||||
time.sleep(1.0)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user