# FIFA17 First-Party Auth Enqueue — Forge & Trigger Plan Clean-room synthesis of four independent reverses of the `FifaOnline::FirstPartyAuthTokenRetriever::DoTick` auth-code path. Every byte/offset below is backed by a decrypted-code VA or a live `/proc/13643/mem` read. Live pid at time of writing: **13643** (`pgrep -x FIFA17.exe`). ## TL;DR — two blockers, not one The brief assumed the only problem is `retriever+0x8 == NULL`. It is not. There are **two** hard gates, both live-verified this run: 1. **Empty queue** — `retriever+0x8` and `+0x10` are both `0x0` (nothing enqueued). *This is the one the brief targets.* 2. **No Origin default user** — `OriginSDK[+0x3a0] == 0x0`. `DoTick` calls `GetDefaultUser()` which returns that slot, then `OriginRequestAuthCodeSync` **rejects any request whose user is NULL** (`test rdx,rdx; je fail` → returns `0xa2000003`, nothing hits the wire). So even a perfectly forged node produces only an "Origin Error(a2000003)" log line unless we also set `OriginSDK[+0x3a0]`. **Forging the node alone is necessary but not sufficient. You must set BOTH.** Every plan below has "set `OriginSDK[+0x3a0]` non-null" as step 0. Live confirmation (this run): ``` OnlineMgr *[0x1448a3b20] = 0x43dc3e70 retriever = +0x4e98 = 0x43dc8d08 +0x00 vptr = 0x1438f5d50 (retriever vtable, 1 real slot = deleting dtor) +0x08 queue slot 0 = 0x0 <-- write &node here +0x10 queue slot 1 = 0x0 guard byte [0x1448a3ac3] = 0x01 (enqueue-wrapper guard PASSES, not the blocker) OriginSDK *[0x144b7c7a0] = 0x25c98c50 +0x3a0 defaultUser = 0x0 <-- BLOCKER: must be non-null node vtable 0x1438f5d58: [0]AddRef 0x147e8f160 [1]Release 0x147e1c480 [2]dtor 0x146f028d0 ret gadget [0x1470e3567] = c3 ``` Note there are **two different "OriginMgr" singletons** — do not confuse them: - `*[0x1448acf50]` → login-state OriginMgr (`m_isLoggedIn` @ +0x13; the one `force_login_flag.py` pins). - `*[0x144b7c7a0]` → OriginSDK object (default-user @ +0x3a0; the one THIS path needs). --- ## 1. `FirstPartyAuthCodeFutureImpl` NODE STRUCT Size **0xF0 (240 bytes)** — from the enqueue allocation constant (`0x146f5b916 mov edx,0xc390a20f; lea edx,[rdx+0x3c6f5ee1]` = `0xF0`) and matched by the ctor `0x146eeecd0`. The clientId capacity `0x40` comes from the same ctor (`r8d = 0xc390a20f + 0x3c6f5e31 = 0x40`). | Offset | Type | Meaning | Ctor init | Read/written by | Forge value (minimal) | |---|---|---|---|---|---| | `+0x00` | `void**` | primary vtable | `0x1438f5d58` | DoTick calls `[vptr+8]`=Release at end | **`0x1438f5d58`** (real) | | `+0x08` | `void**` | secondary vtable (base) | `0x1438f5d90` | dtor adjustor thunk only | `0x1438f5d90` (real) or `0` | | `+0x10` | `u32` | atomic refcount | `0` (xchg) | AddRef/Release | **`2`** (see refcount note) | | `+0x14` | `u32` | pad | — | — | `0` | | `+0x18` | `char[0x40]` | **ClientId** (inline C-string) | `strncpy(+0x18,arg,0x40)` | DoTick `lea rdx,[rsi+0x18]` → passed as `const char*`; Origin deref's byte-wise, must be non-empty | **`"FIFA17PC\0"`** (any non-empty; see Q) | | `+0x58` | `char[0x80]` | message/error buffer | `[+0x58]=0` | `SetError` vsnprintf's here (cap 0x80, ends at 0xD8) | `0` | | `+0xD8` | `char*` | **authCode result** (heap) | `0` | DoTick success: `mov [rsi+0xd8],rax`; dtor frees it | `0` | | `+0xE0` | `u32` | status/error code | `0` | `SetError` → `200 (0xC8)` on failure | `0` | | `+0xE4` | `u32` | kind/userIndex | `= ctor arg2` | wrapper always passes `0` | `0` | | `+0xE8` | `u8` | **isComplete / poll flag** | `0` | DoTick sets `1` on BOTH success and failure | `0` | | `+0xE9`..`+0xEF` | pad | — | — | — | `0` | There is **NO `next` pointer.** The "queue" at `retriever+0x8` is a **fixed 2-slot array** of ref-counted node pointers, not a linked list. DoTick iterates the two slots with `lea rbx,[rcx+8]; mov ebp,2; ... add rbx,8; dec rbp; jne`. No node field is ever chased as a link. (Confirmed: `0x146f199cd/d1/e0` and tail `0x146f19ae1/e5/e8`.) **Node vtable `0x1438f5d58`** (real, live-read): `[0]`AddRef `0x147e8f160` · `[1]`Release `0x147e1c480` · `[2]`dtor `0x146f028d0` · `[5]`GetResult `0x1466cc0d0` (`mov rax,[rcx+0xd8];ret`) · `[4]`GetStatus `0x1471a0630` (`mov eax,[rcx+0xe0];ret`). ### Minimal forged node — exact 240 bytes (little-endian) ``` off bytes meaning 0x00 58 5d 8f 43 01 00 00 00 vptr = 0x1438f5d58 0x08 90 5d 8f 43 01 00 00 00 vptr2 = 0x1438f5d90 0x10 02 00 00 00 refcount = 2 (survives one Release, never freed) 0x14 00 00 00 00 pad 0x18 46 49 46 41 31 37 50 43 00.. clientId = "FIFA17PC", NUL, rest 0 (fills to 0x58) 0x58 00 * 0x80 message buffer = 0 0xD8 00 00 00 00 00 00 00 00 authCode = 0 0xE0 00 00 00 00 status = 0 0xE4 00 00 00 00 kind = 0 0xE8 00 isComplete = 0 0xE9 00 * 7 pad to 0xF0 ``` **Refcount note (important).** DoTick unconditionally ends each processed slot with `mov rcx,[rbx]; mov [rbx],0; mov rax,[rcx]; call [rax+8]` = **Release** (`0x147e1c480`, `lock xadd` decrement of `[node+0x10]`; on reaching zero it invokes the dtor which `free()`s the node via the game allocator `0x1453370b0`). If you forge with **refcount = 1**, DoTick decrements to 0 and tries to **free your node** — safe only if the node lives in game-allocator memory, a crash otherwise. Forge **refcount = 2**: after Release it is 1, never freed. Costs a ~240-byte leak, zero crash risk. (Alternative: use a synthetic vtable whose slot `[1]` is the ret gadget `0x1470e3567` — then Release is a no-op and refcount is irrelevant; but the real vtable + refcount=2 is simpler and keeps the GetResult/GetStatus accessors valid if anything polls.) **Unknowns (marked):** - The **real ClientId string** the game would use is unrecovered (the natural enqueue never runs live). For our local LSX responder any non-empty string is accepted by ``. For a genuine EA endpoint the correct Nucleus client_id would be required. Since OpenFUT answers LSX locally, `"FIFA17PC"` (or whatever our responder keys on) is fine. - Whether the deeper LSX marshalling inside `0x1470e67f0` dereferences **user** object fields beyond the null/equality check. The traced send path builds the request from the SDK object + clientId and does **not** deref the user, but this was not exhaustively followed past the dispatch. Mitigation: set `OriginSDK[+0x3a0]` to a real readable pointer (the SDK object itself) rather than a bare `1`. --- ## 2. DoTick PROCESSING — end to end (`0x146f199c0`) Per slot `i` in `{+0x08, +0x10}`: 1. `rsi = *slot`. If NULL → skip (`je 0x146f19ae1`). *(Live: both NULL → does nothing, forever.)* 2. Zero two stack out-slots `[rsp+0x60]` (authCode out) and `[rsp+0x58]` (length out). 3. `call OriginGetDefaultUser()` (`0x1470da6d0`, zero-arg) → returns `OriginSDK[+0x3a0]` or NULL. Verified: `0x1470da6f4 call 0x1470e3560 (→ *[0x144b7c7a0]); 0x1470da6f9 mov rax,[rax+0x3a0]; ret`. 4. `call OriginRequestAuthCodeSync(user=rax, clientId=&node[0x18], &outAuthCode=r8, &outLen=r9, scope=0)` (`0x1470db3c0`, `146f19a05 lea rdx,[rsi+0x18]`, `146f19a0c mov [rsp+0x20],r14`=0 scope). The wrapper forwards to the real impl `0x1470e67f0`, which: - `test rdx,rdx; je fail` and `cmp rdx,[rcx+0x3a0]; jne fail` — **user must be non-NULL and == the SDK default user** (both are the same slot, so any non-null value is self-consistent). On failure returns `0xa2000003`, **no send**. - clientId must be non-empty (`cmp byte[r8],0`), copies it into `LSXRequest+0x10`. - builds the `Origin::LSXRequest`, **transmits it** (`call [0x148e219f8]`), registers the pending future in the SDK reqId-keyed map (`0x1470e6540`), writes future→out, reqId→out. *This is the point `` goes on the LSX wire.* 5. DoTick inspects the result: - `rc != 0` → `SetError(node, 200, "[%s] Origin Error(%d)\n", ".::DoTick", rc)` → writes `node+0xE0=200`, `node+0xE8=1`, message into `node+0x58`. - `rc==0 && (outAuthCode==0 || outLen==0)` → `SetError(node,200,"[%s] Invalid authcode\n",...)`. - success → alloc `outLen+1` from `*[0x1448a20b8]` (vt+0x38), `mov [node+0xD8]=buf`, `strlcpy(buf,outAuthCode)` (`0x145e27a50`), `mov byte[node+0xE8]=1`. 6. **Dequeue + release (all paths):** `mov rcx,[rbx]; mov [rbx],0` (NULL the slot) then `mov rax,[rcx]; call [rax+8]` = Release. Each enqueued request is consumed in exactly one tick; there is no retry/pending state. DoTick's only caller is the per-frame online-subsystem tick `0x146f7b279` (`lea rcx,[rsi+0x4e98]; call 0x146f199c0`), so a forged node is picked up on the **next frame**. --- ## 3. THE PLAN (ranked by likelihood-of-success × safety) ### STEP 0 (all plans): set the Origin default user — REQUIRED ``` OriginSDK = *[0x144b7c7a0] # live 0x25c98c50 write 8 bytes at OriginSDK+0x3a0 = OriginSDK # a real, readable, self-consistent non-null pointer ``` Writing the SDK object's own address (rather than a bare `0x1`) satisfies the null + equality checks **and** points at valid memory in case anything downstream deref's the "user". GetDefaultUser and the impl both read the same slot, so equality always holds. --- ### (a) PRIMARY — FORGE a node via `/proc/mem` and set `retriever+0x8` ★ recommended Pure memory writes, no code execution, no Win64/SysV ABI hazard. Matches the brief exactly. **Steps** 1. Do STEP 0. 2. Pick a **scratch VA** inside FIFA to host the 240-byte node — a currently-zero, unreferenced, writable region (see "live items", §4). Call it `NODE`. 3. Write the 240-byte forged node (bytes in §1) at `NODE`. 4. Write `NODE` (8 bytes) into `retriever+0x8` = `0x43dc8d10`. 5. Watch `/tmp/lsx.log` for the `` request on the next frame. **Recipe (style of `force_login_flag.py`):** ```python #!/usr/bin/env python3 # forge_node.py — forge a FirstPartyAuthCodeFutureImpl and enqueue it. ptrace_scope=0 required. import struct, glob, os ONLINEMGR_PP = 0x1448a3b20 # *-> OnlineManager RETR_OFF = 0x4e98 # +retriever SDK_PP = 0x144b7c7a0 # *-> OriginSDK SDK_DEFUSER = 0x3a0 # OriginSDK default-user slot (BLOCKER) VPTR = 0x1438f5d58 VPTR2 = 0x1438f5d90 CLIENTID = b"FIFA17PC" def pid(): for d in glob.glob('/proc/[0-9]*'): try: if open(d+'/comm').read().strip()=='FIFA17.exe': return int(d.split('/')[-1]) except: pass raise SystemExit("FIFA17.exe not found") def build_node(): b = bytearray(0xF0) struct.pack_into(' never freed b[0x18:0x18+len(CLIENTID)] = CLIENTID # clientId, NUL-terminated (rest already 0) return bytes(b) def main(): p = pid(); f = open(f"/proc/{p}/mem","r+b") rq = lambda va:(f.seek(va), struct.unpack(' {sdk:#x}") # NODE scratch VA — MUST be a validated unused writable region (see plan §4). NODE = int(os.environ.get("NODE_VA","0"),16) if not NODE: raise SystemExit("set NODE_VA=") f.seek(NODE); f.write(build_node()) print(f"[+] node forged @ {NODE:#x} (clientId={CLIENTID!r})") # enqueue: retriever+0x8 = &node f.seek(retr+0x08); f.write(struct.pack(' {NODE:#x}. Watch /tmp/lsx.log for .") if __name__=='__main__': main() ``` **Crash risks** - *Scratch provenance*: if `NODE` overlaps live game memory, DoTick's writes to `+0xD8/+0xE8` (and any poller) corrupt it. Mitigate by validating the region is zero + unreferenced (§4). - *Refcount*: refcount=2 avoids the terminal free entirely — do **not** use 1 unless `NODE` is game-alloc. - *Deeper user deref*: covered by pointing `+0x3a0` at the real SDK object. - *Race*: DoTick runs every frame; write the node bytes **before** setting `retriever+0x8` (the script does), so a mid-write tick never sees a half-built node. **Success signal**: a single `` LSXRequest on `/tmp/lsx.log` within one frame; on failure instead expect an "Origin Error(a2000003)" trace (means STEP 0 didn't take) or "Invalid authcode" (means our LSX responder returned empty). --- ### (a′) SAFE VARIANT — let the game allocate the node (hybrid forge) ★ safest memory-wise Instead of hosting the node in scratch memory, call the game's own enqueue `RequestFirstPartyAuthCode(clientId)` = **`0x146f57bf0`** (guard byte `[0x1448a3ac3]` already `1`, so it resolves `retriever = mgr+0x4e98` correctly and stores into the first free slot). This allocates a proper 0xF0 node from the game allocator, ctors it, AddRefs, and inserts it — DoTick then processes it with the real vtable and correct refcount/free, and wires the future back into the retriever slot. This removes the scratch-provenance problem entirely but requires a **call** (see (b) for the Win64 ABI caveat). Signature: `void** RequestFirstPartyAuthCode(const char* clientId /*rcx*/)`. Still needs STEP 0. --- ### (b) DIRECT CALL via gdb — fire the send without forging Two call targets, both Win64 `__fastcall`: - **Enqueue** `0x146f57bf0` `RequestFirstPartyAuthCode(const char* clientId /*rcx*/)` — the (a′) route; correct, wires the future into the retriever. - **Raw sync sender** `0x1470db3c0`: ``` int32 OriginRequestAuthCodeSync( rcx void* user, // must be !=0 AND == OriginSDK[+0x3a0] (== GetDefaultUser()) rdx const char* clientId, // non-empty r8 void** pOutFuture,// out, non-null r9 uint64* pOutReqId, // out, non-null [rsp+0x28] const char* scope // optional, pass 0 ) -> 0 ok / 0xa2000003 (bad user) / 0xa2000004 (null out-ptr) ``` Direct-call recipe: STEP 0, then `clientId="FIFA17PC"`; zero `outFuture,outReqId`; `rcx=OriginSDK[+0x3a0]`, `rdx=&clientId`, `r8=&outFuture`, `r9=&outReqId`, `[rsp+0x28]=0`. **Crash / correctness risks** - **ABI mismatch (the big one):** FIFA17.exe is a Win64 PE under Wine (args `rcx/rdx/r8/r9`+stack); host gdb `call` uses SysV (`rdi/rsi/rdx/rcx`). A naive `call` passes args in the wrong registers → garbage user/clientId → fault or `0xa2000003`. Use a **forged thread context** (stop a thread, set `rip` to the target with Win64 regs + 5th arg pushed + a return trap) or a small written trampoline, not `call`. - The **raw sync** call fires GetAuthCode but the future lands in **your** out-param, not the retriever node — it validates the LSX path but does **not** advance FIFA login. The **enqueue** call (0x146f57bf0) does advance it. Prefer the enqueue. - Re-entrancy: calling on a paused thread mid-DoTick could double-process; run when the online tick is idle. **Success signal**: same `` on `/tmp/lsx.log`. For the enqueue call, also expect `node+0xE8` to flip to `1` on the following frame. --- ### (c) FIX THE REAL SKIP REASON — make FIFA enqueue naturally (cleanest, hardest) Why the game never enqueues, root-caused to three independent walls (all live-verified or high-conf): 1. **No default user** (`OriginSDK[+0x3a0]==0`). It is populated only by the SDK connect/user-query round trip at `0x1470e5ad5` (guarded by `0x147118d80` after a ~15 s connect-wait loop; nearby literal "EbisuSDK"). If that LSX exchange never yields a user, the slot stays NULL and neither the natural enqueue nor the auth send can proceed. **Fixing this legitimately (our LSX responder answering the user-query so `+0x3a0` gets set) would unblock BOTH gates at once — the cleanest of all outcomes.** 2. **LoginStatePCLogin sub-state 0 is a hardcoded stub.** `(*[0x144b86bf0])->vt[0x60]` = `0x146f82070` = `xor eax,eax; ret` for the live class → the state always returns NULL and falls to the `TXT_NOT_LOGIN_TO_EBISU` branch (`0x1471b5b64`, sets `TXT_NOT_LOGIN_TO_EBISU` @ `0x1439633e8`, sub-state 1). *Confidence medium* — needs the jump-table decode at `0x141e7f55c` to confirm index-0 mapping. 3. **Blaze-SDK's own auth-code fetchers** (`0x147237340` "blazeServerClientId" / `0x147237440` "blazeSdkClientId") both bail at `mov rcx,[rax+0x750]; test rcx,rcx; je` — the **client-config object** our empty `fetchClientConfig` responses never populate. Populating client-config would let this second, retriever-independent path call `OriginRequestAuthCodeSync` directly. **Recommended natural-fix track**: make our LSX responder answer the Origin user-query so `0x1470e5ad5` writes `OriginSDK[+0x3a0]`, then supply a non-empty `fetchClientConfig` so `[cfg+0x750]` is non-NULL. That is a server-side change (no memory patching) and would let FIFA drive the whole flow itself. **Risk**: highest reverse-effort; may reveal further downstream gates (Blaze login after GetAuthCode). --- ## 4. STILL NEEDS A LIVE DUMP / EXPERIMENT 1. **Scratch VA for plan (a).** Need a validated **unused, zero, writable** ≥0x100-byte region in FIFA's maps to host the forged node (candidates: an anon `rw` mapping with a long zero run; verify it stays zero across several frames = unreferenced). Or sidestep entirely with plan (a′)/(b) using the game allocator. This is the one blocker to running (a) as-is. 2. **Does the transmit fp `*0x148e219f8` write the LSX socket synchronously**, or does `0x1470e6540` only register the future while a separate pump thread flushes it? Determines whether a one-shot forced enqueue puts bytes on the wire in the same frame. 3. **Does `0x1470e67f0` deref user fields** past the null/equality guard (deeper marshalling at `0x1470dbfa0/0x147117fe0/0x1471186f0`)? If yes, `OriginSDK[+0x3a0]` must point at a *shaped* user object, not just the SDK. Dump those before relying on the self-pointer trick. 4. **Real ClientId** the game/our LSX handler expects — confirm our responder's `` handler accepts an arbitrary non-empty string (expected: yes) or keys on a specific value. 5. **Confirm the natural-fix chain**: after our LSX responder answers the user-query, verify `OriginSDK[+0x3a0]` actually becomes non-NULL live (proves gate #1 is server-fixable) and that `fetchClientConfig` content lands at `[cfg+0x750]`. 6. **reqId width** written to `pOutReqId` (`[req+0xc8]`, appears 64-bit) — needed so a forged/emulated response correlates with the request.