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

545 lines
32 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 — "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**; `QueryEntitlements`
`ONLINE_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` | `QueryEntitlementsResponseT``OriginItemT` | enumeration, `0x1439484e0` |
| `GetUserProfileByEmailorEAIDT` | `…ResponseT``OriginFriendT` | |
| `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 `0x14393cfd8``0x14393d208`) | 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 `0x143896a80``trustedLogin = 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 `0x1438db6e0``0x1438db731`):
```
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 `0x1438db760``0x1438db8b0`):
`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` (`0x14395eb60``0x14395ec18`, 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: `LoadConfig`**`LoginStateLogout`** → `Authentication::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` (`0x14395eb60``0x14395ec18`).
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_URL``MENU_WEBGM2_URL`.
* **`OSDK_ABUSE_REPORTING` / `OSDK_XMS_ABUSE_REPORTING`** — `OSDK_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
```xml
<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
```xml
<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`) | `0x14393cfd8``0x14393d208` |
| `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_CORE``OSDK_TICKER`) | `0x143962be8``0x143962c40` |
| OSDK operations `FetchAccountInfo` / `UpdateAccountInfo` | `0x1439623c0` / `0x1439623d8` |
| `EVENT_LOGIN_FETCH_ACCOUNT_INFO_FAILURE` / `_SUCCESS` | `0x1439840b8` / `0x1439840e0` |
| `NUCLEUS_*_URL` config keys (OSDK_NUCLEUS section) | `0x14395eb60``0x14395ec18` |
| 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 (`userAccountInfo``divisionOnline`) | `0x1438db760``0x1438db8b0` |
| EASW/SportsWorld header block | `0x14396f8b0``0x14396fa50` |
| hardcoded `accounts/gateway/signin.ea.com` (web UI only) | `0x143b8b528``0x143b8b5c0` |
| `Nucleus::gNucleusBaseUrl` & friends | `0x143b8b700``0x143b8b778` |
| 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`.