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