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
14 KiB
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:
-
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.navhas exactly two exits. OnlyOriginIsOnlineTruereachesstartFutBlazeLogin; anything else isquit→mainMenu. Blaze is strictly downstream of Origin. -
The Origin stub is physically incapable of reporting online. Unpacked image of
/mnt/games/FIFA 17/stp-origin_emu.dllat0x6ffffc931000-0x6ffffc93d000contains 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 noconnected="1"variant anywhere in the module. There is likewise noAuthCodeResponse, noGetAuthTokenResponse, noQueryEntitlementsResponsetemplate. Its only imports areGetPrivateProfileIntA/StringA(readsstp-origin_emu.ini),SetEnvironmentVariableA,CreateThread,getaddrinfo,sprintf_s,sscanf_s,strstr. It is an offline activation stub, not an Origin emulator. -
The client never asked for an auth code. Scanning all of live memory for LSX frames finds
GetInternetConnectedState,GetGameInfo,GetSetting,GetProfile,IsProgressiveInstallationAvailablerequests/responses — but zeroGetAuthCoderequests. 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:
GetInternetConnectedState→<InternetConnectedState connected="1"/>— for every request id, not just the first. (Client verb strings live at0x14394dd40; theconnectedattribute name at0x14394e0a8.)GetProfile→GetProfileResponsewithPersonaId="33068179",UserId="33068179",Persona="CAGE"— mismatches here tripAUTH_ERR_INVALID_PERSONA/AUTH_ERR_USER_DOES_NOT_MATCH_PERSONA/AUTH_ERR_PERSONA_NOT_FOUNDlater.GetAuthCode→AuthCodeResponse(verb string at0x14394dd30) with a synthetic code — the stub implements nothing here today; this is the token the game then presents to BlazeAuthentication.QueryEntitlements→QueryEntitlementsResponseTcarryingOriginItemTwith theONLINE_ACCESStag for offer/content id1027460(retail exe). Missing this yieldsAUTH_ERR_NO_SUCH_ENTITLEMENT/AUTH_ERR_ENTITLEMENT_TAG_REQUIRED.- Keep
ChallengeResponse/ChallengeAcceptedworking (the stub already handles it).
Only once origin.nav emits OriginIsOnlineTrue does the client enter futBlazeLogin.
Then a1/a3 (Blaze), in this order:
Util::preAuth(9/0x07) — already accepted.Util::fetchClientConfig(9/0x01) — return real config maps forOSDK_CORE,OSDK_CLIENT,OSDK_NUCLEUS,OSDK_WEBOFFER,OSDK_ABUSE_REPORTING,OSDK_XMS_ABUSE_REPORTING(currently empty — a known hole).Authentication(1/0x46) — currently answered empty and tolerated; revisit after naming it.Authentication::loginwith the Origin auth code from step a4.3 → reply must carry the session key / persona consistent with PersonaId 33068179 /CAGE.- Push
UserSessionsnotifications:UserAdded(0x02),UserSessionExtendedDataUpdate(0x01), andUserAuthenticated(0x08) — this is the one that makes the client consider itself logged in. Util::postAuth(9/0x08), thenUtil::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 |