Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c58e7326a1 | |||
| 5aec83ce97 | |||
| e2c6ae8e5b | |||
| 550b23bb26 | |||
| 4c57cf571c | |||
| 55c9757e74 | |||
| 49b3030e34 | |||
| 15a6f877ee | |||
| ce8a3b32ec | |||
| bc6612697a |
@@ -0,0 +1,190 @@
|
||||
# OpenFUT Bridge — Claude Code project context
|
||||
|
||||
Read this first. It's the standing context for every task in this project. Each
|
||||
working session will give you ONE bounded task plus a verification clause; this
|
||||
file is the background that stays true across all of them.
|
||||
|
||||
## What this project is
|
||||
|
||||
OpenFUT is an **offline FIFA 23 FUT (Ultimate Team) emulator** for single-player
|
||||
game preservation. EA permanently shut down FIFA 23's online servers in October
|
||||
2025, which removed FUT. This project lets a legitimately-owned copy run FUT
|
||||
against a **local emulated backend** instead of EA's (dead) servers.
|
||||
|
||||
It has two repos:
|
||||
- **OpenFUT Core** — the game-independent FUT economy backend (already largely built).
|
||||
- **OpenFUT Bridge** — the FIFA 23 integration / reverse-engineering layer (this work).
|
||||
|
||||
## Hard constraints (do not violate)
|
||||
|
||||
- **Clean-room only.** Everything is derived from observing the running client's
|
||||
own behaviour. NEVER use, reference, or reproduce leaked EA source code (e.g.
|
||||
the 2021 FIFA 21 / Blaze leak). If a task seems to need it, stop and say so.
|
||||
Clean-room provenance is the legal foundation of the whole project.
|
||||
- **Mark uncertainty, don't invent.** Several things are genuinely unconfirmed:
|
||||
the Fire2 packet framing, ProtoSSL non-blocking return values, and FIFA 23's
|
||||
Blaze component/command IDs. When you don't know a value, leave a clearly
|
||||
labelled `TODO/CONFIRM` rather than guessing a confident-looking number.
|
||||
- **Verify against the client.** The dead servers mean responses are *synthesized
|
||||
and tested against the live offline client*, never captured from EA. The client
|
||||
is the oracle: a gate is "done" when the client advances to the next command.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
FIFA 23 client (offline, native Windows)
|
||||
| ProtoSSLConnect / ProtoSSLSend / ProtoSSLRecv (in-process)
|
||||
openfut_hook.dll — fakes the connection, captures requests, injects responses
|
||||
| plain localhost TCP, length-prefixed frames (no TLS)
|
||||
blaze_brain (Rust) — decodes requests, crafts Blaze responses <-- iteration surface
|
||||
|
|
||||
OpenFUT Core — supplies real FUT economy data for the FUT gates
|
||||
```
|
||||
|
||||
Why in-process (not a localhost TLS server): FIFA 23's ProtoSSL uses modern TLS
|
||||
with cert pinning, so a fake server gets its cert rejected. Hooking ABOVE the
|
||||
crypto (ProtoSSLSend/Recv take/return plaintext) sidesteps TLS entirely.
|
||||
|
||||
## The entry sequence (the gates to reach FUT)
|
||||
|
||||
The client runs these in order; each must be satisfied before the next fires:
|
||||
|
||||
1. **LSX** (port 3216, plaintext loopback) — launcher→game bootstrap.
|
||||
2. **Blaze redirector** — returns a server host/port.
|
||||
3. **Blaze preauth** (UTIL) — config / component list.
|
||||
4. **Blaze login** (AUTHENTICATION) — persona, session key, UID.
|
||||
5. **Blaze postauth** (UTIL) — telemetry/ticker; "fully online".
|
||||
6. **FUT entry check** — FIFA-specific eligibility/entitlement wall.
|
||||
7. **FUT hub load** — club/squad over REST; hand off to OpenFUT Core.
|
||||
|
||||
Gates 2–5 are largely static/replayable (the client validates little). Gates 6–7
|
||||
need internally-consistent data and connect to OpenFUT Core.
|
||||
|
||||
## Environment
|
||||
|
||||
- Native Windows boot (chosen for predictable PE/hooking behaviour over Proton).
|
||||
- FIFA 23, with EA Anticheat in its offline/neutralized state. Do NOT assume a
|
||||
task is safe to hook unless EAAC is confirmed neutralized.
|
||||
- Rust is the primary language. Hook = `cdylib`, Windows target. Brain = portable.
|
||||
- Self-hosted Gitea (git.aleshym.co, org Quaternions). Self-hosted CI runner.
|
||||
|
||||
## How to work here
|
||||
|
||||
- One rung at a time. Don't run ahead into a gate that depends on an unconfirmed
|
||||
earlier answer.
|
||||
- When decoding a frame, work from REAL captured bytes the user pastes in — not
|
||||
from a guessed layout. Ask for the bytes if they're not provided.
|
||||
- End each change with how the user verifies it on the client.
|
||||
|
||||
|
||||
# OpenFUT Bridge — Task prompts (run in order)
|
||||
|
||||
Each task is its own Claude Code session. Paste the task, attach the named
|
||||
starting file(s), and don't move to the next until the verification clause passes.
|
||||
The project CLAUDE.md is assumed to be in context.
|
||||
|
||||
---
|
||||
|
||||
## TASK 1 — Confirm the transport (read-only memory scan)
|
||||
|
||||
**Goal:** prove whether FIFA 23 uses EA DirtySDK / ProtoSSL before we build any
|
||||
hook around it. Read-only; no hooking, patching, or injection.
|
||||
|
||||
**Do this:**
|
||||
Write a standalone Windows console program in Rust (target
|
||||
`x86_64-pc-windows-msvc` or `-gnu`) that:
|
||||
1. Finds the running FIFA 23 process by name (and accepts a PID argument).
|
||||
2. Enumerates its modules and committed memory regions (e.g. `EnumProcessModules`
|
||||
/ `VirtualQueryEx`), readable regions only.
|
||||
3. Reads memory with `ReadProcessMemory` in chunks (with small overlap so a
|
||||
marker spanning a chunk boundary is still found).
|
||||
4. Searches for these ASCII markers and prints each hit with its absolute address:
|
||||
`protossl:`, `ProtoSSLSend`, `ProtoSSLRecv`, `ProtoSSLConnect`,
|
||||
`gosredirector`, `DirtySDK`, `blaze`.
|
||||
5. Prints a summary: which markers were found, and a clear verdict line
|
||||
("ProtoSSL present" vs "no ProtoSSL markers found").
|
||||
|
||||
Keep it dependency-light (the `windows` crate is fine). Comment it for a Rust
|
||||
beginner. It must only read.
|
||||
|
||||
**Verification:** with FIFA 23 running and sitting at the main menu, the program
|
||||
prints whether the ProtoSSL markers are present. If `protossl:` and the
|
||||
send/recv strings appear, the transport is confirmed and we proceed. If nothing
|
||||
appears after the menu has loaded, STOP — we rethink the transport, don't build
|
||||
the hook.
|
||||
|
||||
**Note:** reach the main menu before scanning; the networking code/strings may
|
||||
not be paged in at the title screen.
|
||||
|
||||
---
|
||||
|
||||
## TASK 2 — Loadable DLL shell (prove we can get code in)
|
||||
|
||||
**Goal:** a `version.dll` proxy that loads into FIFA 23 and runs our code, before
|
||||
any hook logic exists. This isolates "can we inject at all" from "is the hook
|
||||
correct".
|
||||
|
||||
**Do this:**
|
||||
Create a Rust `cdylib` (Windows target) that:
|
||||
1. Exports the functions a real `version.dll` exports, forwarding each to the
|
||||
system `version.dll` (proxy/sideload pattern). List the needed exports and
|
||||
implement the forwarding.
|
||||
2. In `DllMain`, on `DLL_PROCESS_ATTACH`, spawns a thread (NOT work in DllMain
|
||||
itself — loader lock) that writes a line to a log file in a known path, e.g.
|
||||
`C:\openfut\hook.log`, with a timestamp.
|
||||
3. Provide the `Cargo.toml` (`crate-type = ["cdylib"]`) and the exact build
|
||||
command.
|
||||
|
||||
Clean-room: this is generic proxy/loader scaffolding, nothing EA-derived.
|
||||
|
||||
**Verification:** drop the built DLL next to the FIFA 23 executable, launch the
|
||||
game, and confirm `hook.log` gets the timestamped line. Game still reaches the
|
||||
menu normally. If it crashes or the line never appears, fix the proxy/forwarding
|
||||
before continuing.
|
||||
|
||||
**Caution:** confirm EAAC is in its offline/neutralized state before loading the
|
||||
DLL. Do not load it into an anticheat-active session.
|
||||
|
||||
---
|
||||
|
||||
## TASK 3 — One landing hook on ProtoSSLSend
|
||||
|
||||
**Goal:** prove we can hook a ProtoSSL function and see real outbound frames.
|
||||
Just one function, and it calls the original (passive observation).
|
||||
|
||||
**Do this:**
|
||||
Extend the Task 2 DLL:
|
||||
1. Add the `retour` crate. Resolve the `ProtoSSLSend` address — first pass: use
|
||||
the absolute address Task 1 reported, converted to a module-relative offset
|
||||
plus the live module base. (We'll switch to a byte-pattern scan later for
|
||||
patch-resilience; leave a TODO for that.)
|
||||
2. Install a `retour` inline detour on `ProtoSSLSend` with signature
|
||||
`extern "C" fn(*mut c_void, *const u8, i32) -> i32` (x64 = one calling
|
||||
convention; `extern "C"` is correct).
|
||||
3. In the detour: log `length` and the first ~32 bytes of the buffer as hex,
|
||||
then CALL THE ORIGINAL and return its result (do not block or alter traffic
|
||||
yet).
|
||||
|
||||
Mark the ProtoSSL return-value conventions and the eventual pattern-scan as
|
||||
`TODO/CONFIRM` per the project rules.
|
||||
|
||||
**Verification:** launch the game, attempt to go online / load FUT, and confirm
|
||||
`hook.log` shows ProtoSSLSend calls with non-trivial byte dumps — these are the
|
||||
client's real outbound Blaze frames (still TLS-bound on the wire, but plaintext
|
||||
here, which is the whole point). Seeing real frames = hooking works, and we now
|
||||
have actual bytes to decode in later tasks. Paste a few of those captured frames
|
||||
into the next session.
|
||||
|
||||
---
|
||||
|
||||
## What to attach to each task
|
||||
|
||||
- Task 1: nothing (fresh program), or the Linux `protossl_scan.rs` as a logic
|
||||
reference to port.
|
||||
- Task 2: nothing, or your existing `version.dll` proxy shell if you have one.
|
||||
- Task 3: the Task 2 DLL, plus the `openfut_hook_sketch.rs` as the structural
|
||||
reference, plus the address Task 1 reported.
|
||||
|
||||
Once Task 3 yields real captured frames, later tasks (the brain handlers per
|
||||
gate) should always include the actual pasted bytes — decoding real frames is far
|
||||
more reliable than guessing the Fire2 layout.
|
||||
@@ -0,0 +1,262 @@
|
||||
# Connection-gate findings (clean-room)
|
||||
|
||||
**Status:** observed behaviour only — derived from running the client and reading
|
||||
the repack's own files. No EA source used. Unconfirmed values are marked
|
||||
`TODO/CONFIRM` rather than guessed.
|
||||
|
||||
## Components (observed)
|
||||
|
||||
| Component | Role |
|
||||
|---|---|
|
||||
| `FIFA23.exe` | Game. Statically links EA's **Ebisu SDK** (online) and **DirtySDK/ProtoSSL** (transport). Loads at fixed base `0x140000000` — no ASLR seen across launches. |
|
||||
| `_ProtoSSLSendPacket` @ `FIFA23.exe+0xEFA530` | DirtySDK TLS record assembler — plaintext in, encrypts in place. Already located + hooked. |
|
||||
| `anadius64.dll` | anadius EA-app/Origin **LSX server** emulator (ASLR'd). Provides offline DRM / entitlements / persona so the game *launches*; deliberately keeps it **offline**. |
|
||||
| `FakeEAACLauncher` | Anti-cheat bypass only. Irrelevant to the online gate. |
|
||||
|
||||
## Observed online-startup flow
|
||||
|
||||
1. Ebisu SDK opens LSX socket(s) to anadius; a **challenge/response handshake**
|
||||
completes — captured XML: `<Challenge>` / `<ChallengeResponse>` /
|
||||
`<ChallengeAccepted>`, `sender="EALS"`, `ContentId 16115019`.
|
||||
2. River/PIN telemetry `<session type=boot>` is emitted.
|
||||
3. **No further socket traffic.** Clicking Ultimate Team → "connecting to the EA
|
||||
Servers…" → **zero** network attempts (no Blaze DNS, no `connect`, nothing on
|
||||
`send`/`recv`/`WSASend`/`WSARecv`) → falls back to offline.
|
||||
|
||||
## Conclusion: the decision is upstream and in-process
|
||||
|
||||
- The online/offline verdict is delivered through anadius's **in-process
|
||||
detoured Ebisu calls**, not socket messages. (No `GetAuthCode`/`GoOnline`/
|
||||
entitlement query appears on any socket, sync or async.)
|
||||
- The game therefore **never reaches DirtySDK/ProtoSSL for Blaze** — it aborts
|
||||
before any `ProtoSSLConnect(gosredirector…)`. The gate is an **Ebisu-SDK
|
||||
connection-state / online-session check upstream of the transport.**
|
||||
- `ONLINE_ACCESS` is a `Blaze::Nucleus::EntitlementType` (enum value `1`).
|
||||
anadius's `GoOnline` LSX handler (`anadius64.dll+0x2BB90`) is a success stub;
|
||||
`anadius64.dll+0xB6B1` is a large entitlement/profile builder that *does*
|
||||
reference `ONLINE_ACCESS`. Reverse-engineering that entitlement-status logic
|
||||
is the **wrong fight** (see strategy).
|
||||
|
||||
## Strategy (decided)
|
||||
|
||||
Do **not** make anadius report "online." anadius exists to keep the game
|
||||
offline, and it can't be removed without breaking launch/DRM. Instead:
|
||||
|
||||
> Let the game run its **real** online flow and answer that flow ourselves via
|
||||
> the hook + brain. Target the Ebisu connection-state check the FUT-entry path
|
||||
> polls; make the game *pass* it so it issues the real `ProtoSSLConnect` to the
|
||||
> Blaze redirector → our redirect + ProtoSSL hook capture the real frames.
|
||||
|
||||
This deletes the "reverse-engineer anadius's entitlement logic" problem.
|
||||
|
||||
## Next RE step (converges with the ProtoSSL hook)
|
||||
|
||||
1. Locate and **read-only hook `ProtoSSLConnect`** (same DirtySDK module and
|
||||
technique that found `_ProtoSSLSendPacket`). Confirms the game never
|
||||
initiates the Blaze connection (pins the gate strictly upstream) and gives us
|
||||
the exact symbol that will flip from "never called" to "called" on success.
|
||||
2. Locate the Ebisu **connection-state getter** the FUT-entry / "connecting" UI
|
||||
polls. Candidate string anchors (observed): `ONLINE_STATUS_EVENT`,
|
||||
`GoOnline` result handling, the "connecting to the EA Servers" UI trigger.
|
||||
3. Determine the minimal intervention to make that getter report **connected**
|
||||
(out-hook it in our `version.dll`, winning over anadius's detour) so the game
|
||||
proceeds to `ProtoSSLConnect`.
|
||||
- `TODO/CONFIRM`: whether out-hooking the getter is sufficient, or secondary
|
||||
checks (auth-token presence) also gate the attempt.
|
||||
|
||||
---
|
||||
|
||||
## M1 results (read-only investigation)
|
||||
|
||||
Method: `protossl-scan` (xref / disasm / callers, app-module-restricted) against
|
||||
the live client, plus the existing `connect`/DNS/LSX hooks. Observed behaviour
|
||||
only — no EA source.
|
||||
|
||||
### Point 1 — is `ProtoSSLConnect` reached on the "connecting" abort? **NO — gate is upstream. CONFIRMED.**
|
||||
- The existing `connect` / `GetAddrInfoW` / `getaddrinfo` hooks show the client
|
||||
makes **zero** network attempts during the "connecting to the EA Servers"
|
||||
abort: no DNS for any `gosredirector.*` host, no `connect` to any external
|
||||
address.
|
||||
- The DirtySDK/ProtoSSL transport is therefore never reached — the abort is
|
||||
entirely upstream and in-process.
|
||||
- `ProtoSSLConnect` has no debug string and sits behind the DirtySDK API, so an
|
||||
explicit hook on it isn't needed to prove this; the behavioural evidence is
|
||||
conclusive. `TODO/CONFIRM`: locate `ProtoSSLConnect` by signature later, as a
|
||||
positive M2 trip-wire (it should fire once we flip the gate).
|
||||
|
||||
### Surface mapped (clean-room)
|
||||
- **Redirector host table** (`FIFA23.exe` .rdata, pointer table @ `+0x83FC858`):
|
||||
`spring18.gosredirector.{sdev,stest,scert}.ea.com` + production
|
||||
`spring18.gosredirector.ea.com`.
|
||||
- **Redirector request/response config** (same region): `X-BLAZE-ERRORCODE`,
|
||||
**`Authorization:`**, `<errorCode>` — the redirector request carries an
|
||||
**Authorization header (a Nucleus token)**.
|
||||
- **Enum-name tables** (serialization only, NOT decision code): `ONLINE_ACCESS`
|
||||
(`Blaze::Nucleus::EntitlementType` = 1), `ONLINE_STATUS_EVENT`, … These are
|
||||
reflection tables keyed by enum *value*; string-xref of them is a dead end for
|
||||
the decision (the decision compares values, not strings).
|
||||
|
||||
### Point 2 — name the connection-state function: **PARTIAL.**
|
||||
- The exact connection-state decision is behind heavy C++ vtable indirection
|
||||
(the redirector config's two code pointers resolved to a virtual-dispatch
|
||||
thunk @ `FIFA23.exe+0x27BD4C0` and a `ret 0` stub @ `+0x4F3CBC0`). The
|
||||
memory-scan toolkit (string / pattern / xref / callers) cannot efficiently
|
||||
navigate this.
|
||||
- **Pinning the exact function needs a static disassembler with decompilation
|
||||
(Ghidra / IDA) on `FIFA23.exe`.** `protossl-scan` remains the bridge to the
|
||||
live process (mapping static addresses to the ASLR'd runtime, confirming hits,
|
||||
installing hooks).
|
||||
- `TODO/CONFIRM`: name the connection-state getter by module+offset via Ghidra.
|
||||
|
||||
### Point 3 — gate count: **TWO gates (state + token). Evidence-backed.**
|
||||
- The online-connect path **reads/requires an auth (Nucleus) token**: the
|
||||
redirector request carries an `Authorization:` header, and the SDK surface has
|
||||
`GetAuthCode` / `FakeAuth` / `<GameToken>`. So it is **not** a single
|
||||
connection-state boolean flip — even with the state forced "online", the client
|
||||
needs a valid token to build the redirector request.
|
||||
- **Conclusion for M2: plan for two gates** — (a) the connection-state decision,
|
||||
and (b) supplying an auth token the client accepts.
|
||||
- `TODO/CONFIRM` the exact abort point (no-token vs state-says-offline vs both) —
|
||||
needs Ghidra-level control-flow tracing.
|
||||
|
||||
### Dynamic probe result (read-only) — `GoOnline` is NOT the gate
|
||||
A read-only detour on anadius's `GoOnline` handler (`anadius64.dll+0x2BB90`)
|
||||
shows the game **does** call `GoOnline` during the "connecting" attempt (incl.
|
||||
on the Ultimate Team click) and anadius returns success — **yet no Blaze
|
||||
connection follows** (still zero external `CONNECT`/DNS).
|
||||
|
||||
Therefore the gate is **downstream of `GoOnline`**: the game decides to go
|
||||
online and the request is accepted, then it aborts at the **auth-token step
|
||||
(`GetAuthCode`) and/or while waiting for the "online established" status
|
||||
callback** (`ONLINE_STATUS_EVENT`), and times out into offline.
|
||||
|
||||
This sharpens the two-gate picture: `GoOnline` passes; the real blocker is the
|
||||
**token / online-status step**. `TODO/CONFIRM` which (token-missing vs
|
||||
status-never-fires) — via a `GetAuthCode` probe and/or Ghidra.
|
||||
|
||||
### Recommendation / next
|
||||
Name the downstream gate. Two complementary routes:
|
||||
- **Dynamic:** probe anadius's `GetAuthCode` handler (does it return a token or
|
||||
fail?) — distinguishes token-gate from status-callback.
|
||||
- **Static (Ghidra):** xref the `GoOnline` / `GetAuthCode` / `ONLINE_STATUS_EVENT`
|
||||
strings in `FIFA23.exe` to find the FUT online-flow code that issues `GoOnline`
|
||||
then waits for the token/status, and decompile it. FIFA isn't ASLR'd, so Ghidra
|
||||
addresses (image base `0x140000000`) map 1:1 to our recorded offsets.
|
||||
|
||||
---
|
||||
|
||||
## M1 COMPLETE — the gate is `GetInternetConnectedState`
|
||||
|
||||
Found without Ghidra, by reading anadius's LSX **command-registration** function
|
||||
(`anadius64.dll+0x14C0`), which lists every command-name → handler inline. NB the
|
||||
`lea rax,[handler]` is **offset by one** from the `lea rdx,[name]` in that listing
|
||||
(verified empirically: `+0x27060` decompiles to `GetProfile` — it builds a
|
||||
`GetProfileResponse` for persona "fun"). Corrected handler map:
|
||||
|
||||
| anadius LSX command | handler (anadius64.dll + …) |
|
||||
|---|---|
|
||||
| GetProfile | 0x27060 |
|
||||
| **GetInternetConnectedState** | **0x27790** |
|
||||
| GoOnline | 0x2BB90 |
|
||||
| GetAuthCode | 0x2BBC0 |
|
||||
|
||||
### The gate, named: `GetInternetConnectedState` @ `anadius64.dll+0x27790`
|
||||
It serializes an LSX `InternetConnectedState` response with a `connected`
|
||||
attribute whose value is:
|
||||
|
||||
```
|
||||
connected = (byte[+0xCAB1B] != 0 || byte[+0xCAB1A] != 0) ? str(+0xAF530)
|
||||
: str(+0xADE64)
|
||||
```
|
||||
|
||||
Both flag bytes **default to 0**, so it reports the offline value (`+0xADE64`).
|
||||
That is precisely why the client sits at "connecting to the EA Servers" and falls
|
||||
back offline.
|
||||
|
||||
### Answers to the three M1 points
|
||||
1. **ProtoSSLConnect reached on the abort? NO — gate upstream.** Confirmed.
|
||||
2. **The connection-state function:** `GetInternetConnectedState` @
|
||||
`anadius64.dll+0x27790`. Decision = the two-flag branch above.
|
||||
3. **Gate count: ONE actionable gate.** The auth token is already satisfied —
|
||||
`GoOnline` (`+0x2BB90`) is called during the attempt and returns success, and
|
||||
anadius provides a fake auth code; the blocker is purely the connection-state.
|
||||
So M2 = make `GetInternetConnectedState` report **connected** (then the game
|
||||
proceeds with its existing token).
|
||||
|
||||
### M2 attempt results — the gate is an async EVENT, not a poll
|
||||
Tried (read/write, EAAC neutralized):
|
||||
- Forced `GetInternetConnectedState` → connected (set flags +0xCAB1A/+0xCAB1B → value
|
||||
`"1"`; strings confirmed: +0xADE64 = `"0"` offline, +0xAF530 = `"1"` connected).
|
||||
- Flipped `GoOnline` to report `"1"` (replicated its builder `+0x25BE0` with the
|
||||
connected string instead of `"0"`).
|
||||
|
||||
**Neither made the game proceed.** Both handlers fire, no crash — but the game
|
||||
**keeps retrying `GoOnline` every ~7s** and never attempts the Blaze connect.
|
||||
That retry-on-timeout pattern means the FUT-online flow is **event-driven**: the
|
||||
game submits `GoOnline`, gets success, then **waits for an async "online
|
||||
established" event** (ONLINE_STATUS_EVENT-class) that anadius — being offline-only
|
||||
— never pushes (the only `<Event sender="EALS">` it ever sends is the Challenge
|
||||
handshake). So flipping poll/return values can't unblock it.
|
||||
|
||||
**Implication:** getting past "connecting" requires **emulating the EA-app online
|
||||
event sequence** the game waits for (inject the online-status event over LSX, in
|
||||
the format/order EbisuSDK expects), not a single function flip. This is a
|
||||
substantially deeper task (and precedes the Blaze backend emulation).
|
||||
|
||||
Next: trace the FIFA-side FUT online-flow (what the game does after `GoOnline`
|
||||
and exactly which event/condition it waits on) — Ghidra on FIFA23.exe (import
|
||||
saved at `C:\openfut\gh-proj`), or RE anadius's LSX event-send path. `TODO/CONFIRM`.
|
||||
|
||||
### M2 deeper finding — worker-thread + event architecture (confirmed)
|
||||
A live call-stack capture from inside the GoOnline detour (manual stack scan,
|
||||
bounded by `GetCurrentThreadStackLimits`) found **zero FIFA23.exe frames** and
|
||||
showed `sp` sitting ~2.4 KB below the thread's stack top. So the handler runs at
|
||||
the top of a short stack — i.e. on an **anadius worker thread** (IOCP/threadpool),
|
||||
not FIFA's calling thread. anadius **queues** the GoOnline command and a worker
|
||||
services it.
|
||||
|
||||
Combined with the retry behaviour, the architecture is now clear and three-way
|
||||
corroborated: **FIFA calls `EbisuSDK::GoOnline` → anadius queues it → returns →
|
||||
FIFA waits for an async "online established" event → anadius (offline-only, no
|
||||
online-event code) never pushes it → timeout/retry.** No handler-response flip
|
||||
can unblock this; the game waits on a *push* anadius never produces.
|
||||
|
||||
**Conclusion:** crossing this gate requires emulating the EA-app online-event
|
||||
sequence (synthesize + inject the online-status event on the worker→game callback
|
||||
/ LSX path, in EbisuSDK's expected format) — a research-grade emulation effort,
|
||||
preceding the Blaze backend. The cheap in-process flips are exhausted.
|
||||
|
||||
Reaching the FIFA-side flow would need either: (a) locate `EbisuSDK::GoOnline` in
|
||||
FIFA23.exe via anadius's detour table, then `callers` to the online-flow; or
|
||||
(b) find anadius's LSX event-send path and reverse the online-event format. Both
|
||||
are deep. `TODO/CONFIRM`.
|
||||
|
||||
### Path A attempt: reach the FIFA-side online-flow (blocked with live toolkit)
|
||||
Goal: find `EbisuSDK::GoOnline` in FIFA23.exe → `callers` → the game's online-flow
|
||||
→ read what event it waits on. Every angle our live-memory toolkit offers is
|
||||
blocked:
|
||||
- **String xref:** FIFA23.exe contains no `"GoOnline"` string (typed SDK call,
|
||||
not a string-built command).
|
||||
- **Call-stack from the handler:** GoOnline runs on an anadius worker thread; a
|
||||
bounded stack scan finds zero FIFA frames.
|
||||
- **Detour scan (`jmpscan`):** scanning FIFA23.exe for function-entry `E9` jumps
|
||||
leaving the module yields ~3875 hits — overwhelmingly false positives, because
|
||||
the 505 MB image is mostly embedded *data* (not code), and the real detours
|
||||
don't cleanly cluster. (A .text-section-only scan would help but the chain
|
||||
after — isolate GoOnline → callers → event format → emulate — remains long and
|
||||
each link is gated by SDK abstraction / anadius indirection / worker threads.)
|
||||
|
||||
**Verdict:** crossing this gate to *playable* FUT is research-grade. It needs an
|
||||
interactive disassembler (IDA/Ghidra GUI, human-driven) to trace the EbisuSDK
|
||||
online-flow, and then a full EA-online + Blaze emulator. The live-memory toolkit
|
||||
(string/xref/disasm/callers/read/jmpscan) has been exhausted for the FIFA side.
|
||||
The clean-room spec (this document) is the finished, valuable artifact.
|
||||
- Check whether the flags at `+0xCAB1A` / `+0xCAB1B` are settable via anadius
|
||||
config / a hidden option (cheapest flip).
|
||||
- Else out-detour `GetInternetConnectedState` in our `version.dll` to force the
|
||||
`+0xAF530` ("connected") path.
|
||||
- Then watch for the client to attempt the real Blaze connect (our `connect`
|
||||
redirect + ProtoSSL hook capture the first plaintext — milestone M3).
|
||||
- `TODO/CONFIRM`: exact text of the offline/connected value strings
|
||||
(`+0xADE64` / `+0xAF530`); whether any secondary check gates the attempt after
|
||||
the state flips.
|
||||
@@ -0,0 +1,91 @@
|
||||
# OpenFUT Bridge roadmap — from here to "Squad Battles loads"
|
||||
|
||||
Two **first-class** outcomes (not one goal + a consolation prize):
|
||||
|
||||
- **A. Full playable AI FUT** — the emulator build (M1–M7). Realistically a
|
||||
multi-month, expert-level reverse-engineering effort.
|
||||
- **B. Clean-room spec deliverable** — a documented map of the auth / Blaze /
|
||||
ProtoSSL / Fire2 surface (transport, gates, framing, decision points). Produced
|
||||
incrementally as the findings from M1–M5; **finishable and valuable on its
|
||||
own**, and the foundation any future emulator needs.
|
||||
|
||||
Every milestone's "done" is a **client-observable** result. Unconfirmed
|
||||
dependencies are flagged.
|
||||
|
||||
## Done so far
|
||||
|
||||
- In-process `version.dll` hook (injects, forwards all 17 real exports).
|
||||
- Transport confirmed: DirtySDK/ProtoSSL. `_ProtoSSLSendPacket` @
|
||||
`FIFA23.exe+0xEFA530` hooked (plaintext-capture ready).
|
||||
- `connect` / DNS / LSX (`send`/`recv` + `WSASend`/`WSARecv`) capture; mutexed log.
|
||||
- Gate identified: **upstream Ebisu connection-state, in-process** (see
|
||||
`connection-gate-findings.md`).
|
||||
- RE toolkit: `protossl-scan` (scan / xref / disasm / module+offset).
|
||||
|
||||
## M1 — Locate the connection-state decision point · Outcome B core
|
||||
- Read-only hook `ProtoSSLConnect`; find the Ebisu connection-state getter the
|
||||
FUT-entry path polls.
|
||||
- **Done:** we can name the exact function/return that gates "attempt online."
|
||||
- Unknown: coupling to anadius's detour. **Effort:** ~days.
|
||||
|
||||
## M2 — Flip the gate (force "connected") · pure gate-flip proof
|
||||
- Out-hook the getter / patch the polled state so the game attempts the real
|
||||
connection.
|
||||
- **Done (observable):** the game emits a DNS lookup / `connect` for the Blaze
|
||||
redirector (`gosredirector.*`).
|
||||
- Unknown: a secondary auth-token gate may also block. `TODO/CONFIRM`.
|
||||
**Effort:** ~days.
|
||||
|
||||
## M3 — First real ProtoSSL plaintext on the Blaze connection · SMALLEST END-TO-END PROOF (see "Smallest milestone")
|
||||
- Our redirect catches the Blaze connect; the ProtoSSL hook logs the first
|
||||
plaintext it emits (the TLS **ClientHello** to the redirector).
|
||||
- **Done:** `hook.log` shows the ClientHello from the *real* Blaze connection.
|
||||
- Note: this is a TLS handshake frame, **not yet** a Blaze/Fire2 app-data frame.
|
||||
**Effort:** small once M2 lands.
|
||||
|
||||
## M4 — Answer the redirector + decode first Fire2 frame · Outcome B: first decode
|
||||
- Inject a response via the ProtoSSL recv side so the client advances; decode
|
||||
the first Blaze/Fire2 request frame from real captured bytes.
|
||||
- **Done:** the client advances past the redirector (sends the next gate's frame).
|
||||
- Unknown (**BIG**): **Fire2 framing UNCONFIRMED**; **ProtoSSL recv-injection /
|
||||
TLS-bypass convention UNCONFIRMED**; **Blaze component/command IDs UNCONFIRMED**.
|
||||
**Effort:** high.
|
||||
|
||||
## M5 — Blaze preauth / login / postauth (online session) · Outcome B: full auth surface
|
||||
- Answer UTIL preauth, AUTHENTICATION login (reusing anadius's persona surface),
|
||||
UTIL postauth.
|
||||
- **Done:** client reports online / reaches the FUT entry check.
|
||||
- Depends on M4 framing. **Effort:** high.
|
||||
|
||||
## M6 — FUT entry + hub load (route to OpenFUT Core) · Outcome A
|
||||
- Answer the FUT eligibility check; serve the FUT hub (club/squad) from OpenFUT
|
||||
Core via the bridge.
|
||||
- **Done:** the FUT hub UI loads (club/squad screen).
|
||||
- Depends on Core's FUT REST surface. **Effort:** high.
|
||||
|
||||
## M7 — Squad Battles (AI FUT) · Outcome A goal
|
||||
- Wire Squad Battles match setup / rewards against Core.
|
||||
- **Done:** a Squad Battles match starts and rewards apply.
|
||||
- **Effort:** medium-high after M6.
|
||||
|
||||
## Outcome mapping
|
||||
- **B (spec)** completes as M1–M5 are documented — valuable even if A stalls.
|
||||
- **A (playable AI FUT)** requires M1–M7.
|
||||
|
||||
## Smallest provable milestone (step 4)
|
||||
|
||||
Refined from the proposed candidate. The single smallest result that validates
|
||||
the whole architecture end-to-end:
|
||||
|
||||
> **M3 — the client emits its first ProtoSSL plaintext onto the _real_ Blaze
|
||||
> connection (the ClientHello to the redirector), captured by our hook.**
|
||||
|
||||
It proves both halves at once: (1) we got the game past its connection-state
|
||||
check — it went online **for real**; and (2) our redirect + ProtoSSL hook
|
||||
capture real plaintext from that connection.
|
||||
|
||||
It deliberately stops short of the candidate's "first **Blaze** frame" (a Fire2
|
||||
app-data message), which requires answering the TLS handshake (M4) and depends
|
||||
on the unconfirmed Fire2 / recv-injection work. ClientHello capture needs none
|
||||
of that — making it the smallest, safest proof. The first Fire2 frame is the
|
||||
immediate follow-on (M4).
|
||||
Generated
+336
@@ -0,0 +1,336 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libudis86-sys"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "139bbf9ddb1bfc90c1ac64dd2923d9c957cd433cee7315c018125d72ab08a6b0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mmap-fixed-fixed"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0681853891801e4763dc252e843672faf32bcfee27a0aa3b19733902af450acc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openfut-hook"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"retour",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "region"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"mach2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "retour"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9af44d40e2400b44d491bfaf8eae111b09f23ac4de6e92728e79d93e699c527"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generic-array",
|
||||
"libc",
|
||||
"libudis86-sys",
|
||||
"mmap-fixed-fixed",
|
||||
"once_cell",
|
||||
"region",
|
||||
"slice-pool2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "slice-pool2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a3d689654af89bdfeba29a914ab6ac0236d382eb3b764f7454dde052f2821f8"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "openfut-hook"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "FIFA 23 version.dll proxy + ProtoSSL plaintext capture hook"
|
||||
|
||||
[lib]
|
||||
# Output is `version.dll` (the proxy/sideload name FIFA 23 loads).
|
||||
name = "version"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# Inline-hooking library (installs the detour on _ProtoSSLSendPacket).
|
||||
retour = "0.3"
|
||||
|
||||
# Win32 bindings for the loader/threading/module APIs the hook needs.
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_ProcessStatus",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
] }
|
||||
|
||||
# Own workspace root so Cargo doesn't attach this to the openfut-bridge package
|
||||
# in the parent directory.
|
||||
[workspace]
|
||||
@@ -0,0 +1,920 @@
|
||||
//! openfut-hook — FIFA 23 `version.dll` proxy + ProtoSSL plaintext capture.
|
||||
//!
|
||||
//! This DLL is built as `version.dll` and dropped next to `FIFA23.exe`. The
|
||||
//! game loads it (DLL search-order / sideload), at which point we:
|
||||
//!
|
||||
//! 1. (Task 2) Forward every real `version.dll` export to the genuine system
|
||||
//! DLL, so the game keeps working. We prove load with a log line.
|
||||
//! 2. (Task 3) Pattern-scan FIFA23.exe for `_ProtoSSLSendPacket` (the DirtySDK
|
||||
//! TLS record assembler we located at FIFA23.exe+0xEFA530) and install an
|
||||
//! inline detour. At its entry the record payload is still PLAINTEXT, so we
|
||||
//! log the content type + the source buffers, then call the original.
|
||||
//!
|
||||
//! Everything is written to `C:\openfut\hook.log` with timestamps, in stages,
|
||||
//! so the log alone tells us how far initialization got.
|
||||
//!
|
||||
//! Clean-room: this is generic proxy/loader/hook scaffolding plus a byte
|
||||
//! pattern derived from observing the binary the user owns. No EA source.
|
||||
|
||||
use core::ffi::c_void;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use retour::RawDetour;
|
||||
use windows::core::{PCSTR, PCWSTR};
|
||||
use windows::Win32::Foundation::{BOOL, HMODULE};
|
||||
use windows::Win32::Networking::WinSock::{WSAGetLastError, AF_INET, SOCKADDR, SOCKADDR_IN};
|
||||
use windows::Win32::System::LibraryLoader::{
|
||||
DisableThreadLibraryCalls, GetModuleHandleW, GetProcAddress, LoadLibraryW,
|
||||
};
|
||||
use windows::Win32::System::ProcessStatus::{GetModuleInformation, MODULEINFO};
|
||||
use windows::Win32::System::SystemInformation::GetLocalTime;
|
||||
use windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH;
|
||||
use windows::Win32::System::Threading::{
|
||||
CreateThread, GetCurrentProcess, GetCurrentThreadStackLimits, Sleep, THREAD_CREATION_FLAGS,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task 2: version.dll export forwarding
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Resolved addresses of the real system `version.dll` exports. Each proxy stub
|
||||
/// below tail-jumps to its slot. AtomicUsize (not `static mut`) keeps this sound
|
||||
/// and lets the naked stubs read the raw pointer via a simple memory load.
|
||||
static REAL: [AtomicUsize; 17] = [const { AtomicUsize::new(0) }; 17];
|
||||
|
||||
/// The 17 exports a real `version.dll` provides, in the SAME order as the proxy
|
||||
/// stubs' indices below.
|
||||
const EXPORTS: [&str; 17] = [
|
||||
"GetFileVersionInfoA",
|
||||
"GetFileVersionInfoByHandle",
|
||||
"GetFileVersionInfoExA",
|
||||
"GetFileVersionInfoExW",
|
||||
"GetFileVersionInfoSizeA",
|
||||
"GetFileVersionInfoSizeExA",
|
||||
"GetFileVersionInfoSizeExW",
|
||||
"GetFileVersionInfoSizeW",
|
||||
"GetFileVersionInfoW",
|
||||
"VerFindFileA",
|
||||
"VerFindFileW",
|
||||
"VerInstallFileA",
|
||||
"VerInstallFileW",
|
||||
"VerLanguageNameA",
|
||||
"VerLanguageNameW",
|
||||
"VerQueryValueA",
|
||||
"VerQueryValueW",
|
||||
];
|
||||
|
||||
/// Generate an exported, naked proxy stub that tail-jumps to `REAL[idx]`.
|
||||
/// A tail `jmp` leaves every register/stack arg untouched, so it forwards any
|
||||
/// signature correctly regardless of how many arguments the real function takes.
|
||||
macro_rules! proxy_stub {
|
||||
($idx:literal, $name:ident) => {
|
||||
#[no_mangle]
|
||||
#[unsafe(naked)]
|
||||
pub unsafe extern "system" fn $name() {
|
||||
core::arch::naked_asm!(
|
||||
"jmp qword ptr [rip + {base} + {off}]",
|
||||
base = sym REAL,
|
||||
off = const $idx * 8,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
proxy_stub!(0, GetFileVersionInfoA);
|
||||
proxy_stub!(1, GetFileVersionInfoByHandle);
|
||||
proxy_stub!(2, GetFileVersionInfoExA);
|
||||
proxy_stub!(3, GetFileVersionInfoExW);
|
||||
proxy_stub!(4, GetFileVersionInfoSizeA);
|
||||
proxy_stub!(5, GetFileVersionInfoSizeExA);
|
||||
proxy_stub!(6, GetFileVersionInfoSizeExW);
|
||||
proxy_stub!(7, GetFileVersionInfoSizeW);
|
||||
proxy_stub!(8, GetFileVersionInfoW);
|
||||
proxy_stub!(9, VerFindFileA);
|
||||
proxy_stub!(10, VerFindFileW);
|
||||
proxy_stub!(11, VerInstallFileA);
|
||||
proxy_stub!(12, VerInstallFileW);
|
||||
proxy_stub!(13, VerLanguageNameA);
|
||||
proxy_stub!(14, VerLanguageNameW);
|
||||
proxy_stub!(15, VerQueryValueA);
|
||||
proxy_stub!(16, VerQueryValueW);
|
||||
|
||||
/// Load the genuine `version.dll` by full path (so we don't re-load ourselves)
|
||||
/// and fill `REAL[]` with each export's address. Done synchronously in DllMain
|
||||
/// so the slots are ready before the game calls any version function.
|
||||
unsafe fn resolve_exports() {
|
||||
let path = wide("C:\\Windows\\System32\\version.dll");
|
||||
let module = match LoadLibraryW(PCWSTR(path.as_ptr())) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log(&format!("FATAL: could not load real version.dll: {e:?}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut missing = 0;
|
||||
for (i, name) in EXPORTS.iter().enumerate() {
|
||||
let cname = std::ffi::CString::new(*name).unwrap();
|
||||
let addr = GetProcAddress(module, PCSTR(cname.as_ptr() as *const u8))
|
||||
.map(|f| f as usize)
|
||||
.unwrap_or(0);
|
||||
REAL[i].store(addr, Ordering::SeqCst);
|
||||
if addr == 0 {
|
||||
missing += 1;
|
||||
log(&format!("warn: real version.dll missing export {name}"));
|
||||
}
|
||||
}
|
||||
log(&format!("forwarded {} version.dll exports", 17 - missing));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task 3: the _ProtoSSLSendPacket detour
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Trampoline to the original `_ProtoSSLSendPacket`, stored after we hook.
|
||||
static ORIG: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Signature derived from the disassembly (Windows x64 ABI):
|
||||
/// `_ProtoSSLSendPacket(pState, contentType, bufA, lenA, bufB, lenB) -> i32`.
|
||||
type SendPacket =
|
||||
unsafe extern "system" fn(*mut c_void, u32, *const u8, i32, *const u8, i32) -> i32;
|
||||
|
||||
/// Byte pattern for the `_ProtoSSLSendPacket` prologue. The four `0x00` bytes at
|
||||
/// the masked positions are the stack-cookie `mov rax,[rip+disp32]` displacement
|
||||
/// (position-dependent), so they're wildcarded via `MASK`.
|
||||
const PATTERN: [u8; 36] = [
|
||||
0x40, 0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x41, 0x55, 0x41, 0x57, 0x48, 0x81, 0xEC, 0xB0,
|
||||
0x00, 0x00, 0x00, 0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00, 0x48, 0x33, 0xC4, 0x48, 0x89,
|
||||
0x84, 0x24, 0xA0, 0x00, 0x00, 0x00,
|
||||
];
|
||||
/// `false` = wildcard byte (don't compare). Indices 21..=24 are the disp32.
|
||||
const MASK: [bool; 36] = {
|
||||
let mut m = [true; 36];
|
||||
m[21] = false;
|
||||
m[22] = false;
|
||||
m[23] = false;
|
||||
m[24] = false;
|
||||
m
|
||||
};
|
||||
|
||||
/// Our detour. At entry the record payload is still plaintext, so we log the
|
||||
/// content type and source buffers, then forward to the original unchanged.
|
||||
unsafe extern "system" fn hooked(
|
||||
p_state: *mut c_void,
|
||||
content_type: u32,
|
||||
buf_a: *const u8,
|
||||
len_a: i32,
|
||||
buf_b: *const u8,
|
||||
len_b: i32,
|
||||
) -> i32 {
|
||||
log_frame(content_type as u8, buf_a, len_a, buf_b, len_b);
|
||||
|
||||
let orig = ORIG.load(Ordering::SeqCst);
|
||||
if orig != 0 {
|
||||
let orig: SendPacket = core::mem::transmute(orig);
|
||||
orig(p_state, content_type, buf_a, len_a, buf_b, len_b)
|
||||
} else {
|
||||
// Should never happen (we store ORIG before the game can call us), but
|
||||
// never crash the game if it does.
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Background init: pattern-scan FIFA23.exe for the function, then detour it.
|
||||
/// Retries for a while in case the image isn't fully paged in at load.
|
||||
unsafe extern "system" fn init_thread(_: *mut c_void) -> u32 {
|
||||
// Connection capture first. Listen on the LSX port (gate 1, so the launcher
|
||||
// bootstrap succeeds) and on the redirect port (for external TLS/Blaze).
|
||||
store_exe_range();
|
||||
start_listener(LSX_PORT, "LSX");
|
||||
start_listener(LOCAL_PORT, "BLZ");
|
||||
hook_dns();
|
||||
hook_winsock_data();
|
||||
hook_connect();
|
||||
|
||||
// Then the plaintext-capture detour on _ProtoSSLSendPacket, plus the
|
||||
// read-only anadius GoOnline probe (M1). Retry until both are installed.
|
||||
let mut send_done = false;
|
||||
let mut anadius_done = false;
|
||||
for _ in 0..60 {
|
||||
if !send_done {
|
||||
if let Some(addr) = find_send_packet() {
|
||||
install_hook(addr);
|
||||
send_done = true;
|
||||
}
|
||||
}
|
||||
if !anadius_done && hook_anadius_probes() {
|
||||
anadius_done = true;
|
||||
}
|
||||
if send_done && anadius_done {
|
||||
return 0;
|
||||
}
|
||||
Sleep(1000);
|
||||
}
|
||||
if !send_done {
|
||||
log("ERROR: _ProtoSSLSendPacket pattern not found after 60s");
|
||||
}
|
||||
if !anadius_done {
|
||||
log("ERROR: anadius64.dll not loaded after 60s; GoOnline probe not installed");
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// Scan the FIFA23.exe image for the `_ProtoSSLSendPacket` prologue pattern.
|
||||
unsafe fn find_send_packet() -> Option<usize> {
|
||||
let module = GetModuleHandleW(PCWSTR::null()).ok()?; // null => the EXE itself
|
||||
let base = module.0 as usize;
|
||||
|
||||
let mut info = MODULEINFO::default();
|
||||
GetModuleInformation(
|
||||
GetCurrentProcess(),
|
||||
module,
|
||||
&mut info,
|
||||
core::mem::size_of::<MODULEINFO>() as u32,
|
||||
)
|
||||
.ok()?;
|
||||
let size = info.SizeOfImage as usize;
|
||||
let hay = core::slice::from_raw_parts(base as *const u8, size);
|
||||
|
||||
let plen = PATTERN.len();
|
||||
if hay.len() < plen {
|
||||
return None;
|
||||
}
|
||||
for i in 0..=hay.len() - plen {
|
||||
// Cheap first-byte gate before the full compare.
|
||||
if hay[i] != PATTERN[0] {
|
||||
continue;
|
||||
}
|
||||
let mut ok = true;
|
||||
for j in 1..plen {
|
||||
if MASK[j] && hay[i + j] != PATTERN[j] {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
return Some(base + i);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Install the inline detour on the resolved function address.
|
||||
unsafe fn install_hook(addr: usize) {
|
||||
let detour = match RawDetour::new(addr as *const (), hooked as *const ()) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log(&format!("ERROR: could not create detour: {e:?}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = detour.enable() {
|
||||
log(&format!("ERROR: could not enable detour: {e:?}"));
|
||||
return;
|
||||
}
|
||||
// Leak the detour so it lives forever (dropping it would un-hook), and
|
||||
// publish its trampoline so `hooked` can call the original.
|
||||
let detour: &'static RawDetour = Box::leak(Box::new(detour));
|
||||
ORIG.store(detour.trampoline() as *const () as usize, Ordering::SeqCst);
|
||||
log(&format!(
|
||||
"hook installed: _ProtoSSLSendPacket @ 0x{addr:X} (FIFA23.exe+0x{:X})",
|
||||
addr - module_base(),
|
||||
));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connection capture: log every connect() and redirect external attempts to a
|
||||
// local listener, so the client's TCP succeeds and it starts sending TLS.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Local port our in-DLL listener binds; redirected connections land here.
|
||||
const LOCAL_PORT: u16 = 7777;
|
||||
|
||||
/// The EA launcher LSX port. The game connects here for launcher↔game bootstrap
|
||||
/// (gate 1). We listen so the connect succeeds and we can see the LSX protocol.
|
||||
const LSX_PORT: u16 = 3216;
|
||||
|
||||
/// Trampoline to the original ws2_32 `connect`.
|
||||
static ORIG_CONNECT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
type ConnectFn = unsafe extern "system" fn(usize, *const SOCKADDR, i32) -> i32;
|
||||
|
||||
/// Our `connect` detour: log the real destination, and for any non-loopback
|
||||
/// IPv4 target, rewrite it to 127.0.0.1:LOCAL_PORT before calling the original.
|
||||
unsafe extern "system" fn hooked_connect(s: usize, name: *const SOCKADDR, namelen: i32) -> i32 {
|
||||
let orig: ConnectFn = core::mem::transmute(ORIG_CONNECT.load(Ordering::SeqCst));
|
||||
|
||||
if !name.is_null() && (*name).sa_family == AF_INET {
|
||||
let sin = name as *const SOCKADDR_IN;
|
||||
let port = u16::from_be((*sin).sin_port);
|
||||
let octets = (*sin).sin_addr.S_un.S_addr.to_ne_bytes();
|
||||
let is_loopback = octets[0] == 127;
|
||||
let dst = format!("{}.{}.{}.{}:{}", octets[0], octets[1], octets[2], octets[3], port);
|
||||
|
||||
if !is_loopback {
|
||||
// Build a fresh 127.0.0.1:LOCAL_PORT address and connect there.
|
||||
let mut local: SOCKADDR_IN = core::mem::zeroed();
|
||||
local.sin_family = AF_INET;
|
||||
local.sin_port = LOCAL_PORT.to_be();
|
||||
local.sin_addr.S_un.S_addr = u32::from_ne_bytes([127, 0, 0, 1]);
|
||||
let ret = orig(
|
||||
s,
|
||||
&local as *const SOCKADDR_IN as *const SOCKADDR,
|
||||
core::mem::size_of::<SOCKADDR_IN>() as i32,
|
||||
);
|
||||
let err = if ret != 0 { WSAGetLastError().0 } else { 0 };
|
||||
log(&format!("CONNECT -> {dst} [redirected->7777] ret={ret} err={err}"));
|
||||
return ret;
|
||||
} else {
|
||||
let ret = orig(s, name, namelen);
|
||||
let err = if ret != 0 { WSAGetLastError().0 } else { 0 };
|
||||
log(&format!("CONNECT -> {dst} ret={ret} err={err}"));
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
orig(s, name, namelen)
|
||||
}
|
||||
|
||||
// --- DNS logging: what hostnames does the client try to resolve? ----------
|
||||
|
||||
static ORIG_GAIW: AtomicUsize = AtomicUsize::new(0);
|
||||
static ORIG_GAI: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
type GaiWFn = unsafe extern "system" fn(PCWSTR, PCWSTR, *const c_void, *mut *mut c_void) -> i32;
|
||||
type GaiFn = unsafe extern "system" fn(PCSTR, PCSTR, *const c_void, *mut *mut c_void) -> i32;
|
||||
|
||||
unsafe extern "system" fn hooked_gaiw(
|
||||
node: PCWSTR,
|
||||
svc: PCWSTR,
|
||||
hints: *const c_void,
|
||||
res: *mut *mut c_void,
|
||||
) -> i32 {
|
||||
let name = if node.is_null() {
|
||||
"<null>".to_string()
|
||||
} else {
|
||||
node.to_string().unwrap_or_else(|_| "<?>".into())
|
||||
};
|
||||
let orig: GaiWFn = core::mem::transmute(ORIG_GAIW.load(Ordering::SeqCst));
|
||||
let ret = orig(node, svc, hints, res);
|
||||
log(&format!("DNS GetAddrInfoW(\"{name}\") ret={ret}"));
|
||||
ret
|
||||
}
|
||||
|
||||
unsafe extern "system" fn hooked_gai(
|
||||
node: PCSTR,
|
||||
svc: PCSTR,
|
||||
hints: *const c_void,
|
||||
res: *mut *mut c_void,
|
||||
) -> i32 {
|
||||
let name = if node.is_null() {
|
||||
"<null>".to_string()
|
||||
} else {
|
||||
node.to_string().unwrap_or_else(|_| "<?>".into())
|
||||
};
|
||||
let orig: GaiFn = core::mem::transmute(ORIG_GAI.load(Ordering::SeqCst));
|
||||
let ret = orig(node, svc, hints, res);
|
||||
log(&format!("DNS getaddrinfo(\"{name}\") ret={ret}"));
|
||||
ret
|
||||
}
|
||||
|
||||
/// Generic: resolve `name` in `module`, install a detour to `detour`, store the
|
||||
/// trampoline in `slot`.
|
||||
unsafe fn install_detour(
|
||||
module: HMODULE,
|
||||
name: &[u8],
|
||||
detour: *const (),
|
||||
slot: &AtomicUsize,
|
||||
label: &str,
|
||||
) {
|
||||
let addr = match GetProcAddress(module, PCSTR(name.as_ptr())) {
|
||||
Some(f) => f as usize,
|
||||
None => {
|
||||
log(&format!("ERROR: {label} not found"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
install_detour_at(addr, detour, slot, label);
|
||||
}
|
||||
|
||||
/// Install an inline detour at a raw address (for non-exported targets such as
|
||||
/// internal anadius handlers located by module+offset).
|
||||
unsafe fn install_detour_at(addr: usize, detour: *const (), slot: &AtomicUsize, label: &str) {
|
||||
let d = match RawDetour::new(addr as *const (), detour) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log(&format!("ERROR: {label} detour create: {e:?}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if d.enable().is_err() {
|
||||
log(&format!("ERROR: {label} detour enable failed"));
|
||||
return;
|
||||
}
|
||||
let d: &'static RawDetour = Box::leak(Box::new(d));
|
||||
slot.store(d.trampoline() as *const () as usize, Ordering::SeqCst);
|
||||
log(&format!("{label} hook installed"));
|
||||
}
|
||||
|
||||
// --- M1 read-only probe: anadius GoOnline handler -------------------------
|
||||
|
||||
static ORIG_GOONLINE: AtomicUsize = AtomicUsize::new(0);
|
||||
static EXE_BASE: AtomicUsize = AtomicUsize::new(0);
|
||||
static EXE_SIZE: AtomicUsize = AtomicUsize::new(0);
|
||||
static STACK_LOGGED: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Record FIFA23.exe's base + size so we can recognise its frames in a backtrace.
|
||||
unsafe fn store_exe_range() {
|
||||
if let Ok(h) = GetModuleHandleW(PCWSTR::null()) {
|
||||
let mut mi = MODULEINFO::default();
|
||||
if GetModuleInformation(
|
||||
GetCurrentProcess(),
|
||||
h,
|
||||
&mut mi,
|
||||
core::mem::size_of::<MODULEINFO>() as u32,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
EXE_BASE.store(h.0 as usize, Ordering::SeqCst);
|
||||
EXE_SIZE.store(mi.SizeOfImage as usize, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan the raw stack for values that land in FIFA23.exe (the game-side
|
||||
/// online-flow return addresses that called into GoOnline). Unwind-free, so it
|
||||
/// survives the detour trampolines that break RtlCaptureStackBackTrace.
|
||||
/// One-shot to avoid log spam.
|
||||
unsafe fn log_fifa_callstack(tag: &str) {
|
||||
if STACK_LOGGED.swap(1, Ordering::SeqCst) != 0 {
|
||||
return;
|
||||
}
|
||||
let base = EXE_BASE.load(Ordering::SeqCst);
|
||||
let size = EXE_SIZE.load(Ordering::SeqCst);
|
||||
if base == 0 || size == 0 {
|
||||
log(&format!("{tag} stack scan skipped (exe range unknown)"));
|
||||
return;
|
||||
}
|
||||
// Address of a local ~= current rsp; the stack grows down, so callers'
|
||||
// return addresses sit at HIGHER addresses. Scan upward, but NEVER past the
|
||||
// committed stack top (reading beyond it faults — that crashed the game).
|
||||
let mut low: usize = 0;
|
||||
let mut high: usize = 0;
|
||||
GetCurrentThreadStackLimits(&mut low, &mut high);
|
||||
let probe: usize = 0;
|
||||
let sp = &probe as *const usize as usize;
|
||||
let end = high; // scan the whole rest of the stack (committed, safe)
|
||||
let mut line = format!(
|
||||
"{tag} stack[low=0x{low:X} high=0x{high:X} sp=0x{sp:X}] FIFA23.exe refs:"
|
||||
);
|
||||
let mut count = 0;
|
||||
let mut p = sp;
|
||||
while p + 8 <= end {
|
||||
let val = *(p as *const usize);
|
||||
if val >= base && val < base + size {
|
||||
line.push_str(&format!(" +0x{:X}", val - base));
|
||||
count += 1;
|
||||
if count >= 40 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
p += 8;
|
||||
}
|
||||
log(&line);
|
||||
}
|
||||
|
||||
/// M2 flip on anadius's GoOnline handler (anadius64.dll+0x2BB90). The original
|
||||
/// handler is `mov rcx,rdx; lea r8,[+0xADD73]; lea rdx,[+0xADE64 = "0"]; call
|
||||
/// +0x25BE0; mov al,1` — i.e. it builds its ErrorSuccess response with the value
|
||||
/// "0" (offline). We replicate it but pass "1" (+0xAF530 = the connected value),
|
||||
/// so GoOnline reports online, then return success (al=1).
|
||||
unsafe extern "system" fn hooked_goonline(_a: usize, b: usize, _c: usize, _d: usize) -> usize {
|
||||
log_fifa_callstack("GoOnline");
|
||||
let base = ANADIUS_BASE.load(Ordering::SeqCst);
|
||||
if base != 0 {
|
||||
log("FLIP GoOnline -> reporting online (\"1\")");
|
||||
let builder: unsafe extern "system" fn(usize, usize, usize) -> usize =
|
||||
core::mem::transmute(base + 0x25BE0);
|
||||
// 0x25BE0(rcx = handler's rdx, rdx = "1", r8 = +0xADD73)
|
||||
builder(b, base + 0xAF530, base + 0xADD73);
|
||||
return 1;
|
||||
}
|
||||
let orig = ORIG_GOONLINE.load(Ordering::SeqCst);
|
||||
if orig != 0 {
|
||||
let f: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
|
||||
core::mem::transmute(orig);
|
||||
f(_a, b, _c, _d)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// --- M2 flip: force GetInternetConnectedState to report "connected" --------
|
||||
|
||||
static ORIG_ICS: AtomicUsize = AtomicUsize::new(0);
|
||||
static ANADIUS_BASE: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// anadius's GetInternetConnectedState handler (anadius64.dll+0x27790) builds an
|
||||
/// LSX response whose `connected` value is:
|
||||
/// (byte[+0xCAB1B] || byte[+0xCAB1A]) ? connected : offline
|
||||
/// Both default to 0 → offline → the game aborts at "connecting". We force both
|
||||
/// flags to 1 before the original runs, so it builds the "connected" response.
|
||||
unsafe extern "system" fn hooked_ics(a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||
let base = ANADIUS_BASE.load(Ordering::SeqCst);
|
||||
if base != 0 {
|
||||
core::ptr::write_volatile((base + 0xCAB1A) as *mut u8, 1u8);
|
||||
core::ptr::write_volatile((base + 0xCAB1B) as *mut u8, 1u8);
|
||||
}
|
||||
log("FLIP GetInternetConnectedState -> forcing connected (flags set)");
|
||||
let orig = ORIG_ICS.load(Ordering::SeqCst);
|
||||
if orig != 0 {
|
||||
let f: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
|
||||
core::mem::transmute(orig);
|
||||
f(a, b, c, d)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve anadius64.dll's runtime base, detour the GoOnline probe, and install
|
||||
/// the M2 GetInternetConnectedState flip. Returns false if anadius isn't loaded.
|
||||
unsafe fn hook_anadius_probes() -> bool {
|
||||
let base = match GetModuleHandleW(PCWSTR(wide("anadius64.dll").as_ptr())) {
|
||||
Ok(m) => m.0 as usize,
|
||||
Err(_) => return false, // not loaded yet
|
||||
};
|
||||
ANADIUS_BASE.store(base, Ordering::SeqCst);
|
||||
log(&format!("anadius64.dll base = 0x{base:X}"));
|
||||
|
||||
install_detour_at(
|
||||
base + 0x2BB90,
|
||||
hooked_goonline as *const (),
|
||||
&ORIG_GOONLINE,
|
||||
"PROBE anadius GoOnline @ +0x2BB90",
|
||||
);
|
||||
install_detour_at(
|
||||
base + 0x27790,
|
||||
hooked_ics as *const (),
|
||||
&ORIG_ICS,
|
||||
"FLIP anadius GetInternetConnectedState @ +0x27790",
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
/// Detour the DNS resolvers so we see every hostname lookup.
|
||||
unsafe fn hook_dns() {
|
||||
let ws2 = match LoadLibraryW(PCWSTR(wide("ws2_32.dll").as_ptr())) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log(&format!("ERROR: load ws2_32 for DNS: {e:?}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
install_detour(ws2, b"GetAddrInfoW\0", hooked_gaiw as *const (), &ORIG_GAIW, "GetAddrInfoW");
|
||||
install_detour(ws2, b"getaddrinfo\0", hooked_gai as *const (), &ORIG_GAI, "getaddrinfo");
|
||||
}
|
||||
|
||||
// --- LSX capture: read the Ebisu-SDK <-> anadius XML conversation ----------
|
||||
|
||||
static ORIG_SEND: AtomicUsize = AtomicUsize::new(0);
|
||||
static ORIG_RECV: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
type SendFn = unsafe extern "system" fn(usize, *const u8, i32, i32) -> i32;
|
||||
type RecvFn = unsafe extern "system" fn(usize, *mut u8, i32, i32) -> i32;
|
||||
|
||||
/// Cheap test: does this buffer look like LSX/Ebisu XML (not TLS/binary)?
|
||||
fn looks_like_lsx(buf: &[u8]) -> bool {
|
||||
let n = buf.len().min(64);
|
||||
let head = &buf[..n];
|
||||
let has_lt = head.iter().any(|&b| b == b'<');
|
||||
let has_gt = head.iter().any(|&b| b == b'>');
|
||||
head.windows(3).any(|w| w == b"LSX")
|
||||
|| head.windows(5).any(|w| w == b"Ebisu")
|
||||
|| (has_lt && has_gt)
|
||||
}
|
||||
|
||||
unsafe extern "system" fn hooked_send(s: usize, buf: *const u8, len: i32, flags: i32) -> i32 {
|
||||
if len > 0 && !buf.is_null() {
|
||||
let head = core::slice::from_raw_parts(buf, (len as usize).min(64));
|
||||
if looks_like_lsx(head) {
|
||||
let show = core::slice::from_raw_parts(buf, (len as usize).min(800));
|
||||
log(&format!("LSX send sock={s} {len}B: {}", ascii_render(show)));
|
||||
}
|
||||
}
|
||||
let orig: SendFn = core::mem::transmute(ORIG_SEND.load(Ordering::SeqCst));
|
||||
orig(s, buf, len, flags)
|
||||
}
|
||||
|
||||
unsafe extern "system" fn hooked_recv(s: usize, buf: *mut u8, len: i32, flags: i32) -> i32 {
|
||||
let orig: RecvFn = core::mem::transmute(ORIG_RECV.load(Ordering::SeqCst));
|
||||
let ret = orig(s, buf, len, flags);
|
||||
if ret > 0 && !buf.is_null() {
|
||||
let head = core::slice::from_raw_parts(buf, (ret as usize).min(64));
|
||||
if looks_like_lsx(head) {
|
||||
let show = core::slice::from_raw_parts(buf, (ret as usize).min(800));
|
||||
log(&format!("LSX recv sock={s} {ret}B: {}", ascii_render(show)));
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
// Async (overlapped/IOCP) variants. A WSABUF is { len, buf }.
|
||||
#[repr(C)]
|
||||
struct WsaBuf {
|
||||
len: u32,
|
||||
buf: *mut u8,
|
||||
}
|
||||
|
||||
static ORIG_WSASEND: AtomicUsize = AtomicUsize::new(0);
|
||||
static ORIG_WSARECV: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
type WsaSendFn = unsafe extern "system" fn(
|
||||
usize,
|
||||
*const WsaBuf,
|
||||
u32,
|
||||
*mut u32,
|
||||
u32,
|
||||
*mut c_void,
|
||||
*mut c_void,
|
||||
) -> i32;
|
||||
type WsaRecvFn = unsafe extern "system" fn(
|
||||
usize,
|
||||
*const WsaBuf,
|
||||
u32,
|
||||
*mut u32,
|
||||
*mut u32,
|
||||
*mut c_void,
|
||||
*mut c_void,
|
||||
) -> i32;
|
||||
|
||||
unsafe extern "system" fn hooked_wsasend(
|
||||
s: usize,
|
||||
bufs: *const WsaBuf,
|
||||
count: u32,
|
||||
sent: *mut u32,
|
||||
flags: u32,
|
||||
ovl: *mut c_void,
|
||||
cr: *mut c_void,
|
||||
) -> i32 {
|
||||
// Outgoing data is readable before the call — capture the first buffer.
|
||||
if !bufs.is_null() && count > 0 {
|
||||
let b0 = &*bufs;
|
||||
if b0.len > 0 && !b0.buf.is_null() {
|
||||
let head = core::slice::from_raw_parts(b0.buf, (b0.len as usize).min(64));
|
||||
if looks_like_lsx(head) {
|
||||
let show = core::slice::from_raw_parts(b0.buf, (b0.len as usize).min(800));
|
||||
log(&format!("LSX WSASend sock={s} {}B: {}", b0.len, ascii_render(show)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let orig: WsaSendFn = core::mem::transmute(ORIG_WSASEND.load(Ordering::SeqCst));
|
||||
orig(s, bufs, count, sent, flags, ovl, cr)
|
||||
}
|
||||
|
||||
unsafe extern "system" fn hooked_wsarecv(
|
||||
s: usize,
|
||||
bufs: *const WsaBuf,
|
||||
count: u32,
|
||||
recvd: *mut u32,
|
||||
flags: *mut u32,
|
||||
ovl: *mut c_void,
|
||||
cr: *mut c_void,
|
||||
) -> i32 {
|
||||
let orig: WsaRecvFn = core::mem::transmute(ORIG_WSARECV.load(Ordering::SeqCst));
|
||||
let ret = orig(s, bufs, count, recvd, flags, ovl, cr);
|
||||
// Only the synchronous case (no overlapped) has data ready on return.
|
||||
if ret == 0 && ovl.is_null() && !recvd.is_null() && !bufs.is_null() && count > 0 {
|
||||
let n = *recvd as usize;
|
||||
let b0 = &*bufs;
|
||||
if n > 0 && !b0.buf.is_null() {
|
||||
let cap = n.min(b0.len as usize);
|
||||
let head = core::slice::from_raw_parts(b0.buf, cap.min(64));
|
||||
if looks_like_lsx(head) {
|
||||
let show = core::slice::from_raw_parts(b0.buf, cap.min(800));
|
||||
log(&format!("LSX WSARecv sock={s} {n}B: {}", ascii_render(show)));
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Detour ws2_32 send/recv (sync) and WSASend/WSARecv (async) to capture LSX XML.
|
||||
unsafe fn hook_winsock_data() {
|
||||
let ws2 = match LoadLibraryW(PCWSTR(wide("ws2_32.dll").as_ptr())) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log(&format!("ERROR: load ws2_32 for send/recv: {e:?}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
install_detour(ws2, b"send\0", hooked_send as *const (), &ORIG_SEND, "send");
|
||||
install_detour(ws2, b"recv\0", hooked_recv as *const (), &ORIG_RECV, "recv");
|
||||
install_detour(ws2, b"WSASend\0", hooked_wsasend as *const (), &ORIG_WSASEND, "WSASend");
|
||||
install_detour(ws2, b"WSARecv\0", hooked_wsarecv as *const (), &ORIG_WSARECV, "WSARecv");
|
||||
}
|
||||
|
||||
/// Resolve and detour ws2_32 `connect`.
|
||||
unsafe fn hook_connect() {
|
||||
let ws2 = match LoadLibraryW(PCWSTR(wide("ws2_32.dll").as_ptr())) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log(&format!("ERROR: could not load ws2_32.dll: {e:?}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let addr = match GetProcAddress(ws2, PCSTR(b"connect\0".as_ptr())) {
|
||||
Some(f) => f as usize,
|
||||
None => {
|
||||
log("ERROR: connect not found in ws2_32.dll");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let detour = match RawDetour::new(addr as *const (), hooked_connect as *const ()) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log(&format!("ERROR: could not create connect detour: {e:?}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = detour.enable() {
|
||||
log(&format!("ERROR: could not enable connect detour: {e:?}"));
|
||||
return;
|
||||
}
|
||||
let detour: &'static RawDetour = Box::leak(Box::new(detour));
|
||||
ORIG_CONNECT.store(detour.trampoline() as *const () as usize, Ordering::SeqCst);
|
||||
log("connect hook installed (external IPv4 -> 127.0.0.1:7777)");
|
||||
}
|
||||
|
||||
/// Spawn a TCP listener on `127.0.0.1:port`, tagging logged traffic with `tag`.
|
||||
/// Accepts connections and logs what the client sends — as readable text (for
|
||||
/// the text-based LSX protocol) plus a hex prefix (for binary TLS/Blaze).
|
||||
fn start_listener(port: u16, tag: &'static str) {
|
||||
std::thread::spawn(move || {
|
||||
let listener = match std::net::TcpListener::bind(("127.0.0.1", port)) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
log(&format!("ERROR: listener[{tag}] bind {port} failed: {e}"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let bound = listener
|
||||
.local_addr()
|
||||
.map(|a| a.to_string())
|
||||
.unwrap_or_default();
|
||||
log(&format!("listener[{tag}] up, bound {bound}, accepting"));
|
||||
|
||||
// Explicit accept loop so accept errors are visible (not swallowed).
|
||||
loop {
|
||||
match listener.accept() {
|
||||
Ok((stream, peer)) => {
|
||||
log(&format!("ACCEPT[{tag}] from {peer}"));
|
||||
std::thread::spawn(move || handle_conn(stream, tag, peer.to_string()));
|
||||
}
|
||||
Err(e) => {
|
||||
log(&format!("ACCEPT[{tag}] error: {e}"));
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_conn(mut stream: std::net::TcpStream, tag: &'static str, peer: String) {
|
||||
use std::io::Read;
|
||||
let mut buf = [0u8; 2048];
|
||||
let mut total = 0usize;
|
||||
loop {
|
||||
match stream.read(&mut buf) {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(n) => {
|
||||
total += n;
|
||||
let ascii = ascii_render(&buf[..n.min(400)]);
|
||||
let hexp = hex(&buf[..n.min(24)]);
|
||||
log(&format!("RECV[{tag}] {n}B | hex: {hexp} | text: {ascii}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
log(&format!("CLOSE[{tag}] from {peer} after {total}B total"));
|
||||
}
|
||||
|
||||
/// Render bytes as printable ASCII (non-printable -> '.'), for text protocols.
|
||||
fn ascii_render(bytes: &[u8]) -> String {
|
||||
bytes
|
||||
.iter()
|
||||
.map(|&b| {
|
||||
if (0x20..=0x7e).contains(&b) || b == b'\n' || b == b'\r' || b == b'\t' {
|
||||
b as char
|
||||
} else {
|
||||
'.'
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Logging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn log_frame(content_type: u8, buf_a: *const u8, len_a: i32, buf_b: *const u8, len_b: i32) {
|
||||
let label = match content_type {
|
||||
0x14 => "ccs",
|
||||
0x15 => "alert",
|
||||
0x16 => "handshake",
|
||||
0x17 => "appdata",
|
||||
_ => "?",
|
||||
};
|
||||
let mut line = format!("SEND type=0x{content_type:02X}({label}) lenA={len_a} lenB={len_b}");
|
||||
if !buf_a.is_null() && len_a > 0 {
|
||||
let n = (len_a as usize).min(48);
|
||||
let bytes = unsafe { core::slice::from_raw_parts(buf_a, n) };
|
||||
line.push_str(&format!(" | A: {}", hex(bytes)));
|
||||
}
|
||||
if !buf_b.is_null() && len_b > 0 {
|
||||
let n = (len_b as usize).min(16);
|
||||
let bytes = unsafe { core::slice::from_raw_parts(buf_b, n) };
|
||||
line.push_str(&format!(" | B: {}", hex(bytes)));
|
||||
}
|
||||
log(&line);
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/// Serializes log writes so concurrent threads don't corrupt each other's lines.
|
||||
static LOG_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Append a timestamped line to `C:\openfut\hook.log`.
|
||||
fn log(msg: &str) {
|
||||
use std::io::Write;
|
||||
let _guard = LOG_LOCK.lock();
|
||||
let _ = std::fs::create_dir_all("C:\\openfut");
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("C:\\openfut\\hook.log")
|
||||
{
|
||||
let _ = writeln!(f, "[{}] {}", now(), msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn now() -> String {
|
||||
let st = unsafe { GetLocalTime() };
|
||||
format!(
|
||||
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
|
||||
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers + entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn wide(s: &str) -> Vec<u16> {
|
||||
s.encode_utf16().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
/// FIFA23.exe base address (the main module), for pretty logging.
|
||||
fn module_base() -> usize {
|
||||
unsafe {
|
||||
GetModuleHandleW(PCWSTR::null())
|
||||
.map(|h| h.0 as usize)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn DllMain(module: HMODULE, reason: u32, _reserved: *mut c_void) -> BOOL {
|
||||
if reason == DLL_PROCESS_ATTACH {
|
||||
unsafe {
|
||||
let _ = DisableThreadLibraryCalls(module);
|
||||
|
||||
let host = std::env::current_exe()
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_default();
|
||||
log(&format!(
|
||||
"openfut-hook loaded | pid {} | host {host}",
|
||||
std::process::id()
|
||||
));
|
||||
|
||||
// Forward exports synchronously (must be ready before any call)...
|
||||
resolve_exports();
|
||||
|
||||
// ...then do the pattern scan + hook on a background thread (never
|
||||
// do real work directly in DllMain — loader lock).
|
||||
let _ = CreateThread(
|
||||
None,
|
||||
0,
|
||||
Some(init_thread),
|
||||
None,
|
||||
THREAD_CREATION_FLAGS(0),
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
BOOL(1)
|
||||
}
|
||||
Generated
+196
@@ -0,0 +1,196 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "iced-x86"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c447cff8c7f384a7d4f741cfcff32f75f3ad02b406432e8d6c878d56b1edf6b"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protossl-scan"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"iced-x86",
|
||||
"memchr",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.58.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
@@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "protossl-scan"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Task 1: read-only memory scan to confirm FIFA 23 uses EA DirtySDK / ProtoSSL"
|
||||
|
||||
[[bin]]
|
||||
name = "protossl-scan"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# SIMD-accelerated substring search. Orders of magnitude faster than a naive
|
||||
# byte-by-byte scan when sweeping the game's multi-GB address space.
|
||||
memchr = "2"
|
||||
|
||||
# Pure-Rust x86/x64 disassembler. Lets us read the game's own code around a
|
||||
# code anchor (clean-room: we only disassemble the binary the user owns).
|
||||
iced-x86 = "1"
|
||||
|
||||
# The official Microsoft `windows` crate gives us safe-ish bindings to the
|
||||
# Win32 APIs we need. We only turn on the few feature modules we use, to keep
|
||||
# build times and binary size down.
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_Diagnostics_Debug",
|
||||
] }
|
||||
|
||||
# This tool lives inside the openfut-bridge repo but is its OWN crate. Declaring
|
||||
# an empty [workspace] here makes Cargo treat this directory as a standalone
|
||||
# workspace root, so it does not try to attach to the openfut-bridge package
|
||||
# in the parent directory (which would error).
|
||||
[workspace]
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user