# Origin / LSX online-state layer — reversed, and the forced-online fix

**Date:** 2026-07-30 · **Live target:** FIFA17.exe PID 19517 (ptrace_scope=0) ·
**Emu module base:** `0x6ffffc930000` (`/mnt/games/FIFA 17/stp-origin_emu.dll`)

**Clean-room provenance:** everything below comes from (a) static/dynamic analysis of
binaries we own — `FIFA17.exe`, `stp-origin_emu.dll` as unpacked in *our own* process —
and (b) the live LSX byte traffic our own client produced. No 2021 EA/FIFA leak material
was used or consulted.

---

## 1. Verdict: which layer produces the error

**Layer (1), the Origin/LSX online-state layer — and it fires before Blaze auth matters.**

The game's own navigation script (recovered from live memory @ `0x41bc5e2e`) gates FUT on
the Origin online verdict:

```json
,{ "name":"launchFUTFlow", "type":"external", "file":"/online/origin.nav"
 , "outputs": { "OriginIsOnlineTrue":"startFutBlazeLogin", "quit":"mainMenu" } }
,{ "name":"futBlazeLogin", "type":"external", "file":"/online/onlineLoginFlow.nav"
 , "inputs": { "startFutBlazeLogin":"startLoginWithoutMultiplayerCheck" }
 , "outputs": { "loginSuccess":"CheckFUTRosters", "loginFail":"mainMenu" } }
```

`origin.nav` must emit `OriginIsOnlineTrue` before `futBlazeLogin` (our Blaze work) is ever
entered. Today it cannot, because the Origin emu answers the online probe with
`connected="0"`. The message string `"...log in to Origin in Online Mode."` lives at
`0x7b8fab9`.

Captured live LSX traffic (decrypted plaintext buffers still resident in memory):

```
req  <Request recipient="" id="22"><GetInternetConnectedState version="3"/></Request>
resp <Response id="17" sender=""><InternetConnectedState connected="0"/></Response>   <-- THE GATE
resp <Response id="19..22" sender=""><ErrorSuccess Code="0" Description=""/></Response>
```

## 2. What the Steampunks emu actually is (the decisive structural finding)

`stp-origin_emu.dll` is UPX-packed on disk (hence garbled strings); the unpacked image
lives at `0x6ffffc931000-0x6ffffc93d000` (**rwxp**, already writable).

It is **not an LSX server. It never parses a request.** It is a *blind fixed-script
replayer*: it sends 18 hard-coded responses with hard-coded ids 1..18, in a fixed order,
whatever the game asks — then loops forever emitting `ErrorSuccess`.

Reconstructed script (`lea r8,<template>` + `mov r9d,<id>` pairs), **confirmed against the
live capture** (ids 15→18 match exactly):

| id | response template |
|----|---|
| 1 | `ChallengeAccepted response="%s"` |
| 2 | `GetConfigResponse Config="false"` |
| 3 | `GetProfileResponse ... PersonaId=%llu ... Persona=%s` |
| 4 | `GetSettingResponse Setting="false"` |
| 5 | `GetGameInfoResponse GameInfo="false"` |
| 6 | `GetGameInfoResponse GameInfo="ar_SA,…,zh_TW"` |
| 7 | `GetSettingResponse Setting="production"` |
| 8 | `GetSettingResponse Setting="false"` |
| 9 | `IsProgressiveInstallationAvailableResponse Available="false"` |
| 10 | `GetProfileResponse …` |
| 11 | `GetGameInfoResponse GameInfo="ar_SA,…"` |
| 12 | `GetSettingResponse Setting="%s"` |
| 13 | `GetGameInfoResponse GameInfo="false"` |
| 14 | `ErrorSuccess Code="0"` |
| 15 | `GetSettingResponse Setting="production"` |
| 16 | `GetGameInfoResponse GameInfo="false"` |
| **17** | **`InternetConnectedState connected="0"`  ← the offline verdict** |
| 18 | `GetProfileResponse …` (PersonaId 33068179 / CAGE) |
| 19+ | `ErrorSuccess Code="0"` **forever** (loop @ `0x6ffffc932dd3`, `esi++`) |

