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:
funman300
2026-07-02 16:59:27 -07:00
parent 87241acc1a
commit feaff0443f
5 changed files with 250 additions and 42 deletions
+125
View File
@@ -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));
}
}