# 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 `<GetAuthCode>`. 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<lsx::GetAuthCodeT,...>`, **transmits it** (`call [0x148e219f8]`),
     registers the pending future in the SDK reqId-keyed map (`0x1470e6540`), writes future→out, reqId→out.
     *This is the point `<GetAuthCode ClientId Scope>` 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 `<GetAuthCode ClientId="FIFA17PC" .../>` 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('<Q', b, 0x00, VPTR)
    struct.pack_into('<Q', b, 0x08, VPTR2)
    struct.pack_into('<I', b, 0x10, 2)              # refcount=2 -> 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('<Q', f.read(8))[0])[1]
    onlinemgr = rq(ONLINEMGR_PP); retr = onlinemgr + RETR_OFF
    sdk = rq(SDK_PP)
    # STEP 0: default user
    f.seek(sdk+SDK_DEFUSER); f.write(struct.pack('<Q', sdk))
    print(f"[+] OriginSDK={sdk:#x} default-user set -> {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=<hex scratch 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('<Q', NODE))
    print(f"[+] retriever+0x8 ({retr+0x08:#x}) -> {NODE:#x}. Watch /tmp/lsx.log for <GetAuthCode>.")

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 `<GetAuthCode ClientId="FIFA17PC" .../>` 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 `<GetAuthCode>` 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 `<GetAuthCode>` 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.
