Files
OpenFUT/fifa17-recon/tools/acct_retrieval.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

32 KiB
Raw Blame History

FIFA 17 — "account information" retrieval path, end to end

Date: 2026-07-30 · Target: live FIFA17.exe (base 0x140000000, Wine flat PE map), plus the saved live-string dump modstrings.txt after the process exited mid-analysis.

Clean-room provenance: every fact below comes from (a) static/dynamic analysis of binaries we own (FIFA17.exe as mapped in our own process, stp-origin_emu.dll), (b) our own client's observed LSX/Blaze wire traffic (/tmp/lsx.log, /tmp/blaze_responder.log), and (c) the client's own runtime reflection/metadata. No 2021 EA/FIFA leak material was used or consulted.

Builds on auth_schema_reflection.md, auth_statemachine.md, origin_nucleus.md.


0. Verdict (short)

Account information is carried on the Blaze channel, but it is gated by the LSX/Origin channel. The Nucleus/account HTTP channel is not part of the PC login path at all.

  • LSX/Origin (127.0.0.1:4216) supplies the identity and the credential: GetProfile → the OriginSDK's default user id / persona id; QueryEntitlementsONLINE_ACCESS; GetAuthCode → the one-time Origin auth code.
  • Blaze (Fire2, :42130) supplies the account record: Authentication::login (1/0x0A) takes the auth code in LoginRequest.AUTH and returns LoginResponse.SESS (session key, blazeUserId, userId, email, persona details) — this is "account information" for the login flow. The explicit account RPCs (getAccount 1/0x1E → AccountInfo, getPersona 1/0x5A, listPersonas 1/0x64, listUserEntitlements2 1/0x1D) all live behind that login.
  • Nucleus/account HTTP (:42131) is unused by design of this code path, for two independent reasons proven below (§4). It is not a missing piece; it is a dead end for PC/Origin login.

The exact missing piece: LoginRequest.AUTH (authCode, a plain string) can only be filled by LSX GetAuthCode, and the client never issues it. No auth code → no Authentication::login → no LoginResponse → nothing that can answer "account information" → popup. Everything downstream (getAccount, listPersonas, FUT user/accountinfo) is unreachable.


1. Channel 1 — LSX / Origin on 127.0.0.1:4216

