# FIFA 17 offline auth — error trace & state machine Clean-room. All facts below come from (a) the running `FIFA17.exe` we own (PID 19517, `/proc/PID/mem`, ptrace_scope=0), (b) the Steampunks Origin stub on disk/in memory, and (c) our own Blaze session log. **No leak material used.** Module base `0x140000000` (Wine maps the PE flat). --- ## 0. Verdict (answer to the CRITICAL question) **The blocker is LAYER 1 — the ORIGIN / LSX online-state check. It sits in front of Blaze auth as a hard gate, and Blaze login is never even attempted.** Three independent proofs: 1. **The frontend flow graph gates Blaze login behind the Origin check.** Recovered verbatim from the live FeFlow JSON (`0x41bc5df4`): ```json { "name":"launchFUTFlow", "type":"external", "file":"/online/origin.nav", "outputs":{ "OriginIsOnlineTrue":"startFutBlazeLogin", "quit":"mainMenu" } }, { "name":"futBlazeLogin", "type":"external", "file":"/online/onlineLoginFlow.nav", "inputs":{ "startFutBlazeLogin":"startLoginWithoutMultiplayerCheck" }, "outputs":{ "loginSuccess":"CheckFUTRosters", "loginFail":"mainMenu" } }, { "name":"CheckFUTRosters", "type":"external", "file":"/checkFUTRostersFlow.nav", "outputs":{ "advance":"postFUTBlazeLogin", "back":"mainMenu" } }, { "name":"postFUTBlazeLogin", ... "transitions":[{"event":"advanceRequest","targets":["futFlow"]}] } ``` `origin.nav` has exactly two exits. Only `OriginIsOnlineTrue` reaches `startFutBlazeLogin`; anything else is `quit` → `mainMenu`. **Blaze is strictly downstream of Origin.** 2. **The Origin stub is physically incapable of reporting online.** Unpacked image of `/mnt/games/FIFA 17/stp-origin_emu.dll` at `0x6ffffc931000-0x6ffffc93d000` contains its *entire* response repertoire — 11 templates + 1 event: ``` 0x6ffffc9353b0 0x6ffffc9351d0 0x6ffffc9352b0 0x6ffffc935470 ...GetConfigResponse Config="false"... 0x6ffffc935050/0x935170/0x935530 ...GetSettingResponse Setting="%s" / "false" / "production"... 0x6ffffc9350b0/0x9354d0 ...GetGameInfoResponse GameInfo="" / "false"... 0x6ffffc935230 ...IsProgressiveInstallationAvailableResponse ItemId="" Available="false"... 0x6ffffc935410 0x6ffffc935590 ``` `connected="0"` is a **hardcoded literal — there is no `connected="1"` variant anywhere in the module**. There is likewise **no `AuthCodeResponse`, no `GetAuthTokenResponse`, no `QueryEntitlementsResponse`** template. Its only imports are `GetPrivateProfileIntA/StringA` (reads `stp-origin_emu.ini`), `SetEnvironmentVariableA`, `CreateThread`, `getaddrinfo`, `sprintf_s`, `sscanf_s`, `strstr`. It is an offline activation stub, not an Origin emulator. 3. **The client never asked for an auth code.** Scanning all of live memory for LSX frames finds `GetInternetConnectedState`, `GetGameInfo`, `GetSetting`, `GetProfile`, `IsProgressiveInstallationAvailable` requests/responses — but **zero `GetAuthCode` requests**. It stops at the online check. --- ## 1. The error string → exact trigger The message is a **localization entry**, not a code literal (all copies live in heap/loc data, none in the module), so it is reached by loc key: | Item | VA | Value | |---|---|---| | loc key | `0x143b16360` | `TXT_ORIGIN_OFFLINE_POPUP_TEXT` | | popup spec | `0x143b16380` | `ORIGIN_OFFLINE_POPUP\|%s\|Ok\|Ok` | Both have **exactly one xref each**, inside one function: ``` 0x147c3c050 <- function entry (the FeFlow action "onlineLoginPopupShow" handler) 0x147c3c09a call 0x146f38aa0 ; -> returns g_originOnline 0x147c3c09f test al,al 0x147c3c0a1 jne 0x147c3c1b3 ; ONLINE -> skip, fall through to normal login popups ... ; OFFLINE -> build the Origin popup: 0x147c3c108 lea r8, [0x143b16360] ; "TXT_ORIGIN_OFFLINE_POPUP_TEXT" 0x147c3c121 lea rdx,[0x143b16380] ; "ORIGIN_OFFLINE_POPUP|%s|Ok|Ok" 0x147c3c147 lea r8, [0x1438feea8] ; "ShowPopup" 0x147c3c14e lea rdx,[0x1438fc240] ; "_global" ``` The predicate is a bare global read: ``` 0x146f38aa0 call 0x147199590 ; singleton getter: mov rax,[0x144b86bf0]; ret (no refresh) 0x146f38aa9 movzx eax, BYTE PTR [0x1443337f8] ; <<< g_originOnline 0x146f38ab4 ret ``` `g_originOnline @ 0x1443337f8` has exactly **one writer** — the LSX `GetInternetConnectedState` callback: ``` 0x146f1e6b0 sub rsp,0x58 0x146f1e6b4 test r8,r8 ; je out ; r8 = result struct 0x146f1e6d0 movzx eax, BYTE PTR [r8] ; the parsed `connected` attribute 0x146f1e6d9 mov BYTE PTR [0x1443337f8], al ; <<< store 0x146f1e6e8 lea rdx,[0x1438fe758] ; "FE::FIFA::OriginOnlineEvent" 0x146f1e737 call [r10+0x48] ; broadcast the event ``` **Chain:** LSX `GetInternetConnectedState` → callback `0x146f1e6b0` → `g_originOnline` + `FE::FIFA::OriginOnlineEvent` → FeFlow events → `origin.nav` output → and, on the popup path, `onlineLoginPopupShow` → `ORIGIN_OFFLINE_POPUP` / `TXT_ORIGIN_OFFLINE_POPUP_TEXT`. ### FeFlow IDs (from the registration table at `0x147de8480`+) | Name | VA | FeFlow ID | |---|---|---| | `onlineLoginPopupHide` | `0x143b4c918` | `0x27a9` | | `onlineLoginPopupShow` | `0x143b4c900` | `0x27aa` | | `checkOriginConnected` | `0x143b4cb40` | `0x27e1` | | `OriginIsOnline` | `0x143b4cb58` | `0x27e2` | | `OriginIsOffline` | `0x143b4cb68` | `0x27e3` | ### Caveat worth knowing (honest reading of the live state) `g_originOnline` currently reads **`0x01`**, not 0. The stub answered the *first* `GetInternetConnectedState` (id 17) with a well-formed `connected="0"`, but answered the **later ones (ids 19–22) with a generic `ErrorSuccess Code="0"`** — a *type-mismatched* reply to a `GetInternetConnectedState` request (observed verbatim at `0x28793008`/`0x28793668`/ `0x28793728`…). The client's Origin SDK finds no `connected` attribute to parse, so the byte it stores is stale/garbage. Net effect: **the online flag is non-deterministic garbage, never a genuine "online".** This is consistent with the flow still failing while the byte happens to read 1, and it means fixing the LSX layer must make *every* `GetInternetConnectedState` return a well-formed `connected="1"`, not just the first. --- ## 2. Order of operations the client actually performs **Observed LSX order (boot, on 127.0.0.1:4216, in-process):** `IsProgressiveInstallationAvailable` (id 9) → `GetSetting` (→"production") → `GetGameInfo` (`GameInfoId="FREETRIAL"`, id 15) → `GetGameInfo` (id 16) → **`GetInternetConnectedState` (id 17) → `connected="0"`** → `GetProfile` (→ `Persona="CAGE"`) → repeated `GetInternetConnectedState` polls (ids 19–22) → all answered `ErrorSuccess`. **`GetAuthCode` is never reached.** **FeFlow order:** `mainMenu` → `launchFUTFlow` (`/online/origin.nav`, action `checkOriginConnected` 0x27e1) → **[GATE]** `OriginIsOnline` 0x27e2 → output `OriginIsOnlineTrue` → `futBlazeLogin` (`/online/onlineLoginFlow.nav`, entry state `startLoginWithoutMultiplayerCheck`, which fires `sendScreenEvent ["OnlineLogin","0"]`) → on success event `loginSuccess` → `TrialWelcomeCheck` → `CheckFUTRosters` → `advance` → `postFUTBlazeLogin` → `futFlow` (`/fut/futFlow.nav`). Failure branches inside `onlineLoginFlow.nav`: `evt_onlineLoginFailurePopup` / `evt_onlineBootLoginFailurePopup` → `onlineFailureLoginPopup` → `processLoginFailure` → `loginFail` → `mainMenu`. Every one of those popups renders via the C++ action `onlineLoginPopupShow` — i.e. the same function that prints the Origin-offline text. **Observed Blaze order (our session log):** `Util::preAuth` (9/0x07) → `Util::ping` (9/0x02) → `Util::fetchClientConfig` (9/0x01) ×6 for `OSDK_CORE`, `OSDK_CLIENT`, `OSDK_NUCLEUS`, `OSDK_WEBOFFER`, `OSDK_ABUSE_REPORTING`, `OSDK_XMS_ABUSE_REPORTING` → `Authentication` (1/0x46, empty payload) → reconnect loop. **`Authentication::login` is never sent** — consistent with the Origin gate blocking upstream. --- ## 3. What makes it stop looping — the notification it waits on Recovered the **UserSessions notification name table** (jump table `0x141b03f70`, switch at `0x146de19b1`, ids 1-based): | ID | Notification | |---|---| | `0x01` | `UserSessionExtendedDataUpdate` | | `0x02` | `UserAdded` | | `0x03` | `UserRemoved` | | `0x05` | `UserUpdated` | | **`0x08`** | **`UserAuthenticated`** | | `0x09` | `UserUnauthenticated` | | `0x0c` | `ServerDraining` | **`UserAuthenticated` (notification `0x08`) is the signal that flips the session to authenticated.** Expect to also need `UserAdded` (0x02) and `UserSessionExtendedDataUpdate` (0x01) so the local user object is populated. ### Util component (0x0009) — full command table, recovered Jump table `0x141b17af4`, switch `0x146df6dd5`. This *validates the whole technique*: it matches our observed traffic exactly (preAuth=0x07, ping=0x02, fetchClientConfig=0x01). | ID | Command | | ID | Command | |---|---|---|---|---| | `0x01` | `fetchClientConfig` | | `0x0f` | `userSettingsLoadMultiple` | | `0x02` | `ping` | | `0x14` | `filterForProfanity` | | `0x03` | `setClientData` | | `0x15` | `fetchQosConfig` | | `0x04` | `localizeStrings` | | `0x16` | `setClientMetrics` | | `0x05` | `getTelemetryServer` | | `0x17` | `setConnectionState` | | `0x06` | `getTickerServer` | | `0x19` | `getUserOptions` | | `0x07` | `preAuth` | | `0x1a` | `setUserOptions` | | **`0x08`** | **`postAuth`** | | `0x1b` | `suspendUserPing` | | `0x0a` | `userSettingsLoad` | | **`0x1c`** | **`setClientState`** | | `0x0b` | `userSettingsSave` | | `0x0e` | `deleteUserSettings` | | `0x0c` | `userSettingsLoadAll` | | | | ### Unresolved: Authentication (0x0001) command `0x46` The Authentication component's `getCommandName` name table is **not compiled into this binary** (I scanned every MSVC jump-table switch in the module, both the dword-index and byte-index forms — 13 tables total; Util, UserSessions, Stats, Messaging, AssociationLists, Tournaments, GameReporting, OSDK are present, Authentication is not). So `0x46` cannot be named by table lookup. What *is* known: the client sends it with an **empty payload**, immediately after the six `fetchClientConfig` calls and **before** any login, and accepts our empty reply without erroring. Available Authentication TDF types are catalogued in `scratchpad/all_types.txt` (`Blaze::Authentication::*`, 290 entries incl. `ExpressLoginRequest`, `GetAuthTokenResponse`, `Entitlements`, `AcceptLegalDocsRequest`, `CheckLegalDocRequest`). To resolve it properly, use the descriptor-reflection method from `fifa17-recon/tools/preauth_schema_reflection.md` against the handler that dispatches component 1 replies. --- ## 4. Concrete fix order **a4 (LSX / Origin) must be done first — it is the true first blocker.** Replace or shim the Steampunks stub's LSX server on `127.0.0.1:4216` (it is in-process, both socket ends are `FIFA17.exe`, so this means either patching `stp-origin_emu.dll`, or hooking its WS2_32 use, or supplying our own LSX responder). Required, all consistent with `stp-origin_emu.ini [Globals] PersonaId=33068179, PersonaName=CAGE, Language=en_US`: 1. `GetInternetConnectedState` → `` — **for every request id, not just the first.** (Client verb strings live at `0x14394dd40`; the `connected` attribute name at `0x14394e0a8`.) 2. `GetProfile` → `GetProfileResponse` with `PersonaId="33068179"`, `UserId="33068179"`, `Persona="CAGE"` — mismatches here trip `AUTH_ERR_INVALID_PERSONA` / `AUTH_ERR_USER_DOES_NOT_MATCH_PERSONA` / `AUTH_ERR_PERSONA_NOT_FOUND` later. 3. `GetAuthCode` → `AuthCodeResponse` (verb string at `0x14394dd30`) with a synthetic code — **the stub implements nothing here today**; this is the token the game then presents to Blaze `Authentication`. 4. `QueryEntitlements` → `QueryEntitlementsResponseT` carrying `OriginItemT` with the `ONLINE_ACCESS` tag for offer/content id `1027460` (retail exe). Missing this yields `AUTH_ERR_NO_SUCH_ENTITLEMENT` / `AUTH_ERR_ENTITLEMENT_TAG_REQUIRED`. 5. Keep `ChallengeResponse`/`ChallengeAccepted` working (the stub already handles it). Only once `origin.nav` emits `OriginIsOnlineTrue` does the client enter `futBlazeLogin`. **Then a1/a3 (Blaze), in this order:** 1. `Util::preAuth` (9/0x07) — already accepted. 2. `Util::fetchClientConfig` (9/0x01) — return real config maps for `OSDK_CORE`, `OSDK_CLIENT`, `OSDK_NUCLEUS`, `OSDK_WEBOFFER`, `OSDK_ABUSE_REPORTING`, `OSDK_XMS_ABUSE_REPORTING` (currently empty — a known hole). 3. `Authentication` (1/0x46) — currently answered empty and tolerated; revisit after naming it. 4. `Authentication::login` with the Origin auth code from step a4.3 → reply must carry the session key / persona consistent with PersonaId 33068179 / `CAGE`. 5. Push `UserSessions` notifications: `UserAdded` (0x02), `UserSessionExtendedDataUpdate` (0x01), and **`UserAuthenticated` (0x08)** — this is the one that makes the client consider itself logged in. 6. `Util::postAuth` (9/0x08), then `Util::setClientState` (9/0x1c) to enable online features. --- ## Tooling produced (scratchpad, reusable) | File | Purpose | |---|---| | `hunt.py` | ASCII+UTF-16 keyword search over all live memory | | `dumprange.py` | printable-string dump of an arbitrary VA range | | `modstrings.py` / `modstrings.txt` | full module string index (930 795 strings) — grep instead of re-scanning | | `xrefs2.py` | fast numpy rip-relative + abs-pointer xref finder | | `switchtab.py` / `switchtab2.py` | recover MSVC jump-table switches (dword-index / byte-index) → Blaze command & notification name tables | | `feflow.py` | locate + window-dump FeFlow nav JSON blobs | | `lsxscan.py` | recover resident LSX frames | | `alltabs.txt`, `origin_emu_strings.txt`, `clusters.json` | captured outputs |