feat: document and stage FIFA 17 SBC hook workflow
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user