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) <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-29 19:45:15 -07:00
parent 550b23bb26
commit e2c6ae8e5b
3 changed files with 121 additions and 9 deletions
+25 -1
View File
@@ -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 `<Event sender="EALS">` it ever sends is the Challenge
handshake). So flipping poll/return values can't unblock it.
**Implication:** getting past "connecting" requires **emulating the EA-app online
event sequence** the game waits for (inject the online-status event over LSX, in
the format/order EbisuSDK expects), not a single function flip. This is a
substantially deeper task (and precedes the Blaze backend emulation).
Next: trace the FIFA-side FUT online-flow (what the game does after `GoOnline`
and exactly which event/condition it waits on) — Ghidra on FIFA23.exe (import
saved at `C:\openfut\gh-proj`), or RE anadius's LSX event-send path. `TODO/CONFIRM`.
### M2 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
+52 -8
View File
@@ -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
}
+44
View File
@@ -136,6 +136,50 @@ fn main() {
let _ = CloseHandle(process);
}
}
// read <hex-addr | module+0xoffset> [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 <hex-addr | module+0xoffset> [len] [pid]");
std::process::exit(1);
}
};
let len = args
.get(2)
.and_then(|s| s.parse::<usize>().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 <hex-addr | module+0xoffset> [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).