# 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  <LSX><Response id="%d" sender=""><InternetConnectedState connected="0"/></Response></LSX>
   0x6ffffc9351d0  <LSX><Response id="%d" sender=""><ErrorSuccess Code="0" Description=""/></Response></LSX>
   0x6ffffc9352b0  <LSX><Response id="%d" sender="EbisuSDK"><GetProfileResponse IsSubscriber="true"
                     PersonaId="%llu" AvatarId="" Country="US" CommerceCountry="US" GeoCountry="US"
                     UserId="%llu" Persona="%s" IsUnderAge="false" CommerceCurrency="USD"/></Response></LSX>
   0x6ffffc935470  ...GetConfigResponse Config="false"...
   0x6ffffc935050/0x935170/0x935530  ...GetSettingResponse Setting="%s" / "false" / "production"...
   0x6ffffc9350b0/0x9354d0  ...GetGameInfoResponse GameInfo="<locales>" / "false"...
   0x6ffffc935230  ...IsProgressiveInstallationAvailableResponse ItemId="" Available="false"...
   0x6ffffc935410  <LSX><Response id="%d" sender="EALS"><ChallengeAccepted response="%s"/></Response></LSX>
   0x6ffffc935590  <LSX><Event sender="EALS"><Challenge key="2b8ee..." build="release" version="10,4,13,6637"/></Event></LSX>
   ```

   `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` → `<InternetConnectedState connected="1"/>` — **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 |
