# 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. ```json { "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 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 -> 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 `` 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 `OriginRequestAuthCodeSync` → **LSX `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 `` "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::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 `GetAuthCode`** — **CONFIRMED**, and its missing input is identified: the Blaze client config. * **(c) `origin.nav` / `OriginOnlineEvent` not firing** — **REFUTED.** `origin.nav` is a two-node flow, and everything downstream of `OriginIsOnlineTrue` (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 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=`; (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 (`launchFUTFlow` … `futFlow`) | | `scratchpad/nav_checkfutrosters.txt` | `checkFUTRostersFlow.nav` | | `scratchpad/loginreg.asm` | disassembly of the LoginState registration function |