Files
OpenFUT/fifa17-recon/tools/login_dump/CONNECT_PLAN.md
T
funman300 edab23f04a fifa17-recon: package the working offline FUT backend
Emulates FIFA 17's full online + Ultimate Team stack against an offline,
clean-room backend (no EA servers). Proven end-to-end 2026-08-01:
Origin login -> Blaze login -> device-trust -> the FUT hub.

Package:
- tools/openfut-fut.sh   one-command orchestrator (start/stop/status/restart)
- tools/root_arm.sh      idempotent host arm (sysctls, DNAT, /etc/hosts easw)
- tools/{lsx_responder_v2,blaze_responder_v3b,roster_server,utas_server,autopatch}.py
  the 5 servers (Origin LSX :4216, Blaze :42127/42130/42131, roster :8081,
  FUT/UTAS :8099) + heat2.py (Fire2/Heat2 TDF codec)
- FUT-RUNBOOK.md         runbook + gate-ladder troubleshooting
- docs/, tools/login_dump/*.md   the reverse-engineering write-ups

All findings are clean-room, from binaries we own; nothing from any leak.
The wire protocol maps 1:1 to FIFA 23.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
2026-08-01 09:12:17 -07:00

494 lines
30 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.
# CONNECT_PLAN — Origin-SDK connect + Blaze-login trigger (clean-room synthesis)
Synthesis of four independent reverses (Origin connect writer, connect-wait guard, "service 0x1e8"
dispatcher, Blaze LoginManager), plus my own live re-verification against **pid 13643** (`FIFA17.exe`,
still up at time of writing). Every VA/byte below is from our decrypted dumps, `/proc/13643/mem`,
`captures/lsx/full_origin_init_session.log`, `/tmp/lsx.log` or `/tmp/blaze_responder.log`.
---
## 0. HEADLINE — BRIEF3's two premises were both wrong, and the real cause is one line of our responder
| BRIEF3 said | Truth (live-verified) |
|---|---|
| `0x1470dbfa0` = service/interface lookup; "service `0x1e8` returns NULL → not connected" | `0x1470dbfa0` is the **Origin SDK allocator thunk** (`jmp [0x144b7c778]`, live target `0x1470d9380` = aligned-alloc). `0x1e8` is an **allocation size** (488 = `sizeof(LSXRequest<GetAuthCodeT,…>)`), and the neighbouring `0x1e6/0x1e8/0x1ea` are `__LINE__` args to the logger. **There is no service table and nothing to register.** |
| `0xa2080000` comes from the auth path failing a service lookup | `0xa2080000` is the LSXRequest/EventHandler **"response failed to validate"** default, written at `0x1471192dd``[req+0x1e0]`. It means *we replied and the client threw our reply away.* |
| Empty Blaze `fetchClientConfig` leaves `[cfg+0x750]` NULL and blocks login | `[cfg+0x750]` is live **non-NULL** = `0x43c47ff0` (verified: `*[0x144b86bf8]=0x43c46c70`, `+0x360=0x43c47330`, `+0x750=0x43c47ff0`). It is fed by **`Util::preAuth`'s CONF map**, not by `fetchClientConfig`. No `fetchClientConfig` payload can change it. |
**The actual blocker is a one-attribute protocol mismatch in `lsx_responder_v2.py`.**
Every LSX `<Response>` is accepted only if its `sender` attribute **byte-equals the `recipient` the
client put on the matching `<Request>`**. FIFA sends `recipient=""` for everything after `GetConfig`
(because our `GetConfigResponse` is empty, so its 34-entry service-name table is all empty strings),
but we answer `GetProfile`, `GetAuthCode` and `QueryEntitlements` with `sender="EbisuSDK"`.
Those three replies — and only those three — are discarded. `GetProfile` is the *sole* writer of
`OriginSDK+0x3a0/+0x3a8`, so the whole chain (default user → auth code → OSDK login state → Blaze
`Authentication::login`) never starts.
---
## 1. THE CONNECT MECHANISM — end to end
### 1.1 The writer is a synchronous statement, not an event
`0x1470e5ad5` (the `+0x3a0` write) lives inside **`Origin::OriginSDK::Initialize`**, function start
`0x1470e5770` (prologue `push rbp/rsi/rdi/r12/r13/r14/r15; sub rsp,0xf0`, epilogue `0x1470e5caf`).
Its logger passes `# 0x143937c78` = `"Origin::OriginSDK::Initialize"` and
`# 0x143937b70` = `"E:\p4\fifafb\rl\empatch\TnT\Code\External\EA\OriginSDK\src\impl\OriginSDKimpl.cpp"`
(both read live). Sequence inside `Initialize`:
```
0x1470e5959..96 connect loop: call 0x14712ca40(conn=sdk+0x168, …, port)
on false -> Sleep(1000); inc ebx; cmp ebx,0x1e; jl => 30 x 1s (NOT 15 s)
0x1470e59fb version gate: cmp eax,0x9b00000; jb -> 0xa0020007 (passes today)
0x1470e5a2f ecx=0x1a8; call 0x1470dbfa0 <-- ALLOC 424 bytes for the GetConfig request
0x1470e5a46 lea rdx,[rip..] # 0x143937d58 = "EbisuSDK" <-- HARD-CODED recipient/sender
... SendSync(GetConfig) ...
0x1470e5a96 lea rdx,[rbx+0x80]; call 0x1470e0520 <-- consume GetConfigResponse
-> fills serviceNames[0..33]
0x1470e5aa8 call 0x14710df80(sdk) <-- register ~19 event handlers,
each keyed on serviceNames[i]
0x1470e5ab7..c7 lea r9,[rsp+0x70]; xor edx,edx; mov r8d,0x3a98(=15000ms); mov rcx,rdi
call 0x147118d80 <-- GetProfileSync(idx=0)
0x1470e5ad5 mov rax,[rdi+0x80]; mov [rbp+0x3a0],rax <-- UserId
0x1470e5ae1 mov rax,[rdi+0x88]; mov [rbp+0x3a8],rax <-- PersonaId
```
So: **the only stimulus for the `+0x3a0` write is a well-formed `<GetProfileResponse>` to the
`<GetProfile index="0">` FIFA already sends at boot (log id=3).** There is nothing else to serve, no
event to push, no "connect" verb we are missing. `+0x3a0`/`+0x3a8` are **u64 scalars** (UserId /
PersonaId), not object pointers — the deserializer `0x147136140` maps `UserId→resp+0x00`,
`PersonaId→+0x08`, `Persona→+0x10`, `AvatarId→+0x30`, `Country→+0x50`, `IsUnderAge→+0x70`,
`IsSubscriber→+0x71`, `GeoCountry→+0x78`, `CommerceCountry→+0x98`, `CommerceCurrency→+0xb8`, and the
response sub-object sits at `request+0x80`.
`0x147118d80` = `Origin::OriginSDK::GetProfileSync` (both exits converge on
`lea rax,[rip..] # 0x143945d00` = `"Origin::OriginSDK::GetProfileSync"`). It is **not** a poll loop:
it builds the request, calls the send-and-wait template `0x1471186f0`, which serializes
`<LSX><Request recipient="X" id="N"><GetProfile index="0" version="3"/></Request></LSX>`, then blocks
on a timed condvar (`0x145e318f0(&req+0x1e8,&req+0x230)`) until the reader thread sets `[req+0x228]=1`.
Success is `[req+0x1e0]==0` (`0x147118851: cmp DWORD PTR [rbx+0x1e0],0x0; sete al`).
### 1.2 The rejection — verified byte-for-byte, live
Response matcher `0x1471189b0` (disassembled from `/proc/13643/mem` this session):
```
1471189c6 call [rax+0x10] ; reader ok? -> je FAIL
1471189d7 call [rax+0x58] ; root element name
1471189dd lea rdx,[rip..] # 0x143938024 ; "LSX" (4-byte compare loop) -> jne FAIL
147118a14 call [rax+0x18] rdx=[this+0xd8] ; child "Response" -> je FAIL
147118a2a lea rdx,[rip..] # 0x14355de00 = "id" ; optional -> atoi -> [this+0x138]
147118a5d lea rdx,[rip..] # 0x143938028 = "sender"
147118a7d call [rax+0x70] ; GetAttributeValue("sender")
147118a80 test rax,rax ; je 0x147118ad9 ; ATTRIBUTE MUST BE PRESENT
147118a85 lea r8,[rdi+0xf8] ; expected sender (std::string, SSO at +0x18)
147118a99..aae inline byte-wise strcmp ; MISMATCH -> jne FAIL (0x147118ad9)
147118ab3 lea rdx,[rip..] # 0x143945ce8 = "GetProfileResponse" -> je FAIL
147118acf tail-jump to deserializer
```
On FAIL, `HandleMessage 0x147119210` does `call 0x1470e2e30` (look for an `<Error>` node), and when
there is none:
```
1471192dd mov edx,0xc347a20f
1471192e4 lea edx,[rdx-0x213fa20f] ; = 0xA2080000
1471192ef mov DWORD PTR [rbx+0x1e0],edx
147119311 call 0x145e30d20 ; signal condvar (so it fails FAST, never times out)
147119316 mov BYTE PTR [rbx+0x228],1
```
The expected string `[req+0xf8]` and the emitted `recipient` are **the same std::string**, taken from
`sdk->serviceNames[facility]` via accessor `0x1470e4870`
(`cmp edx,0x21; movsxd rax,edx; shl rax,5; add rax,[rcx+0x3b0]`), and used symmetrically by the request
ctor `0x147117fe0` (`"Request"@0x1435a43b8` and `"Response"@0x143938198` both get arg2).
Facilities per verb (from the `add rdx,0xNN` after `mov rdx,[sdk+0x3b0]`, `NN/0x20 = idx`):
| Verb | site | facility |
|---|---|---|
| `GetConfig` | `0x1470e5a46` | **literal `"EbisuSDK"`** (not table-driven) |
| `GetProfile` | `0x147118df2` (`lea edx,[r12+1]`) | 1 PROFILE |
| `GetAuthCode` | `0x1470e6882` (`add rdx,0x120`) | 9 UTILITY |
| `GetSetting` / `GetGameInfo` | `0x1470e4903` / `0x1470e3683` (no add) | 0 SDK |
| `QueryUserId` | `0x1470e62b4` | 26 GET_USERID |
| `IsProgressiveInstallationAvailable` / `SetDownloaderUtilization` | `0x147129ddb` / `0x14712b821` | 31 PROGRESSIVE_INSTALLATION *(inferred)* |
### 1.3 Live state of the table (re-verified myself, pid 13643)
```
OriginSDK = *[0x144b7c7a0] = 0x25c98c50
+0x1b8 = 0x1fc (conn+0x50 socket, ALIVE -> transport healthy)
+0x1e8 = 0xffffffffffffffff (conn+0x80, a SECOND handle, still unset - see §5)
+0x218 = 0x0 (pending-request map size)
+0x270 = 0x1c (28 requests issued == our lsx.log last id 27 + 1)
+0x3a0 = 0x0 <-- default user STILL NULL
+0x3a8 = 0x0
+0x3b0 = 0x25cb93e0 .. +0x3b8 = 0x25cb9820 -> (0x440)/0x20 = 34 entries
all 34 std::strings: size == 0 ("all-empty")
```
34 entries allocated proves `Initialize` **did** reach and accept our `GetConfigResponse`
(`0x1470e0520`: `lea rbp,[rcx+0x3b0]; lea edx,[rbx+0x22]; call 0x1470eadd0` resizes to 34) — and all
empty proves it contained **zero `<Service>` children**, because we answer
`<GetConfigResponse Config="false"/>`.
### 1.4 On the wire — the smoking gun
`captures/lsx/full_origin_init_session.log`:
```
id=1 <Request recipient="EALS" …><ChallengeResponse …> (plaintext handshake)
id=2 <Request recipient="EbisuSDK" …><GetConfig version="3"/> -> we reply sender="EbisuSDK" MATCH ✔
id=3 <Request recipient="" …><GetProfile index="0"/> -> we reply sender="EbisuSDK" MISMATCH ✘
id=4 <Request recipient="" …><GetSetting …/> -> we reply sender="" MATCH ✔
id=5 <Request recipient="" …><GetGameInfo …/> -> we reply sender="" MATCH ✔
… ids 6..14 all recipient=""
```
Independent live proof that an **empty** `sender` attribute really does validate (i.e. `vt[0x70]`
returns a pointer to `""`, not NULL): FIFA's OSDK failure classifier `0x14717d5d0` only reaches its
`cmp eax,0xa2000003` branch **after** `OriginGetGameInfoSync(id=0)` returns rc==0 **and** the buffer
strncmp-equals `"true"@0x14354be6c`; otherwise it would set `TXT_ORIGIN_GAME_VERSION_OUT_OF_DATE`.
Live `[0x43d189d8+0x80] = 0x14395ca10 = "OSDK_INVALID_USER"` and `[+0x260] = 16` — so our
`GetGameInfo(UPTODATE) → GameInfo="true"` reply, sent with `sender=""`, **was accepted**. (It also
pins `GameInfoId` enum 0 == `UPTODATE`.)
### 1.5 Events are broken by the same mechanism
Event validator `0x1471015e0` (per report 3) requires root `"LSX"@0x143938024`, child literal
`"Event"@0x14365d8fc` (len 5), optional `id`, then `call [rax+0x70]` for `"sender"@0x143938028`
`test rax,rax; je FAIL` → inline strcmp against the per-handler string `[handler+0x10]`.
I disassembled the registrar `0x14710df80` live and confirmed **that string is
`serviceNames[facility]`** — every registration is `mov edx,<idx>; call 0x1470e4870; mov rdx,rax;
call <per-event registrar>`:
```
0x14710df8a edx=0x0c IGO_EVENT -> 0x1470eb6a0
0x14710dfac edx=0x0f INVITE_EVENT -> 0x1470eb060 (0xe7ffa20f+0x18005e00 = 0x0f)
0x14710dfcd edx=0x0e LOGIN_EVENT -> 0x1470eb100 (0xc390050f+0x3c6ffaff = 0x0e) << ours
0x14710dfee edx=0x10 PROFILE_EVENT -> 0x1470eb1a0
0x14710e009 edx=0x11 PRESENCE_EVENT -> 0x1470eb240
0x14710e024 edx=0x12 FRIENDS_EVENT -> 0x1470eb2e0
0x14710e03f edx=0x13 COMMERCE_EVENT -> 0x1470eb380
0x14710e05a edx=0x15 DOWNLOAD_EVENT -> 0x1470eb420
0x14710e075 edx=0x19 BLOCKED_USER_EVENT -> 0x1470eb4c0
0x14710e090 edx=0x14 CHAT_EVENT -> 0x1470ebfc0
0x14710e0ab edx=0x1b ONLINE_STATUS_EVENT -> 0x1470eb560 << ours
0x14710e0c6 edx=0x1d ACHIEVEMENT_EVENT -> 0x1470eb600
0x14710e0e1 edx=0x0f INVITE_EVENT (2nd) -> 0x1470ebf20
… 0x1e BROADCAST_EVENT, 0x20 PROGRESSIVE_INSTALLATION_EVENT, plus repeats of 0x11/0x13
```
**This corrects report 1's open question #5: `LOGIN_EVENT` (14) and `ONLINE_STATUS_EVENT` (27)
handlers ARE registered here** — they exist and are listening. They just expect `sender == ""` right
now, while we push `sender="LOGIN_EVENT"` / `"LOGIN"` / `"ONLINE_STATUS_EVENT"`. All 3219 pushed
`<Event>` frames in `/tmp/lsx.log` have been silently dropped.
### 1.6 The connect story, one paragraph
There is no separate "connect" handshake to serve. `Initialize` connects the socket (already alive,
`[sdk+0x1b8]=0x1fc`), passes the version gate (`[sdk+0x360]="10,4,13,6637"`), does GetConfig (accepted,
table sized to 34 but left empty), registers ~19 event handlers keyed on the now-empty names, and then
calls `GetProfileSync`. **Our `GetProfileResponse` is thrown away for a `sender` mismatch**, so
`+0x3a0`/`+0x3a8` stay 0, `OriginGetDefaultUser (0x1470da6d0)` keeps returning NULL,
`OriginRequestAuthCodeSync (0x1470e67f0)` keeps failing its `test rdx,rdx` / `cmp rdx,[rcx+0x3a0]`
guards with `0xa2000003`, and FIFA's OSDK classifier turns that into `OSDK_INVALID_USER`.
---
## 2. THE BLAZE-LOGIN TRIGGER
### 2.1 `login` is compiled in and is exactly `1/0x0A` — it is simply never invoked
* Stub `0x146e15070` ends `mov r9d,0xa; movzx r8d,WORD PTR [rdi+0x10]; call 0x146df0e80` (generic
`sendRequest`). Live component id: `WORD[0x7c17170+0x10] = 0x0001` = Authentication. **Our
responder's `1/0x0A = login` mapping is correct.**
* Its only caller `0x146e12d83` sits in `0x146e12d20`, which appears at **vtable slot +0xd0** of all
four LoginState vtables (`0x14389f828/…938/…a70/…b98`).
* Live: all four `LoginManagerImpl` (vptr `0x14389f5a0`) are still in **`LoginStateInit`**
(`stateMachine+0x28 == +0x08 == stateInit`, `+0x30 stateId = 0xFFFFFFFF`). The state machine has
never been asked to advance.
* `Authentication::logout (1/0x46)` comes from `LoginStateBase::logout` = vt+0x48 (`0x146e15eb0`
RPC stub `0x146e109d0`). It is a **normal step** of FIFA's OSDK sequence
(Connect → LoadConfig → **Logout** → VersionCheck → Login) and matches our wire log exactly
(preAuth → 6× fetchClientConfig → logout, 37 occurrences in `/tmp/blaze_responder.log`).
**It is not an error signal and our empty REPLY to it is correct.**
### 2.2 The precondition FIFA checks — it is an Origin check, not a Blaze one
FIFA's own OSDK login state (`0x43d189d8`, vptr `0x14395c180`, Update `0x1471b58e0`, jump table
`0x141e7f55c`):
* case 0 → `mov rcx,[0x144b86bf0]; call [rax+0x60]` — live `vt[0x60] = 0x146f82070 = xor eax,eax; ret`,
so it always falls to `0x1471b5b42`, sets `TXT_NOT_LOGIN_TO_EBISU@0x1439633e8`, `[state+0x260]=1`,
and calls the classifier `0x14717d5d0`.
* Classifier `0x14717d5d0`: `OriginGetGameInfoSync(0)` must be rc==0 and `"true"` (it is — see §1.4),
then reads the Ebisu/Origin manager's last error via `[rsi]->vt[0x80]()` and
`cmp eax,0xa2000003; jne`. On match → `[state+0x80] = "OSDK_INVALID_USER"@0x14395ca10`,
`call 0x147190d50(state, 0xe)`.
* **Live right now: `[0x43d189d8+0x260] = 16`, `[+0x80] = "OSDK_INVALID_USER"`.**
`0xa2000003` is precisely `OriginRequestAuthCodeSync`'s "NULL/invalid user" return —
`OriginSDK+0x3a0 == 0`. The dword `0xa2000003` is resident at `0x43d320f8`, adjacent to the state heap.
### 2.3 `fetchClientConfig` is a dead end for this gate — do not touch it
* `[cfg+0x750]` is **live non-NULL** (`0x43c47ff0`), holding the Blaze ConnectionManager whose string
map at `+0x1218` (16 entries, 0x30 bytes each, begin `0x43f56450`) is **byte-for-byte our
`Util::preAuth` CONF** — `blazeSdkClientId=FIFA17PC`, `blazeServerClientId=FIFA17PC-SERVER`,
`blazeSdkClientSecret=openfut-secret`, `identityRedirectUri=http://127.0.0.1/login_successful.html`,
etc. `NUCLEUS_ADDED_URL`/`NUCLEUS_CREATE_URL` (which we only send via
`fetchClientConfig(CFID=OSDK_NUCLEUS)`) are **absent**, proving the two containers are different.
* The getter the Blaze auth fetchers use is `vt[0x48] = 0x146e1bc50`, a binary search over that same
`[this+0x1218]` map; the keys it asks for are `"blazeServerClientId"@0x143972690`,
`"blazeSdkClientId"@0x1439726a8`, `"blazeSdkClientSecret"@0x1439726c0`,
`"identityRedirectUri"@0x1439726d8` — all present.
* The class that owns `blaze_authfetch1/2` (vtable base **`0x143972480`**, not `0x143972560`) has
**zero live instances** (full-memory scan for the vptr: 0 hits, while the same scanner finds exactly
1 hit for the config vtable `0x1438a0850`). That code never runs. Even if it did, it would bail on
`call 0x1470da6d0` (GetDefaultUser) → `0x1470db3c0``0xa2000003`, storing it at `[this+0x950]`.
**Conclusion: the Blaze-login trigger is `OriginSDK+0x3a0 != 0`.** Once GetProfile validates, the
Origin default user exists, `OriginRequestAuthCodeSync` succeeds, the classifier stops writing
`OSDK_INVALID_USER`, the OSDK machine advances past sub-state 16, and LoginState vt+0xd0
(`0x146e12d20`) fires `Authentication::login (1/0x0A)` with no Blaze-side change at all.
---
## 3. CONCRETE SERVER-SIDE CHANGES (ranked, minimal)
### RANK 1 — `lsx_responder_v2.py`: echo the request's `recipient` back as the response's `sender` ★ do this first
This is the whole fix, and it is correct for both the current empty-table state **and** any future
populated table.
**Edits (line numbers as of today's file):**
1. **L418** — widen the request regex so we capture `recipient`:
```python
REQ_RE = re.compile(
r'<Request[^>]*\brecipient="([^"]*)"[^>]*\bid="(\d+)"[^>]*>\s*<([A-Za-z]+)([^>]*)/?>')
```
(`recipient` precedes `id` in every captured frame — ids 1..14 all have
`<Request recipient="…" id="N">`. If you prefer robustness, keep the old `REQ_RE` and add a
separate `RECIP_RE = re.compile(r'<Request[^>]*\brecipient="([^"]*)"')`, defaulting to `""`.)
2. **L475-481** — thread it through:
```python
recip, mid, name, rest = mm.group(1), mm.group(2), mm.group(3), mm.group(4)
attrs = dict(ATTR_RE.findall(rest))
reply = build_reply(mid, name, attrs, conn, recip)
```
3. **L312 / L316** — make it the default sender:
```python
def resp(mid, body, sender=""): # unchanged
return f'<LSX><Response id="{mid}" sender="{sender}"><{body}/></Response></LSX>'
def build_reply(mid, req_name, attrs, conn, recipient=""):
... # pass sender=recipient at every return site
```
4. **DELETE the three `sender="EbisuSDK"` overrides** — **L352** (`GetAuthCode`), **L358**
(`QueryEntitlements`), **L371** (`GetProfile`). These are the only three rejected verbs.
**L400** (`GetConfig`) may stay as-is or become `sender=recipient`; with echo it is identical,
since FIFA really does send `recipient="EbisuSDK"` there (its literal is hard-coded at
`0x1470e5a46`).
5. **Always emit the attribute, even when empty.** `0x147118a80: test rax,rax; je FAIL` rejects a
*missing* `sender`; an empty-valued one is fine (proven in §1.4). `resp()` already does this —
don't "optimise" it away.
6. **L231-232 / L235** — events: with the table empty, the handler strings are empty too, so push
`sender=""`:
```python
LOGIN_EVENT_SENDERS = ("", "LOGIN_EVENT", "LOGIN") # "" first; the others are harmless no-ops
ONLINE_EVENT_SENDERS = ("", "ONLINE_STATUS_EVENT")
```
Mismatched senders are dropped silently and cost nothing, so keeping the old candidates as a hedge
is free. Optionally add `id="N"` to `<Event>` (read at `0x14710164c-72` → `handler+0x78`, optional).
**Observables (restart FIFA; boot `GetProfile` is id=3):**
| # | Signal | How to read |
|---|---|---|
| 1 | `Origin Error(a2080000)` disappears for GetProfile/GetAuthCode | `/tmp/lsx.log`, FIFA log |
| 2 | **`OriginSDK+0x3a0` and `+0x3a8` become `0x1f89493` (33068179)** | `SDK=*[0x144b7c7a0]`, read `+0x3a0` — this is *the* pass/fail bit |
| 3 | `[0x43d189d8+0x80]` stops being `"OSDK_INVALID_USER"`, `[+0x260]` leaves 16 | live read (state VA may move across restarts — re-find via vptr `0x14395c180`) |
| 4 | FIFA issues `<GetAuthCode ClientId="FIFA17PC">` **without** our forging anything | `/tmp/lsx.log` |
| 5 | **`Authentication::login (1/0x0A)`** appears | `/tmp/blaze_responder.log` (currently: 37× `logout`, 0× `login`) |
> Note: `[SDK+0x1e8]` (= `conn+0x80`, live `-1`) is **not** "service 0x1e8" and is *not* the success
> signal BRIEF3 asked for — there is no service to become non-null. Use signal #2 instead. Whether
> `+0x1e8` flips as a side effect is a bonus observation (see §5).
---
### RANK 2 — `lsx_responder_v2.py`: make `GetConfigResponse` actually populate the service-name table (EA-faithful; do AFTER rank 1 confirms)
Not required once rank 1 is in (echo is correct for empty names), but it restores the real protocol,
gives events non-empty senders, and de-risks any code path that dislikes empty names.
Consumer chain, all verified: matcher `0x1470e304f` checks child `"GetConfigResponse"@0x143937b30` →
deserializer `0x147135610` builds the child element name as prefix + `"Service"@0x14394dd78` (len 7;
namespaced variant `":Service"@0x14394e1a8` with ns prefix `"lsx"@0x14394def0`) and loops
`0x14713af50` per element → reads `"Name"@0x1435604fc` into `entry+0x00` and `"Facility"@0x14394e198`
via `0x1471400a0`→`0x147140010`, which **linearly strcmps the value against the 34-pointer table at
`0x144341420`** and writes the matched index to `entry+0x20` (init 0). Then `0x1470e0520` assigns
`names[entry.index] = entry.name`, skipping `index > 0x21` and empty names.
Records are 40 bytes (`{std::string name; int32 index;}`), which is why `0x1470e0520` divides by 0x28.
**Facility enum strings — read live from `0x144341420` this session (case-exact, required):**
```
0 SDK 1 PROFILE 2 PRESENCE 3 FRIENDS
4 COMMERCE 5 RECENTPLAYER 6 IGO 7 MISC
8 LOGIN 9 UTILITY 10 XMPP 11 CHAT
12 IGO_EVENT 13 EALS_EVENTS 14 LOGIN_EVENT 15 INVITE_EVENT
16 PROFILE_EVENT 17 PRESENCE_EVENT 18 FRIENDS_EVENT 19 COMMERCE_EVENT
20 CHAT_EVENT 21 DOWNLOAD_EVENT 22 PERMISSION 23 RESOURCES
24 BLOCKED_USERS 25 BLOCKED_USER_EVENT 26 GET_USERID 27 ONLINE_STATUS_EVENT
28 ACHIEVEMENT 29 ACHIEVEMENT_EVENT 30 BROADCAST_EVENT 31 PROGRESSIVE_INSTALLATION
32 PROGRESSIVE_INSTALLATION_EVENT 33 CONTENT
```
Reply (keep the envelope `sender` = echo of `recipient`, i.e. `"EbisuSDK"`):
```xml
<LSX><Response id="{id}" sender="EbisuSDK"><GetConfigResponse>
<Service Name="SDK" Facility="SDK"/>
<Service Name="PROFILE" Facility="PROFILE"/>
...one line per facility, Name == the Facility enum name...
<Service Name="CONTENT" Facility="CONTENT"/>
</GetConfigResponse></Response></LSX>
```
Choosing **`Name == Facility`** is deliberate: with rank 1's echo, every response stays correct
automatically, and the pushed events' *original* senders (`"LOGIN_EVENT"`, `"ONLINE_STATUS_EVENT"`)
become correct for the first time. `Name` is free-form as far as anything we found goes (we own both
ends), but see §5.
**Ordering hazard (important):** populating the table while any verb still hard-codes
`sender="EbisuSDK"` — or while events still push `""` — breaks verbs that work today.
**Rank 1 must land first (or in the same commit).** Once echo is in place, rank 2 is a pure superset.
**Observable:** all 34 `std::string`s at `[SDK+0x3b0]` become non-empty (script in §5), and the
`<Event sender="LOGIN_EVENT">` push stops being dropped.
---
### RANK 3 — `blaze_responder_v3b.py`: change nothing that matters now; pre-stage the post-login RPCs
* **DO NOT** enlarge `fetchClientConfig` hoping to fill `[cfg+0x750]` — it is already `0x43c47ff0`
and is fed by `preAuth`. Keep `blazeSdkClientId` / `blazeServerClientId` / `blazeSdkClientSecret` /
`identityRedirectUri` **exactly where they are, in `blazesdk_config()` (preAuth CONF)**; moving them
would break getter `0x146e1bc50`.
* Keep the empty REPLY to `Authentication::logout (1/0x46)` — normal, not a failure.
**Fix the misleading log line (~L5935/6777/6947 pattern, emitted around `blaze_responder_v3b.py:1091`):**
it currently blames "layer 1 or blazeSdkClientId". Replace with: *"logout is a normal OSDK step; the
login gate is `OriginSDK+0x3a0 != 0` (LSX GetProfile), not Blaze config."*
* **Pre-stage Authentication handlers** for the stubs actually compiled into this client (found by
enumerating all 188 rel32 callers of `sendRequest 0x146df0e80` and reading the preceding
`mov r9d,imm32`): `0x0A` login, `0x14`, `0x1E`, `0x26`, `0x2D`, `0x2F`, `0x46` logout, `0xF1`,
`0xF2`, `0xF6`, `0x122`.
`0x1E` is the one that will stall you next: callers `0x146e15a6e`/`0x146e15cbe` (vtable
`0x14389fb98` slots +0xe8/+0xe0, discriminated by a bool arg) =
`getTermsOfServiceContent` / `getPrivacyPolicyContent`; `LoginStateAuthenticated` caches the result
at `state+0x68/+0x78` (strings `"LoginStateAuthenticated::mTermsOfServiceBuffer"@0x14389fe98`,
`"…mPrivacypolicyBuffer"@0x14389fec8`) and will hang on an empty body. Serve non-empty text.
* Expect `login`'s `LoginRequest.AUTH` to carry the auth code we hand out at
`lsx_responder_v2.py:352` (`OPENFUT_AUTHCODE`, default `OPENFUT-000000000000000000000000`).
---
## 4. MEMORY-FORGE SHORTCUT (fallback only — server-side is strictly better)
If rank 1 somehow does not take, you can force the same state. Note `+0x3a0` is a **u64 scalar**
(UserId), not a pointer — ENQUEUE_PLAN's "point it at the SDK object" advice works only because the
guards are `!=0` and `==[sdk+0x3a0]`; the *correct* value is our UserId.
```python
# force_defaultuser.py (ptrace_scope=0)
import struct
pid = 13643 # pgrep -x FIFA17.exe
f = open(f"/proc/{pid}/mem","r+b")
def q(va): f.seek(va); return struct.unpack('<Q', f.read(8))[0]
sdk = q(0x144b7c7a0) # live 0x25c98c50
f.seek(sdk+0x3a0); f.write(struct.pack('<Q', 33068179)) # UserId 0x1f89493
f.seek(sdk+0x3a8); f.write(struct.pack('<Q', 33068179)) # PersonaId 0x1f89493
```
Raw bytes: at `SDK+0x3a0` write `93 94 f8 01 00 00 00 00`, and the same eight bytes at `SDK+0x3a8`.
This alone satisfies `OriginGetDefaultUser (0x1470da6d0)` and both guards in
`OriginRequestAuthCodeSync (0x1470e67f0)` (`test rdx,rdx` / `cmp rdx,[rcx+0x3a0]`). It does **not**
fix the response matcher, so the resulting `<GetAuthCode>` reply would still be rejected with
`0xa2080000` unless rank 1 is also in — i.e. **the forge is only useful as a same-run A/B control**
("does `+0x3a0 != 0` really clear `OSDK_INVALID_USER`?"), not as a shipping path.
Explicitly **not recommended any more**: forging the `FirstPartyAuthCodeFutureImpl` node
(ENQUEUE_PLAN §1/§3a). With the matcher fixed, FIFA enqueues it itself, and the forged-node route
leaves the OSDK state machine untouched — which is why the earlier forge produced a `<GetAuthCode>`
on the wire but never a Blaze `login`.
---
## 5. STILL NEEDS A LIVE EXPERIMENT
1. **The whole of §3 rank 1 needs a FIFA restart.** `GetProfile` has not recurred since log id=18
(line 1068 of 3385) and `+0x3a0` is still 0 in pid 13643 — the running instance will **not**
self-heal. `Initialize` runs once.
2. **`Name` semantics in `<Service>`.** Nothing in `0x1470d0000-0x147160000` compares a service *Name*
against a literal, but the search was not exhaustive. If some path does, `Name == Facility-enum-name`
would break and real Origin-client Names would be needed. Cheap probe: rank 2 with `Name==Facility`;
if a verb that worked under rank 1 starts failing, that is the culprit.
3. **Facility index for `GetInternetConnectedState`, `SetDownloaderUtilization`,
`IsProgressiveInstallationAvailable`** is inferred (9 / 31 / 31). Echo-back makes this moot for
rank 1; it only matters if you ever hard-code senders again. Confirm by reading the
`add rdx,0xNN` after `mov rdx,[sdk+0x3b0]` at `0x1470e040a`, `0x14712b821`, `0x147129ddb`.
4. **`[SDK+0x1e8]` (= `conn+0x80`, live `-1`)** — a second connection handle, read by `0x14712cc00`
and waited on by `0x1470e6b70` (condvar `[sdk+0xf8]`, alt-wake `[sdk+0x240]`, both live 0). Its
*writer* is untraced; the conn object at `SDK+0x168` has vptr `0x14394c218` with 2 slots
(`0x14712c9b0`, `0x14712ce00`) — disassemble those. Watch whether it flips after rank 1; if it
stays `-1` and something blocks on `0x1470e6b70`, that is the next gate.
Also unidentified: `0x14712cc30(SDK+0x168, &outString)`, called after that wait, whose result is
inserted into the map at `SDK+0x1f8` (size `[SDK+0x218]`, live 0).
5. **Which of `UserId` / `PersonaId` lands in `+0x3a0` vs `+0x3a8`** is unobservable while we send the
same number for both. Make them differ once, then read both slots.
6. **Does the fixed chain reach Blaze in one go**, or is there a further OSDK sub-state after 16?
Watch `[0x43d189d8+0x260]` (re-find via vptr `0x14395c180`) walk off 16 and
`LoginManagerImpl+0x148 → stateMachine+0x30` leave `0xFFFFFFFF`.
7. **After `login` fires**, `Authentication 0x1E` (ToS / privacy content) is the next likely stall —
see rank 3.
8. **Post-fix event senders.** Confirm the `LOGIN_EVENT`/`ONLINE_STATUS_EVENT` handlers stop dropping
frames: under rank 1 push `sender=""`; under rank 2 push `sender="LOGIN_EVENT"`. Note the earlier
claim that `0xa2080000` in the log means failure is **weak** for events — with 73 EventHandler
template instantiations, non-matching handlers log it as ordinary fan-out noise. Use
`OriginMgr(*[0x1448acf50])+0x13` (`m_isLoggedIn`) flipping to 1 *by itself* as the event observable.
### Live verification snippet (run before and after)
```python
#!/usr/bin/env python3
import struct, glob
pid = next(int(d.split('/')[-1]) for d in glob.glob('/proc/[0-9]*')
if open(d+'/comm').read().strip() == 'FIFA17.exe')
f = open(f"/proc/{pid}/mem", "rb")
def rd(va,n): f.seek(va); return f.read(n)
def q(va): return struct.unpack('<Q', rd(va,8))[0]
sdk = q(0x144b7c7a0)
print(f"SDK={sdk:#x} +0x3a0={q(sdk+0x3a0):#x} +0x3a8={q(sdk+0x3a8):#x} "
f"+0x1e8={q(sdk+0x1e8):#x} +0x270={q(sdk+0x270):#x}")
b, e = q(sdk+0x3b0), q(sdk+0x3b8)
for i in range((e-b)//0x20):
s = rd(b+i*0x20, 0x20)
n, cap = struct.unpack('<Q', s[0x10:0x18])[0], struct.unpack('<Q', s[0x18:0x20])[0]
if n:
print(i, (rd(struct.unpack('<Q', s[:8])[0], n) if cap >= 16 else s[:n]).decode())
```
Baseline recorded today (pid 13643): `+0x3a0=0x0 +0x3a8=0x0 +0x1e8=0xffffffffffffffff +0x270=0x1c`,
all 34 names empty, `[0x43d189d8+0x80]="OSDK_INVALID_USER"`, `[+0x260]=16`,
`/tmp/blaze_responder.log`: 37× `logout`, 0× `login`.