probe: retarget to game-side connect layer + add guarded session-context sampler
Replaces the anadius-connectivity probes with the game-side Nucleus-connect functions (nucleusConnectREST/Trusted, connect-state tick) and adds a VirtualQuery-guarded sampler thread that reads X=[0x14acd02c0] -> M=[X+0x360] -> ctx=[M+0x778] once/sec to observe the session context directly. Per-slot log cap prevents per-frame handlers flooding the log. Run 3 result: ctx is non-null but the connect functions are never called (see bridge findings). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+103
-15
@@ -17,7 +17,70 @@
|
||||
//! r8/r9) and returns in rax. All targets here are SDK methods with few args.
|
||||
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
|
||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||
use windows_sys::Win32::System::Memory::{
|
||||
VirtualProtect, VirtualQuery, MEMORY_BASIC_INFORMATION, MEM_COMMIT, PAGE_EXECUTE_READWRITE,
|
||||
PAGE_GUARD, PAGE_NOACCESS,
|
||||
};
|
||||
|
||||
/// Fault-safe pointer read: returns None unless `ptr` lands in a committed, readable
|
||||
/// page (checked via VirtualQuery). Avoids crashing FIFA when we sample pointers that
|
||||
/// may be null/garbage mid-construction.
|
||||
unsafe fn read_ptr(ptr: usize) -> Option<usize> {
|
||||
if ptr < 0x10000 || ptr & 7 != 0 {
|
||||
return None;
|
||||
}
|
||||
let mut mbi: MEMORY_BASIC_INFORMATION = core::mem::zeroed();
|
||||
let n = VirtualQuery(ptr as _, &mut mbi, core::mem::size_of::<MEMORY_BASIC_INFORMATION>());
|
||||
if n == 0 || mbi.State != MEM_COMMIT {
|
||||
return None;
|
||||
}
|
||||
if mbi.Protect & (PAGE_NOACCESS | PAGE_GUARD) != 0 {
|
||||
return None;
|
||||
}
|
||||
// Ensure the full 8 bytes are inside the region.
|
||||
if ptr + 8 > mbi.BaseAddress as usize + mbi.RegionSize {
|
||||
return None;
|
||||
}
|
||||
Some(core::ptr::read_volatile(ptr as *const usize))
|
||||
}
|
||||
|
||||
/// Poll the Nucleus session-context chain directly, independent of the login state
|
||||
/// machine: X = OriginSDK singleton [FIFA23.exe+0xacd02c0]; M = [X+0x360] (online/
|
||||
/// Nucleus manager); ctx = [M+0x778] (the session context that nucleusConnectREST/
|
||||
/// Trusted null-check). Logs each level so we see exactly if/when the context is
|
||||
/// created. All reads are VirtualQuery-guarded, so a null/garbage pointer is logged,
|
||||
/// never dereferenced blind.
|
||||
pub fn install_state_sampler() {
|
||||
std::thread::spawn(|| unsafe {
|
||||
let base = GetModuleHandleA(core::ptr::null());
|
||||
if base.is_null() {
|
||||
crate::write_log("SAMPLE: main exe not found\n");
|
||||
return;
|
||||
}
|
||||
let x_slot = base as usize + 0xacd02c0; // VA 0x14acd02c0
|
||||
let mut last = String::new();
|
||||
for i in 0..180u32 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
let line = match read_ptr(x_slot) {
|
||||
None => "X=<unreadable>".to_string(),
|
||||
Some(0) => "X=null".to_string(),
|
||||
Some(x) => match read_ptr(x + 0x360) {
|
||||
None => format!("X={x:#x} M=<unreadable>"),
|
||||
Some(0) => format!("X={x:#x} M=null"),
|
||||
Some(m) => match read_ptr(m + 0x778) {
|
||||
None => format!("X={x:#x} M={m:#x} ctx=<unreadable>"),
|
||||
Some(c) => format!("X={x:#x} M={m:#x} ctx[M+0x778]={c:#x}"),
|
||||
},
|
||||
},
|
||||
};
|
||||
// Only log on change (plus a heartbeat every 20s) to keep the log clean.
|
||||
if line != last || i % 20 == 0 {
|
||||
crate::write_log(&format!("SAMPLE #{i} {line}\n"));
|
||||
last = line;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── live game-side listener capture ────────────────────────────────────────────
|
||||
//
|
||||
@@ -108,22 +171,35 @@ struct Target {
|
||||
main_exe: bool,
|
||||
}
|
||||
|
||||
/// Probe targets. Addresses recovered by RE (see docs/connection-gate-findings.md):
|
||||
/// - anadius64.dll GetInternetConnectedState / GoOnline — FIFA's in-process online
|
||||
/// decision points (anadius emulates EbisuSDK in-process).
|
||||
/// - FIFA23.exe OnlineStatusEvent/Login deserializers — confirm our pushed events
|
||||
/// actually reach FIFA's parser, and with what timing.
|
||||
/// Probe targets. Addresses recovered by RE (see docs/connection-gate-findings.md).
|
||||
///
|
||||
/// Dig-2 gate: FIFA's game-side Nucleus-connect layer requests the auth code, but both
|
||||
/// paths null-check the Nucleus session context `[NucleusManager+0x778]` and bail when
|
||||
/// it's null. These probes tell us *where FIFA is parked*:
|
||||
/// - `nucleusConnectREST` (+0x2861910): the state that would send GetAuthCode. Returns 0
|
||||
/// (no request) when `[mgr+0x778]` is null. If this NEVER fires, FIFA never reached it.
|
||||
/// - `nucleusConnectTrusted` (+0x5078370): sibling connect path. Returns HRESULT
|
||||
/// 0x80060000 ("not ready") when the context is null — its return value is diagnostic.
|
||||
/// - `connect-state.tick` (+0x507d660): the login state-machine handler that calls
|
||||
/// nucleusConnectTrusted. If it ticks, FIFA entered the connect state; if not, it's
|
||||
/// parked earlier.
|
||||
/// - `OnlineStatus.deser` (+0x278a4d0): confirms our pushed OnlineStatusEvent still
|
||||
/// arrives during the test (control signal).
|
||||
const TARGETS: &[Target] = &[
|
||||
Target { module: b"anadius64.dll\0", rva: 0x27790, label: "GetInternetConnectedState", main_exe: false },
|
||||
Target { module: b"anadius64.dll\0", rva: 0x2bb90, label: "GoOnline", main_exe: false },
|
||||
Target { module: b"\0", rva: 0x278a4d0, label: "OnlineStatus.deser", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x2787a30, label: "Login.deser", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x2861910, label: "nucleusConnectREST", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x5078370, label: "nucleusConnectTrusted", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x507d660, label: "connectState.tick", main_exe: true },
|
||||
Target { module: b"\0", rva: 0x278a4d0, label: "OnlineStatus.deser", main_exe: true },
|
||||
];
|
||||
|
||||
const N: usize = 4; // must equal TARGETS.len()
|
||||
|
||||
static ADDRS: [AtomicUsize; N] = [const { AtomicUsize::new(0) }; N];
|
||||
static mut ORIG: [[u8; 14]; N] = [[0u8; 14]; N];
|
||||
/// Per-slot call counter, used to cap logging so a per-frame handler (e.g. a state
|
||||
/// tick) can't flood the log. We still call through the real function every time.
|
||||
static CALLS: [AtomicUsize; N] = [const { AtomicUsize::new(0) }; N];
|
||||
const LOG_CAP: usize = 24;
|
||||
|
||||
type ProbeFn = unsafe extern "system" fn(usize, usize, usize, usize) -> usize;
|
||||
const PROBE_FNS: [ProbeFn; N] = [p0, p1, p2, p3];
|
||||
@@ -150,15 +226,26 @@ unsafe fn restore(slot: usize) {
|
||||
unsafe fn generic(slot: usize, a: usize, b: usize, c: usize, d: usize) -> usize {
|
||||
let label = TARGETS[slot].label;
|
||||
let addr = ADDRS[slot].load(Ordering::Relaxed) as *mut u8;
|
||||
// Unhook, log entry, call the real function, re-hook, log return.
|
||||
// Cap logging per slot so a per-frame handler can't flood the log; still call
|
||||
// through every time. Log the first LOG_CAP calls (entry + return), then just the
|
||||
// running count once at the cap so we know it kept firing.
|
||||
let n = CALLS[slot].fetch_add(1, Ordering::Relaxed);
|
||||
let log = n < LOG_CAP;
|
||||
// Unhook, (maybe) log entry, call the real function, re-hook, (maybe) log return.
|
||||
restore(slot);
|
||||
crate::write_log(&format!(
|
||||
"PROBE {label} enter rcx={a:#x} rdx={b:#x} r8={c:#x} r9={d:#x}\n"
|
||||
));
|
||||
if log {
|
||||
crate::write_log(&format!(
|
||||
"PROBE {label} #{n} enter rcx={a:#x} rdx={b:#x} r8={c:#x} r9={d:#x}\n"
|
||||
));
|
||||
}
|
||||
let f: ProbeFn = core::mem::transmute(addr);
|
||||
let r = f(a, b, c, d);
|
||||
write_jmp(addr, PROBE_FNS[slot] as u64);
|
||||
crate::write_log(&format!("PROBE {label} ret={r:#x}\n"));
|
||||
if log {
|
||||
crate::write_log(&format!("PROBE {label} #{n} ret={r:#x}\n"));
|
||||
} else if n == LOG_CAP {
|
||||
crate::write_log(&format!("PROBE {label} (capped; still firing past {LOG_CAP})\n"));
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
@@ -180,6 +267,7 @@ pub fn install_probes_deferred() {
|
||||
}
|
||||
install_probes();
|
||||
install_listener_probe();
|
||||
install_state_sampler();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user