Files
OpenFUT/fifa17-recon/tools/fut_flow.md
T
funman300 6ddd5e9d47 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
2026-08-01 20:24:30 -07:00

21 KiB
Raw Blame History

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 LoginStateLogoutAuthentication::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)

  • 0x14727dc00starts the fetch. It resolves a component/service (call [r8+0x60], obfuscated fourcc arg), then:
    • success: call [rax+0x40] (kick async op) → store handle via 0x147173690 into [this+0x1e0];
    • null service → immediately dispatches EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE (0x1439840b8) at 0x14727dcbe, with no network I/O at all.
  • 0x14727dd70 — the completion callback (this, status, data):
    • status == 0: copies 4 bytes from data[0..3] into [this+0xa0..0xa3], then dispatches EVENT_LOGIN_FETCH_ACCOUNT_INFO_SUCCESS (0x1439840e0, lea @ 0x14727de34);
    • status != 0: dispatches EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE (lea @ 0x14727de7b).
    • Dispatcher: [[0x144b8f498]] + 0x8, event-source string 0x14354b5f0.
  • Related adaptor-level events also present: EVENT_ACCOUNT_FETCH_INFO_SUCCESS/FAILURE (0x143983648 / 0x143983670), adaptor actions FetchAccountInfo (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::pingUtil::fetchClientConfig (9/0x01) ×6 for OSDK_CORE, OSDK_CLIENT, OSDK_NUCLEUS, OSDK_WEBOFFER, OSDK_ABUSE_REPORTING, OSDK_XMS_ABUSE_REPORTINGall answered EMPTY by blaze_responder_v3.pyAuthentication 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:

  1. the account-info/identity service has nothing to initialise from → fails locally → EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE"Unable to retrieve account information.";
  2. the FirstPartyAuthTokenRetriever pending list is never fed → DoTick never calls OriginRequestAuthCodeSyncLSX GetAuthCode is never sent;
  3. LoginRequest.AUTH (Blaze::Authentication::LoginRequest @ 0x14487ca10, members AUTH/EXTB/EXTI) can never be filled → LoginStatePCLogin (800) is skipped → Authentication::login (1/0x0A) is never sent;
  4. LoginStateLogout (500) tears the session down → the observed 1/0x46.

Verdict on the three prior hypotheses

  • (a) a pushed LSX <Event> "user logged in"not supported. Origin is already connected="1", the flow demonstrably got past origin.nav into Blaze, and the client stopped asking LSX anything at all after UPTODATE. A pushed event is not what it is waiting on. (IsLoggedIn does exist in the LSX attribute pool at 0x14394e0f0 and Origin::EventHandler<lsx::LoginT,unsigned int>::HandleMessage at 0x14393f900, so a Login/IsLoggedIn LSX event is implementable — but nothing here indicates it is the gate.)
  • (b) an account-info step fails first and aborts before GetAuthCodeCONFIRMED, and its missing input is identified: the Blaze client config.
  • (c) origin.nav / OriginOnlineEvent not firingREFUTED. origin.nav is a two-node flow, and everything downstream of OriginIsOnlineTrue (Blaze connect, preAuth, fetchClientConfig) demonstrably ran.

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 the NUCLEUS_*_URL set (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 minimum SV_ENABLE_SERVER_VERSIONING=0 (else LoginStateVersionCheck (700) can trip "Client/server version mismatch! Client is at version (%08d)…", 0x14395d1d0), plus netres.
  • OSDK_CLIENT, OSDK_WEBOFFER, OSDK_ABUSE_REPORTING, OSDK_XMS_ABUSE_REPORTING: non-empty but can stay minimal. (OSDK_TICKER is 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 OnlineLoginViewModel message-index → loc-key mapping (0x147c3c050 online branch → 0x147d92a80(this, idx) with idx 0x21/0x23/0x24/0x25 chosen by [vm+0x158], then 0x147c4bfd0 / 0x147c4ca50 / 0x147c4a4b0). Attribution of the string to EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE is by (i) exact semantic match, (ii) the zero-network-traffic failure shape matching 0x14727dc00's early-out, (iii) elimination of every other observed step. The alternative emitter is the adaptor-level EVENT_ACCOUNT_FETCH_INFO_FAILURE (0x143983670) — same root cause either way.
  • Whether blazeSdkClientId & co. arrive in OSDK_NUCLEUS vs OSDK_CORE is 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 from mov r8d/edx,K; lea …,[r+C] pairs and are worth re-checking on a live PID.
  • LoginStateLogin (generic) vs LoginStatePCLogin (800): on PC the machine is expected to use PCLogin; 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 (launchFUTFlowfutFlow)
scratchpad/nav_checkfutrosters.txt checkFUTRostersFlow.nav
scratchpad/loginreg.asm disassembly of the LoginState registration function