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
21 KiB
FIFA 17 — FUT online-login flow graph + auth state machine (2026-07-30)
Target: FIFA17.exe, ImageBase 0x140000000, Wine flat map. Live PID 3362053 (read-only
/proc/pid/mem) until it exited mid-session; the rest is from captured dumps
(navregion.bin, loginreg.asm) and the on-disk .srdata (plaintext in the file; .text
is packer-encrypted on disk, so no further static disassembly is possible without a live PID).
Clean-room: everything below comes from our own FIFA17.exe (live memory + its own
plaintext .srdata), our own nav JSON as loaded by that binary, and our own captured
LSX/Blaze traffic. No leak material.
0. Answer in one paragraph
origin.nav passes and the flow does reach startFutBlazeLogin. onlineLoginFlow.nav
turns out to be a pure UI shell — it contains no login logic at all; it only emits
sendScreenEvent ["OnlineLogin","0"] and then waits for the C++ to fire loginSuccess /
loginFail / evt_onlineLoginFailurePopup. The real state machine is the OSDK
LoginController / LoginStateMachineImpl (OSDK 8.01.03.00-fifa.01) inside FIFA17.exe.
That machine ran LoginStateConnect (Blaze connect + Util::preAuth — observed on the wire)
and LoginStateLoadConfig (Util::fetchClientConfig ×6 — observed), but our Blaze
responder answers all six fetchClientConfig calls with an EMPTY payload. The next step is
an account-info step that needs values from that config (blazeSdkClientId,
blazeSdkClientSecret, blazeServerClientId, identityRedirectUri). With an empty config it
fails locally, emitting zero network traffic (no further LSX verb, no HTTP to the Nucleus
stub, no further Blaze RPC), raises the OSDK event EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE
("Unable to retrieve account information. Please try again."), and short-circuits to
LoginStateLogout → Authentication::logout (1/0x46) → disconnect. GetAuthCode and
Authentication::login (1/0x0A) live downstream in LoginStatePCLogin and are therefore
never reached. The prerequisite is not a pushed LSX event and not a persona/entitlement
check — it is a populated Blaze client configuration.
1. The full node path (recovered verbatim from live nav JSON)
Nav JSON is resident in one heap region (0xba5b0000-0xbc620000 this run, dumped to
scratchpad/navregion.bin). 152 .nav files are referenced; all are resident. Frostbite path
of the login flow: data/ui/nav/online/onlineLoginFlow.nav.
mainMenu
└─ launchFUTFlow external /online/origin.nav
outputs: OriginIsOnlineTrue -> startFutBlazeLogin
quit -> mainMenu
└─ futBlazeLogin external /online/onlineLoginFlow.nav
inputs: startFutBlazeLogin -> startLoginWithoutMultiplayerCheck
outputs: loginSuccess -> CheckFUTRosters
loginFail -> mainMenu
└─ CheckFUTRosters external /checkFUTRostersFlow.nav
outputs: advance -> postFUTBlazeLogin
back -> mainMenu
└─ postFUTBlazeLogin onEnter: invoke evt_sign_out_flow_ready, evt_invite_flow_not_ready
transitions: advanceRequest -> futFlow
└─ futFlow external /fut/futFlow.nav
1a. origin.nav — complete (2 nodes). This gate is PASSING now.
{ "name":"origin", "states":[
{ "name":"preCheckCoopOrigin",
"onEnter":[ ["sendAction",["checkOriginConnected"]] ],
"transitions":[ {"event":"OriginIsOnline","targets":["OriginIsOnlineTrue"]},
{"event":"OriginIsOffline","targets":["OriginOfflinePopup"]} ] },
{ "name":"OriginOfflinePopup",
"onEnter":[ ["loadView",["popup","OriginOfflinePopup",
"TXT_HUB_SCREEN_ORIGIN_ONLINE_CHECK|OK|popupYes"]] ],
"transitions":[ {"event":"popupYes","targets":["quit"]} ] } ] }
FeFlow action ids: checkOriginConnected = 0x27e1, OriginIsOnline = 0x27e2,
OriginIsOffline = 0x27e3. Backing predicate g_originOnline @0x1443337f8, written only by
the LSX GetInternetConnectedState callback 0x146f1e6b0, which also broadcasts
FE::FIFA::OriginOnlineEvent.
1b. onlineLoginFlow.nav — complete, and it contains NO login logic
onlineLoginFlow
onEnter: loadViewModel OnlineLoginViewModel
onExit : unloadViewModel OnlineLoginViewModel
(outer transitions, i.e. events the C++ can raise at any time)
onlineLoginToEaPopup -> onlineLoginToEaPopup
onlineBootLoginToEaPopup -> onlineBootLoginToEaPopup
evt_onlineAlertPopup -> onlineAlertLoginPopup
evt_onlineBootLoginFailurePopup -> onlineFailureLoginPopup
evt_onlineLoginFailurePopup -> onlineFailureLoginPopup <<< OUR PATH
evt_online_disconnected -> processLoginFailure
loginIdle / membershipCheck / firstPartyCommerceCheck
processBootLoginFailure / processLoginFailure
states:
startLoginWithMultiplayerCheck onEnter: sendScreenEvent ["OnlineLogin","1"]
startLoginWithoutMultiplayerCheck onEnter: sendScreenEvent ["OnlineLogin","0"] <<< ENTRY
in_startSilentSignIn -> skipSilentSignInCheck (conditionAardvark SKIP_SILENT_SIGN_IN)
true -> loginFail
false -> executeSilentSignIn (sendScreenEvent SilentSignIn)
onlineLoginToEaPopup onEnter: sendAction onlineLoginPopupShow
onExit : onlineLoginPopupHide "LOGIN_POPUP"
cancelLogin -> cancelLoginToEA (sendScreenEvent CancelLoginToEA)
onlineBootLoginToEaPopup (same, boot variant)
onlineAlertLoginPopup onEnter: onlineLoginPopupShow / onExit: hide "ALERT_POPUP"
processLoginAlert (sendScreenEvent ProcessLoginAlert)
onlineFailureLoginPopup onEnter: sendAction onlineLoginPopupShow <<< THE POPUP
onExit : onlineLoginPopupHide "ALERT_POPUP"
processLoginFailure -> sendScreenEvent ProcessLoginFailure
processBootLoginFailure -> sendScreenEvent ProcessBootLoginFailure
loginIdle (empty)
membershipCheck popup MembershipCheckPopup / TXT_CHECKING_MEMBERSHIP_LEVEL
firstPartyCommerceCheck popup FirstPartyCommerceCheckPopup
TrialWelcome -> TrialWelcomeCheck (sendAction trialCheck welcomeScreenCheck)
evt_goTrialWelcomeScreen -> TrialWelcomeScreen -> advance -> loginSuccess
evt_skipTrialWelcomeScreen-> loginSuccess
transitions: loginSuccess -> TrialWelcomeCheck ; loginFail -> loginFail(output)
Key structural finding: every node here is a popup or a sendScreenEvent. There is no
GetAuthCode node, no persona node, no entitlement node. The nav delegates 100 % of the login
to the C++ via one screen event, and only reacts to C++-raised events. So the question
"why no GetAuthCode" cannot be answered in the flow graph — it is answered in the OSDK login
state machine (§2).
1c. checkFUTRostersFlow.nav (downstream, never reached)
LoadFUTDatabase (condition loadFUTDatabase) → LoadFUTSquad (AutoLoadFUTSquad) →
CheckFUTRosterUpdateXML (isFUTRosterXMLAvailable) → CheckFUTSquadBinFile → … ;
failure → FailFUTRosterXMLDownloadPopup (FUT_SQUAD_DOWNLOAD_FAIL) / unloadFUTDatabaseOnFail.
onEnter loads FIFAFutLoginViewModel.
2. The real state machine: OSDK LoginController
Build tag in the binary: E:/p4/fifafb/rl/empatch/TnT/Code/fifa/gamemodes/extern/OSDK/ 8.01.03.00-fifa.01/source/...
2a. States recovered (name string, vtable, registered id)
Each LoginState* class has an 8-byte GetStateName() stub (lea rax,[str]; ret) in the
block 0x14719b360-0x14719b4e8; the stub sits at vtable+0x08, which pins vtable→name.
The registration function 0x147158e00-0x1471597xx (dumped: scratchpad/loginreg.asm) does
alloc → set vtable → set name → map-insert(machine, obj, id).
| id | state | vtable | name str |
|---|---|---|---|
| 50 | (unnamed, vt 0x14395bb90) |
0x14395bb90 |
— |
| 100 | LoginStateCheckUser |
0x14395bc30 |
0x14395c850 |
| 200 | LoginStateIsp |
0x14395bc78 |
0x14395bd08 |
| 300 | LoginStateRecheckUser |
0x14395bc30 |
0x14395c868 |
| 310 | LoginStateLoadIspAccountInfo |
0x14395bd18 |
0x14395bd60 |
| 400 | LoginStateConnect |
0x14395bd80 |
0x14395be08 |
| 500 | LoginStateLogout |
0x14395be80 |
0x14395bf28 |
| 700 | LoginStateVersionCheck |
0x14395bf40 |
0x14395bf88 |
| 800 | LoginStatePCLogin |
0x14395c180 |
0x14395c398 |
| 1000 | LoginStateLoginComplete |
0x14395c580 |
0x14395c5c8 |
| 1300 | LoginStateVerifyAccount |
0x14395c3b0 |
0x14395c4c8 |
| 1350 | LoginStateUpgradeAccount |
0x14395c4e0 |
0x14395c560 |
| — | LoginStateLogin (generic, non-PC) |
0x14395bfa0 |
0x14395c170 |
| — | LoginStateLoadConfig |
0x14395be20 |
0x14395be68 |
| — | LoginStateShowMaintenance / LoginStateUnsuspend / LoginStateWebOffer |
0x14395c5e0 / … |
0x14395bc10 / 0x14395c640 / 0x14395d2c0 |
Ctors: 0x1471610xx = LoginStateLogin, 0x147161250 = LoginStatePCLogin,
0x147161340 = LoginStateVerifyAccount, 0x147165ea0 = LoginStateLoadConfig.
State-machine map global: 0x144b86c08; insert helper 0x14717c4f0(machine, state, id, 1).
NOTE: ids are an enum, not strictly a sequence (500 = Logout is a failure/teardown state).
2b. LoginStateLoadIspAccountInfo (id 310) — what it actually is
Its event handler is 0x1471b4b40 (vtable+0x20; vtable+0x10 = 0x1472243c0 = [this+0x20]=0
i.e. sub-state reset). It switches on a 4-way sub-state [this+0x20] and, in sub-states 0 and
1, does GetComponent('cnnc') on the OSDK singleton 0x144b86bf8 (call [vt+0x60]), then
[vt+0x38], then [vt+0xb0] → returns an int country code, and tests it against 0 and
0x5a5a (= ASCII "ZZ", the unknown-country sentinel). It also reads a 2-char country string
from [0x144b86bf8 + 0xb0] and validates 'A'..'Z'/'a'..'z'.
So "Isp account info" here = the ISP/connection-derived country/geo (feeds SetPingSiteLatency
/ ping-site selection), not the EA account. That is consistent with it running before
LoginStateConnect (id 310 < 400) and with it having succeeded this run (Blaze connect
happened, Country="US" is served by our LSX GetProfile).
2c. The account-info fetch + its two failure exits (binary-pinned)
0x14727dc00— starts the fetch. It resolves a component/service (call [r8+0x60], obfuscated fourcc arg), then:- success:
call [rax+0x40](kick async op) → store handle via0x147173690into[this+0x1e0]; - null service → immediately dispatches
EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE(0x1439840b8) at0x14727dcbe, with no network I/O at all.
- success:
0x14727dd70— the completion callback(this, status, data):status == 0: copies 4 bytes fromdata[0..3]into[this+0xa0..0xa3], then dispatchesEVENT_LOGIN_FETCH_ACCOUNT_INFO_SUCCESS(0x1439840e0, lea @0x14727de34);status != 0: dispatchesEVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE(lea @0x14727de7b).- Dispatcher:
[[0x144b8f498]] + 0x8, event-source string0x14354b5f0.
- Related adaptor-level events also present:
EVENT_ACCOUNT_FETCH_INFO_SUCCESS/FAILURE(0x143983648/0x143983670), adaptor actionsFetchAccountInfo(0x1439623c0),UpdateAccountInfo(0x1439623d8),GetAccountInfo(0x14398aac8),GetNucleusAccountInfo(0x14398a920),OSDK_NucleusAdaptor(0x14398b038).
2d. The GetAuthCode chain (only reachable from LoginStatePCLogin)
FifaOnline::FirstPartyAuthTokenRetriever::DoTick 0x146f199c0
walks pending list [this+8]; per entry:
0x1470da6d0 OriginGetDefaultUser/singleton getter
0x1470db3c0 OriginRequestAuthCodeSync (log str 0x143936158)
gate 0x1470e2840 "Origin SDK available?" -> false: return 0xa0010000
else 0x1470e3560 get SDK
0x1470e67f0 Origin::OriginSDK::RequestAuthCodeSync (0x143937d68)
-> LSX <GetAuthCode ClientId="..."/> -> <AuthCode .../>
status != 0 -> "[%s] Origin Error(%d)" 0x1438f5e18 (lea @0x146f19ab2)
len==0 || ptr==0 -> "[%s] Invalid authcode" 0x1438f5e00 (lea @0x146f19a85)
ok -> build FifaOnline::FirstPartyAuthCodeFutureImpl (0x1438f5d98, lea @0x146f19a39),
store [entry+0xd8], set [entry+0xe8]=1
DoTick only does anything if the pending list [this+8] is non-empty — i.e. only if some
upstream state actually requested a first-party token. Nothing requested one this run
(/tmp/openfut_authcode.txt empty, no GetAuthCode in /tmp/lsx.log).
Auth-code consumer parameters, all present in .rdata:
client_id= &client_secret= &scope= &redirect_uri= &code= &grant_type=
authorization_code connect/token (0x456f58-0x457010 file offsets), scope literal
signin basic.identity basic.persona basic.domaindata offline, redirect
http://127.0.0.1/login_successful.html (0x143b04870), Nucleus::gNucleusBaseUrl /
gNucleusClientSideRedirectUri (0x143b8b718 / 0x143b8b778).
3. Where it dead-ends, and on what it waits
Observed reality this run
| layer | evidence |
|---|---|
| LSX | /tmp/lsx.log ends at id 19 (GetGameInfo UPTODATE -> "true"). No further LSX request of any kind. No GetAuthCode. |
| Nucleus/HTTP | zero requests to the nucleusConnect stub on 127.0.0.1:42131; no dial to accounts/gateway/nucleus.ea.com. |
| Blaze | connect → Util::preAuth (9/0x07) → Util::ping → Util::fetchClientConfig (9/0x01) ×6 for OSDK_CORE, OSDK_CLIENT, OSDK_NUCLEUS, OSDK_WEBOFFER, OSDK_ABUSE_REPORTING, OSDK_XMS_ABUSE_REPORTING — all answered EMPTY by blaze_responder_v3.py → Authentication 1/0x46 = logout, empty payload → socket closed → 3-second transport-PING reconnect loop (/tmp/blaze_responder.log, still looping at the time of writing). No Authentication::login (1/0x0A) ever. |
| UI | popup text "Unable to retrieve account information. Please try again." resident at 0xb79ad020 / 0x78f5380 / 0xbc81ba70, UI-side id string KEY_2002 adjacent at 0xb79ace58/0xb79ad068. |
Mapping that onto the state machine
LoginStateConnect (400) ran and succeeded (preAuth on the wire). LoginStateLoadConfig
ran and completed (six replies received) but with empty config maps. The very next
step is the account-info step, and it failed without producing a single byte of network
traffic on any of the three transports. A timeout or a rejected request would have produced
traffic. A local precondition failure would not — and that is exactly the shape of the
0x14727dc00 early-out (service == null → EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE, no I/O).
The state machine then entered LoginStateLogout (500), which is what emits the observed
Authentication::logout.
The prerequisite, precisely
startFutBlazeLogin does require a prerequisite before GetAuthCode, and it is the Blaze
client configuration delivered by Util::fetchClientConfig — specifically the identity block.
The four config keys the client needs are literals in .rdata, adjacent, right after
QueryEbisuCallback:
| key | file offset | what it feeds |
|---|---|---|
blazeServerClientId |
0x458c90 | Blaze-side identity |
blazeSdkClientId |
0x458ca8 | the ClientId attribute of the LSX <GetAuthCode ClientId="…"/> request (attribute name confirmed in the LSX attribute pool at 0x14394e098) |
blazeSdkClientSecret |
0x458cc0 | &client_secret= in the Nucleus connect/token exchange |
identityRedirectUri |
0x458cd8 | &redirect_uri= (pairs with http://127.0.0.1/login_successful.html) |
With fetchClientConfig returning empty, none of these exist, so:
- the account-info/identity service has nothing to initialise from → fails locally →
EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE→ "Unable to retrieve account information."; - the
FirstPartyAuthTokenRetrieverpending list is never fed →DoTicknever callsOriginRequestAuthCodeSync→ LSXGetAuthCodeis never sent; LoginRequest.AUTH(Blaze::Authentication::LoginRequest@0x14487ca10, membersAUTH/EXTB/EXTI) can never be filled →LoginStatePCLogin(800) is skipped →Authentication::login(1/0x0A) is never sent;LoginStateLogout(500) tears the session down → the observed1/0x46.
Verdict on the three prior hypotheses
- (a) a pushed LSX
<Event>"user logged in" — not supported. Origin is alreadyconnected="1", the flow demonstrably got pastorigin.navinto Blaze, and the client stopped asking LSX anything at all afterUPTODATE. A pushed event is not what it is waiting on. (IsLoggedIndoes exist in the LSX attribute pool at0x14394e0f0andOrigin::EventHandler<lsx::LoginT,unsigned int>::HandleMessageat0x14393f900, so aLogin/IsLoggedInLSX event is implementable — but nothing here indicates it is the gate.) - (b) an account-info step fails first and aborts before
GetAuthCode— CONFIRMED, and its missing input is identified: the Blaze client config. - (c)
origin.nav/OriginOnlineEventnot firing — REFUTED.origin.navis a two-node flow, and everything downstream ofOriginIsOnlineTrue(Blaze connect, preAuth, fetchClientConfig) demonstrably ran.
4. Recommended next action (single change, testable)
Stop returning empty Util::fetchClientConfig (9/0x01) replies in
fifa17-recon/tools/blaze_responder_v3.py. Return a populated CONF string→string map per
CFID, minimally:
OSDK_NUCLEUS:blazeSdkClientId,blazeSdkClientSecret,blazeServerClientId,identityRedirectUri(=http://127.0.0.1/login_successful.html), plus theNUCLEUS_*_URLset (NUCLEUS_CREATE_URL,NUCLEUS_ADDED_URL,NUCLEUS_INCOMPLETE_URL,NUCLEUS_CREATE_INFO_URL,NUCLEUS_DUPACCT_INFO_URL,NUCLEUS_DEACTIVATED_INFO_URL) pointed at our own stub.OSDK_CORE: at minimumSV_ENABLE_SERVER_VERSIONING=0(elseLoginStateVersionCheck(700) can trip "Client/server version mismatch! Client is at version (%08d)…",0x14395d1d0), plusnetres.OSDK_CLIENT,OSDK_WEBOFFER,OSDK_ABUSE_REPORTING,OSDK_XMS_ABUSE_REPORTING: non-empty but can stay minimal. (OSDK_TICKERis a seventh CFID the client knows about.)
Success signals, in order: (1) the client stops sending 1/0x46 right after the six
config fetches; (2) /tmp/lsx.log shows a GetAuthCode request carrying ClientId= and
/tmp/openfut_authcode.txt becomes non-empty; (3) Blaze receives Authentication::login
(1/0x0A) with AUTH=<our auth code>; (4) after our LoginResponse (5 members
ANON/NTOS/SESS/SPAM/UNDR, 0x14487d170) plus UserSessions notifications UserAdded (0x02),
UserSessionExtendedDataUpdate (0x01), UserAuthenticated (0x08), the nav advances
loginSuccess → TrialWelcomeCheck → CheckFUTRosters → postFUTBlazeLogin → futFlow.
Keep identity consistent everywhere: PersonaId/UserId 33068179, Persona CAGE, en_US,
contentId 1027460, entitlement ONLINE_ACCESS.
5. Open / not pinned (be honest)
- The exact emitting state for the popup was not binary-pinned: the live PID exited before
I could disassemble the
OnlineLoginViewModelmessage-index → loc-key mapping (0x147c3c050online branch →0x147d92a80(this, idx)with idx 0x21/0x23/0x24/0x25 chosen by[vm+0x158], then0x147c4bfd0/0x147c4ca50/0x147c4a4b0). Attribution of the string toEVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILUREis by (i) exact semantic match, (ii) the zero-network-traffic failure shape matching0x14727dc00's early-out, (iii) elimination of every other observed step. The alternative emitter is the adaptor-levelEVENT_ACCOUNT_FETCH_INFO_FAILURE(0x143983670) — same root cause either way. - Whether
blazeSdkClientId& co. arrive inOSDK_NUCLEUSvsOSDK_COREis an inference from the CFID set + key naming; the fastest resolution is empirical (put them in both and watch which one the client consumes). - The obfuscated fourcc component ids in
0x14727dc00/0x14727dd70/0x1471b4b40('cnnc'= 0x636E6E63 for the ISP/connection component, and 0x6E756D67 for the one used by the account-info starter) were reconstructed frommov r8d/edx,K; lea …,[r+C]pairs and are worth re-checking on a live PID. LoginStateLogin(generic) vsLoginStatePCLogin(800): on PC the machine is expected to usePCLogin; not re-verified at runtime.
6. Artifacts produced
| file | contents |
|---|---|
scratchpad/navscan.py |
live-memory nav/JSON keyword + blob scanner |
scratchpad/xr.py |
numpy rip-rel + absolute xref finder (FIFA17 module range) |
scratchpad/dis.sh |
dump live VA range + objdump at correct VA |
scratchpad/navregion.bin |
34 MB heap region containing every loaded .nav (this run) |
scratchpad/navseg_origin_login.txt |
onlineLoginFlow.nav + origin.nav as text |
scratchpad/nav_root_fut.txt |
root.nav FUT chain (launchFUTFlow … futFlow) |
scratchpad/nav_checkfutrosters.txt |
checkFUTRostersFlow.nav |
scratchpad/loginreg.asm |
disassembly of the LoginState registration function |