From e2c6ae8e5b31d0d82f8982509bf7c596882ef098 Mon Sep 17 00:00:00 2001 From: funman300 Date: Mon, 29 Jun 2026 19:45:15 -0700 Subject: [PATCH] M2: flips fire but gate is async event (not a poll) - Hook: force GetInternetConnectedState->connected (flags +0xCAB1A/+0xCAB1B) and flip GoOnline to report "1" (+0xAF530) instead of "0" (+0xADE64). - protossl-scan: add `read` mode (hex/ascii dump at addr|module+off). - Finding: neither flip unblocks the game; it keeps retrying GoOnline every ~7s. The FUT-online flow is event-driven -- the game waits for an async "online established" event anadius (offline-only) never pushes. Documented in connection-gate-findings.md. Next: trace FIFA-side flow (Ghidra) / anadius event-send to inject the online event. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/connection-gate-findings.md | 26 +++++++++++++- openfut-hook/src/lib.rs | 60 +++++++++++++++++++++++++++----- tools/protossl-scan/src/main.rs | 44 +++++++++++++++++++++++ 3 files changed, 121 insertions(+), 9 deletions(-) diff --git a/docs/connection-gate-findings.md b/docs/connection-gate-findings.md index e09bb92..bbcd1f5 100644 --- a/docs/connection-gate-findings.md +++ b/docs/connection-gate-findings.md @@ -183,7 +183,31 @@ back offline. So M2 = make `GetInternetConnectedState` report **connected** (then the game proceeds with its existing token). -### M2 directions (do NOT do this session) +### M2 attempt results — the gate is an async EVENT, not a poll +Tried (read/write, EAAC neutralized): +- Forced `GetInternetConnectedState` → connected (set flags +0xCAB1A/+0xCAB1B → value + `"1"`; strings confirmed: +0xADE64 = `"0"` offline, +0xAF530 = `"1"` connected). +- Flipped `GoOnline` to report `"1"` (replicated its builder `+0x25BE0` with the + connected string instead of `"0"`). + +**Neither made the game proceed.** Both handlers fire, no crash — but the game +**keeps retrying `GoOnline` every ~7s** and never attempts the Blaze connect. +That retry-on-timeout pattern means the FUT-online flow is **event-driven**: the +game submits `GoOnline`, gets success, then **waits for an async "online +established" event** (ONLINE_STATUS_EVENT-class) that anadius — being offline-only +— never pushes (the only `` it ever sends is the Challenge +handshake). So flipping poll/return values can't unblock it. + +**Implication:** getting past "connecting" requires **emulating the EA-app online +event sequence** the game waits for (inject the online-status event over LSX, in +the format/order EbisuSDK expects), not a single function flip. This is a +substantially deeper task (and precedes the Blaze backend emulation). + +Next: trace the FIFA-side FUT online-flow (what the game does after `GoOnline` +and exactly which event/condition it waits on) — Ghidra on FIFA23.exe (import +saved at `C:\openfut\gh-proj`), or RE anadius's LSX event-send path. `TODO/CONFIRM`. + +### M2 directions (superseded — see above) - Check whether the flags at `+0xCAB1A` / `+0xCAB1B` are settable via anadius config / a hidden option (cheapest flip). - Else out-detour `GetInternetConnectedState` in our `version.dll` to force the diff --git a/openfut-hook/src/lib.rs b/openfut-hook/src/lib.rs index a1613e7..fc8789b 100644 --- a/openfut-hook/src/lib.rs +++ b/openfut-hook/src/lib.rs @@ -419,12 +419,49 @@ unsafe fn install_detour_at(addr: usize, detour: *const (), slot: &AtomicUsize, static ORIG_GOONLINE: AtomicUsize = AtomicUsize::new(0); -/// Read-only detour on anadius's GoOnline handler (anadius64.dll+0x2BB90): log -/// that it was reached, then call the original unchanged. Tells us whether the -/// game even asks to go online during the "connecting" attempt. -unsafe extern "system" fn hooked_goonline(a: usize, b: usize, c: usize, d: usize) -> usize { - log(&format!("PROBE anadius GoOnline CALLED (rcx=0x{a:X} rdx=0x{b:X})")); +/// M2 flip on anadius's GoOnline handler (anadius64.dll+0x2BB90). The original +/// handler is `mov rcx,rdx; lea r8,[+0xADD73]; lea rdx,[+0xADE64 = "0"]; call +/// +0x25BE0; mov al,1` — i.e. it builds its ErrorSuccess response with the value +/// "0" (offline). We replicate it but pass "1" (+0xAF530 = the connected value), +/// so GoOnline reports online, then return success (al=1). +unsafe extern "system" fn hooked_goonline(_a: usize, b: usize, _c: usize, _d: usize) -> usize { + let base = ANADIUS_BASE.load(Ordering::SeqCst); + if base != 0 { + log("FLIP GoOnline -> reporting online (\"1\")"); + let builder: unsafe extern "system" fn(usize, usize, usize) -> usize = + core::mem::transmute(base + 0x25BE0); + // 0x25BE0(rcx = handler's rdx, rdx = "1", r8 = +0xADD73) + builder(b, base + 0xAF530, base + 0xADD73); + return 1; + } let orig = ORIG_GOONLINE.load(Ordering::SeqCst); + if orig != 0 { + let f: unsafe extern "system" fn(usize, usize, usize, usize) -> usize = + core::mem::transmute(orig); + f(_a, b, _c, _d) + } else { + 0 + } +} + +// --- M2 flip: force GetInternetConnectedState to report "connected" -------- + +static ORIG_ICS: AtomicUsize = AtomicUsize::new(0); +static ANADIUS_BASE: AtomicUsize = AtomicUsize::new(0); + +/// anadius's GetInternetConnectedState handler (anadius64.dll+0x27790) builds an +/// LSX response whose `connected` value is: +/// (byte[+0xCAB1B] || byte[+0xCAB1A]) ? connected : offline +/// Both default to 0 → offline → the game aborts at "connecting". We force both +/// flags to 1 before the original runs, so it builds the "connected" response. +unsafe extern "system" fn hooked_ics(a: usize, b: usize, c: usize, d: usize) -> usize { + let base = ANADIUS_BASE.load(Ordering::SeqCst); + if base != 0 { + core::ptr::write_volatile((base + 0xCAB1A) as *mut u8, 1u8); + core::ptr::write_volatile((base + 0xCAB1B) as *mut u8, 1u8); + } + log("FLIP GetInternetConnectedState -> forcing connected (flags set)"); + let orig = ORIG_ICS.load(Ordering::SeqCst); if orig != 0 { let f: unsafe extern "system" fn(usize, usize, usize, usize) -> usize = core::mem::transmute(orig); @@ -434,21 +471,28 @@ unsafe extern "system" fn hooked_goonline(a: usize, b: usize, c: usize, d: usize } } -/// Resolve anadius64.dll's runtime base and detour the GoOnline handler. -/// Returns true once installed (or if anadius isn't present and we should stop -/// retrying is decided by the caller). Returns false if anadius isn't loaded yet. +/// Resolve anadius64.dll's runtime base, detour the GoOnline probe, and install +/// the M2 GetInternetConnectedState flip. Returns false if anadius isn't loaded. unsafe fn hook_anadius_probes() -> bool { let base = match GetModuleHandleW(PCWSTR(wide("anadius64.dll").as_ptr())) { Ok(m) => m.0 as usize, Err(_) => return false, // not loaded yet }; + ANADIUS_BASE.store(base, Ordering::SeqCst); log(&format!("anadius64.dll base = 0x{base:X}")); + install_detour_at( base + 0x2BB90, hooked_goonline as *const (), &ORIG_GOONLINE, "PROBE anadius GoOnline @ +0x2BB90", ); + install_detour_at( + base + 0x27790, + hooked_ics as *const (), + &ORIG_ICS, + "FLIP anadius GetInternetConnectedState @ +0x27790", + ); true } diff --git a/tools/protossl-scan/src/main.rs b/tools/protossl-scan/src/main.rs index 6ecf9b0..c4924ab 100644 --- a/tools/protossl-scan/src/main.rs +++ b/tools/protossl-scan/src/main.rs @@ -136,6 +136,50 @@ fn main() { let _ = CloseHandle(process); } } + // read [len] [pid|name] + // Dump raw bytes (hex + ASCII) at an address — to read short strings / + // data the disassembler doesn't resolve. + Some("read") => { + let arg = match args.get(1) { + Some(a) => a.clone(), + None => { + eprintln!("Usage: protossl-scan read [len] [pid]"); + std::process::exit(1); + } + }; + let len = args + .get(2) + .and_then(|s| s.parse::().ok().or_else(|| parse_hex(s))) + .unwrap_or(64); + let pid = resolve_pid(args.get(3).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}'"); + std::process::exit(1); + } + }; + println!("== read {} len {len} ==", describe(target, &modules)); + match read_bytes(process, target, len) { + Some(b) => { + for off in (0..b.len()).step_by(16) { + let row = &b[off..(off + 16).min(b.len())]; + let hexp: String = row.iter().map(|x| format!("{x:02X} ")).collect(); + let asc: String = row + .iter() + .map(|&x| if (0x20..=0x7e).contains(&x) { x as char } else { '.' }) + .collect(); + println!(" 0x{:016X} {:<48} {}", target + off, hexp, asc); + } + } + None => println!(" (could not read memory at that address)"), + } + unsafe { + let _ = CloseHandle(process); + } + } // callers [pid|name] // Find direct call/jmp sites that target an address — walks up the call // graph (e.g. from a connect helper to the code that gates it).