# 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 |