From 3d3239bab9154fec4fae147503e1112b59188714 Mon Sep 17 00:00:00 2001 From: funman300 Date: Fri, 7 Aug 2026 11:44:05 -0700 Subject: [PATCH] feat: document and stage FIFA 17 SBC hook workflow --- fifa17-recon/.gitignore | 1 + ...26-08-07-sbc-client-hook-implementation.md | 256 +++++++++++++ fifa17-recon/docs/plan-2026-08-07-sbc-hook.md | 237 ++++++++++++ .../docs/plan-2026-08-07-sbc-predicate.md | 138 +++++++ .../plan-2026-08-07-sbc-response-reconcile.md | 211 +++++++++++ fifa17-recon/docs/sbc-hook-dll-spec.md | 337 ++++++++++++++++++ fifa17-recon/tools/fifa17-hook-m1.sh | 252 +++++++++++++ fifa17-recon/tools/run-x64dbg-fifa17.sh | 25 ++ openfut-launcher | 2 +- 9 files changed, 1458 insertions(+), 1 deletion(-) create mode 100644 fifa17-recon/docs/plan-2026-08-07-sbc-client-hook-implementation.md create mode 100644 fifa17-recon/docs/plan-2026-08-07-sbc-hook.md create mode 100644 fifa17-recon/docs/plan-2026-08-07-sbc-predicate.md create mode 100644 fifa17-recon/docs/plan-2026-08-07-sbc-response-reconcile.md create mode 100644 fifa17-recon/docs/sbc-hook-dll-spec.md create mode 100755 fifa17-recon/tools/fifa17-hook-m1.sh create mode 100755 fifa17-recon/tools/run-x64dbg-fifa17.sh diff --git a/fifa17-recon/.gitignore b/fifa17-recon/.gitignore index 55bf695..0c424d4 100644 --- a/fifa17-recon/.gitignore +++ b/fifa17-recon/.gitignore @@ -9,4 +9,5 @@ *.log __pycache__/ captures/ +staging/ tools/fifa17_profile.json diff --git a/fifa17-recon/docs/plan-2026-08-07-sbc-client-hook-implementation.md b/fifa17-recon/docs/plan-2026-08-07-sbc-client-hook-implementation.md new file mode 100644 index 0000000..7d9c3cc --- /dev/null +++ b/fifa17-recon/docs/plan-2026-08-07-sbc-client-hook-implementation.md @@ -0,0 +1,256 @@ +# FIFA 17 SBC client-hook implementation plan + +## Outcome + +Implement an opt-in, fail-closed hook that repairs the native response-to-deserializer +dispatch for `GET /ut/game/fifa17/sbs/sets`. The hook must reuse the genuine response +object and SAX reader from the real HTTP 200 transaction, run synchronously on the native +transaction thread, and preserve the game's allocator, object ownership, callbacks, and +index rebuilds. + +This plan supersedes the intervention direction in `plan-2026-08-07-sbc-hook.md` and +`sbc-hook-dll-spec.md` wherever those documents claim the client never issues `/sbs/sets` +or recommend constructing a synthetic reader. The fresh 10:20:20 exchange proves the +request is issued and receives populated JSON. The reconciliation report is authoritative. + +## Proven anchors + +All addresses are static VAs in `CardsDLL_Win64_retail.dll`, image base `0x180000000`. +Runtime addresses are `CardsDLL base + (static VA - 0x180000000)`. + +| Purpose | Address / identity | +|---|---| +| Category request constructor | `0x18017a7c0`, request vtable `0x18022e5c0`, tag `0x753c` | +| `/sets` URI builder | `0x18017a980` | +| Typed response factory | `0x18017aa10`, response vtable `0x18022e5b0` | +| Typed category deserializer | `0x18017b2b0`, `rcx=response`, `rdx=genuine reader` | +| Generic completion | `0x18016cca0`, exact-200 check at `0x18016cdd0` | +| FUT root | `A = *0x1802e6398`, expected vtable `0x18021c2a0` | +| SBC gate cache | `B=A+0x1f9d8`; ready byte `B+0x28` | +| Category store | `M=*(A+0x20a68)`; count `WORD[M+0x50]` | +| Renderer count read | `0x1800b5eda` | + +Entering `0x18017b2b0` necessarily invokes the `A+0x20a68` lazy getter before JSON-key +parsing. The fresh transaction left that pointer null, proving that the typed category +deserializer was not entered. + +## Architecture decision + +Use the existing `openfut-hook` Rust `cdylib` and FIFA 17 feature boundary. Retain its +deferred CardsDLL discovery, RVA calculation, guarded reads, default-off environment +gates, and logging. Replace the stale Tier-1 idea of constructing a reader with this flow: + +```text +real /sbs/sets HTTP 200 + -> native generic completion and typed-response factory + -> observe the real response object and real reader/body cursor + -> at the proven skipped dispatch boundary, call the original typed method once + -> native parser populates M and rebuilds its indices + -> resume the native callback/completion chain + -> validate M; use native gate state if available + -> only if necessary, arm B+0x28 while B+0x08 remains zero +``` + +Do not intercept at the socket layer, fabricate a SAX reader, retain response/reader +pointers beyond their synchronous lifetime, hand-build EASTL category/set records, or +write `B+0x08`/`B+0x20`. + +## State and feature gates + +Use independent flags; no stronger stage should be implied by a weaker one: + +- `OPENFUT_SBC_HOOK=1`: resolve and fingerprint only. +- `OPENFUT_SBC_TRACE=1`: install passive probes and structured logging. +- `OPENFUT_SBC_DISPATCH=1`: enable the one-shot native dispatch repair. +- `OPENFUT_SBC_COMMIT=1`: permit gate/refresh action after validated parse success. +- Keep `OPENFUT_SBC_ARM_ONLY=1` solely as a separate negative-control experiment. + +Represent runtime progress with an atomic state machine: + +```text +Disabled -> Resolved -> Intercepted -> Parsed -> Validated -> Committed + \-> Failed +``` + +Add a recursion-depth guard and a transaction one-shot keyed by request/response identity. +Any fingerprint, pointer, status, class, thread, reader, or postcondition mismatch moves to +`Failed` and resumes native execution without a write. + +## Milestones + +### M0 — reconcile and freeze the baseline + +1. Mark the reconciliation report as the address/path authority. +2. Record SHA-256, PE timestamp, `SizeOfImage`, and selected section hashes for the shipped + CardsDLL, FIFA executable, built hook, and deployed proxy DLL. +3. Preserve a known-good launcher and proxy DLL. Do not overwrite a game-directory DLL + without an exact backup and hashes. +4. Capture a baseline: FUT hub succeeds, `/sbs/sets` returns 200, SBC shows the modal, + `M==0`, and the category deserializer is not observed. + +Exit: the baseline is repeatable and its artifacts identify one binary build exactly. + +### M1 — stabilize DLL loading + +The existing `version.dll` injection has one historical successful log, but the current +FIFA 17 launcher disables it after later crashes. Resolve this before SBC detours: + +1. Port or implement the complete VERSION proxy export surface and forward every export. +2. Build only `--features fifa17` for `x86_64-pc-windows-gnu` into a staging directory. +3. Inspect PE architecture, exports, and imports with the MinGW binutils. +4. Add a FIFA-17-specific launch path using the existing prefix/UMU configuration and + explicit `WINEDLLOVERRIDES=version=n,b`. +5. Run three cold launches with every SBC mutation/trace flag disabled. + +Exit: all three launches reach the FUT hub, VERSION calls forward correctly, and disabling +the override restores the pre-hook baseline. + +### M2 — strengthen runtime resolution + +Before any detour or byte write, validate: + +- exact CardsDLL identity (`SizeOfImage`, PE metadata, and multiple section/function hashes); +- FNV control bytes at `0x180180d00`; +- expected bytes at every proposed patch site; +- `A` and its expected vtable; +- `B` and its expected vtable; +- readable `M` slot and sane cache fields; and +- that runtime VAs lie inside the expected CardsDLL sections. + +Use the external read-only `futmem`/probe tooling as an independent oracle. Never cache an +ASLR slide across launches. + +Exit: resolve-only mode passes on two launches with different slides and aborts cleanly on +a deliberately mismatched fingerprint fixture. + +### M3 — passive transaction tracing + +Instrument, without changing return values or state: + +1. generic completion `0x18016cca0`; +2. typed response factory `0x18017aa10`; +3. typed category deserializer `0x18017b2b0`; and +4. once found, the common body/SAX virtual-dispatch callsite. + +Log a monotonic timestamp, session/build ID, thread ID, recursion depth, status, request +pointer/vtable, response pointer/vtable, reader/body pointer and vtable, and `M`/`B` +before and after. Correlate a request ordinal with `/tmp/utas.log`; do not log SID/auth +values or full response bodies. + +Do not use the existing generic four-register probe wrapper for `0x18016cca0`. That routine +has a fifth stack argument. Use a relocated trampoline or a narrowly verified assembly +stub that preserves the full Win64 ABI: nonvolatile GPRs, XMM6-XMM15 if touched, 32-byte +shadow space, 16-byte call alignment, and all stack arguments. The diagnostic +unhook/call/rehook mechanism is also racy and is not acceptable for the final repair. + +Exit: one fresh exchange unambiguously identifies whether the factory is skipped, the typed +object exists without a body/reader, or virtual deserialization dispatch is skipped. + +### M4 — reverse the exact dispatch contract + +Use M3 captures and static analysis to answer all of these before enabling intervention: + +- the exact common body-to-response-deserializer callsite; +- the relationship between response vtable `0x18022e5b0` slot `+0x08` and the older + message-object vtable `0x18022e598` slot `+0x20`; +- which completion argument or object field owns the genuine reader; +- the reader's valid synchronous lifetime; +- whether `0x1800b8c30` executes after a successful forced parse; +- the native transaction/game thread identity; and +- whether the parser can be reached more than once for one response. + +Exit: a written call contract identifies the exact hook site, preserved instructions, +original target, arguments, ownership, thread, and resume address. + +### M5 — behavior-preserving detour + +Install the production-form detour at the chosen boundary but initially tail-call the +original path unchanged. Prefer a small audited trampoline abstraction over copying the +repository's unhook/rehook diagnostic pattern. + +Exit: exactly one balanced entry/exit is recorded per SBC exchange; HTTP traffic, modal, +M/B state, timing, and unrelated FUT screens remain unchanged. + +### M6 — guarded dispatch repair + +On the native transaction thread and only while the genuine objects are live: + +1. require request vtable `0x18022e5c0`, response vtable `0x18022e5b0`, and status 200; +2. require a readable reader pointer/vtable and recursion depth zero; +3. require that this transaction has not already been parsed; +4. call the original typed method `0x18017b2b0(response, reader)` exactly once; +5. capture its return and the resulting M state; and +6. resume the native completion/callback path. + +Never run this from the deferred worker or while the SBC controller is iterating. Do not +attempt in-place memory repair after an exception or partial parse; preserve logs and +relaunch FIFA. + +Exit: the deserializer is observed once, returns successfully, and native execution +continues without gate or refresh writes. + +### M7 — validate and commit UI state + +Before exposing populated data, require: + +- `M != 0` and a bounded category count; +- category vector `begin <= end <= capacity`; +- `(end-begin) % 0xf0 == 0` and vector length equals `WORD[M+0x50]`; +- sane, unique category/set identifiers and bounded nested counts; +- all native index-rebuild/finalization calls observed; and +- no duplicate parse or partial state. + +First allow the native callback to arm the cache. If it does not, the only fallback is +`BYTE[B+0x28]=1` while `B+0x08==0`; never write `B+0x08` or `B+0x20`. Initially require +the user to close/reopen SBC for refresh. Do not synthesize Scaleform events until the +signature and ownership contract of `0x1801a4a70` are independently proven. + +Exit: no modal; displayed categories and set counts match the served response. + +### M8 — regression, soak, and rollback proof + +1. Open/close SBC ten times; enter every set/challenge and return. +2. Verify a second `/sets` response is idempotent and does not duplicate data. +3. Smoke-test hub, club, store, squads, and normal service traffic. +4. Repeat from two fresh launches with different ASLR slides. +5. Soak 30–60 minutes with navigation and, if supported, repeated FUT enter/exit. +6. Disable all SBC flags and confirm the baseline behavior returns without detours/writes. +7. Disable `WINEDLLOVERRIDES`, restore the exact backed-up proxy if needed, and prove hard + rollback with FIFA closed. + +Exit: zero crashes/freezes, stable counts and memory behavior, no unrelated FUT regression, +and both soft and hard rollback are demonstrated. + +## Testing and build checks + +Run at minimum: + +```text +cargo fmt --check +cargo test --features fifa17 +cargo check --release --features fifa17 --target x86_64-pc-windows-gnu +cargo build --release --features fifa17 --target x86_64-pc-windows-gnu +``` + +Extract pure, host-testable helpers for RVA calculation, fingerprint comparison, state +transitions, bounded vector validation, and structured event formatting. Windows calls, +raw pointer reads, and patching should remain behind small interfaces so guard logic can be +tested without launching FIFA. + +## Stop conditions + +Stop and roll back on any unknown binary fingerprint, patch-byte mismatch, wrong vtable, +wrong thread, unexpected factory/deserializer count, recursion, invalid vector geometry, +missing finalizer, partial parse, crash/freeze, unrelated FUT regression, or save/profile +change. Preserve hook log, UTAS log, binary hashes, and crash evidence before relaunching. + +## Definition of done + +- The hook is default-off and endpoint/class-specific. +- Exact binary and patch-site fingerprints are verified before intervention. +- The real category deserializer runs exactly once for each intended HTTP 200 response, + using the genuine response and reader on their native thread. +- `M` passes structural validation and the populated SBC menu supports drill-down. +- No communication modal appears and non-SBC FUT behavior is unchanged. +- Two fresh ASLR-distinct launches and the soak test pass. +- Unsetting flags restores inert behavior; removing the proxy restores the original launch. diff --git a/fifa17-recon/docs/plan-2026-08-07-sbc-hook.md b/fifa17-recon/docs/plan-2026-08-07-sbc-hook.md new file mode 100644 index 0000000..bcdf183 --- /dev/null +++ b/fifa17-recon/docs/plan-2026-08-07-sbc-hook.md @@ -0,0 +1,237 @@ +# SBC Menu Render Intervention — Plan (2026-08-07) + +**STATUS (one line): YES, WITH CAVEATS — a populated SBC menu is achievable via a +client-side hook, but ONLY by making the game's own parser fill its store; a +/proc/mem byte poke alone can open the menu (negative control) but renders EMPTY, and +the one remaining un-reversed item (the SAX input-source `vtable[+0x8]` byte-yield +contract) blocks the fully-offline populate until a served /sbs/sets response or a +completed reader is wired.** + +All addresses are on-disk RVAs against CardsDLL image base `0x180000000` +(`/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll`, working copy `/tmp/fut/cardsdll.dll`). +Live slide this session = `0x6ffe7c140000` (mapped base `0x6ffffc140000`), proven via +FNV prologue at `0x180180d00`. Live values below are from read-only `/proc/12201/mem`. + +--- + +## 1. Definitive SBC data-flow + +### Object graph +- **A** = FUT root singleton = `*[0x1802e6398]`. Getter `0x18011a830`. A.vtable static + `0x18021c2a0`. Live A = `0xb83e2b60` (vtable matches static — CONFIRMED). +- **B** = SBC request/TTL gate cache = `A + 0x1f9d8`. B-getter = A.vtable[+0x4e8] = + thunk `0x18011c1f0` (`lea rax,[rcx+0x1f9d8]; ret`). B.vtable static `0x1801fae70` + (3 slots: dtor `0x180063040`, isValid `0x180065d40`, clear `0x180065d20`). Live B = + `0xb8402538` (vtable matches). **B is the GATE, not the render source.** +- **M** = SBC categories/sets store = `*(A + 0x20a68)`. Reached via A.vtable[+0x9b0] = + lazy getter `0x18011b7d0` (if `A[+0x20a68]==0` it factory-creates an EMPTY M, type-id + `0x13f0`, and caches it). Live M = `0x0` (never built this session — SBC menu not + opened). **M IS the render source.** +- The "SBC manager" is **A itself**: service-id `0xed84b12` resolver A.vtable[+0x18] = + `0x180113f50` returns `this`, so `manager.vtable[+0x9b0] == A.vtable[+0x9b0] == + 0x18011b7d0`. The old lead `0x1801e9010` is DEBUNKED — it is an `.rdata` function + pointer slot (`->0x18018577a`), not a manager global. + +### Render source (CLIENT authority) +The SBC hub/squads controller (ctor `0x1800b5267`) caches M into `controller+0x140` +by calling A.vtable[+0x9b0] once (`0x1800b554d`→`0x1800b5571`→store `[rsi+0x140]`), +then registers Scaleform events `0x756c`–`0x7574`. The tile-build method (`0x1800b5e00` +region) reads `[ctrl+0x140]=M` and at **`0x1800b5eda`** does +`movzx ebx,WORD[M+0x50]; add bx,0x2; call [scaleform.vtable+0x58](count)` → emits +**(category_count + 2) tiles**. This region reads `[ctrl+0x140]` seven times and reads +B/`A+0x1fa00` **zero** times. M layout: cat count `WORD[M+0x50]`; cat vector +`[M+0x58]..[M+0x60]` stride `0xf0`; per-cat set count `WORD[cat+0xb8]`, set vector +`[cat+0xc0]` stride `0x3570`; secondary/featured vec `[M+0xa10]..[M+0xa18]`; +indices at `+0x9e0/+0xa10/+0xa40`. **Correction on record:** earlier passes that +called `B[+0x08]` the render source conflated the gate with the data source — the empty +render was because M was null/empty, NOT because `B[+0x08]` was null. + +### Populate path (CLIENT authority) +The sbs/sets deserializer **`0x18017b2b0`** (rcx=this IGNORED; rdx=SAX cursor is the +only live input) does the whole populate: fetch manager → get store M via +`[manager.vtable+0x9b0]` (at `0x18017b327`) → clear `0x18015f3a0` → loop atom `0x6f` +"categories": per item ctor `0x180159da0` (0xf0, vtable `0x18021b520`), cat-deser +`0x18017ab80`, cat-finalize `0x180160e50`, APPEND `0x18015a770` (copy-ctor +`0x18015a2b0`), dtor `0x1801105d0` → after loop rebuild indices `0x180160e00` + +`0x180160f30` + `0x180161020` → commit `manager.vtable[+0x8]`. Always returns true. +Set-row deser `0x18017ad60`. **Populate-target == render-source (both are M).** + +### Prefetch gate (SERVER/front-end authority — THE WALL) +There is **no native flag** to flip. The only native online check `0x1801642c0` +(inside isValid) is stubbed `mov al,1; ret` — NOT the wall. The block is upstream in +the Flash/ActionScript FUT front-end (FNV-name-hash bound; `RequestChallengeData` = +`0x1801f9b30`, `futsbchubviewmodel` = `0x1801ee0a0` — no native xref), which refuses to +issue `GET ut/game/fifa17/sbs/sets` offline, so deser `0x18017b2b0` never runs. +**Newly proven:** the URL template `"ut/%s/sbs"` (`0x18021d908`) has ZERO references +in the image (siblings `ut/%s/tournament`, `ut/%s/season` ARE referenced) — so +**CardsDLL has no native code that self-builds/issues the sbs GET.** This kills any +"force the req-mgr at A+0x2a0 to fetch on its own" idea. This is why the fix must be +client-side and must FORCE the populate. + +### Ready-arm (CLIENT authority) +isValid `0x180065d40(B)` verified: `if !0x1801642c0() ret0` (stub→always passes); +`cmp [rbx+0x28],0; je fail`; **`cmp QWORD[rbx+0x8],0; je 0x180065d75` → returns 1 +immediately (short-circuit)**; else QueryPerformanceCounter (`0x1801e50c0`) and compare +`[rbx+0x20]` deadline. Normally B is armed by the completion callback `0x1800b8c30` +(subscribed in svc ctor `0x1800b5765` via `manager.vtable[+0xa90]`) through the generic +cache copy-assign `0x1800c21a0` (sets B+0x08=collection, B+0x20=deadline, B+0x28=1). +Offline that callback never fires (no response). Live: `B[+0x08]=0`, `B[+0x28]=0`. + +--- + +## 2. Chosen minimal intervention and WHY + +**Reuse the client's own parser; do NOT hand-build structs; arm ONLY `B[+0x28]`.** + +Two tiers, safest-first: + +- **Tier-0 (negative control — proves the gate):** write ONLY `BYTE[B+0x28]=1`. + isValid short-circuits (B+0x08==0 branch) → menu OPENS instead of the error modal + (`0x18016c330`), but renders EMPTY (M is null/empty). Do NOT write `B+0x08` or + `B+0x20` — pointing B+0x08 at a collection forces isValid into the QPC-deadline + branch, and with the live-stale deadline (`0xf10fb8cb9`) the gate SHUTS → modal, i.e. + it DEFEATS the fix. This is the load-bearing correction from adversarial verification. + +- **Tier-1 (real fix — populates M):** + - **Preferred (Option 1, cleanest, zero forged state):** inject a canned + `/sbs/sets` JSON response at the message-receive layer so the game builds the + response-msg (ctor `0x18017b1c0`, vtable `0x18022e598`, deser slot +0x20 = + `0x18017b2b0`), seats a genuine SAX cursor, its OWN chain populates M, and the + native completion callback `0x1800b8c30` arms B for you. The bridge/core serves the + JSON. Nothing forged. + - **Fallback (Option 2):** from the hook, stand up a real SAX cursor over canned JSON + (ctx `0x1801c63e0` + lexer `0x1801c8060` + an input-source whose `vtable[+0x8]` + yields bytes), call deser `0x18017b2b0(rcx=ignored, rdx=cursor)`, then arm ONLY + `BYTE[B+0x28]=1`. **Blocker:** the input-source `vtable[+0x8]` byte-yield contract + is the ONE un-reversed item — a cold call with a null-source cursor CLEARS M + (`0x18015f3a0`) then byte-scans a garbage pointer (`mov rdi,[rdi]` ~`0x18017b353`) + → wipes state + segfault. So Option 2 is NOT safe to run until the reader is + reversed. + +**Why not hand-build:** feeding `0x18015a770` a hand-built 0xf0 category (with nested +0x3570 set records / EASTL sub-vectors) is the highest crash risk — the copy-ctor +`0x18015a2b0` deep-copies inner sub-vectors; any bad begin/end/cap → heap corruption. +The parser writes the correct geometry AND runs the index-rebuild finalizers that +hand-built appends get wrong. Ruled out. + +**Refresh:** after M is populated, fire refresh events `0x756c`–`0x7574` (or re-open the +menu) so `0x1800b5eda` re-reads `WORD[M+0x50]`. + +--- + +## 3. STAGED MORNING TEST PLAN (safest-first) + +Precondition: FIFA at the FUT hub with CardsDLL loaded. Rollback for EVERY step = +**relaunch FIFA** (all effects are volatile — single-byte poke or in-session hook state, +cleared on restart). NEVER run `--apply` while the SBC menu is open/mid-iterate. + +### Step 1 — Dry-run read confirm (ZERO writes) +``` +python3 /home/alex/Documents/OpenFUT/fifa17-recon/tools/sbc_hook_poke.py +``` +Expect: CONTROL FNV MATCH; A vtable match; B offset decoded live = `0x1f9d8`; B/A vtables +match statics; `B+0x28=0`; `M=*(A+0x20a68)=0` (until SBC menu opened once). +PASS = addresses match the model. Rollback: none needed (read-only). + +### Step 2 — Review the DLL populate spec (ZERO writes) +``` +python3 /home/alex/Documents/OpenFUT/fifa17-recon/tools/sbc_hook_poke.py --spec +``` +Expect: printed injected-DLL spec (Option 1 preferred, Option 2 fallback). Read-only. + +### Step 3 — Negative control (Tier-0, ONE byte write) — proves the GATE +With the SBC menu **CLOSED**: +``` +python3 /home/alex/Documents/OpenFUT/fifa17-recon/tools/sbc_hook_poke.py --apply +``` +Writes exactly `BYTE[B+0x28]=1` (re-proves slide+vtables at write time; aborts on any +mismatch; hard-refuses to write B+0x08/B+0x20). Then re-open the SBC menu. +Expect: menu OPENS, no error modal, ~2 empty/placeholder tiles. This proves the gate + +isValid short-circuit LIVE — it does NOT prove data. If it CRASHES: stop — B +resolution/slide is wrong. Rollback: relaunch FIFA (byte clears on restart). + +### Step 4 — Real fix (Tier-1) — proves the DATA (NOT for a blind run) +Do this only after the DLL populate is implemented. Preferred: bring up the bridge/core +`/sbs/sets` responder and let Option 1 (message-layer injection) drive the native chain; +the completion callback arms B and M fills. Then the same gate opens a POPULATED menu +(N+2 tiles). The hook module scaffold is `openfut-hook/src/sbc_hook.rs` — Tier-1 +`populate_m()` is present but deliberately refuses to call the deser until the SAX +input-source reader is reversed (else it clears M and crashes). Build (when ready): +``` +cd /home/alex/Documents/OpenFUT/openfut-launcher/openfut-hook && \ + cargo build --release --features fifa17 --target x86_64-pc-windows-gnu +``` +Deploy as `version.dll` per launcher setup. Env gates (all default OFF): +`OPENFUT_SBC_HOOK=1` (read-only resolve+log), `OPENFUT_SBC_ARM_ONLY=1` (Tier-0), +`OPENFUT_SBC_POPULATE=1` (Tier-1, currently logs the blocker and returns). +Rollback: unset env vars and relaunch FIFA. + +### Step 5 — Cleanup +Unset all `OPENFUT_SBC_*` env vars; relaunch FIFA to a clean state. + +--- + +## 4. Crash-risk assessment + +1. **Cold-calling `0x18017b2b0` without a real seated cursor** — CLEARS M + (`0x18015f3a0`) first, then `mov rdi,[rdi]` byte-scan on a garbage ptr → wipes + state + segfault. HIGHEST. Tier-1 code refuses this until the reader is reversed. +2. **Writing `B+0x08`/`B+0x20`** — forces isValid into the QPC-deadline branch; stale + deadline → gate SHUTS (modal), or garbage-ptr iterate crash. Self-defeating. + Tool/code write ONLY `B+0x28`. +3. **Populate off the game thread / mid-iterate** — lazy getter allocates on game heap, + appender mutates EASTL vectors; a foreign thread races the allocator/menu iterate → + heap corruption. Tier-1 must run on the game/message-pump thread with the menu closed. +4. **Skipping the index-rebuild finalizers** (`0x180160e00/0x180160f30/0x180161020`) + after append → stale `+0x9e0/+0xa10/+0xa40` indices → by-index getter `0x180160a80` + reads OOB → crash/garbage tiles. +5. **`WORD[M+0x50]` > actual 0xf0-stride entries** → tile loop walks past vector end + (OOB read). +6. **Hand-built 0xf0/0x3570 structs fed to `0x18015a770`** — copy-ctor `0x18015a2b0` + deep-copies inner EASTL sub-vectors; bad begin/end/cap → heap corruption. Avoid. +7. **No refresh after populate** (non-crash) — controller keeps the cached empty M at + `ctrl+0x140`; `0x1800b5eda` won't re-run → still 2 placeholder tiles. Fire + `0x756c`–`0x7574` or re-open. +8. **Manager/store null** — deser does `mov rax,[rbx]` on the manager; registry lookup + (hashes `0xed84b11`/`0xed84b12`) returning null → null-deref. Live registry + `*[0x1802c2988]` non-null, so low risk; hook must still null-check M/store. + +Tier-0 (single `B+0x28=1` write, B+0x08 left 0) is the verified-SAFE case: isValid +short-circuits to 1, renders empty, no crash; bg-thread-tolerant like the /proc poke. + +--- + +## 5. Poke tool + DLL-spec locations + +- Poke tool (read-only default; `--spec`; `--apply` = ONLY `BYTE[B+0x28]=1`): + `/home/alex/Documents/OpenFUT/fifa17-recon/tools/sbc_hook_poke.py` +- Negative-control byte poke (older, triple-guarded): + `/home/alex/Documents/OpenFUT/fifa17-recon/tools/sbc_populate_poke.py` +- Slide/read template + FNV control proof: + `/home/alex/Documents/OpenFUT/fifa17-recon/tools/gate_byte_probe.py` +- DLL integration spec (RVA math, object graph, gate disasm, function-signature table, + 3 intervention tiers, 8-item crash register, staged test plan): + `/home/alex/Documents/OpenFUT/fifa17-recon/docs/sbc-hook-dll-spec.md` +- Injected-DLL module (fifa17-only; Tier-0 live, Tier-1 scaffolded/refusing): + `/home/alex/Documents/OpenFUT/openfut-launcher/openfut-hook/src/sbc_hook.rs` + (wired via `lib.rs` `#[cfg(feature="fifa17")] mod sbc_hook;` + `fifa17.rs` + `crate::sbc_hook::install();`) +- Atoms table: `/home/alex/Documents/OpenFUT/fifa17-recon/docs/fut_atoms.tsv` + +--- + +## Client-vs-server authority boundaries (flagged) + +- **RENDER (M, tiles at `0x1800b5eda`)** — CLIENT. The client draws tiles solely from + M; the server never touches this. Fix is client-side. +- **POPULATE (deser `0x18017b2b0` → M)** — CLIENT parser, SERVER-fed data. The parser + is native and reusable; the DATA it needs (`/sbs/sets` JSON) is a server response. + Preferred fix has the bridge/core supply that JSON so the client parses it natively. +- **PREFETCH GATE (issue `GET sbs/sets`)** — SERVER/front-end. THE WALL. No native + flag; the SWF/ActionScript front-end refuses to request offline, and CardsDLL has no + native code that issues the GET (`ut/%s/sbs` unreferenced). This cannot be fixed + server-side by responding — the request is never sent. The hook must force the + populate (inject the response at the message layer or drive the parser). +- **READY-ARM (`B[+0x28]`, callback `0x1800b8c30`/commit `0x1800c21a0`)** — CLIENT. + Normally armed by the completion callback (server-response-driven); offline the hook + arms it (Tier-0 byte, or Option 1 lets the native callback arm it). diff --git a/fifa17-recon/docs/plan-2026-08-07-sbc-predicate.md b/fifa17-recon/docs/plan-2026-08-07-sbc-predicate.md new file mode 100644 index 0000000..4f77bd0 --- /dev/null +++ b/fifa17-recon/docs/plan-2026-08-07-sbc-predicate.md @@ -0,0 +1,138 @@ +# SBC "problem communicating with the FIFA Ultimate Team servers" — definitive analysis + +**Date:** 2026-08-07 +**Binary under study:** `/tmp/fut/cardsdll.dll` (on-disk PE, image base `0x180000000`; copy of `/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll`) +**Method:** clean-room, read-only. On-disk `objdump` re-verified in this pass; live values quoted from prior read-only `/proc//mem` reads (pid 12201, slide `0x6ffe7c140000`, FNV control MATCH). No memory was written; FIFA was not touched. + +--- + +## VERDICT (one line) + +**The SBC modal is a CLIENT-SIDE, per-feature completion-path defect — the FUT client never re-arms a fetch/re-render for `sbs/sets` the way it does for the hub — so NO server response can cure it; the only offline lever is a client-memory patch, and the clean single-byte patch (`model+0x1fa00 = 1`) only SUPPRESSES the modal by forcing the completion predicate true, rendering from an empty, never-populated cache. It is NOT the go-online wall.** + +--- + +## 1. What the SBC completion predicate actually checks (CONFIRMED on-disk) + +The SBC menu entry runs a completion continuation whose gate is the shared predicate **`0x180065d40`**, called as `[cache_vtable+0x08]`. Re-disassembled this pass, byte-for-byte: + +``` +180065d40 call 0x1801642c0 ; online/liveness sub-check +180065d4e test al,al +180065d50 je fail +180065d52 cmp byte [rbx+0x28],0 ; <-- THE GATE: "value ready" flag +180065d56 je fail +180065d58 cmp qword [rbx+0x8],0 ; pending-op ptr +180065d5d je pass (mov al,1) ; empty-collection shortcut -> success +180065d5f lea rcx,[rsp+0x38] +180065d64 call QueryPerformanceCounter ; [rip]->0x1801e50c0 +180065d6a mov rax,[rbx+0x20] ; QPC deadline +180065d6e sub rax,[rsp+0x38] +180065d73 js fail ; deadline passed -> fail +180065d75 mov al,1 ; pass +... +180065d7d xor al,al ; fail +``` + +Reduces to: `subcheck() && byte[cache+0x28]!=0 && (qword[cache+0x08]==0 || deadline[cache+0x20] not yet past)`. + +- **The online/liveness sub-check `0x1801642c0` is stubbed OUT.** On-disk bytes are `b0 01 c3` = `mov al,1; ret` — always true, in the shipped file (not a live loader patch). **This is the reason SBC is NOT the go-online wall** (see §5). +- `cache` (`rbx`) is an **embedded sub-object of the FUT root singleton** `A = *[0x1802e6398]`, selected by a vtable thunk (see §2). Its `+0x28` byte is a "value-ready" flag (init 0 by ctor `0x180062460`); `+0x08` is a pending-op pointer; `+0x20` is a QPC deadline. This is a copyable future/async-result value type. **The predicate never reads the parsed SBC categories, HTTP status, session, or any live-connection boolean.** + +> **AUTHORITY BOUNDARY:** everything the predicate reads lives inside client process memory (`A+…`). Nothing in the `sbs/sets` HTTP response is an input to it. This is a **client-authority** decision end to end. + +--- + +## 2. Why hub passes but `sbs/sets` fails (CORRECTED after adversarial verification) + +Both features run the **same predicate function** `0x180065d40`, but on **different embedded caches**, reached through **different per-response-class continuations**. That structural divergence is real and confirmed. **The originally-stated reason ("hub passes because its cache `+0x28` is set") is WRONG** and is corrected here — corroborated by a live measurement (HUB cache `+0x28 = 0` while the hub is displayed with no modal) and by the on-disk FALSE-branch disassembly gathered this pass. + +### The two continuations, side by side (on-disk, this pass) + +| | SBC (`FutLoadSetTypesServerResponse`) | HUB (`FutGetHubDataServerResponse`) | +|---|---|---| +| continuation | `0x180154860` | `0x180173770` | +| get singleton A | `call 0x18011a830` (`mov rax,[0x1802e6398]`) | same | +| select cache | `call [rdx+0x4e8]` → thunk `0x18011c1f0` = `lea rax,[rcx+0x1f9d8]` → **SBC cache A+0x1f9d8** | `call [rdx+0x1f8]` → thunk `0x18011a810` = `lea rax,[rcx+0x1fd70]` → **HUB cache A+0x1fd70** | +| predicate | `call [rdx+0x08]` = `0x180065d40` | **same** `0x180065d40` | +| on TRUE (jne) | render `0x18015491a → 0x180154600` | render `0x18017383d → 0x1801735e0` | +| **on FALSE** | `lea rdx,[rbp-0x9]` (descriptor `0x18020a8b8`); **`call 0x18016c330`**; `jmp` return | **`call 0x1801213b0` (state reset)**; `lea 0x1801736f0` (continuation fn); **`call 0x18011f8e0` (register completion closure)**; `lea 0x18022cd30` (descriptor); **`call 0x18016c330`**; **`call 0x18011f900` (cleanup)** | + +### What this proves + +1. **`0x18016c330` is NOT an SBC-only "modal" function.** The HUB continuation calls the very same `0x18016c330` (at `0x18017382c`) on its own not-ready branch. It is a shared, descriptor-parameterized async dispatcher; SBC passes descriptor `0x18020a8b8`, hub passes `0x18022cd30`. + +2. **At idle both predicates return FALSE.** Live: HUB cache `A+0x1fd70+0x28 = 0` **and** SBC cache `A+0x1f9d8+0x28 = 0`, both `+0x08 = 0`. The hub is on screen with no modal *while its own predicate would return FALSE*. So "hub `+0x28` is set" is false; a set flag is not what makes the hub pass. + +3. **The real asymmetry is the FALSE-branch work.** On not-ready the HUB continuation **resets its request-state region** (`0x1801213b0`), **registers a completion closure** (`0x18011f8e0`, continuation `0x1801736f0`) so the arriving response re-runs the continuation and re-renders, then cleans up (`0x18011f900`). It is a proper get-or-fetch: cache-miss → (re)issue request → render on completion. **The SBC continuation does NONE of that** — it fires the dispatcher once with delegate `0x180154590`/descriptor `0x18020a8b8` and returns. It never re-arms a fetch and never wires the `sbs/sets` response back into a re-render. + +**Conclusion:** hub and SBC diverge at the cache-selection call site (`[rdx+0x1f8]` vs `[rdx+0x4e8]`, one instruction apart), and — decisively — in the not-ready handling. The modal is produced **downstream in the SBC dispatched path** (dispatcher `0x18016c330` + delegate `0x180154590`), because the SBC feature is wired as a one-shot with no re-fetch/re-render, whereas the hub is wired as a self-rearming get-or-fetch. It is **not** decided by cache selection alone, **not** by the shared predicate, and **not** by the `+0x28` byte value at idle. + +--- + +## 3. VERDICT by route — is SBC beatable, and how? + +| Route | Outcome | Why | +|---|---|---| +| **A. Server response field / header / status** | **RULED OUT — no offline fix here** | No field in the `sbs/sets` body reaches the predicate (client-authority §1). Deeper: the SBC continuation never registers a completion closure to consume the response and re-render, so *even a perfect response is dropped on the floor*. The deserializer `0x18017b2b0` returning TRUE is genuinely irrelevant. | +| **B. Client memory byte patch** `model+0x1fa00 = 1` | **Suppresses the modal, but empty menu — cosmetic** | Forces predicate TRUE → routes to the SBC render branch `0x18015491a → 0x180154600`, which reads the embedded SBC cache. That cache was never populated (`+0x08 == 0`, empty collection), so the likely result is an empty / non-functional SBC screen, not populated SBCs. **Untested under the read-only rule.** | +| **C. Config `FUT/SBC_USE_STUBS`** (rdata `0x1802270f8`) | **Not the gate** | Read at the deser top only; the normal (off) path already runs. Flipping it does not touch `+0x28` or the continuation wiring. | +| **D. "Needs the go-online wall solved"** | **REFUTED** | The only connection-like sub-check on this path (`0x1801642c0`) is stubbed to always-true on-disk. SBC is blocked by local per-feature completion wiring, not by the reconnect gate. See §5. | +| **E. Client CODE patch of the SBC FALSE-branch** | **The only route to a *functional* SBC menu** | Make `0x180154860`'s not-ready branch replicate the hub's sequence: state reset `0x1801213b0` + register completion closure `0x18011f8e0`/`0x1801736f0` + dispatch + cleanup `0x18011f900`, so the `sbs/sets` response is fetched and rendered. This is a code patch, not a byte flip and not a server change. Out of scope for a server-side preservation fix; a client-side authority modification. | + +**Bottom line:** there is **no server-side fix**. SBC is "beatable" only in the client-authority sense — either cosmetically (byte B, hides the modal over an empty menu) or functionally (route E, a code patch replicating the hub's re-arm). Neither is a change our offline server can make. + +--- + +## 4. Memory patch details (if used) — flagged CLIENT-SIDE AUTHORITY + +> **CLIENT-SIDE AUTHORITY — this is a modification of the FIFA client's own process memory, not an OpenFUT server response. It changes what the client decides, and it violates the current read-only rule; it is documented for completeness, not endorsed as the fix.** + +- **Cosmetic modal-suppression (route B):** + - **Absolute displacement into FUT root singleton:** `A + 0x1f9d8 + 0x28` = **`model + 0x1fa00`**, where `A = *[0x1802e6398]`. + - **Live absolute (pid 12201 snapshot):** `0xb8402538 + 0x28 = 0xb8402560`. + - **Value:** write `0x01` (one byte). + - **Effect:** predicate `0x180065d40` short-circuits at `cmp byte[rbx+0x28],0` → with `+0x08==0` the empty-collection shortcut returns TRUE → continuation `jne 0x18015491a` renders. **Modal gone; SBC cache empty → expect an empty/possibly-broken menu.** Not verified (read-only). + - **Persistence:** the object is embedded in the singleton (singleton lifetime). The SBC path calls only `[vt+0x08]`; nothing on this path calls the invalidator `[vt+0x10]=0x180065d20`, so a write should persist across menu re-entry (inferred from structure, not demonstrated). + +- **Functional fix (route E)** requires a `.text` patch to the SBC continuation, not a data byte — see §3 row E. Do not confuse the two. + +--- + +## 5. Relationship to the online-modes / go-online-wall finding + +SBC is **not** the same wall as online Draft's "PRESS Q TO RECONNECT": + +- The single connection-like sub-check reachable from the SBC predicate, `0x1801642c0`, is compiled out (`mov al,1; ret`) in the shipped binary. The SBC gate therefore encodes **no** unmet network condition — it is a purely local completion-wiring problem. +- The online modes differ structurally: their gate keeps a real pending network op at `+0x08` and/or a non-stubbed sub-check, so their predicate encodes a network state a local byte-flip cannot satisfy. That is why the online wall is not beatable by a byte and SBC's modal is (cosmetically). +- This is consistent with the prior **"refusing modes = no server fix"** finding: no field, count, header, or status in any HTTP response flips the client-side completion state for these features. SBC extends that finding with the precise mechanism — the client never re-arms the `sbs/sets` fetch/re-render at all. + +--- + +## Appendix — confirmed addresses (image base `0x180000000`) + +| Symbol | Address | Note | +|---|---|---| +| FUT root singleton getter | `0x18011a830` | `mov rax,[0x1802e6398]; ret` | +| FUT root singleton ptr | `[0x1802e6398]` | live `A = 0xb83e2b60` | +| FUT root vtable (static) | `0x18021c2a0` | | +| SBC cache selector thunk | `0x18011c1f0` | `lea rax,[rcx+0x1f9d8]` (slot `A.vt+0x4e8`) | +| HUB cache selector thunk | `0x18011a810` | `lea rax,[rcx+0x1fd70]` (slot `A.vt+0x1f8`) | +| SBC cache | `A+0x1f9d8` | vtable `0x1801fae70`; live `0xb8402538` | +| HUB cache | `A+0x1fd70` | vtable `0x18021c1e0` | +| shared predicate `isValid` | `0x180065d40` | `cache.vt+0x08` for both | +| stubbed online sub-check | `0x1801642c0` | `b0 01 c3` = `mov al,1; ret` | +| cache ctor / copy-ctor | `0x180062460` / `0x1800c21f3` | init `byte[+0x28]=0` | +| invalidator | `0x180065d20` | `cache.vt+0x10`; not called on SBC path | +| SBC continuation | `0x180154860` | class `RS4:FutLoadSetTypesServerResponse` (str `0x1802270b8`, vt row `0x180227090`) | +| HUB continuation | `0x180173770` | class `RS4:FutGetHubDataServerResponse` (str `0x18022ce40`, vt row `0x18022ce18`) | +| shared async dispatcher | `0x18016c330` | called by BOTH FALSE-branches (SBC `0x180154913`, HUB `0x18017382c`) | +| SBC delegate / descriptor | invoke `0x180154590` / desc `0x18020a8b8` | | +| HUB re-arm: state reset | `0x1801213b0` | HUB-only, `0x1801737bd` | +| HUB re-arm: register closure | `0x18011f8e0` (cont. `0x1801736f0`) | HUB-only, `0x180173815` | +| HUB re-arm: cleanup | `0x18011f900` | HUB-only, `0x180173836` | +| SBC render branch (on TRUE) | `0x18015491a → 0x180154600` | reads empty SBC cache | +| `sbs/sets` deserializer | `0x18017b2b0` | returns TRUE unconditionally (`mov al,1 @0x18017b751`); irrelevant to predicate | +| QueryPerformanceCounter import | `0x1801e50c0` | | + +**Which prior conclusion won:** the structural divergence (same predicate, different cache, different continuation; online sub-check stubbed; not server-fixable) is upheld. The specific pass/fail *reason* is corrected: it is the **FALSE-branch re-arm asymmetry**, not a set `+0x28` byte and not an SBC-exclusive `0x18016c330`. diff --git a/fifa17-recon/docs/plan-2026-08-07-sbc-response-reconcile.md b/fifa17-recon/docs/plan-2026-08-07-sbc-response-reconcile.md new file mode 100644 index 0000000..e7db1aa --- /dev/null +++ b/fifa17-recon/docs/plan-2026-08-07-sbc-response-reconcile.md @@ -0,0 +1,211 @@ +# FIFA 17 SBC response reconciliation + +**Verdict:** the live client receives HTTP 200 for `GET /ut/game/fifa17/sbs/sets`, but the +typed `FutSBCLoadCategoryDetailsServerResponse` deserializer is not invoked. The evidence +does **not** identify a server-controlled header, envelope field, or correlation value that +can fix this. The previous `0x180154860` “SBC continuation” diagnosis was based on the wrong +request class and is retracted. + +## Scope and authority + +This pass used only: + +- the shipped `CardsDLL_Win64_retail.dll` copied to `/tmp/fut/cardsdll.dll`; +- read-only `/proc//mem` access to the running game; +- the local OpenFUT request log; and +- existing clean-room notes and scripts in this repository. + +No game memory was written, no breakpoint was inserted, and no service or game process was +restarted during the measurement. + +## Fresh live observation + +The control run used fresh FIFA process **PID 59054**. The CardsDLL mapping resolved to +`0x6ffffc140000`, giving slide `0x6ffe7c140000`. Bytes at static control function +`0x180180d00` matched the on-disk DLL, proving the mapping/slide before data reads. + +At the FUT hub, before opening SBC: + +- `A = *[0x1802e6398] = 0xb78f7c50`; +- `M = *(A+0x20a68) = 0`; +- hub cache byte `*(A+0x1fd70+0x28) = 1` (fresh hub response ready); and +- SBC cache byte `*(A+0x1f9d8+0x28) = 0`. + +The user then opened the SBC tile. The real client exchange was: + +```text +[10:20:20] GET /ut/game/fifa17/sbs/sets +User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows) +Accept: application/json +Content-Type: application/json +X-UT-SID: OPENFUT-SID-0000000000000001 +Accept-Encoding: gzip +-> 200 {"categories":[...]} +``` + +The game displayed “There was a problem communicating with the FIFA Ultimate Team servers.” +With that modal still open, the same slide was re-proved and `M` was still exactly zero. + +### What `M == 0` proves + +The typed `/sets` deserializer is `0x18017b2b0`. At `0x18017b309`–`0x18017b327` it obtains +the FUT root and calls vtable slot `+0x9b0`, the lazy getter `0x18011b7d0`. That getter +allocates and stores `A+0x20a68` before the deserializer examines the root object or the +`categories` key. + +Consequently: + +- valid JSON would leave `M` non-null; +- malformed or empty JSON reaching this function would also leave `M` non-null; and +- `M == 0` after the completed HTTP transaction means `0x18017b2b0` was not invoked. + +The normal reset of `M` is `0x180114ee0`; its observed use belongs to broad FUT-root +initialization/reset work, not the `/sets` completion path. There is no evidence that the +deserializer ran and then immediately cleared `M` during this transaction. + +## Correct class map + +Three classes were conflated in earlier notes: + +| Function/class | Proven URI | Role | +|---|---|---| +| `FutSBCLoadCategoryDetailsServerResponse`, request URI builder `0x18017a980`, factory `0x18017aa10`, response deser `0x18017b2b0` | `/sets` under the `ut/%s/sbs` base | Initial category/set list; this is the live failing request | +| `FutSBCSetDataServerResponse`, factory `0x18016fca0`, deser `0x18016fe90` | `/squadBuildingSets` (`0x18022bd88`) | Parses `reset`; not the observed `/sbs/sets` request | +| `FutLoadSetTypesServerResponse`, deser `0x180154990` | `/challenge/%d/squad` (`0x1802270e0`) | Parses `challengeId`, `playerRequirements`, and `squad`; later challenge flow | + +This corrects two prior claims: + +1. `FutSBCSetDataServerResponse` does **not** share the literal `/sets` URI in this binary; + its URI string is `/squadBuildingSets`. +2. `0x180154860` is not a dedicated completion continuation for the initial category-list + request. `0x180154830` is a generic callback thunk used by multiple request classes, while + the nearby `0x180154990` parser and `/challenge/%d/squad` URI belong to + `FutLoadSetTypesServerResponse`. + +Therefore the earlier hub-versus-`0x180154860` comparison contrasted the hub with a later +challenge-squad operation, not with `GET /sbs/sets`. Its proposed “copy the hub re-arm path” +fix is unsupported for the category-list failure. + +## What the generic completion code actually checks + +The shared request completion routine `0x18016cca0`: + +1. calls request vtable slot `+0x80` at `0x18016cd32` to create the class-selected typed + response object; +2. stores the received status at request offset `+0x48` (`0x18016cd3d`); and +3. compares it with decimal 200 at `0x18016cdd0`. + +Exactly 200 takes the success branch to `0x18016d0b9`. Non-200 status invokes the error +translation path through request slot `+0x60` first. Response construction is selected by +the request vtable; it is not selected by an HTTP response header or a JSON envelope field. + +No pre-deserialization branch found in this path reads `Content-Type`, a request/correlation +ID, the `X-UT-SID` response header, or a top-level JSON key. The live server already supplies +the one proven transport-level success input: status 200. + +## Hub comparison + +The fresh hub response was consumed successfully and set the hub cache byte to one. After +the subsequent navigation its resting value returned to zero. The SBC cache byte remained +zero. This confirms that cache `+0x28` is transient async-result/TTL state; a later resting +zero does not establish which completion branch ran. + +The previous report's live snapshot—where both values were zero long after the requests—was +therefore insufficient to infer the hub/SBC divergence. The fresh before/after measurement +supersedes it. + +## Server-fixability verdict + +**Not demonstrated.** In particular: + +- changing the category JSON cannot make the typed parser start, because the lazy store is + allocated before any JSON key is inspected; +- the server already returns the proven success status, 200; +- request-class/response-class selection is client-owned; and +- no header, envelope, or correlation field was found feeding a pre-parser decision. + +This does not mathematically prove that no transport variation could ever affect the client. +It does prove that the specific server-fix candidates proposed by the killed workflow were +speculative and had no reading instruction behind them. + +## Exact remaining unknown and next measurement + +The unresolved boundary is between: + +```text +ProtoHttp completion with status 200 + -> class-selected response object creation + -> delivery of response bytes/SAX cursor + -> response vtable +0x08 (`0x18017b2b0`) +``` + +The next useful experiment is transient tracing of calls—not another resting-state scan. +Instrument, in a disposable/local diagnostic build or a non-mutating tracing facility: + +- request factory `0x18017aa10`; +- typed deserializer `0x18017b2b0`; +- generic completion entry `0x18016cca0` and its status at `0x18016cdd0`; and +- the generic response-body/SAX dispatch site that calls response vtable slot `+0x08`. + +Record whether the factory is called, whether it returns an object with vtable +`0x18022e5b0`, and whether a body/SAX object is delivered. That separates three remaining +client-side possibilities: wrong request instance despite the URI, typed object created but +body not attached, or body attached but virtual deserialization dispatch skipped. + +Until that transient trace exists, the defensible implementation direction remains the +client-side hook described in `docs/sbc-hook-dll-spec.md`, but its rationale must be stated +as “native category deserializer is not reached,” not the retracted `0x180154860` +hub-rearm theory. + +## 2026-08-07 passive-trace result: deserialization is proven + +The first gated passive client trace supersedes the final inference above. During exactly +one SBC navigation, with every mutation feature disabled, the hook recorded: + +```text +SBC_TRACE: factory entry=1 exit=1 tid=652 this=0xb80cd910 result=0x7a99178; +deser entry=1 exit=1 tid=652 this=0x7a99178 reader=0x7fcff7f8 result=true +``` + +The matching UTAS request occurred at `11:09:01`: `GET /ut/game/fifa17/sbs/sets` returned +HTTP 200 with one category and two sets. No degraded hook state was reported, and FIFA +remained alive until the operator closed it after the single permitted attempt. + +This proves all of the following for the observed request: + +- the category response factory is called exactly once and returns a non-null object; +- the native category deserializer is called exactly once on that same object; +- the body reader is non-null; +- deserialization returns success (`true`); and +- both calls return normally on the same native thread. + +Therefore the earlier `M == 0` resting snapshot did not prove that `0x18017b2b0` was +skipped. The failure boundary is now strictly **after successful native deserialization**. +The next measurement must trace the response object's post-deserializer completion, +ownership handoff, and publication into the SBC UI/cache collection. Repeating the factory +or deserializer trace will not add useful information. + +## 2026-08-07 post-deserializer handoff trace + +A second one-shot run combined the factory/deserializer probes with atomic replacements of +the category request vtable slots `+0x90` (completion callback dispatch) and `+0x88` +(response ownership transfer). All four calls completed on native thread 656: + +```text +request = 0xb80cdfe0 +factory response = 0x7c94808 +deserializer this = 0x7c94808, result=true ++0x90 callback argument = 0x7c94808 ++0x88 owner-slot address = 0xbc51f7e8 +``` + +The matching `GET /ut/game/fifa17/sbs/sets` at `11:24:00` returned HTTP 200, and the same +communication modal appeared. Both callback probes reported `entry=1 exit=1`; no degraded +hook state or process failure occurred. + +This proves that the parsed response reaches the category request's completion dispatcher +and that its ownership-transfer routine also returns normally. The remaining failure +boundary begins at the receiving owner object's vtable `+0x18` consumer invoked from +`0x1801631e0`, or later collection/cache/UI validation. Network transport, response +construction, native parsing, callback dispatch, and request-side ownership handoff are no +longer candidate root causes. diff --git a/fifa17-recon/docs/sbc-hook-dll-spec.md b/fifa17-recon/docs/sbc-hook-dll-spec.md new file mode 100644 index 0000000..a5cc6a2 --- /dev/null +++ b/fifa17-recon/docs/sbc-hook-dll-spec.md @@ -0,0 +1,337 @@ +# SBC render intervention — injected-DLL integration spec + +**Goal:** make the FIFA 17 FUT **SBC menu render real SBC data** from inside the +process (client-side), proven not server-fixable. The DLL is the existing +`openfut-hook` (`version.dll`, cross-compiled `x86_64-pc-windows-gnu`, feature +`fifa17`). In-process calls to client functions are safe here (unlike `/proc/mem` +writes), because we run on the game's own threads with the real allocator. + +**Binary of record (clean-room):** `/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll` +(on-disk copy `/tmp/fut/cardsdll.dll`), PE image base `0x180000000`. Every address +below was re-verified byte-exact against this PE in this pass (vtable slots read from +`.rdata`, prologues from `.text`). Do **not** build/deploy from this spec without the +staged morning test (§9). + +--- + +## 1. Module base + RVA math + +CardsDLL is **not** present at `DllMain`/worker time — the boot module dump +(`C:\openfut_hook.log`) has no `CardsDLL*` entry. It is loaded lazily **only when the +user first enters Ultimate Team**. Therefore the hook must **defer** and poll for it, +exactly like `probe::install_probes_deferred` polls for `anadius64.dll`. + +- Loaded module name (Wine keeps the on-disk filename): **`CardsDLL_Win64_retail.dll`**. + `GetModuleHandleA(b"CardsDLL_Win64_retail.dll\0")`. Fallback: ToolHelp module walk + matching a name containing `CardsDLL` (see `fifa17::dump_modules` for the pattern). +- Image base in the PE is `0x180000000`. For any static VA in this doc: + + ``` + rva = VA_static - 0x180000000 + VA_runtime = cards_base + rva + ``` + + `cards_base` is the runtime `HMODULE` of `CardsDLL_Win64_retail.dll` (its in-memory + load address). All the "0x180…" addresses below are **static VAs**; subtract + `0x180000000` to get the RVA, add `cards_base` to get the live pointer/callable. + +- Slide-proof control (optional sanity, mirrors `tools/gate_byte_probe.py`): the FNV + prologue at VA `0x180180d00` must match the on-disk PE bytes + `48 83 ec 28 48 85 c9 74 50 45 33 c0 ba c5 9d 1c 81 …`. If it does not, **abort** — + the module map moved and the offsets are untrustworthy. + +--- + +## 2. Verified object graph + +``` +A = FUT root singleton = *(0x1802e6398) getter thunk 0x18011a830 = { mov rax,[rip→0x1802e6398]; ret } + A.vtable (live [A]) = static 0x18021c2a0 + A.vtable[+0x4e8] = 0x18011c1f0 = { lea rax,[rcx+0x1f9d8]; ret } -> B getter + A.vtable[+0x9b0] = 0x18011b7d0 = M lazy getter (see §3) -> M getter + A.vtable[+0x18] = 0x180113f50 = service-id 0xed84b12 -> returns `this` (proves manager == A) + +B = SBC request/ready TTL cache = A + 0x1f9d8 vtable static 0x1801fae70 + B+0x08 collection ptr (live 0 offline) + B+0x20 QPC deadline + B+0x28 ready byte (== A+0x1fa00 alias) <- the isValid gate byte + B.vtable[+0x00] dtor = 0x180063040 + B.vtable[+0x08] isValid = 0x180065d40 (see §4) + B.vtable[+0x10] clear = 0x180065d20 + +M = SBC categories/sets store = *(A + 0x20a68) <- THE RENDER SOURCE (see §3, §5) + M+0x50 WORD category count + M+0x58 cat-vector begin (element stride 0xf0) + M+0x60 cat-vector end + M+0xa10 secondary/featured vec begin (8-byte elems) (emptiness-checked at render) + M+0xa18 secondary vec end + per category (+0xf0 stride): + cat+0xb8 WORD set count + cat+0xc0 set-vector begin (element stride 0x3570) + set+0x1c9 byte per-set flag +``` + +HUB cache (works online) is the **same class** at `A + 0x1fd70` (vtable `0x18021c1e0`) +— reference only. + +**Manager fetch used by BOTH the deser and the render controller** (so +populate-target == render-source): + +``` +reg = 0x1800d7170() ; -> ®istry (static 0x1802c2988) +mgr = 0x180009c80(&out, reg) ; out = manager (hashes 0xed84b11 / 0xed84b12) +M = mgr.vtable[+0x9b0](mgr) ; 0x18011b7d0, lazily creates/returns *(A+0x20a68) +``` + +Because svc-id `0xed84b12` resolves to `A` (A.vtable[+0x18] returns `this`), +`mgr == A` and `mgr.vtable[+0x9b0] == A.vtable[+0x9b0] == 0x18011b7d0`. The hook may +therefore fetch M the short way — `A = *(0x1802e6398); M = (*(void***)A)[0x9b0/8](A)` — +**or** the long way (registry) — they return the identical object. + +--- + +## 3. M lazy getter — 0x18011b7d0 (verified disassembly) + +``` +18011b7d0 push rbx; push rdi; sub rsp,0x38 +18011b7e0 mov rdi,rcx ; rcx = A (this) +18011b7e3 cmp QWORD [rcx+0x20a68],0 ; M already built? +18011b7eb jne 18011b873 ; yes -> return it +18011b7f1 call 0x18019e3c0 ; factory: allocate an EMPTY M (type-id 0x13f0) + … … ; init fields, cache at A+0x20a68, return +``` + +Cold-calling this alone **creates an EMPTY M** (`WORD[M+0x50]==0`) → the menu draws +**2 placeholder tiles** (count+2). It does **not** populate. Populating is §5. + +--- + +## 4. The gate — isValid 0x180065d40 (verified disassembly) + +``` +180065d40 push rbx; sub rsp,0x20; mov rbx,rcx ; rcx = B +180065d49 call 0x1801642c0 ; online sub-check — STUBBED `mov al,1;ret` +180065d4e test al,al ; je fail ; never the wall +180065d52 cmp BYTE [rbx+0x28],0 ; je fail ; <-- READY BYTE gate +180065d58 cmp QWORD [rbx+0x8],0 ; je 0x180065d75 ; <-- if collection==0 -> RETURN 1 (short-circuit) +180065d5f lea rcx,[rsp+0x38]; call [rip→0x1801e50c0]; QueryPerformanceCounter +180065d6a mov rax,[rbx+0x20]; sub rax,[rsp+0x38] ; deadline - now +180065d73 js fail ; past deadline -> fail +180065d75 mov al,1 ; …; ret ; success +``` + +**Load-bearing correction (adversarially confirmed, verified in this pass):** arm +**only** `BYTE[B+0x28]=1` and **leave `QWORD[B+0x08]=0`**. With `B+0x08==0` the function +takes the `je 0x180065d75` short-circuit and returns 1 immediately. If you instead +write `B+0x08` (pointing it at the collection), isValid falls into the QPC-deadline +branch; with the live-stale deadline (`B+0x20 = 0xf10fb8cb9`, already in the past) it +returns **0 → modal → gate SHUTS**. So **never** manually write `B+0x08` or `B+0x20`. +Rendering reads **M** (§5), not `B+0x08`, so nothing needs `B+0x08` set. + +--- + +## 5. Render source — M, not B (verified disassembly) + +Controller ctor caches M into `controller+0x140`: + +``` +1800b554d call 0x1800d7170 ; reg +1800b555d call 0x180009c80 ; mgr = out +1800b556b mov rax,[rbx] ; mgr.vtable +1800b5571 call [rax+0x9b0] ; M = 0x18011b7d0(mgr) +1800b5577 mov [rsi+0x140], rax ; controller+0x140 = M + …then registers Scaleform events 0x756c-0x7574 via 0x1801a4a70 +``` + +Tile-count emit (each menu build): + +``` +1800b5eda mov rax,[r13+0x140] ; rax = M +1800b5ee1 movzx ebx,WORD [rax+0x50] ; ebx = category count +1800b5ee5 add bx,0x2 ; +2 placeholder tiles +1800b5ee9 mov rax,[r15] ; Scaleform model vtable + call [rax+0x58](count) ; push (category_count + 2) list tiles +``` + +Helper thunks (verified): `0x18015fff0 = lea rax,[rcx+0x58]` (&M cat-vector), +`0x1801607e0 = lea rax,[rcx+0xa10]` (&M secondary vector). **Zero** reads of +`B`/`A+0x1f9d8`/`A+0x1fa00` exist in the tile-build region — B is purely the entry +gate. Populate M ⇒ tiles appear. + +--- + +## 6. Populate path — reuse the real parser (deser 0x18017b2b0) + +The category rows are appended **only** by the sbs/sets deserializer. Its geometry and +finalizers are the correct way to fill M (hand-building `0xf0`/`0x3570` structs is +brittle and rejected — §8). + +``` +18017b2b0 (rcx = this, IGNORED) (rdx = a PRIMED SAX reader over the token stream) +18017b2ef mov rdi,rdx ; keeps the incoming reader in rdi (the byte source) +18017b2fb call 0x1801c63e0(&localctx, 0, 0) ; builds a SECONDARY ctx with a NULL source +18017b309 call 0x1800d7170 ; reg +18017b316 call 0x180009c80 ; mgr +18017b327 call [mgr.vtable+0x9b0] ; M (0x18011b7d0) + … clear 0x18015f3a0(M) ; ALWAYS clears M first (see crash risk C1) + … loop atom 0x6f "categories": + 0x180159da0(&tmp) ; cat ctor (0xf0, vtable 0x18021b520) + 0x18017ab80(&tmp, reader) ; cat deser (needs the reader) + 0x180160e50(&tmp) ; cat finalize (set index) + 0x18015a770(M, &tmp) ; APPEND (copy-in; copy-ctor 0x18015a2b0) + 0x1801105d0(&tmp) ; cat dtor + … 0x180160e00(M); 0x180160f30(M); 0x180161020(M) ; rebuild M indices (+0x9e0/+0xa10/+0xa40) + … commit mgr.vtable[+0x8](mgr) +18017b751 ret (always true) +``` + +**The reader (`rdx`) is the crux.** The deser does **not** ingest `rdx` through the +`0x1801c63e0` ctx it builds (that one is created with a NULL source, `rdx=0/r8=0`); +instead it keeps the **incoming** `rdx` in `rdi` and scans its bytes directly (e.g. the +NUL-terminated backslash-unescape at `~0x18017b353` does `mov rdi,[rdi]`). So `rdx` +must be a **fully-constructed, already-primed SAX reader/cursor object** seated over +your canned `sbs/sets` JSON — the same object type the message framework produces on a +real response. **Building that reader from scratch is the one remaining un-reversed +contract** (its vtable, and specifically the `[+0x8]` byte-yield slot, are not yet +pinned). Until it is, the fully-offline parser-reuse call is **not turnkey** — see the +three tiers in §7. + +SAX primitives already known (for when the reader is reconstructed): ctx init +`0x1801c63e0(rcx=ctx,rdx=source,r8=flags)`, lexer `0x1801c8060`, next-token +`0x1801c7f10`, begin-object `0x1801c8270`, INT `0x1801c79d0`, STR `0x1801c7aa0`, +BOOL `0x1801c7620`, SKIP `0x180135ff0`. + +Response-msg object (for the message-layer tier): ctor `0x18017b1c0` installs vtable +`0x18022e598`; slot `[+0x20] == 0x18017b2b0` (deser) — **verified**. Constructing this +object alone still does **not** seat the reader (the framework does that from received +bytes), so it doesn't remove the reader gap. + +--- + +## 7. Three intervention tiers (implement in this order) + +**Tier 0 — arm-only negative control (SAFE, non-crash, renders EMPTY).** +Resolve A→B, write `BYTE[B+0x28]=1`, leave `B+0x08=0`. isValid short-circuits true, the +menu opens and draws **2 placeholder tiles** (M empty/null). Proves the gate model live +without any populate. This is the first morning step and the baseline. Implemented and +env-gated in `sbc_hook.rs` (`OPENFUT_SBC_ARM_ONLY=1`). + +**Tier 1 — parser-reuse populate (the intended fix, BLOCKED on the reader).** +On the game thread: build a primed SAX reader over canned `sbs/sets` JSON served by the +bridge/core, `call 0x18017b2b0(rcx=0, rdx=reader)` (self-locates mgr, clears, appends, +finalizes, commits → fills M), then Tier-0 arm (`BYTE[B+0x28]=1` only), then trigger a +menu refresh (§ below). **Cannot be enabled** until the reader contract (§6) is +reversed. `sbc_hook.rs` contains the guarded scaffold that logs the blocker and returns +— it does **not** call the deser with a fabricated reader (that would clear M and/or +crash — C1/C6). + +**Tier 2 — message-layer injection (cleanest long-term, feasibility unproven).** +Push a canned `sbs/sets` response through the real receive path so the framework builds +the response-msg (`0x18017b1c0`), seats the reader itself, runs `0x18017b2b0`, fires the +completion callback (`0x1800b8c30`, subscribed in svc ctor `0x1800b5765` via +`mgr.vtable[+0xa90]`), and arms B natively (generic copy-assign `0x1800c21a0`) — **zero +forged state**. Requires reconstructing the message-receive entry + response-msg wiring; +treat as the target, not the default. + +**Refresh trigger** (Tier 1/2): the controller re-reads `WORD[M+0x50]` at `0x1800b5eda` +on every build, so **re-opening the SBC menu** suffices. Programmatic alternative: fire +Scaleform refresh events `0x756c-0x7574` via `0x1801a4a70`. If M is populated but no +refresh fires and the controller already cached an empty M at `ctrl+0x140`, you still see +2 placeholder tiles (no crash, just no data) — see C7. + +--- + +## 8. Function signatures (Win64 `extern "system"`; rcx, rdx, r8, r9 → rax) + +| Purpose | Static VA | Signature (Rust `unsafe extern "system"`) | +|---|---|---| +| A getter thunk | 0x18011a830 | `fn() -> *mut u8` (returns `*(0x1802e6398)`) | +| B getter (via A vtable +0x4e8) | 0x18011c1f0 | `fn(a: *mut u8) -> *mut u8` (`a+0x1f9d8`) | +| M lazy getter (A vtable +0x9b0) | 0x18011b7d0 | `fn(mgr: *mut u8) -> *mut u8` (`*(mgr+0x20a68)`, lazily built) | +| isValid (B vtable +0x08) | 0x180065d40 | `fn(b: *mut u8) -> bool` | +| registry getter | 0x1800d7170 | `fn() -> *mut u8` | +| manager getter | 0x180009c80 | `fn(out: *mut *mut u8, reg: *mut u8) -> *mut u8` | +| sbs/sets deser (whole) | 0x18017b2b0 | `fn(this_ignored: *mut u8, reader: *mut u8) -> bool` | +| SAX ctx init | 0x1801c63e0 | `fn(ctx: *mut u8, source: *mut u8, flags: u64) -> *mut u8` | +| clear M | 0x18015f3a0 | `fn(m: *mut u8)` | +| cat ctor (0xf0) | 0x180159da0 | `fn(tmp: *mut u8) -> *mut u8` | +| cat deser | 0x18017ab80 | `fn(tmp: *mut u8, reader: *mut u8) -> bool` | +| cat finalize | 0x180160e50 | `fn(tmp: *mut u8)` | +| append into M | 0x18015a770 | `fn(m: *mut u8, tmp: *mut u8)` | +| cat dtor | 0x1801105d0 | `fn(tmp: *mut u8)` | +| M index rebuild ×3 | 0x180160e00 / 0x180160f30 / 0x180161020 | `fn(m: *mut u8)` each | +| QueryPerformanceCounter thunk | 0x1801e50c0 | (indirect; not needed if B+0x08 left 0) | +| Scaleform refresh dispatch | 0x1801a4a70 | `fn(ctrl: *mut u8, event_id: u32, …)` (event ids 0x756c-0x7574) | + +M is **per-session heap** — never hardcode its address; always go A → `A.vtable[+0x9b0]`. + +--- + +## 9. Staged morning test plan (human, live) + +Preconditions: FIFA 17 at the FUT hub (so CardsDLL is loaded). One env var flips each +tier; all default **off/inert**. Watch `C:\openfut_hook.log`. + +1. **Injection + resolution (read-only).** Launch with `OPENFUT_SBC_HOOK=1` only. The + deferred thread should log: CardsDLL base + slide-control OK, then `A=…`, `B=…`, + `B+0x28=0`, `M=*(A+0x20a68)=…` (0 until the SBC menu is opened once). No writes. + *Pass:* addresses match the model; control FNV OK. +2. **Tier-0 arm-only (negative control).** Add `OPENFUT_SBC_ARM_ONLY=1`. Open the SBC + menu. Expected: **menu opens, draws ~2 empty placeholder tiles, no modal, no crash.** + Confirms the gate byte and short-circuit live. If it crashes → stop (means B + resolution is wrong; recheck slide). +3. **Tier-1 populate — BLOCKED.** Do **not** enable until the SAX reader contract (§6) + is reversed. `OPENFUT_SBC_POPULATE=1` currently only logs the blocker and returns. + Next RE session: pin the reader vtable (`[+0x8]` byte-yield) and the reader ctor, + then wire the §6 sequence and re-test on the game thread with the menu **closed**, + then re-open to refresh. +4. Revert env vars to unset when done. + +--- + +## 10. Crash-risk register (verified against the PE + prior adversarial passes) + +- **C1 — cold-calling deser without a real reader.** `0x18017b2b0` **clears M first** + (`0x18015f3a0` before any append). A null/garbage reader → parses nothing but **wipes + M** (renders empty, destroys prior state), and the byte-scan at `~0x18017b353` + (`mov rdi,[rdi]`) segfaults on a bad pointer. This is exactly why Tier 1 is gated off. +- **C2 — clear/finalize race.** Deser clears then rebuilds M's vectors+indices; if the + render thread reads `WORD[M+0x50]` (`0x1800b5eda`) or by-index `0x180160a80` mid-build + → OOB/crash. Populate on the game thread with the menu **closed**, then refresh. +- **C3 — skipping finalizers.** Any manual append via `0x18015a770` **must** be followed + by `0x180160e00`/`0x180160f30`/`0x180161020` or the `+0x9e0/+0xa10/+0xa40` indices go + stale and by-index lookups read OOB. +- **C4 — hand-built `0xf0`/`0x3570` structs.** Append's copy-ctor `0x18015a2b0` + deep-copies EASTL sub-vectors; a bad begin/end/cap → heap corruption. **Rejected** + (§8): drive the real parser instead. +- **C5 — writing `B+0x08`/`B+0x20`.** Forces isValid into the deadline branch; the + live-stale deadline shuts the gate → modal. **Set only `B+0x28`, leave `B+0x08=0`.** +- **C6 — null manager/M.** Deser does `mov rax,[mgr]`; if the registry lookup returned + null it's a null-deref. Live registry `*(0x1802c2988)` is non-null offline, but the + hook must null-check A, mgr, M before any use. +- **C7 — no refresh (non-crash).** Populate without firing refresh / re-open → controller + keeps its cached empty M → still 2 placeholder tiles. Fails the goal, not a crash. +- **C8 — foreign-thread allocation.** The lazy getter and appenders allocate on / mutate + the game heap; running them off the main/render thread races the allocator. Execute the + populate on a game thread (message-pump / a game-thread detour), not a bg thread. The + Tier-0 single-byte arm is tolerant of a bg write (it's what the `/proc` poke does), but + populate is not. + +--- + +## 11. Live-probe baseline (this pass, read-only `O_RDONLY`, zero writes) + +FIFA17.exe **was running** at spec time (pid 12201), CardsDLL mapped. Fresh live reads +this pass match the static model 1:1: + +``` +slide 0x6ffe7c140000 CONTROL FNV OK +A 0xb83e2b60 (= *(0x1802e6398)) +B 0xb8402538 vt=0x1801fae70 (matches static) B+0x08(coll)=0 B+0x20=0xf10fb8cb9 B+0x28(ready)=0 +HUB 0xb84028d0 vt=0x18021c1e0 coll=0 ready=0 (reference only) +M *(A+0x20a68)=0 (SBC menu not opened this session -> M not yet built) +``` + +So live: gate SHUT (`B+0x28=0`), collection null, **M null** — Tier-0 arm alone would +render empty (matches the model). All §2–§6 addresses + all vtable slots were +re-verified byte-exact against the on-disk PE in this pass. diff --git a/fifa17-recon/tools/fifa17-hook-m1.sh b/fifa17-recon/tools/fifa17-hook-m1.sh new file mode 100755 index 0000000..914fe00 --- /dev/null +++ b/fifa17-recon/tools/fifa17-hook-m1.sh @@ -0,0 +1,252 @@ +#!/usr/bin/env bash +# FIFA 17 hook M1 staging/deployment helper. +# +# Safe defaults: +# inspect (the default) is read-only; +# stage writes only below the repository; +# deploy and launch require separate, exact confirmation variables. +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +hook_root="${repo_root}/openfut-launcher/openfut-hook" +default_dll="${hook_root}/target/x86_64-pc-windows-gnu/release/openfut_hook.dll" +stage_root="${repo_root}/fifa17-recon/staging/fifa17-hook-m1" + +game_dir="${OPENFUT_FIFA17_GAME_DIR:-/mnt/games/FIFA 17}" +wine_prefix="${OPENFUT_FIFA17_WINEPREFIX:-/home/alex/Games/umu/fifa17}" +proton_path="${OPENFUT_FIFA17_PROTONPATH:-UMU-Proton-10.0-4}" +hook_dll="${OPENFUT_FIFA17_HOOK_DLL:-${default_dll}}" +system_version="${wine_prefix}/drive_c/windows/system32/version.dll" +deployed_dll="${game_dir}/version.dll" + +required_exports=( + GetFileVersionInfoA GetFileVersionInfoExA GetFileVersionInfoExW + GetFileVersionInfoSizeA GetFileVersionInfoSizeExA GetFileVersionInfoSizeExW + GetFileVersionInfoSizeW GetFileVersionInfoW VerFindFileA VerFindFileW + VerInstallFileA VerInstallFileW VerLanguageNameA VerLanguageNameW + VerQueryValueA VerQueryValueW +) + +die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; } +note() { printf '%s\n' "$*"; } +need_file() { [[ -f "$1" ]] || die "missing file: $1"; } + +sha256() { sha256sum -- "$1" | awk '{print $1}'; } + +pe_exports() { + x86_64-w64-mingw32-objdump -p "$1" | + awk '/\[Ordinal\/Name Pointer\] Table/{in_names=1; next} in_names && /\+base\[/ {print $NF}' +} + +verify_pe64() { + local dll=$1 + local format + format="$(x86_64-w64-mingw32-objdump -f "$dll" | awk '/file format/{print $NF}')" + [[ "$format" == "pei-x86-64" ]] || die "$dll is not a 64-bit PE DLL (format=${format:-unknown})" +} + +verify_exports() { + local dll=$1 export_name + local exports + exports="$(pe_exports "$dll")" + for export_name in "${required_exports[@]}"; do + grep -Fxq "$export_name" <<<"$exports" || + die "$dll lacks VERSION export $export_name; refusing to stage/deploy" + done +} + +verify_inputs() { + command -v sha256sum >/dev/null || die "sha256sum is required" + command -v x86_64-w64-mingw32-objdump >/dev/null || + die "x86_64-w64-mingw32-objdump is required" + need_file "$hook_dll" + need_file "$system_version" + verify_pe64 "$hook_dll" +} + +inspect() { + verify_inputs + note "mode=inspect (read-only)" + note "hook=$hook_dll" + note "hook_sha256=$(sha256 "$hook_dll")" + note "system_version=$system_version" + note "system_version_sha256=$(sha256 "$system_version")" + note "game_dir=$game_dir" + if [[ -f "$deployed_dll" ]]; then + note "deployed_version_sha256=$(sha256 "$deployed_dll")" + else + note "deployed_version=absent" + fi + verify_exports "$hook_dll" + note "version_exports=complete" +} + +build() { + command -v cargo >/dev/null || die "cargo is required" + note "Building the inert FIFA 17 hook into the package-local staging source path." + CARGO_TARGET_DIR="${hook_root}/target" \ + cargo build --offline --release --features fifa17 \ + --target x86_64-pc-windows-gnu --manifest-path "${hook_root}/Cargo.toml" + inspect +} + +stage() { + verify_inputs + verify_exports "$hook_dll" + need_file "${game_dir}/CardsDLL_Win64_retail.dll" + need_file "${game_dir}/FIFA17.exe" + mkdir -p "$stage_root" + local staged="${stage_root}/version.dll" + cp -- "$hook_dll" "$staged" + { + printf 'artifact=%s\n' "$staged" + printf 'artifact_sha256=%s\n' "$(sha256 "$staged")" + printf 'source=%s\n' "$hook_dll" + printf 'source_sha256=%s\n' "$(sha256 "$hook_dll")" + printf 'system_version=%s\n' "$system_version" + printf 'system_version_sha256=%s\n' "$(sha256 "$system_version")" + printf 'cards_dll_sha256=%s\n' "$(sha256 "${game_dir}/CardsDLL_Win64_retail.dll")" + printf 'fifa17_exe_sha256=%s\n' "$(sha256 "${game_dir}/FIFA17.exe")" + } >"${stage_root}/manifest.txt" + note "staged=$staged" + note "manifest=${stage_root}/manifest.txt" + note "No game-directory file was changed." +} + +require_game_stopped() { + if pgrep -fi '(FIFA17|_fifa17)\.exe' >/dev/null; then + die "FIFA 17 appears to be running; close it before deployment" + fi +} + +deploy() { + [[ "${OPENFUT_FIFA17_DEPLOY:-}" == "I_ACCEPT_VERSION_DLL_REPLACEMENT" ]] || + die "deploy requires OPENFUT_FIFA17_DEPLOY=I_ACCEPT_VERSION_DLL_REPLACEMENT" + require_game_stopped + local staged="${stage_root}/version.dll" + local manifest="${stage_root}/manifest.txt" + need_file "$staged" + need_file "$manifest" + verify_pe64 "$staged" + verify_exports "$staged" + local recorded actual + recorded="$(awk -F= '$1=="artifact_sha256"{print $2}' "$manifest")" + actual="$(sha256 "$staged")" + [[ -n "$recorded" && "$recorded" == "$actual" ]] || die "staged artifact hash does not match manifest" + + local backup_dir="${game_dir}/openfut-backups" + mkdir -p "$backup_dir" + if [[ -f "$deployed_dll" ]]; then + local old_hash backup + old_hash="$(sha256 "$deployed_dll")" + backup="${backup_dir}/version.dll.${old_hash}.bak" + if [[ ! -e "$backup" ]]; then + cp -- "$deployed_dll" "$backup" + fi + [[ "$(sha256 "$backup")" == "$old_hash" ]] || die "backup verification failed: $backup" + note "backup=$backup" + fi + cp -- "$staged" "$deployed_dll" + [[ "$(sha256 "$deployed_dll")" == "$actual" ]] || die "deployed DLL hash verification failed" + note "deployed=$deployed_dll" + note "deployed_sha256=$actual" +} + +launch() { + local mode=${1:-baseline} + local hook_enabled=0 + local trace_enabled=0 + local request_trace_enabled=0 + local notifier_trace_enabled=0 + case "$mode" in + baseline) + [[ "${OPENFUT_FIFA17_LAUNCH:-}" == "I_ACCEPT_M1_BASELINE_LAUNCH" ]] || + die "launch requires OPENFUT_FIFA17_LAUNCH=I_ACCEPT_M1_BASELINE_LAUNCH" + ;; + resolve) + [[ "${OPENFUT_FIFA17_RESOLVE:-}" == "I_ACCEPT_M2_RESOLVE_LAUNCH" ]] || + die "launch-resolve requires OPENFUT_FIFA17_RESOLVE=I_ACCEPT_M2_RESOLVE_LAUNCH" + hook_enabled=1 + ;; + trace) + [[ "${OPENFUT_FIFA17_TRACE:-}" == "I_ACCEPT_M3_PASSIVE_TRACE" ]] || + die "launch-trace requires OPENFUT_FIFA17_TRACE=I_ACCEPT_M3_PASSIVE_TRACE" + hook_enabled=1 + trace_enabled=1 + request_trace_enabled=1 + notifier_trace_enabled=1 + ;; + *) die "unknown launch mode: $mode" ;; + esac + need_file "$deployed_dll" + local staged="${stage_root}/version.dll" + local manifest="${stage_root}/manifest.txt" + need_file "$staged" + need_file "$manifest" + verify_pe64 "$deployed_dll" + verify_exports "$deployed_dll" + local recorded + recorded="$(awk -F= '$1=="artifact_sha256"{print $2}' "$manifest")" + [[ -n "$recorded" && "$(sha256 "$staged")" == "$recorded" ]] || + die "staged artifact hash does not match manifest" + [[ "$(sha256 "$deployed_dll")" == "$recorded" ]] || + die "deployed version.dll does not match the staged M1 artifact" + command -v umu-run >/dev/null || die "umu-run is required" + for name in OPENFUT_SBC_DISPATCH OPENFUT_SBC_COMMIT OPENFUT_SBC_ARM_ONLY OPENFUT_SBC_POPULATE; do + [[ -z "${!name:-}" || "${!name}" == "0" ]] || die "$name must be unset or 0 for this launch" + done + mkdir -p "${wine_prefix}/dosdevices" + ln -sfn /mnt "${wine_prefix}/dosdevices/w:" + note "Launching $mode mode (SBC_HOOK=$hook_enabled; SBC_TRACE=$trace_enabled; SBC_REQUEST_TRACE=$request_trace_enabled; SBC_NOTIFIER_TRACE=$notifier_trace_enabled; every mutation feature disabled); log=/tmp/fifa17-hook-m1-launch.log" + cd "$game_dir" + env \ + GAMEID=fifa17 \ + PROTONPATH="$proton_path" \ + WINEPREFIX="$wine_prefix" \ + WINEDLLOVERRIDES='version=n,b' \ + OPENFUT_SBC_HOOK="$hook_enabled" \ + OPENFUT_SBC_TRACE="$trace_enabled" \ + OPENFUT_SBC_REQUEST_TRACE="$request_trace_enabled" \ + OPENFUT_SBC_NOTIFIER_TRACE="$notifier_trace_enabled" \ + OPENFUT_SBC_DISPATCH=0 \ + OPENFUT_SBC_COMMIT=0 \ + OPENFUT_SBC_ARM_ONLY=0 \ + OPENFUT_SBC_POPULATE=0 \ + umu-run _fifa17.exe 2>&1 | tee /tmp/fifa17-hook-m1-launch.log +} + +usage() { + cat <<'EOF' +Usage: fifa17-hook-m1.sh [inspect|build|stage|deploy|launch|launch-resolve|launch-trace] + + inspect Read-only PE/hash/export preflight (default). + build Cross-build the inert FIFA17 hook, then run inspect. + stage Copy a verified DLL into repo-local staging and write a hash manifest. + deploy Back up and install version.dll; requires: + OPENFUT_FIFA17_DEPLOY=I_ACCEPT_VERSION_DLL_REPLACEMENT + launch Start the M1 inert-hook baseline; requires: + OPENFUT_FIFA17_LAUNCH=I_ACCEPT_M1_BASELINE_LAUNCH + launch-resolve + Start M2 resolve-only mode (guarded reads/logging, no detours/writes); requires: + OPENFUT_FIFA17_RESOLVE=I_ACCEPT_M2_RESOLVE_LAUNCH + launch-trace + Start the single M3 passive factory/deserializer trace; requires: + OPENFUT_FIFA17_TRACE=I_ACCEPT_M3_PASSIVE_TRACE + +Optional path overrides: + OPENFUT_FIFA17_HOOK_DLL, OPENFUT_FIFA17_GAME_DIR, + OPENFUT_FIFA17_WINEPREFIX, OPENFUT_FIFA17_PROTONPATH +EOF +} + +case "${1:-inspect}" in + inspect) inspect ;; + build) build ;; + stage) stage ;; + deploy) deploy ;; + launch) launch baseline ;; + launch-resolve) launch resolve ;; + launch-trace) launch trace ;; + -h|--help|help) usage ;; + *) usage >&2; die "unknown command: $1" ;; +esac diff --git a/fifa17-recon/tools/run-x64dbg-fifa17.sh b/fifa17-recon/tools/run-x64dbg-fifa17.sh new file mode 100755 index 0000000..2d90059 --- /dev/null +++ b/fifa17-recon/tools/run-x64dbg-fifa17.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Launch x64dbg inside FIFA 17's UMU/Proton prefix. +set -euo pipefail + +x64dbg_root="${OPENFUT_X64DBG_ROOT:-/home/alex/Games/x64dbg}" +wine_prefix="${OPENFUT_FIFA17_WINEPREFIX:-/home/alex/Games/umu/fifa17}" +proton_path="${OPENFUT_FIFA17_PROTONPATH:-UMU-Proton-10.0-4}" +debugger="${x64dbg_root}/release/x64/x64dbg-unsigned.exe" + +[[ -f "$debugger" ]] || { + printf 'ERROR: x64dbg executable not found: %s\n' "$debugger" >&2 + exit 1 +} +command -v umu-run >/dev/null || { + printf 'ERROR: umu-run is required\n' >&2 + exit 1 +} + +cd "${x64dbg_root}/release/x64" +exec env \ + GAMEID=fifa17 \ + PROTONPATH="$proton_path" \ + WINEPREFIX="$wine_prefix" \ + QT_OPENGL=desktop \ + umu-run "$debugger" "$@" diff --git a/openfut-launcher b/openfut-launcher index 87241ac..3d895fb 160000 --- a/openfut-launcher +++ b/openfut-launcher @@ -1 +1 @@ -Subproject commit 87241acc1ae50792573f13b565ede264145c10b7 +Subproject commit 3d895fb7ac061c17977181b71f9ce9087475301b