edab23f04a
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
318 lines
18 KiB
Markdown
318 lines
18 KiB
Markdown
# 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.
|