Two consequences:

1. The offline verdict is a **hard-coded string literal**, not a computed decision. There
   is no "check" to patch — only a canned answer.
2. **The emu can never answer `GetAuthCode`, `QueryEntitlements`, or a *second*
   `GetInternetConnectedState`.** It has no `AuthCode` / `QueryEntitlementsResponse`
   template at all. Everything after step 18 is `ErrorSuccess`. That is exactly why the
   re-probe at `id=22` above got `ErrorSuccess` instead of a connected-state answer.

## 3. Socket setup — why we can preempt it

`sub_0x6ffffc932130` (the `DllInit` server thread):

```
WSAStartup(0x202)
getaddrinfo("127.0.0.1", "4216", {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP})
socket() -> bind() -> listen(0x7fffffff) -> accept()
closesocket(listen_fd)        <-- @0x6ffffc9322bb, immediately after accept()
```

* **No `SO_REUSEADDR`.** On `bind()` failure it branches to `0x6ffffc932245`:
  `freeaddrinfo → closesocket → WSACleanup → return 1`. It exits **cleanly** — no crash,
  no retry.
* It accepts **exactly one** connection then closes the listener. Confirmed live: `ss`
  shows the ESTAB pair `127.0.0.1:4216 <-> 127.0.0.1:51162` but **no LISTEN on 4216**.

