# 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 `` 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 ``
Our responder is request-driven only and never pushes. The client's OriginSDK *does* register
event handlers. Complete inventory of `Origin::EventHandler::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 `` 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 `` 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 <- 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
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 = `.
### 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= ❌ 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* `` / `` event before it considers a user authenticated. | `Origin::EventHandler` 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 `` (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:/`** — see §6.4.
### 6.2 LSX `GetAuthCode` — be ready the instant it is asked
```xml
```
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
```
### 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":,"assetId":}],
"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 `` 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::HandleMessage` | `0x14393f900` |
| `Origin::EventHandler::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`.