257 lines
12 KiB
Markdown
257 lines
12 KiB
Markdown
# 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.
|