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

14 KiB
Raw Blame History

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):

    { "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 quitmainMenu. 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 0x146f1e6b0g_originOnline + FE::FIFA::OriginOnlineEvent → FeFlow events → origin.nav output → and, on the popup path, onlineLoginPopupShowORIGIN_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: mainMenulaunchFUTFlow (/online/origin.nav, action checkOriginConnected 0x27e1) → [GATE] OriginIsOnline 0x27e2 → output OriginIsOnlineTruefutBlazeLogin (/online/onlineLoginFlow.nav, entry state startLoginWithoutMultiplayerCheck, which fires sendScreenEvent ["OnlineLogin","0"]) → on success event loginSuccessTrialWelcomeCheckCheckFUTRostersadvancepostFUTBlazeLoginfutFlow (/fut/futFlow.nav).

Failure branches inside onlineLoginFlow.nav: evt_onlineLoginFailurePopup / evt_onlineBootLoginFailurePopuponlineFailureLoginPopupprocessLoginFailureloginFailmainMenu. 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_REPORTINGAuthentication (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. GetProfileGetProfileResponse 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. GetAuthCodeAuthCodeResponse (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. QueryEntitlementsQueryEntitlementsResponseT 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