Files
OpenFUT/fifa17-recon/tools/auth_statemachine.md
T
funman300 6ddd5e9d47 fifa17-recon: offline FUT squad-shell working + full card-system RE
Milestone: FIFA 17 Ultimate Team boots end-to-end on our offline backend
past every EA gate into the hub and a live Squads editor (correct 4-4-2,
5-star squad, no freezes).

Key findings this session:
- userMassInfo MUST stay {} (any content desyncs the massinfo parser
  0x180174630 -> tokenizer busy-loop freeze). Deliver the squad via
  GET /squad/0 (fetched on Squads-tab entry) instead.
- Player cards render generic because the card view-model (0x1800d7920)
  reads identity/rating/face from a resolved record at item+0x10, filled
  by a lookup (0x18011cca0) in the FUT item-definition std::map at
  CardsDb+0x160c0 -- which is EMPTY offline -> default blank record.
- Version advertising (itemDbVersion/checkServerDbVersion) is proven inert
  (JSON fields routed to the skip handler). Owned items don't auto-trigger
  a definition fetch. In-place map overwrite is dead (map stays empty).
- Definition-serving endpoints (item/resource, defid, item?idList) built +
  ready; the fetch trigger lives in the packed FIFA17.exe.

New: docs/CARD_SYSTEM.md (findings + ordered next-steps plan for real
player cards: patch-POC, dbdata extractor, drive FIFA17.exe fetch, or
live-memory store injection). Plus tools: fut_seed.py (squad ladder +
definition serving), fifadrive.sh, vgamepad.py, and the login-RE toolset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
2026-08-01 20:24:30 -07:00

275 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 1922) 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 1922) → 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 |