Files
OpenFUT/fifa17-recon/docs/plan-2026-08-07-sbc-client-hook-implementation.md
T
2026-08-07 11:44:05 -07:00

12 KiB
Raw Blame History

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:

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:

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 3060 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:

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.