So: **bind 127.0.0.1:4216 before launching the game and the stub politely stands down**,
and the game's OriginSDK connects to us. No DNAT, no hosts trick, no DLL patching needed.
(Wine's WS2_32 maps to real Linux sockets, so a normal Linux listener wins the port.)

## 4. LSX wire protocol (fully reversed)

Transport: TCP `127.0.0.1:4216`, each message a **NUL-terminated** byte string
(`send(len = strlen+1)`).

**Handshake — plaintext:**

1. server → client:
   `<LSX><Event sender="EALS"><Challenge key="<32 hex>" build="release" version="10,4,13,6637"/></Event></LSX>`
2. client → server: message carrying `response="…"` and `key="…"`
3. server → client:
   `<LSX><Response id="1" sender="EALS"><ChallengeAccepted response="H"/></Response></LSX>`
   where `H = hex(AES128_ECB_encrypt(clientKeyAscii[0:32], K_FIXED))`, 64 hex chars,
   and `K_FIXED = 000102030405060708090a0b0c0d0e0f` (emu `.rdata` @ `0x6ffffc935038`,
   read live).

**Session key derivation** (`sub_0x6ffffc931f10` tail, `0x9320bf`–`0x932101`), using
MSVCR120 `srand`/`rand` (IAT `0x6ffffc9340a0` / `0x6ffffc934100`, resolved by export name):

```
srand(7); r0 = rand()                      # r0 == 61   (verified numerically)
bx = (uint16)((H[0] << 8) + H[1])          # first two ASCII chars of H
srand(bx + r0)
key[i] = (uint8)rand()   for i in 0..15
```

**All later messages:** `hex_lower( AES128_ECB( pkcs7_pad16( xml ) ) )` + `NUL`
(encoder `sub_0x931dc0`, decoder `sub_0x931ce0`, byte-wise AES with S-box @ `0x934330`
and inverse S-box @ `0x934430`, hex format `"%02x"` @ `0x9345d0`).

Reference implementation, round-trip verified:
`/tmp/claude-1000/-home-alex-Documents-OpenFUT/b89d9ca6-265d-4444-969c-6923501c168a/scratchpad/lsx_responder.py`

```
ChallengeAccepted response = 00b9c8afef744cbc1dd1b1e8aca6a2ed5fb0f43c5e287f833ea2750983772e0f
derived session key        = 4a216b49ea0b8c8a7b9864c3d0dd07c9
roundtrip OK               = True
```

## 5. The fix

### 5a. Minimal /proc/mem patch — one byte (stopgap only)

| | |
|---|---|
| **VA** | `0x6ffffc9353f4` |
| **before** | `30` (`'0'`) |
| **after** | `31` (`'1'`) |

Context (`0x6ffffc9353b0`, verified live):
`<LSX><Response id="%d" sender=""><InternetConnectedState connected="0"/></Response></LSX>`

```bash
python3 .../scratchpad/memtool.py patch 6ffffc9353f4 31
```

Page is already `rwxp`; the format string is re-read on every use.

**Limits — read these before relying on it.** (i) It must be applied **before the emu
reaches script step 17**, i.e. right after launch; pressing **Q to re-connect will not
help**, because the emu is a linear script and is permanently parked in the `ErrorSuccess`
loop for the rest of this run. (ii) Even applied in time it only fixes the *one* scripted
occurrence — the game's later re-probe still gets `ErrorSuccess`. (iii) It does **not**
give us `GetAuthCode` or `QueryEntitlements`, so it gets us past gate 1 straight into
gate 2. Use it only as a cheap one-shot experiment to confirm `OriginIsOnlineTrue` fires.

### 5b. The real fix — replace the emu with our own LSX responder (recommended)

Because the offline verdict is a canned string in a script that also cannot answer the
auth-code or entitlement questions, **patching cannot get us to a logged-in state**. Serve
LSX ourselves:

```bash
python3 /tmp/.../scratchpad/lsx_responder.py     # bind 4216 FIRST
# then launch FIFA 17 — stub's bind() fails, it returns 1, we own the socket
```

This is request-*driven* (parses `<Request id=… ><Verb …/>`), so it survives re-probes,
arbitrary ordering, and the reconnect loop.

## 6. Exact LSX responses the forced-online path must emit

Element and attribute names below were read out of FIFA17.exe's own LSX name tables
(element table @ `0x143937900` / `0x14394dc00`, attribute pool @ `0x14394de00`).

```xml
<!-- THE GATE -->
<LSX><Response id="N" sender=""><InternetConnectedState connected="1"/></Response></LSX>

<!-- auth code handed to Blaze; element name confirmed as <AuthCode> -->
<LSX><Response id="N" sender="EbisuSDK"><AuthCode Code="<blob>" Return="<blob>"/></Response></LSX>

<!-- identity: MUST match stp-origin_emu.ini [Globals] and our Blaze side -->
<LSX><Response id="N" sender="EbisuSDK"><GetProfileResponse IsSubscriber="true"
     PersonaId="33068179" AvatarId="" Country="US" CommerceCountry="US" GeoCountry="US"
     UserId="33068179" Persona="CAGE" IsUnderAge="false" CommerceCurrency="USD"/></Response></LSX>

<!-- online entitlement -->
<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>
```

Recovered attribute pool relevant here: `connected` (InternetConnectedState),
`ClientId` (GetAuthCode request), `PersonaId · Persona · AvatarId · Country · IsUnderAge ·
IsSubscriber · GeoCountry · CommerceCountry · CommerceCurrency` (GetProfileResponse),
`ItemId · EntitlementId · ResourceId · GrantDate · OfferId · bIsOwned · Uses` (Entitlement).

**One open item:** the value attribute of `<AuthCode>` is not 100 % pinned. The pool
position between `ClientId` (GetAuthCode) and `connected` (InternetConnectedState) is
empty, which means it is suffix-shared — `Code` (the tail of the pooled string
`"AuthCode"`) is the strong candidate, with `Return` the alternative. The responder emits
**both attributes**; a name-keyed XML attribute reader takes the one it knows and ignores
the other, so this resolves itself on the next run. Confirm from the log which one the
client consumes.

## 7. Auth-code → Blaze handoff (keeping a1/a3 consistent)

Flow, from the OriginSDK symbols in FIFA17.exe
(`…\External\EA\OriginSDK\src\impl\…`) and the `Blaze::Authentication` symbol set:

1. `origin.nav` probes `GetInternetConnectedState` → needs `connected="1"` →
   emits `OriginIsOnlineTrue` → `startFutBlazeLogin`.
2. The client calls LSX `GetAuthCode` (with a `ClientId`) → our `<AuthCode Code="…"/>`.
   This is `lsx::GetAuthCodeT → lsx::AuthCodeT`, handler
   `…GetAuthCodeT,struct lsx::AuthCodeT…::HandleMessage` @ `0x1439385cf`.
3. That code is then presented to **Blaze component `0x0001` (Authentication)** — the
   nucleus path (`nucleusConnect` / `nucleusConnectTrusted` / `nucleus_id`,
   `ExpressLoginRequest`, `GetAuthTokenResponse`, `GetUserAccessTokenResponse`). This is
   the empty-payload `0x0001/0x0046` call we already saw stall in the session log.
4. Blaze then validates entitlements (`AUTH_ERR_NO_SUCH_ENTITLEMENT`,
   `AUTH_ERR_ENTITLEMENT_TAG_REQUIRED`) and persona
   (`AUTH_ERR_INVALID_PERSONA`, `AUTH_ERR_USER_DOES_NOT_MATCH_PERSONA`,
   `AUTH_ERR_PERSONA_NOT_FOUND`).

**Consistency contract for the Blaze side (a1/a3):**

| field | value | source of truth |
|---|---|---|
| persona / nucleus id | `33068179` | `stp-origin_emu.ini` + LSX `GetProfileResponse` |
| persona name | `CAGE` | same |
| entitlement tag | `ONLINE_ACCESS` | FIFA17.exe `0x1438991e8` (retail exe) |
| content / offer id | `1027460` | EA offer id for FIFA 17 |
| country / currency | `US` / `USD` | LSX `GetProfileResponse` |
| auth code | whatever our LSX `<AuthCode Code=…>` returned | must be echoed/accepted verbatim by Blaze Authentication |

Since we author *both* ends, the auth code can be any opaque token — but the Blaze
Authentication reply **must** return the same `33068179` / `CAGE`, or the client trips the
persona-mismatch errors above. Recommend a shared constants module so the LSX responder
and `blaze_responder_v2.py` cannot drift.

## 8. Suggested next run

1. Start `lsx_responder.py` (binds 4216).
2. Start the Blaze stack (`blaze_responder_v2.py`).
3. Launch FIFA 17. Watch the LSX log for `GetInternetConnectedState` → `connected="1"`,
   then for `GetAuthCode` and which attribute the client reads back.
4. Expect the nav flow to advance `OriginIsOnlineTrue → startFutBlazeLogin`, putting the
   stall back on Blaze `0x0001` — which is then a1/a3's territory, now with a real auth
   code and a consistent persona.

## Appendix — addresses

| what | VA |
|---|---|
| emu image base | `0x6ffffc930000` |
| emu unpacked code (rwxp) | `0x6ffffc931000-0x6ffffc93d000` |
| `InternetConnectedState connected="0"` template | `0x6ffffc9353b0` |
| **patch byte** (`'0'`→`'1'`) | **`0x6ffffc9353f4`** |
| fixed AES key `000102…0f` | `0x6ffffc935038` |
| encoder (pad+AES+hex) | `0x6ffffc931dc0` |
| decoder (hex+AES+unpad) | `0x6ffffc931ce0` |
| challenge parse + key derive | `0x6ffffc931f10` |
| server thread (socket setup) | `0x6ffffc932130` |
| `closesocket(listener)` after accept | `0x6ffffc9322bb` |
| bind-failure exit path | `0x6ffffc932245` |
| `ErrorSuccess` forever-loop | `0x6ffffc932dd3` |
| AES S-box / inv S-box | `0x6ffffc934330` / `0x6ffffc934430` |
| nav flow `OriginIsOnlineTrue` | `0x41bc5e2e` |
| error string "log in to Origin in Online Mode." | `0x7b8fab9` |
| LSX response element table | `0x143937900` |
| LSX request element table | `0x14394dc00` |
| LSX attribute name pool | `0x14394de00` |
| `ONLINE_ACCESS` / `TRIAL_ONLINE_ACCESS` | `0x1438991e8` |

Tools written this pass (all in `…/scratchpad/`):
`origin/lsxdump.py` (harvest all LSX messages from live memory),
`origin/emu_live.bin` + `origin/emu_text.asm` (unpacked emu image + disassembly),
`lsx_responder.py` (clean-room LSX server).
