Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4bab06826 |
@@ -1,190 +0,0 @@
|
|||||||
# 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.
|
|
||||||
Generated
-336
@@ -1,336 +0,0 @@
|
|||||||
# 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"
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[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]
|
|
||||||
@@ -1,752 +0,0 @@
|
|||||||
//! 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, 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).
|
|
||||||
start_listener(LSX_PORT, "LSX");
|
|
||||||
start_listener(LOCAL_PORT, "BLZ");
|
|
||||||
hook_dns();
|
|
||||||
hook_winsock_data();
|
|
||||||
hook_connect();
|
|
||||||
|
|
||||||
// Then the plaintext-capture detour on _ProtoSSLSendPacket.
|
|
||||||
for _ in 0..60 {
|
|
||||||
if let Some(addr) = find_send_packet() {
|
|
||||||
install_hook(addr);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Sleep(1000);
|
|
||||||
}
|
|
||||||
log("ERROR: _ProtoSSLSendPacket pattern not found after 60s");
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
+88
-3
@@ -381,6 +381,36 @@ const EXACT: &[ExactRoute] = &[
|
|||||||
core_method: "GET", core_path: "/notifications",
|
core_method: "GET", core_path: "/notifications",
|
||||||
notes: "FUT active messages → Core notifications (mapped to nearest equivalent)",
|
notes: "FUT active messages → Core notifications (mapped to nearest equivalent)",
|
||||||
},
|
},
|
||||||
|
// ── Dynamic objectives (FIFA 23 actual path) ──────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/user/dynamicobjectives",
|
||||||
|
core_method: "GET", core_path: "/objectives",
|
||||||
|
notes: "FIFA 23 dynamic objectives list → Core objectives",
|
||||||
|
},
|
||||||
|
// ── SBS challenges (FIFA 23 uses /sbs/ not /sbc/) ────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/sbs/challenges",
|
||||||
|
core_method: "GET", core_path: "/sbc",
|
||||||
|
notes: "FIFA 23 SBS challenges list → Core SBC list",
|
||||||
|
},
|
||||||
|
// ── Squad Battles result (separate from rivals/result) ───────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/game/fut/squadbattle/result",
|
||||||
|
core_method: "POST", core_path: "/matches/result",
|
||||||
|
notes: "Squad Battles match result → Core match result",
|
||||||
|
},
|
||||||
|
// ── Match submission (FIFA 23 uses /game/r) ───────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "POST", ea_path: "/ut/game/fut/game/r",
|
||||||
|
core_method: "POST", core_path: "/matches/result",
|
||||||
|
notes: "FIFA 23 match result (short path /game/r) → Core match result",
|
||||||
|
},
|
||||||
|
// ── Squad list ────────────────────────────────────────────────────────────
|
||||||
|
ExactRoute {
|
||||||
|
ea_method: "GET", ea_path: "/ut/game/fut/squads/user",
|
||||||
|
core_method: "GET", core_path: "/squad",
|
||||||
|
notes: "FUT squad list for user → Core squad",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── Prefix mappings (dynamic path segments after a known prefix) ───────────────
|
// ── Prefix mappings (dynamic path segments after a known prefix) ───────────────
|
||||||
@@ -431,6 +461,36 @@ fn prefix_routes() -> &'static [PrefixRoute] {
|
|||||||
core_path_fn: |_| "/sbc/submit".to_string(),
|
core_path_fn: |_| "/sbc/submit".to_string(),
|
||||||
notes: "FUT SBC set submission → Core SBC submit",
|
notes: "FUT SBC set submission → Core SBC submit",
|
||||||
},
|
},
|
||||||
|
// /ut/game/fut/sbs/challenges/{id} → GET /sbc/{id}
|
||||||
|
PrefixRoute {
|
||||||
|
ea_method: "GET",
|
||||||
|
ea_prefix: "/ut/game/fut/sbs/challenges/",
|
||||||
|
core_path_fn: |suffix| {
|
||||||
|
// suffix is "{id}" or "{id}/sets/{setId}" — strip extra segments
|
||||||
|
let id = suffix.split('/').next().unwrap_or(suffix);
|
||||||
|
format!("/sbc/{id}")
|
||||||
|
},
|
||||||
|
notes: "FIFA 23 SBS challenge detail → Core SBC by ID",
|
||||||
|
},
|
||||||
|
// /ut/game/fut/sbs/challenges/{id}/sets/{setId}/trade → POST /sbc/submit
|
||||||
|
PrefixRoute {
|
||||||
|
ea_method: "POST",
|
||||||
|
ea_prefix: "/ut/game/fut/sbs/challenges/",
|
||||||
|
core_path_fn: |_| "/sbc/submit".to_string(),
|
||||||
|
notes: "FIFA 23 SBS challenge trade → Core SBC submit",
|
||||||
|
},
|
||||||
|
// /ut/game/fut/user/dynamicobjectives/{id}/milestones/{m}/claim
|
||||||
|
// → POST /objectives/{id}/claim
|
||||||
|
PrefixRoute {
|
||||||
|
ea_method: "POST",
|
||||||
|
ea_prefix: "/ut/game/fut/user/dynamicobjectives/",
|
||||||
|
core_path_fn: |suffix| {
|
||||||
|
// suffix is "{id}/milestones/{m}/claim" — extract just the obj id
|
||||||
|
let obj_id = suffix.split('/').next().unwrap_or(suffix);
|
||||||
|
format!("/objectives/{obj_id}/claim")
|
||||||
|
},
|
||||||
|
notes: "FIFA 23 dynamic objective claim → Core objective claim by ID",
|
||||||
|
},
|
||||||
// /ut/game/fut/trade/{trade_id} (DELETE) → DELETE /market/listings/{trade_id}
|
// /ut/game/fut/trade/{trade_id} (DELETE) → DELETE /market/listings/{trade_id}
|
||||||
PrefixRoute {
|
PrefixRoute {
|
||||||
ea_method: "DELETE",
|
ea_method: "DELETE",
|
||||||
@@ -523,7 +583,7 @@ pub fn map_to_core(method: &str, path: &str) -> Option<CoreMapping> {
|
|||||||
if !suffix.is_empty() {
|
if !suffix.is_empty() {
|
||||||
let core_path = (r.core_path_fn)(suffix);
|
let core_path = (r.core_path_fn)(suffix);
|
||||||
return Some(CoreMapping {
|
return Some(CoreMapping {
|
||||||
method: "GET", // overridden per-route below
|
method: r.ea_method,
|
||||||
core_path,
|
core_path,
|
||||||
notes: r.notes,
|
notes: r.notes,
|
||||||
});
|
});
|
||||||
@@ -720,14 +780,39 @@ mod tests {
|
|||||||
fn test_quick_sell_maps() {
|
fn test_quick_sell_maps() {
|
||||||
let m = map_to_core("DELETE", "/ut/game/fut/item/owned-111");
|
let m = map_to_core("DELETE", "/ut/game/fut/item/owned-111");
|
||||||
assert!(m.is_some());
|
assert!(m.is_some());
|
||||||
assert_eq!(m.unwrap().core_path, "/collection/owned-111");
|
let m = m.unwrap();
|
||||||
|
assert_eq!(m.core_path, "/collection/owned-111");
|
||||||
|
assert_eq!(m.method, "DELETE");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cancel_listing_maps() {
|
fn test_cancel_listing_maps() {
|
||||||
let m = map_to_core("DELETE", "/ut/game/fut/trade/trade-222");
|
let m = map_to_core("DELETE", "/ut/game/fut/trade/trade-222");
|
||||||
assert!(m.is_some());
|
assert!(m.is_some());
|
||||||
assert_eq!(m.unwrap().core_path, "/market/listings/trade-222");
|
let m = m.unwrap();
|
||||||
|
assert_eq!(m.core_path, "/market/listings/trade-222");
|
||||||
|
assert_eq!(m.method, "DELETE");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prefix_routes_preserve_method() {
|
||||||
|
let cases = [
|
||||||
|
("POST", "/ut/game/fut/draft/abc-123/pick", "POST"),
|
||||||
|
("POST", "/ut/game/fut/champs/sess-456/result", "POST"),
|
||||||
|
("DELETE", "/ut/game/fut/trade/trade-222", "DELETE"),
|
||||||
|
("DELETE", "/ut/game/fut/item/owned-111", "DELETE"),
|
||||||
|
("PUT", "/ut/game/fut/item/owned-999", "PUT"),
|
||||||
|
("POST", "/ut/game/fut/store/pack/pack-789/open", "POST"),
|
||||||
|
];
|
||||||
|
for (ea_method, path, expected_method) in cases {
|
||||||
|
let m = map_to_core(ea_method, path)
|
||||||
|
.unwrap_or_else(|| panic!("no mapping for {ea_method} {path}"));
|
||||||
|
assert_eq!(
|
||||||
|
m.method, expected_method,
|
||||||
|
"{ea_method} {path} → expected method {expected_method}, got {}",
|
||||||
|
m.method
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Generated
-196
@@ -1,196 +0,0 @@
|
|||||||
# 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"
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
[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]
|
|
||||||
@@ -1,820 +0,0 @@
|
|||||||
//! protossl-scan — OpenFUT Bridge, Task 1: confirm the transport (read-only).
|
|
||||||
//!
|
|
||||||
//! GOAL
|
|
||||||
//! ----
|
|
||||||
//! Before we build any hook around FIFA 23's networking, we want to *prove*
|
|
||||||
//! that the game uses EA's DirtySDK / ProtoSSL stack, and then locate the
|
|
||||||
//! actual `ProtoSSLSend` function so a later task can hook it. We do that by
|
|
||||||
//! reading the running game's memory (read-only!).
|
|
||||||
//!
|
|
||||||
//! This program ONLY READS memory. It never writes, patches, injects, or hooks
|
|
||||||
//! anything. It is completely passive and safe to run against the live client.
|
|
||||||
//!
|
|
||||||
//! TWO MODES
|
|
||||||
//! ---------
|
|
||||||
//! protossl-scan [pid|name]
|
|
||||||
//! Marker scan. Searches memory for DirtySDK/ProtoSSL/Blaze strings and
|
|
||||||
//! prints a verdict on whether the transport is present.
|
|
||||||
//!
|
|
||||||
//! protossl-scan xref <hex-addr> [pid|name]
|
|
||||||
//! Cross-reference scan. Given the address of something (e.g. the
|
|
||||||
//! "ProtoSSLSend" string the marker scan found), finds:
|
|
||||||
//! (a) absolute 8-byte pointers to it (likely a name/function table in
|
|
||||||
//! .rdata) — and dumps the neighbouring pointers so we can spot the
|
|
||||||
//! matching *function* pointer; and
|
|
||||||
//! (b) RIP-relative references to it from executable code (e.g. a
|
|
||||||
//! `lea reg, [rip+disp]` that loads the string).
|
|
||||||
//! This is how we turn the *string* address into the *function* address.
|
|
||||||
//!
|
|
||||||
//! USAGE EXAMPLES
|
|
||||||
//! --------------
|
|
||||||
//! protossl-scan # marker scan of "FIFA23.exe"
|
|
||||||
//! protossl-scan 8088 # marker scan of PID 8088
|
|
||||||
//! protossl-scan xref 0x147D198B9 # who references that address?
|
|
||||||
//! protossl-scan xref 0x147D198B9 8088 # ...in PID 8088
|
|
||||||
//!
|
|
||||||
//! NOTE: reach the game's MAIN MENU before scanning (networking strings/code
|
|
||||||
//! may not be paged in at the title screen). Run the terminal "as
|
|
||||||
//! administrator" so you're allowed to read the game's memory, and make sure EA
|
|
||||||
//! Anticheat is in its offline/neutralized state.
|
|
||||||
|
|
||||||
use core::ffi::c_void;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use memchr::memmem;
|
|
||||||
|
|
||||||
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
|
||||||
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
|
|
||||||
use windows::Win32::System::Diagnostics::ToolHelp::{
|
|
||||||
CreateToolhelp32Snapshot, Module32FirstW, Module32NextW, Process32FirstW, Process32NextW,
|
|
||||||
MODULEENTRY32W, PROCESSENTRY32W, TH32CS_SNAPMODULE, TH32CS_SNAPMODULE32, TH32CS_SNAPPROCESS,
|
|
||||||
};
|
|
||||||
use windows::Win32::System::Memory::{
|
|
||||||
VirtualQueryEx, MEMORY_BASIC_INFORMATION, MEM_COMMIT, PAGE_EXECUTE_READ,
|
|
||||||
PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_GUARD, PAGE_READONLY, PAGE_READWRITE,
|
|
||||||
PAGE_WRITECOPY,
|
|
||||||
};
|
|
||||||
use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ};
|
|
||||||
|
|
||||||
/// The ASCII strings the marker scan hunts for. Finding any ProtoSSL* /
|
|
||||||
/// `protossl:` marker is strong evidence the game speaks DirtySDK/ProtoSSL.
|
|
||||||
const MARKERS: &[&[u8]] = &[
|
|
||||||
b"protossl:",
|
|
||||||
b"ProtoSSLSend",
|
|
||||||
b"ProtoSSLRecv",
|
|
||||||
b"ProtoSSLConnect",
|
|
||||||
b"gosredirector",
|
|
||||||
b"DirtySDK",
|
|
||||||
b"blaze",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// How much memory we read per `ReadProcessMemory` call. Larger chunks mean
|
|
||||||
/// fewer syscalls when sweeping a multi-GB address space.
|
|
||||||
const CHUNK: usize = 4 << 20; // 4 MiB
|
|
||||||
|
|
||||||
/// One module loaded in the target process: its name, base address and size.
|
|
||||||
struct ModuleInfo {
|
|
||||||
name: String,
|
|
||||||
base: usize,
|
|
||||||
size: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
||||||
|
|
||||||
// Decide the mode from the first argument.
|
|
||||||
match args.first().map(|s| s.as_str()) {
|
|
||||||
// xref-str <text> [pid|name]
|
|
||||||
// Convenience: find the CURRENT address of a string ourselves, then
|
|
||||||
// xref it. This is ASLR-proof — no copy-pasting absolute addresses.
|
|
||||||
Some("xref-str") => {
|
|
||||||
let text = match args.get(1) {
|
|
||||||
Some(t) => t.clone(),
|
|
||||||
None => {
|
|
||||||
eprintln!("Usage: protossl-scan xref-str <text> [pid|name]");
|
|
||||||
eprintln!("Example: protossl-scan xref-str ProtoSSLSend");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let pid = resolve_pid(args.get(2).map(|s| s.as_str()));
|
|
||||||
let process = open_for_read(pid);
|
|
||||||
let modules = enumerate_modules(pid);
|
|
||||||
|
|
||||||
let addrs = find_string_addresses(process, text.as_bytes());
|
|
||||||
if addrs.is_empty() {
|
|
||||||
println!("String \"{text}\" not found in PID {pid}. Did you reach the menu?");
|
|
||||||
} else {
|
|
||||||
println!("Found \"{text}\" at {} location(s):", addrs.len());
|
|
||||||
for a in addrs {
|
|
||||||
println!();
|
|
||||||
// Show the surrounding bytes and find the TRUE start of the
|
|
||||||
// C-string containing the hit. A pointer/reference targets
|
|
||||||
// the string's start, not a substring offset, so we xref
|
|
||||||
// that start rather than the raw hit address.
|
|
||||||
let start = dump_string_context(process, a, &modules);
|
|
||||||
run_xref(process, &modules, start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unsafe {
|
|
||||||
let _ = CloseHandle(process);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// disasm <hex-addr> [pid|name]
|
|
||||||
// Disassemble the function enclosing an address, annotating string
|
|
||||||
// loads and call targets. Use this on a code anchor (e.g. the lea that
|
|
||||||
// loads the "_ProtoSSLSendPacket" string) to read the real code.
|
|
||||||
Some("disasm") => {
|
|
||||||
let arg = match args.get(1) {
|
|
||||||
Some(a) => a.clone(),
|
|
||||||
None => {
|
|
||||||
eprintln!("Usage: protossl-scan disasm <hex-addr | module+0xoffset> [pid|name]");
|
|
||||||
eprintln!("Examples: protossl-scan disasm 0x140EFA631");
|
|
||||||
eprintln!(" protossl-scan disasm anadius64.dll+0x2BB90");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let pid = resolve_pid(args.get(2).map(|s| s.as_str()));
|
|
||||||
let process = open_for_read(pid);
|
|
||||||
let modules = enumerate_modules(pid);
|
|
||||||
let target = match resolve_target(&arg, &modules) {
|
|
||||||
Some(t) => t,
|
|
||||||
None => {
|
|
||||||
eprintln!("Could not resolve '{arg}' (unknown module or bad address)");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
run_disasm(process, &modules, target);
|
|
||||||
unsafe {
|
|
||||||
let _ = CloseHandle(process);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// xref <hex-addr> [pid|name] (power-user form, exact absolute address)
|
|
||||||
Some("xref") => {
|
|
||||||
let arg = match args.get(1) {
|
|
||||||
Some(a) => a.clone(),
|
|
||||||
None => {
|
|
||||||
eprintln!("Usage: protossl-scan xref <hex-addr | module+0xoffset> [pid|name]");
|
|
||||||
eprintln!("Example: protossl-scan xref 0x147D198B9");
|
|
||||||
eprintln!("Or just use: protossl-scan xref-str ProtoSSLSend");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let pid = resolve_pid(args.get(2).map(|s| s.as_str()));
|
|
||||||
let process = open_for_read(pid);
|
|
||||||
let modules = enumerate_modules(pid);
|
|
||||||
let target = match resolve_target(&arg, &modules) {
|
|
||||||
Some(t) => t,
|
|
||||||
None => {
|
|
||||||
eprintln!("Could not resolve '{arg}' (unknown module or bad address)");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
run_xref(process, &modules, target);
|
|
||||||
unsafe {
|
|
||||||
let _ = CloseHandle(process);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Default: marker scan. An optional argument selects the process.
|
|
||||||
_ => {
|
|
||||||
let pid = resolve_pid(args.first().map(|s| s.as_str()));
|
|
||||||
let process = open_for_read(pid);
|
|
||||||
let modules = enumerate_modules(pid);
|
|
||||||
run_marker_scan(process, &modules);
|
|
||||||
unsafe {
|
|
||||||
let _ = CloseHandle(process);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find every absolute address where the byte string `text` currently appears
|
|
||||||
/// in the target process. Used by `xref-str` so the user never has to copy an
|
|
||||||
/// ASLR'd address by hand.
|
|
||||||
fn find_string_addresses(process: HANDLE, text: &[u8]) -> Vec<usize> {
|
|
||||||
let mut hits: HashSet<usize> = HashSet::new();
|
|
||||||
let overlap = text.len().saturating_sub(1);
|
|
||||||
let finder = memmem::Finder::new(text);
|
|
||||||
walk_regions(process, false, overlap, |chunk_base, bytes| {
|
|
||||||
for off in finder.find_iter(bytes) {
|
|
||||||
hits.insert(chunk_base + off);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let mut v: Vec<usize> = hits.into_iter().collect();
|
|
||||||
v.sort_unstable();
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Mode 1: marker scan
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn run_marker_scan(process: HANDLE, modules: &[ModuleInfo]) {
|
|
||||||
println!("== protossl-scan : marker scan ==");
|
|
||||||
println!("Loaded modules: {}", modules.len());
|
|
||||||
|
|
||||||
// For each marker, the set of absolute addresses where we found it. A
|
|
||||||
// HashSet de-duplicates hits landing in the overlap between two chunks.
|
|
||||||
let mut hits: Vec<HashSet<usize>> = vec![HashSet::new(); MARKERS.len()];
|
|
||||||
let overlap = MARKERS.iter().map(|m| m.len()).max().unwrap_or(1) - 1;
|
|
||||||
|
|
||||||
// Build one SIMD finder per marker, reused across every chunk.
|
|
||||||
let finders: Vec<memmem::Finder> = MARKERS.iter().map(|m| memmem::Finder::new(m)).collect();
|
|
||||||
|
|
||||||
walk_regions(process, false, overlap, |chunk_base, bytes| {
|
|
||||||
for (i, finder) in finders.iter().enumerate() {
|
|
||||||
for off in finder.find_iter(bytes) {
|
|
||||||
hits[i].insert(chunk_base + off);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
println!("\n-- Results --");
|
|
||||||
let mut protossl_present = false;
|
|
||||||
for (i, marker) in MARKERS.iter().enumerate() {
|
|
||||||
let name = String::from_utf8_lossy(marker);
|
|
||||||
let addrs = &hits[i];
|
|
||||||
if addrs.is_empty() {
|
|
||||||
println!(" {name:<16} : not found");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if matches!(
|
|
||||||
*marker,
|
|
||||||
b"protossl:" | b"ProtoSSLSend" | b"ProtoSSLRecv" | b"ProtoSSLConnect"
|
|
||||||
) {
|
|
||||||
protossl_present = true;
|
|
||||||
}
|
|
||||||
let mut sorted: Vec<usize> = addrs.iter().copied().collect();
|
|
||||||
sorted.sort_unstable();
|
|
||||||
println!(" {name:<16} : {} hit(s)", sorted.len());
|
|
||||||
for addr in sorted.iter().take(8) {
|
|
||||||
println!(" {}", describe(*addr, modules));
|
|
||||||
}
|
|
||||||
if sorted.len() > 8 {
|
|
||||||
println!(" ... and {} more", sorted.len() - 8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n-- Verdict --");
|
|
||||||
if protossl_present {
|
|
||||||
println!("ProtoSSL present — transport confirmed.");
|
|
||||||
println!("Next: `protossl-scan xref <ProtoSSLSend-string-addr>` to find the function.");
|
|
||||||
} else {
|
|
||||||
println!("No ProtoSSL markers found.");
|
|
||||||
println!("STOP: did you reach the MAIN MENU before scanning? If you did and");
|
|
||||||
println!("still see nothing, the transport assumption is wrong — rethink it.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Mode 2: cross-reference scan
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn run_xref(process: HANDLE, modules: &[ModuleInfo], target: usize) {
|
|
||||||
println!("== protossl-scan : xref of 0x{target:X} ==");
|
|
||||||
println!("({})", describe(target, modules));
|
|
||||||
|
|
||||||
// (a) Absolute 8-byte pointers to `target`. These usually live in a
|
|
||||||
// read-only data table. If the table pairs names with functions, a
|
|
||||||
// neighbouring slot will hold the function pointer we actually want.
|
|
||||||
let needle = (target as u64).to_le_bytes();
|
|
||||||
let ptr_finder = memmem::Finder::new(&needle);
|
|
||||||
let mut ptr_hits: HashSet<usize> = HashSet::new();
|
|
||||||
walk_regions(process, false, needle.len() - 1, |chunk_base, bytes| {
|
|
||||||
for off in ptr_finder.find_iter(bytes) {
|
|
||||||
ptr_hits.insert(chunk_base + off);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
println!("\n-- Absolute pointers to target --");
|
|
||||||
if ptr_hits.is_empty() {
|
|
||||||
println!(" none");
|
|
||||||
} else {
|
|
||||||
let mut sorted: Vec<usize> = ptr_hits.iter().copied().collect();
|
|
||||||
sorted.sort_unstable();
|
|
||||||
for at in sorted.iter().take(8) {
|
|
||||||
println!(" pointer stored at {}", describe(*at, modules));
|
|
||||||
// Dump the neighbouring pointer-sized slots so a name/function
|
|
||||||
// table becomes visible. A neighbour resolving to a LOW FIFA23.exe
|
|
||||||
// offset (the .text/code section) is a strong function candidate;
|
|
||||||
// the string itself lives at a HIGH offset (.rdata).
|
|
||||||
dump_neighbours(process, *at, modules);
|
|
||||||
}
|
|
||||||
if sorted.len() > 8 {
|
|
||||||
println!(" ... and {} more", sorted.len() - 8);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// (b) RIP-relative references from executable code. For x64, an instruction
|
|
||||||
// like `lea rcx, [rip+disp32]` encodes a 4-byte signed displacement
|
|
||||||
// relative to the address of the *next* instruction. So if the 4 bytes
|
|
||||||
// at address P are `disp`, the referenced target is `P + 4 + disp`.
|
|
||||||
// We scan executable pages for any P where that equals our target.
|
|
||||||
let mut code_hits: HashSet<usize> = HashSet::new();
|
|
||||||
walk_regions(process, true, 3, |chunk_base, bytes| {
|
|
||||||
if bytes.len() < 4 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for i in 0..=bytes.len() - 4 {
|
|
||||||
let disp = i32::from_le_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]);
|
|
||||||
let after = chunk_base + i + 4; // address just past the disp field
|
|
||||||
let referenced = after.wrapping_add(disp as i64 as usize);
|
|
||||||
if referenced == target {
|
|
||||||
code_hits.insert(chunk_base + i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
println!("\n-- RIP-relative code references --");
|
|
||||||
if code_hits.is_empty() {
|
|
||||||
println!(" none");
|
|
||||||
} else {
|
|
||||||
let mut sorted: Vec<usize> = code_hits.iter().copied().collect();
|
|
||||||
sorted.sort_unstable();
|
|
||||||
for at in sorted.iter().take(12) {
|
|
||||||
// The instruction opcode starts a few bytes before the disp field
|
|
||||||
// (e.g. `48 8D 05 <disp32>` => opcode begins 3 bytes earlier).
|
|
||||||
println!(
|
|
||||||
" disp field at {} (instruction begins ~3 bytes earlier)",
|
|
||||||
describe(*at, modules)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if sorted.len() > 12 {
|
|
||||||
println!(" ... and {} more", sorted.len() - 12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n-- Next --");
|
|
||||||
println!("Look for a neighbour pointer (or a code reference) that resolves to a LOW");
|
|
||||||
println!("FIFA23.exe offset — that is the candidate ProtoSSLSend function address.");
|
|
||||||
println!("TODO/CONFIRM: validate the candidate by disassembling around it before hooking.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read and print the pointer-sized slots immediately around `at`, resolving
|
|
||||||
/// each stored value to module+offset. Reveals name/function tables.
|
|
||||||
fn dump_neighbours(process: HANDLE, at: usize, modules: &[ModuleInfo]) {
|
|
||||||
// 4 slots before through 4 slots after (8 bytes each).
|
|
||||||
for k in -4i64..=4 {
|
|
||||||
let slot = (at as i64 + k * 8) as usize;
|
|
||||||
match read_u64(process, slot) {
|
|
||||||
Some(val) => {
|
|
||||||
let marker = if k == 0 { " <- string ptr" } else { "" };
|
|
||||||
println!(
|
|
||||||
" [{:+}] 0x{:016X} -> {}{}",
|
|
||||||
k,
|
|
||||||
val,
|
|
||||||
describe(val as usize, modules),
|
|
||||||
marker
|
|
||||||
);
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Mode 3: disassemble the enclosing function
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn run_disasm(process: HANDLE, modules: &[ModuleInfo], target: usize) {
|
|
||||||
use iced_x86::{
|
|
||||||
Decoder, DecoderOptions, Formatter, Instruction, Mnemonic, NasmFormatter, OpKind,
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("== protossl-scan : disasm around 0x{target:X} ==");
|
|
||||||
println!("({})\n", describe(target, modules));
|
|
||||||
|
|
||||||
// Read a window of code: enough before the anchor to capture the function
|
|
||||||
// prologue, and enough after to see the body.
|
|
||||||
const BACK: usize = 0x400;
|
|
||||||
const FWD: usize = 0x300;
|
|
||||||
let win_start = target.saturating_sub(BACK);
|
|
||||||
let buf = match read_bytes(process, win_start, BACK + FWD) {
|
|
||||||
Some(b) => b,
|
|
||||||
None => {
|
|
||||||
println!("Could not read code memory around 0x{target:X}.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let target_off = target - win_start;
|
|
||||||
|
|
||||||
// Find the start of the enclosing function. MSVC pads the gap between
|
|
||||||
// functions with int3 (0xCC) bytes, so the nearest 0xCC before the anchor
|
|
||||||
// marks the end of the previous function; ours starts right after it.
|
|
||||||
let mut p = target_off.min(buf.len());
|
|
||||||
while p > 0 && buf[p - 1] != 0xCC {
|
|
||||||
p -= 1;
|
|
||||||
}
|
|
||||||
let func_start_addr = win_start + p;
|
|
||||||
if p == 0 {
|
|
||||||
println!("(warning: no int3 padding found in window — disassembly may begin mid-function)\n");
|
|
||||||
} else {
|
|
||||||
println!(
|
|
||||||
"Enclosing function starts at {}\n",
|
|
||||||
describe(func_start_addr, modules)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let code = &buf[p..];
|
|
||||||
let mut decoder = Decoder::with_ip(64, code, func_start_addr as u64, DecoderOptions::NONE);
|
|
||||||
let mut formatter = NasmFormatter::new();
|
|
||||||
let mut instr = Instruction::default();
|
|
||||||
let mut text = String::new();
|
|
||||||
|
|
||||||
let mut count = 0;
|
|
||||||
while decoder.can_decode() && count < 220 {
|
|
||||||
decoder.decode_out(&mut instr);
|
|
||||||
count += 1;
|
|
||||||
|
|
||||||
let ip = instr.ip() as usize;
|
|
||||||
text.clear();
|
|
||||||
formatter.format(&instr, &mut text);
|
|
||||||
|
|
||||||
// Raw instruction bytes.
|
|
||||||
let idx = ip - func_start_addr;
|
|
||||||
let len = instr.len();
|
|
||||||
let raw: String = code
|
|
||||||
.get(idx..idx + len)
|
|
||||||
.unwrap_or(&[])
|
|
||||||
.iter()
|
|
||||||
.map(|b| format!("{b:02X}"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
// Annotate RIP-relative data loads (strings!) and call/jmp targets.
|
|
||||||
let mut note = String::new();
|
|
||||||
if instr.is_ip_rel_memory_operand() {
|
|
||||||
let tgt = instr.ip_rel_memory_address() as usize;
|
|
||||||
match read_string(process, tgt, 48) {
|
|
||||||
Some(s) => note = format!(" ; -> \"{s}\""),
|
|
||||||
None => note = format!(" ; -> {}", describe(tgt, modules)),
|
|
||||||
}
|
|
||||||
} else if matches!(instr.mnemonic(), Mnemonic::Call | Mnemonic::Jmp)
|
|
||||||
&& matches!(
|
|
||||||
instr.op0_kind(),
|
|
||||||
OpKind::NearBranch64 | OpKind::NearBranch32 | OpKind::NearBranch16
|
|
||||||
)
|
|
||||||
{
|
|
||||||
let tgt = instr.near_branch_target() as usize;
|
|
||||||
note = format!(" ; -> {}", describe(tgt, modules));
|
|
||||||
}
|
|
||||||
|
|
||||||
let here = if ip == target { " <== anchor" } else { "" };
|
|
||||||
println!(" 0x{ip:012X} {raw:<30} {text}{note}{here}");
|
|
||||||
|
|
||||||
// Stop at the function's terminating `ret` (followed by int3 padding).
|
|
||||||
if instr.mnemonic() == Mnemonic::Ret {
|
|
||||||
let next = idx + len;
|
|
||||||
if next >= code.len() || code[next] == 0xCC {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!("\n-- Next --");
|
|
||||||
println!("Look for the public ProtoSSLSend: it's the function that CALLS this one");
|
|
||||||
println!("with plaintext. Use `protossl-scan callers 0x{func_start_addr:X}` (coming next)");
|
|
||||||
println!("or scan upward for a nearby function that calls into here.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a printable ASCII C-string at `addr` (up to `max` bytes). Returns None
|
|
||||||
/// if there isn't at least a short run of printable characters.
|
|
||||||
fn read_string(process: HANDLE, addr: usize, max: usize) -> Option<String> {
|
|
||||||
let bytes = read_bytes(process, addr, max)?;
|
|
||||||
let mut s = String::new();
|
|
||||||
for &b in &bytes {
|
|
||||||
if (0x20..=0x7e).contains(&b) {
|
|
||||||
s.push(b as char);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.len() >= 3 {
|
|
||||||
Some(s)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Shared helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Turn an optional process selector (numeric PID or name) into a PID, exiting
|
|
||||||
/// with a helpful message if it can't be resolved. No selector => "FIFA23.exe".
|
|
||||||
fn resolve_pid(arg: Option<&str>) -> u32 {
|
|
||||||
match arg {
|
|
||||||
Some(a) if a.chars().all(|c| c.is_ascii_digit()) => {
|
|
||||||
a.parse::<u32>().expect("PID should be a valid number")
|
|
||||||
}
|
|
||||||
Some(name) => find_process_by_name(name).unwrap_or_else(|| {
|
|
||||||
eprintln!("Could not find a running process named '{name}'.");
|
|
||||||
std::process::exit(1);
|
|
||||||
}),
|
|
||||||
None => find_process_by_name("FIFA23.exe").unwrap_or_else(|| {
|
|
||||||
eprintln!("Could not find 'FIFA23.exe'. Is the game running?");
|
|
||||||
eprintln!("Tip: pass a process name or PID, e.g. `protossl-scan 8088`.");
|
|
||||||
std::process::exit(1);
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open the target process with read-only rights, or exit with guidance.
|
|
||||||
fn open_for_read(pid: u32) -> HANDLE {
|
|
||||||
match unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid) } {
|
|
||||||
Ok(h) => {
|
|
||||||
println!("Target PID: {pid}");
|
|
||||||
h
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("OpenProcess failed for PID {pid}: {e}");
|
|
||||||
eprintln!("Try running this terminal as administrator, and make sure");
|
|
||||||
eprintln!("EA Anticheat is in its offline/neutralized state.");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse a hex address that may or may not have a "0x" prefix.
|
|
||||||
fn parse_hex(s: &str) -> Option<usize> {
|
|
||||||
let trimmed = s.trim_start_matches("0x").trim_start_matches("0X");
|
|
||||||
usize::from_str_radix(trimmed, 16).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve a target that is either a raw hex address or a `module+0xoffset`
|
|
||||||
/// form (e.g. `anadius64.dll+0x2BB90`). The module form is ASLR-robust: it adds
|
|
||||||
/// the offset to the module's CURRENT base in the running process.
|
|
||||||
fn resolve_target(s: &str, modules: &[ModuleInfo]) -> Option<usize> {
|
|
||||||
if let Some(idx) = s.find('+') {
|
|
||||||
let name = s[..idx].trim().to_ascii_lowercase();
|
|
||||||
let want = name.strip_suffix(".dll").unwrap_or(&name);
|
|
||||||
let off = parse_hex(s[idx + 1..].trim())?;
|
|
||||||
for m in modules {
|
|
||||||
let mn = m.name.to_ascii_lowercase();
|
|
||||||
let mn = mn.strip_suffix(".dll").unwrap_or(&mn);
|
|
||||||
if mn == want {
|
|
||||||
return Some(m.base + off);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
parse_hex(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a single u64 from the target process at `addr`. Returns None if the
|
|
||||||
/// memory can't be read (e.g. unmapped).
|
|
||||||
fn read_u64(process: HANDLE, addr: usize) -> Option<u64> {
|
|
||||||
let b = read_bytes(process, addr, 8)?;
|
|
||||||
if b.len() == 8 {
|
|
||||||
Some(u64::from_le_bytes(b.try_into().unwrap()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read up to `len` bytes from the target process at `addr`. Returns however
|
|
||||||
/// many bytes were actually readable (possibly fewer than `len`), or None.
|
|
||||||
fn read_bytes(process: HANDLE, addr: usize, len: usize) -> Option<Vec<u8>> {
|
|
||||||
let mut buf = vec![0u8; len];
|
|
||||||
let mut got = 0usize;
|
|
||||||
let ok = unsafe {
|
|
||||||
ReadProcessMemory(
|
|
||||||
process,
|
|
||||||
addr as *const c_void,
|
|
||||||
buf.as_mut_ptr() as *mut c_void,
|
|
||||||
len,
|
|
||||||
Some(&mut got),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if ok.is_ok() && got > 0 {
|
|
||||||
buf.truncate(got);
|
|
||||||
Some(buf)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Print the bytes around a string hit (full containing C-string + a hex/ASCII
|
|
||||||
/// dump) and return the address of the TRUE start of that C-string. This tells
|
|
||||||
/// us whether the hit is a standalone string or embedded in a larger blob.
|
|
||||||
fn dump_string_context(process: HANDLE, hit: usize, modules: &[ModuleInfo]) -> usize {
|
|
||||||
const BACK: usize = 256;
|
|
||||||
const FWD: usize = 256;
|
|
||||||
let win_start = hit.saturating_sub(BACK);
|
|
||||||
let buf = match read_bytes(process, win_start, BACK + FWD) {
|
|
||||||
Some(b) => b,
|
|
||||||
None => {
|
|
||||||
println!(" (could not read memory around 0x{hit:X})");
|
|
||||||
return hit;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let hit_off = hit - win_start; // index of the hit within the window
|
|
||||||
|
|
||||||
// A printable ASCII byte (the alphabet C-strings are made of here).
|
|
||||||
let printable = |b: u8| (0x20..=0x7e).contains(&b);
|
|
||||||
|
|
||||||
// Walk backward/forward to the NUL (or non-printable) boundaries.
|
|
||||||
let mut s = hit_off;
|
|
||||||
while s > 0 && printable(buf[s - 1]) {
|
|
||||||
s -= 1;
|
|
||||||
}
|
|
||||||
let mut e = hit_off;
|
|
||||||
while e < buf.len() && printable(buf[e]) {
|
|
||||||
e += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let start_addr = win_start + s;
|
|
||||||
let full = String::from_utf8_lossy(&buf[s..e]);
|
|
||||||
println!(" containing string: \"{full}\"");
|
|
||||||
println!(" string starts at : {}", describe(start_addr, modules));
|
|
||||||
if start_addr != hit {
|
|
||||||
println!(" (hit was a substring; xref-ing the string start instead)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hex + ASCII dump of ~96 bytes centred on the hit, for structural insight
|
|
||||||
// (e.g. is this a NUL-separated table of names, or one long message?).
|
|
||||||
println!(" context:");
|
|
||||||
let dump_start = hit_off.saturating_sub(32) & !0xF; // 16-byte aligned
|
|
||||||
for row in 0..6 {
|
|
||||||
let off = dump_start + row * 16;
|
|
||||||
if off >= buf.len() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let end = (off + 16).min(buf.len());
|
|
||||||
let slice = &buf[off..end];
|
|
||||||
let mut hex = String::new();
|
|
||||||
let mut asc = String::new();
|
|
||||||
for &b in slice {
|
|
||||||
hex.push_str(&format!("{b:02X} "));
|
|
||||||
asc.push(if printable(b) { b as char } else { '.' });
|
|
||||||
}
|
|
||||||
println!(" 0x{:016X} {:<48} {}", win_start + off, hex, asc);
|
|
||||||
}
|
|
||||||
|
|
||||||
start_addr
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Format an address as "module+0xOFFSET" or note it's outside any module.
|
|
||||||
fn describe(addr: usize, modules: &[ModuleInfo]) -> String {
|
|
||||||
for m in modules {
|
|
||||||
if addr >= m.base && addr < m.base + m.size {
|
|
||||||
return format!("0x{addr:016X} ({}+0x{:X})", m.name, addr - m.base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
format!("0x{addr:016X} (outside any module)")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walk every committed, readable region of the process. If `exec_only`, only
|
|
||||||
/// executable regions are visited. Each readable chunk is passed to `f` as
|
|
||||||
/// `(absolute_base_of_chunk, bytes)`, in overlapping `CHUNK`-sized pieces.
|
|
||||||
fn walk_regions<F: FnMut(usize, &[u8])>(
|
|
||||||
process: HANDLE,
|
|
||||||
exec_only: bool,
|
|
||||||
overlap: usize,
|
|
||||||
mut f: F,
|
|
||||||
) {
|
|
||||||
let mut buf = vec![0u8; CHUNK];
|
|
||||||
let mut address: usize = 0;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let mut mbi = MEMORY_BASIC_INFORMATION::default();
|
|
||||||
let written = unsafe {
|
|
||||||
VirtualQueryEx(
|
|
||||||
process,
|
|
||||||
Some(address as *const c_void),
|
|
||||||
&mut mbi,
|
|
||||||
core::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if written == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let region_base = mbi.BaseAddress as usize;
|
|
||||||
let region_size = mbi.RegionSize;
|
|
||||||
|
|
||||||
let wanted = if exec_only {
|
|
||||||
is_executable(mbi.Protect.0)
|
|
||||||
} else {
|
|
||||||
is_readable(mbi.Protect.0)
|
|
||||||
};
|
|
||||||
if mbi.State == MEM_COMMIT && wanted {
|
|
||||||
// Read this region in overlapping chunks.
|
|
||||||
let end = region_base.saturating_add(region_size);
|
|
||||||
let mut pos = region_base;
|
|
||||||
while pos < end {
|
|
||||||
let want = CHUNK.min(end - pos);
|
|
||||||
let mut got: usize = 0;
|
|
||||||
let ok = unsafe {
|
|
||||||
ReadProcessMemory(
|
|
||||||
process,
|
|
||||||
pos as *const c_void,
|
|
||||||
buf.as_mut_ptr() as *mut c_void,
|
|
||||||
want,
|
|
||||||
Some(&mut got),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if ok.is_err() || got == 0 {
|
|
||||||
pos = pos.saturating_add(want.max(1));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
f(pos, &buf[..got]);
|
|
||||||
if pos + got >= end {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let step = if got > overlap { got - overlap } else { got };
|
|
||||||
pos += step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match region_base.checked_add(region_size) {
|
|
||||||
Some(next) if next > address => address = next,
|
|
||||||
_ => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find a running process by executable name (case-insensitive). Returns PID.
|
|
||||||
fn find_process_by_name(target: &str) -> Option<u32> {
|
|
||||||
unsafe {
|
|
||||||
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).ok()?;
|
|
||||||
let mut entry = PROCESSENTRY32W {
|
|
||||||
dwSize: core::mem::size_of::<PROCESSENTRY32W>() as u32,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let mut found = None;
|
|
||||||
if Process32FirstW(snapshot, &mut entry).is_ok() {
|
|
||||||
loop {
|
|
||||||
if wide_to_string(&entry.szExeFile).eq_ignore_ascii_case(target) {
|
|
||||||
found = Some(entry.th32ProcessID);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if Process32NextW(snapshot, &mut entry).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let _ = CloseHandle(snapshot);
|
|
||||||
found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List every module (EXE + DLLs) in the target process with base and size.
|
|
||||||
fn enumerate_modules(pid: u32) -> Vec<ModuleInfo> {
|
|
||||||
let mut modules = Vec::new();
|
|
||||||
unsafe {
|
|
||||||
let snapshot =
|
|
||||||
match CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return modules,
|
|
||||||
};
|
|
||||||
let mut entry = MODULEENTRY32W {
|
|
||||||
dwSize: core::mem::size_of::<MODULEENTRY32W>() as u32,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
if Module32FirstW(snapshot, &mut entry).is_ok() {
|
|
||||||
loop {
|
|
||||||
modules.push(ModuleInfo {
|
|
||||||
name: wide_to_string(&entry.szModule),
|
|
||||||
base: entry.modBaseAddr as usize,
|
|
||||||
size: entry.modBaseSize as usize,
|
|
||||||
});
|
|
||||||
if Module32NextW(snapshot, &mut entry).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let _ = CloseHandle(snapshot);
|
|
||||||
}
|
|
||||||
modules
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Readable page? Accept read/write/execute-read variants; reject NOACCESS,
|
|
||||||
/// plain EXECUTE (not readable), and guard pages.
|
|
||||||
fn is_readable(protect: u32) -> bool {
|
|
||||||
if protect & PAGE_GUARD.0 != 0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let base = protect & 0xFF;
|
|
||||||
base == PAGE_READONLY.0
|
|
||||||
|| base == PAGE_READWRITE.0
|
|
||||||
|| base == PAGE_WRITECOPY.0
|
|
||||||
|| base == PAGE_EXECUTE_READ.0
|
|
||||||
|| base == PAGE_EXECUTE_READWRITE.0
|
|
||||||
|| base == PAGE_EXECUTE_WRITECOPY.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Executable AND readable page? (We can only scan code we can also read.)
|
|
||||||
fn is_executable(protect: u32) -> bool {
|
|
||||||
if protect & PAGE_GUARD.0 != 0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let base = protect & 0xFF;
|
|
||||||
base == PAGE_EXECUTE_READ.0
|
|
||||||
|| base == PAGE_EXECUTE_READWRITE.0
|
|
||||||
|| base == PAGE_EXECUTE_WRITECOPY.0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a fixed-size, NUL-terminated UTF-16 (wide) buffer into a String.
|
|
||||||
fn wide_to_string(wide: &[u16]) -> String {
|
|
||||||
let len = wide.iter().position(|&c| c == 0).unwrap_or(wide.len());
|
|
||||||
String::from_utf16_lossy(&wide[..len])
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user