hook: in-process RE instrumentation + LSX redirect + capture tooling
Hook-side tooling for the LSX/Blaze reverse-engineering effort: - probe.rs (new, `probe` feature): passive logging detours on FIFA's online-flow functions via the unhook/rehook pattern (no trampoline/relocation, works on RIP-relative prologues). Deferred install waits for anadius64.dll to load, then logs enter/return for GoOnline + GetInternetConnectedState (anadius) and the OnlineStatusEvent/Login deserializers (FIFA23.exe). Revealed that our pushed LSX events reach FIFA and parse OK, while GoOnline never fires — localizing the online gate to FIFA's game-side event consumer. - connect_hook.rs: redirect FIFA's LSX connect :3216 → :3217 so it lands on the native openfut-bridge LSX server (slips past anadius's in-process :3216 intercept); gated off under the `capture_baseline` feature. - recv_hook.rs: boundary-safe trampolines + LSX peer filtering for the capture_baseline path (log anadius's real LSX frames when the redirect is off). Build the instrumented DLL with `--features probe` (or `--features capture_baseline` for the anadius-baseline capture). Both features are off by default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
//! In-process RE probes: passive logging detours on FIFA's online-flow functions.
|
||||
//!
|
||||
//! Purpose: FIFA reacts to our pushed LSX events (OnlineStatusEvent/Login) but
|
||||
//! never starts the GetAuthCode→Nucleus→Blaze chain, and that decision lives in
|
||||
//! FIFA's game-side / in-process EbisuSDK logic that is invisible from the LSX
|
||||
//! wire. These probes log when the key online-flow functions are called (and
|
||||
//! their return values), so we can SEE where FIFA stalls after our events.
|
||||
//!
|
||||
//! Mechanism: the same unhook/rehook detour `connect_hook` uses — on entry we
|
||||
//! restore the original bytes, log, call the real function, then re-install the
|
||||
//! jump. This needs no trampoline/relocation, so it works even on prologues with
|
||||
//! RIP-relative operands (e.g. GoOnline). Not thread-safe (a concurrent call
|
||||
//! during the unhook window runs un-logged) but never corrupts the target — fine
|
||||
//! for read-only RE.
|
||||
//!
|
||||
//! Signature assumption: each probed fn takes ≤4 integer args (Win64: rcx/rdx/
|
||||
//! r8/r9) and returns in rax. All targets here are SDK methods with few args.
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA;
|
||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||
|
||||
struct Target {
|
||||
/// DLL name (nul-terminated) or ignored when `main_exe` is true.
|
||||
module: &'static [u8],
|
||||
/// Offset from the module base (RVA). For main_exe, VA − 0x140000000.
|
||||
rva: usize,
|
||||
label: &'static str,
|
||||
/// True → resolve against the main executable (FIFA23.exe) base.
|
||||
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.
|
||||
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 },
|
||||
];
|
||||
|
||||
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];
|
||||
|
||||
type ProbeFn = unsafe extern "system" fn(usize, usize, usize, usize) -> usize;
|
||||
const PROBE_FNS: [ProbeFn; N] = [p0, p1, p2, p3];
|
||||
|
||||
unsafe fn write_jmp(addr: *mut u8, dest: u64) {
|
||||
let mut old: u32 = 0;
|
||||
VirtualProtect(addr as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||
// FF 25 00 00 00 00 JMP [rip+0] ; then absolute dest
|
||||
addr.write(0xFF);
|
||||
addr.add(1).write(0x25);
|
||||
(addr.add(2) as *mut u32).write(0);
|
||||
(addr.add(6) as *mut u64).write(dest);
|
||||
VirtualProtect(addr as _, 14, old, &mut old);
|
||||
}
|
||||
|
||||
unsafe fn restore(slot: usize) {
|
||||
let addr = ADDRS[slot].load(Ordering::Relaxed) as *mut u8;
|
||||
let mut old: u32 = 0;
|
||||
VirtualProtect(addr as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||
core::ptr::copy_nonoverlapping((&raw const ORIG[slot]) as *const u8, addr, 14);
|
||||
VirtualProtect(addr as _, 14, old, &mut old);
|
||||
}
|
||||
|
||||
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.
|
||||
restore(slot);
|
||||
crate::write_log(&format!(
|
||||
"PROBE {label} 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"));
|
||||
r
|
||||
}
|
||||
|
||||
unsafe extern "system" fn p0(a: usize, b: usize, c: usize, d: usize) -> usize { generic(0, a, b, c, d) }
|
||||
unsafe extern "system" fn p1(a: usize, b: usize, c: usize, d: usize) -> usize { generic(1, a, b, c, d) }
|
||||
unsafe extern "system" fn p2(a: usize, b: usize, c: usize, d: usize) -> usize { generic(2, a, b, c, d) }
|
||||
unsafe extern "system" fn p3(a: usize, b: usize, c: usize, d: usize) -> usize { generic(3, a, b, c, d) }
|
||||
|
||||
/// Spawn a background thread that waits for anadius64.dll to load, then installs
|
||||
/// all probes. anadius may not be present when our DllMain runs, so we defer
|
||||
/// off the loader lock and poll for it (up to ~30s) before installing.
|
||||
pub fn install_probes_deferred() {
|
||||
std::thread::spawn(|| unsafe {
|
||||
for _ in 0..60 {
|
||||
if !GetModuleHandleA(b"anadius64.dll\0".as_ptr()).is_null() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
}
|
||||
install_probes();
|
||||
});
|
||||
}
|
||||
|
||||
/// Install all probe detours. Modules must already be loaded (call late in
|
||||
/// DllMain, after anadius64.dll is present).
|
||||
pub unsafe fn install_probes() {
|
||||
for (i, t) in TARGETS.iter().enumerate() {
|
||||
let base = if t.main_exe {
|
||||
GetModuleHandleA(core::ptr::null())
|
||||
} else {
|
||||
GetModuleHandleA(t.module.as_ptr())
|
||||
};
|
||||
if base.is_null() {
|
||||
crate::write_log(&format!("PROBE {}: module not loaded, skipped\n", t.label));
|
||||
continue;
|
||||
}
|
||||
let addr = (base as usize + t.rva) as *mut u8;
|
||||
core::ptr::copy_nonoverlapping(addr, (&raw mut ORIG[i]) as *mut u8, 14);
|
||||
ADDRS[i].store(addr as usize, Ordering::Relaxed);
|
||||
write_jmp(addr, PROBE_FNS[i] as u64);
|
||||
crate::write_log(&format!("PROBE {} installed @ {:#x}\n", t.label, addr as usize));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user