12 KiB
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=1solely 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
- Mark the reconciliation report as the address/path authority.
- Record SHA-256, PE timestamp,
SizeOfImage, and selected section hashes for the shipped CardsDLL, FIFA executable, built hook, and deployed proxy DLL. - Preserve a known-good launcher and proxy DLL. Do not overwrite a game-directory DLL without an exact backup and hashes.
- Capture a baseline: FUT hub succeeds,
/sbs/setsreturns 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:
- Port or implement the complete VERSION proxy export surface and forward every export.
- Build only
--features fifa17forx86_64-pc-windows-gnuinto a staging directory. - Inspect PE architecture, exports, and imports with the MinGW binutils.
- Add a FIFA-17-specific launch path using the existing prefix/UMU configuration and
explicit
WINEDLLOVERRIDES=version=n,b. - 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;
Aand its expected vtable;Band its expected vtable;- readable
Mslot 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:
- generic completion
0x18016cca0; - typed response factory
0x18017aa10; - typed category deserializer
0x18017b2b0; and - 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
0x18022e5b0slot+0x08and the older message-object vtable0x18022e598slot+0x20; - which completion argument or object field owns the genuine reader;
- the reader's valid synchronous lifetime;
- whether
0x1800b8c30executes 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:
- require request vtable
0x18022e5c0, response vtable0x18022e5b0, and status 200; - require a readable reader pointer/vtable and recursion depth zero;
- require that this transaction has not already been parsed;
- call the original typed method
0x18017b2b0(response, reader)exactly once; - capture its return and the resulting M state; and
- 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 != 0and a bounded category count;- category vector
begin <= end <= capacity; (end-begin) % 0xf0 == 0and vector length equalsWORD[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
- Open/close SBC ten times; enter every set/challenge and return.
- Verify a second
/setsresponse is idempotent and does not duplicate data. - Smoke-test hub, club, store, squads, and normal service traffic.
- Repeat from two fresh launches with different ASLR slides.
- Soak 30–60 minutes with navigation and, if supported, repeated FUT enter/exit.
- Disable all SBC flags and confirm the baseline behavior returns without detours/writes.
- 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.
Mpasses 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.