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:
@@ -183,7 +183,31 @@ back offline.
|
|||||||
So M2 = make `GetInternetConnectedState` report **connected** (then the game
|
So M2 = make `GetInternetConnectedState` report **connected** (then the game
|
||||||
proceeds with its existing token).
|
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
|
- Check whether the flags at `+0xCAB1A` / `+0xCAB1B` are settable via anadius
|
||||||
config / a hidden option (cheapest flip).
|
config / a hidden option (cheapest flip).
|
||||||
- Else out-detour `GetInternetConnectedState` in our `version.dll` to force the
|
- Else out-detour `GetInternetConnectedState` in our `version.dll` to force the
|
||||||
|
|||||||
+52
-8
@@ -419,12 +419,49 @@ unsafe fn install_detour_at(addr: usize, detour: *const (), slot: &AtomicUsize,
|
|||||||
|
|
||||||
static ORIG_GOONLINE: AtomicUsize = AtomicUsize::new(0);
|
static ORIG_GOONLINE: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
/// Read-only detour on anadius's GoOnline handler (anadius64.dll+0x2BB90): log
|
/// M2 flip on anadius's GoOnline handler (anadius64.dll+0x2BB90). The original
|
||||||
/// that it was reached, then call the original unchanged. Tells us whether the
|
/// handler is `mov rcx,rdx; lea r8,[+0xADD73]; lea rdx,[+0xADE64 = "0"]; call
|
||||||
/// game even asks to go online during the "connecting" attempt.
|
/// +0x25BE0; mov al,1` — i.e. it builds its ErrorSuccess response with the value
|
||||||
unsafe extern "system" fn hooked_goonline(a: usize, b: usize, c: usize, d: usize) -> usize {
|
/// "0" (offline). We replicate it but pass "1" (+0xAF530 = the connected value),
|
||||||
log(&format!("PROBE anadius GoOnline CALLED (rcx=0x{a:X} rdx=0x{b:X})"));
|
/// 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);
|
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 {
|
if orig != 0 {
|
||||||
let f: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
|
let f: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
|
||||||
core::mem::transmute(orig);
|
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.
|
/// Resolve anadius64.dll's runtime base, detour the GoOnline probe, and install
|
||||||
/// Returns true once installed (or if anadius isn't present and we should stop
|
/// the M2 GetInternetConnectedState flip. Returns false if anadius isn't loaded.
|
||||||
/// retrying is decided by the caller). Returns false if anadius isn't loaded yet.
|
|
||||||
unsafe fn hook_anadius_probes() -> bool {
|
unsafe fn hook_anadius_probes() -> bool {
|
||||||
let base = match GetModuleHandleW(PCWSTR(wide("anadius64.dll").as_ptr())) {
|
let base = match GetModuleHandleW(PCWSTR(wide("anadius64.dll").as_ptr())) {
|
||||||
Ok(m) => m.0 as usize,
|
Ok(m) => m.0 as usize,
|
||||||
Err(_) => return false, // not loaded yet
|
Err(_) => return false, // not loaded yet
|
||||||
};
|
};
|
||||||
|
ANADIUS_BASE.store(base, Ordering::SeqCst);
|
||||||
log(&format!("anadius64.dll base = 0x{base:X}"));
|
log(&format!("anadius64.dll base = 0x{base:X}"));
|
||||||
|
|
||||||
install_detour_at(
|
install_detour_at(
|
||||||
base + 0x2BB90,
|
base + 0x2BB90,
|
||||||
hooked_goonline as *const (),
|
hooked_goonline as *const (),
|
||||||
&ORIG_GOONLINE,
|
&ORIG_GOONLINE,
|
||||||
"PROBE anadius GoOnline @ +0x2BB90",
|
"PROBE anadius GoOnline @ +0x2BB90",
|
||||||
);
|
);
|
||||||
|
install_detour_at(
|
||||||
|
base + 0x27790,
|
||||||
|
hooked_ics as *const (),
|
||||||
|
&ORIG_ICS,
|
||||||
|
"FLIP anadius GetInternetConnectedState @ +0x27790",
|
||||||
|
);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,50 @@ fn main() {
|
|||||||
let _ = CloseHandle(process);
|
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]
|
// callers <hex-addr | module+0xoffset> [pid|name]
|
||||||
// Find direct call/jmp sites that target an address — walks up the call
|
// 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).
|
// graph (e.g. from a connect helper to the code that gates it).
|
||||||
|
|||||||
Reference in New Issue
Block a user