1.1 Complete OriginSDK LSX surface (from the client's own template instantiations)

Origin::LSXRequest<> / LSXEnumeration<> instantiations present in the image:

Request Response Notes
GetConfigT GetConfigResponseT
GetProfileT GetProfileResponseT sets the default user/persona — see §1.2
GetSettingT GetSettingResponseT
GetGameInfoT GetGameInfoResponseT UPTODATE gate
GetInternetConnectedStateT InternetConnectedStateT the online gate
GetAuthCodeT AuthCodeT handler 0x1439385b0, callback 0x143938660
QueryEntitlementsT QueryEntitlementsResponseTOriginItemT enumeration, 0x1439484e0
GetUserProfileByEmailorEAIDT …ResponseTOriginFriendT
IsProgressiveInstallationAvailableT, AreChunksInstalledT, QueryChunkStatusT, SetDownloaderUtilizationT, QueryOffersT, GetWalletBalanceT, CheckoutT, ConsumeEntitlementT, GetPresenceT, SetPresenceT, QueryFriendsT, GetBlockListT, SendInviteT, ShowIGOT, GrantAchievementT, ExtendTrialT, SelectStoreT not on the login path

1.2 GetProfile is what defines "who is logged in" — proven

OriginGetDefaultUser() @ 0x1470da6d0 and OriginGetDefaultPersona() @ 0x1470da680 are bare field reads:

1470da6d0  call 0x1470e2840        ; SDK-ready predicate
1470da6f4  call 0x1470e3560        ; get impl singleton
1470da6f9  mov  rax,[rax+0x3a0]    ; <<< default USER id
1470da680  ... mov rax,[rax+0x3a8] ; <<< default PERSONA id

Both fields are zeroed in the SDK constructor (0x1470deb2f / 0x1470deb36, rsi = 0) and are written in exactly one place — Origin::OriginSDK::Initialize @ ~0x1470e5a30:

1470e5ac7  call 0x147118d80        ; sync GetProfile(index=0), 15000 ms timeout (0x3a98)
1470e5acc  test eax,eax
1470e5ace  jne  ...                ; on failure, leave 0
1470e5ad0  mov  rax,[rsp+0x70]
1470e5ad5  mov  [rdi+0x3a0],rax    ; <<< userId   from GetProfileResponse
1470e5adc  mov  rax,[rsp+0x78]
1470e5ae1  mov  [rdi+0x3a8],rax    ; <<< personaId from GetProfileResponse

0x147118d80 builds the request through 0x147117fe0 with index = 0 and waits via 0x1471186f0. The service name used is "EbisuSDK" (0x143937d58), which is why our responder must answer GetProfile with sender="EbisuSDK" — it already does.

Consequence: our GetProfileResponse with UserId="33068179" PersonaId="33068179" is the mechanism that populates the SDK's identity. That part is working; the client asked GetProfile three times (ids 3, 10, 17) and we answered correctly each time.

1.3 The auth-code request path — fully mapped

FifaOnline::FirstPartyAuthTokenRetriever::DoTick @ 0x146f199c0:

  rbx = this+8 ; rbp = 2                      ; two request slots: this+0x08, this+0x10
loop:
  rsi = [rbx];  if (!rsi) goto next            ; nothing pending -> nothing happens, silently
  [rsp+0x60] = 0                               ; out: auth-code buffer
  [rsp+0x58] = 0                               ; out: auth-code length
  rax = call 0x1470da6d0                       ; OriginGetDefaultUser()   -> sdk[+0x3a0]
  r9  = &len ; r8 = &buf ; rdx = rsi+0x18      ; rdx = ClientId string from the request object
  rcx = rax                                    ; user handle
  eax = call 0x1470db3c0                       ; Origin::OriginSDK::RequestAuthCodeSync
  if (eax != 0) -> 0x146f19aab                 ; log "[%s] Origin Error(%d)"      (0x1438f5e18)
  if (buf == 0 || len == 0) -> 0x146f19a7e     ; log "[%s] Invalid authcode"      (0x1438f5e00)
  ; success: store the code at rsi+0xd8, set rsi+0xe8 = 1

Origin::OriginSDK::RequestAuthCodeSync @ 0x1470db3c0:

1470db3da  lea rdx,[0x143936158]   ; trace "OriginRequestAuthCodeSync entered"
1470db3f2  call 0x1470dbf30        ; trace
1470db3f7  call 0x1470e2840        ; <<< SDK-ready predicate
1470db3fe  je   0x1470db424        ; NOT ready -> error 0xa0010000, NO LSX TRAFFIC AT ALL
1470db400  call 0x1470e3560        ; impl
1470db41d  call 0x1470e67f0        ; impl->RequestAuthCodeSync(user, clientId, &buf, &len, 0)

Signature (recovered): OriginRequestAuthCodeSync(OriginUserT user, const char* clientId, char** outBuf, size_t* outLen, ...). clientId comes from the request object at +0x18 and is what lands in the LSX <GetAuthCode ClientId="…"/> attribute (attribute name ClientId @ 0x14394e098).

Note the wire-silent failure path: if 0x1470e2840 returns false, RequestAuthCodeSync logs and returns 0xa0010000 without ever touching the socket. That is a failure mode that looks exactly like our symptom (nothing in the LSX log). It is, however, unlikely here, because the same predicate guards OriginGetDefaultUser, OriginCheckOnline, OriginGetProfile etc., all of which demonstrably worked. Ranked below in §5.

1.4 Pushed LSX Events — the SDK does support them, including <Login>

Our responder is request-driven only and never pushes. The client's OriginSDK does register event handlers. Complete inventory of Origin::EventHandler<lsx::…T, X>::HandleMessage:

Element (name table 0x14393cfd80x14393d208) Payload type
Login @ 0x14393d0ac unsigned int
OnlineStatusEvent @ 0x14393d120 bool
ProfileEvent @ 0x14393d0b8 OriginProfileChangeT
CurrentUserPresenceEvent, PresenceVisibilityEvent, BroadcastEvent, IGOEvent, IGOUnavailable, MinimizeRequest, RestoreRequest, MultiplayerInvite, MultiplayerInvitePending, UserInvitedEvent, PurchaseEvent, ChatMessageEvent, GameMessageEvent, CoreContentUpdated, BlockListUpdated, AchievementSets, ChunkStatus, GroupEvent, GroupEnterEvent, GroupLeaveEvent, GroupInviteEvent, VoipStatusEvent various

Origin::LSXEvent<> (the handshake event path) is instantiated for ChallengeT only — that is the <Event sender="EALS"><Challenge …/></Event> we already send. Everything in the table above goes through the EventHandler/EventEnumerator dispatch, whose element-name match is at 0x1471028ee (it first compares the sender attribute — sender @ 0x143938028 — then the element name).

Relevant attribute names in the pool (0x14394de00+) that belong to this family: SessionInformation 0x14394e0d8, IsLoggedIn 0x14394e0f0, Changed 0x14394e100, userid 0x14394e108, isOnline 0x14394e180, initial 0x14394e0c8, from 0x14394e0d0.

So hypothesis (a) from the brief is structurally possible — a <Login> event exists and the client can consume it. It is not proven to be required (see §5).


2. Channel 2 — Blaze (Fire2 on :42130) — this is where account info actually lives

Schemas already reflected out in auth_schema_reflection.md; the load-bearing parts:

Authentication::LoginRequest   (0x14487ca10, 3)   AUTH authCode:string   EXTB externalBlob:blob
                                                  EXTI externalId:uint64
Authentication::LoginResponse  (0x14487d170, 5)   ANON NTOS SESS SPAM UNDR
  SESS = UserLoginInfo (0x14487cb00, 8)           KEY_ sessionKey  BUID blazeUserId  UID_ userId
                                                  MAIL email  PDTL personaDetails  LLOG FRST 1CON
Authentication::AccountInfo    (0x14487c810, 16)  <- reply of getAccount (1/0x1E), request is EMPTY
  AMU anonymousUser:bool   ASRC authenticationSource:string   CO country:string   DOB dOB:string
  DTCR dateCreated:string  GOPT globalOptin:int8              LATH lastAuth:string
  LN language:string       MAIL email:string      PML parentalEmail:string   RC reasonCode:enum
  STAS status:enum         STAT emailStatus:enum  TPOT thirdPartyOptin:int8
  UDU underageUser:bool    UID userId:int64
Authentication::Entitlements   (0x14487d4e0, 1)   NLST list<Entitlement>   <- listUserEntitlements2 (1/0x1D)
Authentication::Entitlement    (0x14487d490, 16)  TAG entitlementTag  PRID productId  STAT status
                                                  PID personaId  GDAY grantDate  UCNT useCount …

Command ids (recovered by calling the client's own getCommandName, cross-checked against the static REST binding at 0x143896a80trustedLogin = 0x0B): login 0x0A · trustedLogin 0x0B · listUserEntitlements2 0x1D · getAccount 0x1E · getAuthToken 0x24 · listPersonaEntitlements2 0x30 · expressLogin 0x3C · logout 0x46 · getPersona 0x5A · listPersonas 0x64 · getOriginPersona 0x104.

Observed wire behaviour (14:46 session): Util::preAuth (9/0x07) ✓ → Util::ping ✓ → 6× Util::fetchClientConfig (OSDK_CORE, OSDK_CLIENT, OSDK_NUCLEUS, OSDK_WEBOFFER, OSDK_ABUSE_REPORTING, OSDK_XMS_ABUSE_REPORTING) ✓ → Authentication::logout (1/0x46), empty payload → disconnect → 3-second transport-ping reconnect loop forever.

logout has no request and no response TDF (confirmed by absence of LogoutRequest/LogoutResponse anywhere in the client's type index). It is the OSDK LoginStateLogout state — i.e. the login state machine aborted between LoadConfig and Login.

2.1 The OSDK login state machine (recovered state list)

LoginStateMachineImpl states, from the client's GetStateName thunks at 0x14719b360+:

LoginStateShowMaintenance  LoginStateIsp            LoginStateLoadIspAccountInfo
LoginStateConnect          LoginStateLoadConfig     LoginStateVersionCheck
LoginStateLogin            LoginStatePCLogin        LoginStateVerifyAccount
LoginStateUpgradeAccount   LoginStateLoginComplete  LoginStateUnsuspend
LoginStateCheckUser        LoginStateRecheckUser    LoginStateWebOffer
LoginStateLogout

Mapped to observed traffic: Connect = preAuth; LoadConfig = the 6 fetchClientConfig calls; then it should proceed VersionCheck → PCLogin → Login → VerifyAccount → LoadIspAccountInfo → LoginComplete. It went to Logout instead.

Associated OSDK events (0x143984000+): EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE / …_SUCCESS, EVENT_LOGIN_FAILURE, EVENT_LOGIN_ABORTED, EVENT_LOGIN_QUEUED, EVENT_LOGIN_TOS_NOT_ACCEPTED, EVENT_LOGIN_TOLLBOOTH, … Associated operation names (0x1439623c0+): FetchAccountInfo, UpdateAccountInfo, LookupOriginPersona, FetchOriginPersona, CreateOriginPersona.

LoginStateVersionCheck reads three keys out of the Blaze client config — SV_ENABLE_SERVER_VERSIONING 0x14395d148, SV_CLIENT_CHANGELIST 0x14395d168, SV_SERVER_VERSION 0x14395d180 — and on mismatch emits "Client/server version mismatch! Client is at version (%08d). Server is at version (%08d).%s" (0x14395d1d0). We currently return none of these keys. Absent ⇒ almost certainly treated as disabled, but see §6 for the cheap belt-and-braces fix.


3. Channel 3 — the FUT web API (UTAS) — real, but strictly post-login

This is a fourth surface the brief did not list, and it is the one that literally serves a thing called "account info". It is not the current blocker (it is unreachable without a Blaze session), but it will be the next wall, so it is documented here.

0x1438dbe88  FUT_RS4_BASE_URL          <- config key; NO hardcoded host anywhere in the image
0x1438dbe58  ut/game/%s/               <- path prefix, %s = sku ("fifa17", 0x1438dac20)
0x1438db6c0  user/accountinfo          <- THE FUT account-info endpoint
0x1438db810  users/club?sku=           <- POST body {"idList":[%I64d,…]}
0x1438db8f8  utStats?sku=
0x1438dbe48  FutServerCall / FutServerCall::RequestBuffer
0x1438dc378  FutGetUserAccountInfoServerCallConfig
0x1438dc3a0  FutGetUtStatsServerCallConfig
0x1438dc3c0  FutGetUsersClubInfoServerCallConfig

Request headers for user/accountinfo (contiguous at 0x1438db6e00x1438db731):

Accept: application/json
Content-Type: application/json
Accept-Encoding: gzip
Easw-Session-Data-Nucleus-Id: %lld          <- the nucleus/user id, i.e. 33068179

Response JSON keys the client parses (contiguous at 0x1438db7600x1438db8b0): userAccountInfo · personas · userClubList · clubName · established · assetId · returningUser · userPersonaInfos · divisionOnline.

A separate OSDK "SportsWorld"/EASFC HTTP module uses its own header set (0x14396f8b0+): EASW-Version: 2.0.5.0, EASW-Token:, EASW-Session:, EASW-Nucleus-Persona:, EASW-Userid:, EASW-Request-Signature:, EASW-Content-Signature:.

Because FUT_RS4_BASE_URL has no hardcoded default, we can point the entire FUT web API at our own HTTP server purely by serving that config key — no DNS/DNAT needed. Worth banking now.


4. Why the nucleusConnect stub on 127.0.0.1:42131 is unused — answered

Two independent reasons, both proven.

4.1 nucleusConnect / nucleusConnectTrusted are server-supplied config keys, and we never send them

0x14389fef8 = "nucleusConnect" has exactly one reader, 0x147237862:

147237830  sub rsp,0x28
147237834  call 0x1471995b0            ; get service locator
14723783f  call [rdx+0x188]            ; -> Blaze connection/config object
147237845  mov rcx,[rax+0x750]
147237855  test rcx,rcx ; je -> return 0
147237862  lea rdx,[0x14389fef8]       ; "nucleusConnect"
147237869  call [rax+0x48]             ; getConfigString(key, &out)
14723786c  mov rax,[rsp+0x38]          ; return the string

[vtbl+0x48] is the Blaze config-string getter. The value therefore comes from the server — the CONF map in our PreAuthResponse, or a fetchClientConfig section. Our PreAuthResponse CONF map contains only timing values (pingPeriod etc.) and our six fetchClientConfig replies are fabricated key sets that contain no nucleusConnect*. The client therefore gets an empty string and never dials anything. Zero requests on :42131 is the expected, correct outcome of what we are currently serving.

4.2 The Nucleus path that does exist is S2S/client-cert-only, and belongs to trustedLogin

nucleusConnectTrusted (0x14389fdf8) is read at 0x146e1658b and immediately feeds a URL builder:

146e1658b  lea rdx,[0x14389fdf8]       ; "nucleusConnectTrusted"
146e16595  call [rax+0x48]             ; getConfigString
14e1659d   lea r8,[0x14389fe10]        ; "%s/connect/token"
146e165ae  call 0x146dc0950            ; snprintf(buf, 0x400, "%s/connect/token", url)
146e165db  movups xmm0,[0x14389fe28]   ; "grant_type=client_credentials"

Surrounding literals, contiguous, all in the BlazeSDK LoginStateMachine block:

0x14389fd90  Content-Type: application/x-www-form-urlencoded
0x14389fdc1  enable-client-cert-auth: true
0x14389fde0  LoginStateMachine
0x14389fdf8  nucleusConnectTrusted
0x14389fe10  %s/connect/token
0x14389fe28  grant_type=client_credentials
0x14389fe50  NEXUS_S2S 
0x14389fe60  "access_token" : "
0x14389fef8  nucleusConnect

So the shape, if it were ever used, is:

POST {nucleusConnectTrusted}/connect/token
Content-Type: application/x-www-form-urlencoded
enable-client-cert-auth: true
Authorization: NEXUS_S2S <credential>

grant_type=client_credentials
→ 200 {"access_token" : "…"}

…and the token then goes into TrustedLoginRequest {ID_ id, ITYP idType, TOKN accessToken} = Authentication::trustedLogin (1/0x0B), whose REST binding at 0x143896a80 confirms the shape (GET, headers Authorization: accessToken, X-Forwarded-UserType: idType, X-Forwarded-UserId: id).

This is the console/dedicated-server trusted path. It requires a client certificate (enable-client-cert-auth: true) and is not what the PC/Origin build uses. The PC build uses login (1/0x0A) with AUTH = <Origin auth code>.

4.3 The hardcoded EA account hosts are for the web UI, not for login

0x143b8b528  https://accounts.int.ea.com/     0x143b8b588  https://accounts.ea.com/
0x143b8b548  https://gateway.int.ea.com/      0x143b8b5a8  https://gateway.ea.com/
0x143b8b568  https://signin.int.ea.com/       0x143b8b5c0  https://signin.ea.com/
0x143b8b700  Nucleus::gNucleusLocale          0x143b8b718  Nucleus::gNucleusBaseUrl
0x143b8b738  Nucleus::gNucleusBaseProxyUrl    0x143b8b758  Nucleus::gNucleusBasePortalUrl
0x143b8b778  Nucleus::gNucleusClientSideRedirectUri

These back the EAWebKit account-management pages, reached through the OSDK WebOffer config keys NUCLEUS_CREATE_URL / NUCLEUS_ADDED_URL / NUCLEUS_INCOMPLETE_URL / NUCLEUS_CREATE_INFO_URL / NUCLEUS_DUPACCT_INFO_URL / NUCLEUS_DEACTIVATED_INFO_URL (0x14395eb600x14395ec18, sitting in the middle of the WEB_OFFER_URL / NEWS_URL / FAQ_URL / TOSA_URL block) — i.e. the OSDK_NUCLEUS fetchClientConfig section is a set of account-web-page URLs, not login plumbing. That the game does not dial accounts.ea.com is therefore correct behaviour, not a symptom.

Bottom line on channel 3: do not build a Nucleus HTTP server. If you want the :42131 stub to ever receive traffic you would have to (a) advertise nucleusConnectTrusted in the Blaze config and (b) satisfy enable-client-cert-auth over TLS — and that would put you on the trustedLogin path, which is not the path this build's login state machine takes. Serve the auth code instead.


5. The dependency chain, and where it actually breaks

 LSX GetInternetConnectedState connected="1"        ✅ done
   -> g_originOnline @0x1443337f8 (writer 0x146f1e6b0) + FE::FIFA::OriginOnlineEvent
   -> origin.nav emits OriginIsOnlineTrue -> startFutBlazeLogin
 LSX GetGameInfo UPTODATE="true"                    ✅ done
 LSX GetConfig / GetProfile                         ✅ done -> sdk[+0x3a0]=UserId, [+0x3a8]=PersonaId
 --------------------------------------------------------------------------------
 OSDK LoginStateConnect   -> Blaze Util::preAuth                       ✅ answered
 OSDK LoginStateLoadConfig-> Blaze Util::fetchClientConfig × 6         ✅ answered (fabricated data)
 OSDK LoginStateVersionCheck -> SV_* keys from config                  ⚠️ keys absent
 OSDK LoginStatePCLogin   -> FirstPartyAuthTokenRetriever::DoTick
                             -> OriginGetDefaultUser()
                             -> LSX GetAuthCode(ClientId)              ❌ NEVER SENT   <<< BREAK
                          -> Blaze Authentication::login AUTH=<code>   ❌ never sent
 OSDK LoginStateVerifyAccount / LoadIspAccountInfo
                          -> Blaze getAccount 1/0x1E -> AccountInfo    ❌ unreachable
                          -> QueryEntitlements / listUserEntitlements2 ❌ unreachable
 OSDK LoginStateLoginComplete                                          ❌ unreachable
 --------------------------------------------------------------------------------
 FUT: CheckFUTRosters -> {FUT_RS4_BASE_URL}ut/game/fifa17/user/accountinfo   ❌ unreachable

Observed instead: LoadConfigLoginStateLogoutAuthentication::logout (1/0x46) → disconnect → reconnect ping loop. In the most recent run the client did not even re-reach preAuth; it sat in a bare PING/close loop every 3 s, i.e. the connection manager gave up.

Ranked hypotheses for "no GetAuthCode", with the discriminating test for each

# Hypothesis Evidence for Evidence against Discriminating probe
H1 The login state machine aborts before LoginStatePCLogin — i.e. nothing is ever enqueued into DoTick's two slots, so RequestAuthCodeSync is never called. logout arrives immediately after the last fetchClientConfig, with no intervening RPC and no LSX traffic. QueryEntitlements is also never issued — consistent with "the whole PC-login step never ran", not with "the auth-code call failed". Breakpoint 0x146f199c0 (DoTick) and 0x1470db3c0. If DoTick runs but both slots are null → H1 confirmed. Do this first.
H2 Our fabricated fetchClientConfig payloads are wrong/insufficient, so LoadConfig "succeeds" but a required key is missing and the next state fails. All six replies contain keys we invented. SV_ENABLE_SERVER_VERSIONING / SV_CLIENT_CHANGELIST / SV_SERVER_VERSION are read by LoginStateVersionCheck and we send none. OSDK_NUCLEUS should be six NUCLEUS_*_URL keys; we send four invented OSDK_NUCLEUS_* keys. Missing keys usually degrade to ""/0 in OSDK. Serve the corrected config (§6.1) and re-run; watch whether the client advances past LoadConfig. Cheap, no RE needed.
H3 The OriginSDK needs a pushed <Login> / <OnlineStatusEvent> event before it considers a user authenticated. Origin::EventHandler<lsx::LoginT, unsigned int> exists (0x14393f900), element Login @ 0x14393d0ac; attributes IsLoggedIn / SessionInformation / userid exist in the pool. Our responder never pushes anything. The Steampunks emu never pushed events either, yet the game still got as far as preAuth. OriginGetDefaultUser is fed by GetProfile, not by the Login event (§1.2) — so the SDK's notion of "who" does not depend on it. Push <LSX><Event sender="EbisuSDK"><Login userid="33068179" IsLoggedIn="1"/></Event></LSX> (encrypted, right after ChallengeAccepted) and see whether behaviour changes. Cheap; try alongside H2.
H4 RequestAuthCodeSync early-outs at the SDK-ready predicate 0x1470e2840 and returns 0xa0010000 without wire traffic. It is the only silent failure path inside the auth-code call. The same predicate guards OriginGetDefaultUser / OriginGetProfile / OriginCheckOnline, all of which demonstrably worked in this session. Breakpoint 0x1470db3f7; check al after 0x1470e2840. Only worth doing if H1's DoTick probe shows the call is being made.

H1 is by far the most likely. H2 is the cheapest thing that could plausibly cause H1.


6. What we must serve

6.1 Blaze Util::fetchClientConfig — replace the invented keys

Serve authentic key names (values can be conservative). Recovered key names, by section:

  • OSDK_CORE — include the version-check trio explicitly so nothing is ambiguous: SV_ENABLE_SERVER_VERSIONING = 0, SV_CLIENT_CHANGELIST = 0, SV_SERVER_VERSION = 0. Keep OSDK_PRESENCE_DELAY = 5, OSDK_PRESENCE_POLL = 60 (real key names, 0x143962848/0x143962860).
  • OSDK_NUCLEUS — the real keys are web-page URLs, not the OSDK_NUCLEUS_* we invented: NUCLEUS_CREATE_URL, NUCLEUS_ADDED_URL, NUCLEUS_INCOMPLETE_URL, NUCLEUS_CREATE_INFO_URL, NUCLEUS_DUPACCT_INFO_URL, NUCLEUS_DEACTIVATED_INFO_URL (0x14395eb600x14395ec18). Empty strings are fine and honest; invented OSDK_NUCLEUS_ENABLED=1 is not.
  • OSDK_WEBOFFER — real keys: WEB_OFFER_URL, NEWS_URL, NEWS_TIME_STAMP_URL, FAQ_URL, TOSA_URL, TOSAC_URL, MENU_ESPN_URL, MENU_WEBGM0_URLMENU_WEBGM2_URL.
  • OSDK_ABUSE_REPORTING / OSDK_XMS_ABUSE_REPORTINGOSDK_ABUSE_REPORTING_ENABLED = 0, OSDK_ABUSE_NUM_TYPES = 0 (already correct).
  • OSDK_CLIENT — the OSDK_CLUBS_* limits we already send are real key names.
  • Consider adding FUT_RS4_BASE_URL = http://127.0.0.1:<port>/ — see §6.4.

6.2 LSX GetAuthCode — be ready the instant it is asked

<LSX><Response id="N" sender="EbisuSDK">
  <AuthCode Code="QUXbLm3…opaque…" Return="QUXbLm3…opaque…"/>
</Response></LSX>

Emit both Code and Return until the log shows which one the client consumes (the attribute pool position between ClientId and connected is suffix-shared, so Code is the strong candidate). The code is opaque — we author both ends — but it must be echoed verbatim into LoginRequest.AUTH and accepted there. Log the incoming ClientId value; that tells us which EA client id FIFA 17 presents and is worth recording.

6.3 LSX QueryEntitlements — pre-stage the answer

<LSX><Response id="N" sender="EbisuSDK"><QueryEntitlementsResponse>
  <OriginItem ItemId="ONLINE_ACCESS" EntitlementId="1" ResourceId="1027460" OfferId="1027460"
              GrantDate="2016-09-01T00:00:00Z" bIsOwned="true" Uses="0"/>
</QueryEntitlementsResponse></Response></LSX>

6.4 Blaze Authentication — the account-info replies to have ready

Cmd Reply Must contain
login 0x0A LoginResponse ANON=0 NTOS=0 UNDR=0 SPAM=1; SESS.KEY_ non-empty; SESS.BUID = SESS.UID_ = 33068179; SESS.PDTL.PID_ = 33068179; SESS.PDTL.DSNM = "CAGE"; SESS.PDTL.STAS = 0; SESS.PDTL.PLAT = the PLAT from PreAuthResponse
getAccount 0x1E AccountInfo (empty request) UID = 33068179, CO = "US", LN = "en_US", MAIL = any well-formed address, STAS = active, STAT = verified, UDU = false, AMU = false, ASRC = "cem_ea_id" (matches the NASP we already return in PreAuthResponse), DTCR / LATH ISO-8601
listPersonas 0x64 ListPersonasResponse one persona: id 33068179, name CAGE, status active
getPersona 0x5A GetPersonaResponse same single persona
listUserEntitlements2 0x1D Entitlements one Entitlement: TAG = "ONLINE_ACCESS", PRID tied to offer 1027460, PID = 33068179, STAT active
logout 0x46 empty REPLY already correct — but treat its arrival as the failure signal it is

Also serve UserSessions notification UserAuthenticated (0x08) after login (notification ids already decoded: 0x141b03f70).

6.5 FUT web ({FUT_RS4_BASE_URL}ut/game/fifa17/user/accountinfo) — the next wall, pre-built

GET  {FUT_RS4_BASE_URL}ut/game/fifa17/user/accountinfo
     Accept: application/json
     Content-Type: application/json
     Accept-Encoding: gzip
     Easw-Session-Data-Nucleus-Id: 33068179

200 {"userAccountInfo": {
       "personas": [{
         "personaId": 33068179, "personaName": "CAGE",
         "returningUser": 0,
         "userClubList": [{"clubName":"…","established":<epoch>,"assetId":<id>}],
         "userPersonaInfos": [], "divisionOnline": 0
       }]
    }}

Because FUT_RS4_BASE_URL has no hardcoded default, serving it as a config key is enough to point the whole FUT web API at us — plain HTTP on 127.0.0.1 is fine, no TLS or DNAT needed.


7. Immediate next actions (ordered)

  1. Probe H1. Relaunch, breakpoint 0x146f199c0 (FirstPartyAuthTokenRetriever::DoTick) and 0x1470db3c0 (RequestAuthCodeSync). Whether DoTick runs at all, and whether its two slots are null, decides H1 vs H4 in one shot.
  2. Ship the corrected fetchClientConfig (§6.1) — real key names, explicit SV_* trio. Cheapest possible fix for H2 and it removes a class of ambiguity permanently.
  3. Add GetAuthCode + QueryEntitlements responses (§6.2/6.3) to lsx_responder.py so the moment the client asks, it is answered — and log the ClientId it presents.
  4. Try the pushed <Login> event (§H3) — one extra encrypted frame after ChallengeAccepted. Low cost, and it is the only untested structural difference between us and a real Origin client.
  5. Keep the Nucleus HTTP stub on :42131 parked. It is on the trustedLogin/client-cert path and is not what this build uses.

Appendix — addresses

What VA
FifaOnline::FirstPartyAuthTokenRetriever::DoTick 0x146f199c0
Origin::OriginSDK::RequestAuthCodeSync 0x1470db3c0
… its SDK-ready predicate (wire-silent bail) call 0x1470e2840 @ 0x1470db3f7
… its impl call call 0x1470e67f0 @ 0x1470db41d
OriginGetDefaultUser()sdk[+0x3a0] 0x1470da6d0
OriginGetDefaultPersona()sdk[+0x3a8] 0x1470da680
Origin::OriginSDK::Initialize — writes +0x3a0/+0x3a8 from GetProfile 0x1470e5ad5 / 0x1470e5ae1
sync GetProfile(index=0) helper (timeout 0x3a98 = 15 s) 0x147118d80
OriginSDK ctor zeroing +0x3a0/+0x3a8 0x1470deb2f / 0x1470deb36
"[%s] Invalid authcode" / "[%s] Origin Error(%d)" 0x1438f5e00 / 0x1438f5e18
"OriginRequestAuthCodeSync entered" 0x143936158
nucleusConnect (config key) / its only reader 0x14389fef8 / 0x147237862
nucleusConnectTrusted / its reader 0x14389fdf8 / 0x146e1658b
%s/connect/token · grant_type=client_credentials · enable-client-cert-auth: true · NEXUS_S2S 0x14389fe10 · 0x14389fe28 · 0x14389fdc1 · 0x14389fe50
trustedLogin REST binding (GET, Authorization/X-Forwarded-*) 0x143896a80
LSX event element-name table (incl. Login @ 0x14393d0ac) 0x14393cfd80x14393d208
Origin::EventHandler<lsx::LoginT,unsigned int>::HandleMessage 0x14393f900
Origin::EventHandler<lsx::OnlineStatusEventT,bool>::HandleMessage 0x143940050
LSX event dispatch / element-name compare 0x1471028ee
LSX request element table / attribute pool 0x14394dc00 / 0x14394de00
"EbisuSDK" / "EALS" service names 0x143937d58 / 0x143937c60
OSDK login-state name thunks 0x14719b360+
SV_ENABLE_SERVER_VERSIONING / SV_CLIENT_CHANGELIST / SV_SERVER_VERSION 0x14395d148 / 0x14395d168 / 0x14395d180
version-mismatch message 0x14395d1d0
OSDK config section names (OSDK_COREOSDK_TICKER) 0x143962be80x143962c40
OSDK operations FetchAccountInfo / UpdateAccountInfo 0x1439623c0 / 0x1439623d8
EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE / _SUCCESS 0x1439840b8 / 0x1439840e0
NUCLEUS_*_URL config keys (OSDK_NUCLEUS section) 0x14395eb600x14395ec18
FUT FUT_RS4_BASE_URL / ut/game/%s/ / user/accountinfo 0x1438dbe88 / 0x1438dbe58 / 0x1438db6c0
FUT Easw-Session-Data-Nucleus-Id: %lld 0x1438db731
FUT response JSON keys (userAccountInfodivisionOnline) 0x1438db7600x1438db8b0
EASW/SportsWorld header block 0x14396f8b00x14396fa50
hardcoded accounts/gateway/signin.ea.com (web UI only) 0x143b8b5280x143b8b5c0
Nucleus::gNucleusBaseUrl & friends 0x143b8b7000x143b8b778
Blaze AccountInfo / LoginRequest / LoginResponse descriptors 0x14487c810 / 0x14487ca10 / 0x14487d170

Tools written this pass (all in …/scratchpad/): acct_probe1.py (CommandInfo neighbourhood dump), strdump.py (string run at a VA), wsearch.py (ASCII+UTF-16 phrase search), navdump.py (nav-flow JSON extractor). Reused: memtool.py, strsearch.py, xref.py, dis.sh, cmdinfo.py.