diff --git a/openfut-hook/src/probe.rs b/openfut-hook/src/probe.rs index 210add5..e54d83d 100644 --- a/openfut-hook/src/probe.rs +++ b/openfut-hook/src/probe.rs @@ -15,10 +15,89 @@ //! //! 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 core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use windows_sys::Win32::System::LibraryLoader::GetModuleHandleA; use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE}; +// ─── live game-side listener capture ──────────────────────────────────────────── +// +// OnlineStatusEventT::HandleMessage dispatches the parsed bool to the game's online +// listener via a virtual call `call [rax+0x28]` at FIFA23.exe+0x274d4e2, where rax +// is the vtable of the object at [rsi-0x38]. The concrete listener is only known at +// runtime. We capture it with a behavior-preserving mid-function detour: patch the +// 14 bytes at +0x274d4d7 (which are exactly `lea rcx,[rsi-0x38]; mov rax,[rcx]; lea +// rdx,[rbp-0x49]; call [rax+0x28]`) to jump to a stub that replicates those four +// instructions but logs the resolved listener address in between, then resumes at +// +0x274d4e5. Non-volatile regs (rsi/rbp/…) are preserved by the ABI; volatiles +// match the original dispatch's clobbers. + +/// Runtime absolute address to resume at after the replicated dispatch +/// (main-exe base + 0x274d4e5). Read by the asm stub. +#[no_mangle] +static mut RESUME_ADDR: u64 = 0; +static MAIN_BASE: AtomicUsize = AtomicUsize::new(0); +static LISTENER_LOGGED: AtomicBool = AtomicBool::new(false); + +/// Called by the stub with the listener object's vtable and the resolved listener +/// function pointer (vtable[0x28]). Logs once (RVAs for static RE). +unsafe extern "C" fn listener_log(vtable: usize, func: usize) { + if LISTENER_LOGGED.swap(true, Ordering::Relaxed) { + return; + } + let base = MAIN_BASE.load(Ordering::Relaxed); + crate::write_log(&format!( + "PROBE OnlineStatus.listener: vtable={vtable:#x} (rva {:#x}) fn={func:#x} (rva {:#x})\n", + vtable.wrapping_sub(base), + func.wrapping_sub(base), + )); +} + +core::arch::global_asm!( + ".intel_syntax noprefix", + ".global openfut_listener_stub", + "openfut_listener_stub:", + "lea rcx, [rsi - 0x38]", + "mov rax, [rcx]", // rax = listener vtable + "mov rdx, [rax + 0x28]", // rdx = listener fn (arg2) + "mov rcx, rax", // rcx = vtable (arg1) + // Align the stack to 16 before the call, saving the original rsp so we can + // restore it (0x28 misaligns; SSE code in the logger then faults). + "mov r11, rsp", + "and rsp, -16", + "sub rsp, 0x30", // 0x20 shadow + 0x10 spare, stays 16-aligned + "mov [rsp + 0x20], r11", // stash original rsp + "call {log}", + "mov rsp, [rsp + 0x20]", // restore original rsp + "lea rcx, [rsi - 0x38]", // replicate the original dispatch + "mov rax, [rcx]", + "lea rdx, [rbp - 0x49]", + "call qword ptr [rax + 0x28]", + "mov r10, qword ptr [rip + {resume}]", + "jmp r10", + ".att_syntax prefix", + log = sym listener_log, + resume = sym RESUME_ADDR, +); + +extern "C" { + fn openfut_listener_stub(); +} + +/// Patch the OnlineStatusEvent dispatch site to route through the logging stub. +pub unsafe fn install_listener_probe() { + let base = GetModuleHandleA(core::ptr::null()); + if base.is_null() { + crate::write_log("PROBE listener: main exe not found\n"); + return; + } + let base = base as usize; + MAIN_BASE.store(base, Ordering::Relaxed); + RESUME_ADDR = (base + 0x274d4e5) as u64; + let target = (base + 0x274d4d7) as *mut u8; + write_jmp(target, openfut_listener_stub as usize as u64); + crate::write_log(&format!("PROBE listener: dispatch site patched @ {:#x}\n", target as usize)); +} + struct Target { /// DLL name (nul-terminated) or ignored when `main_exe` is true. module: &'static [u8], @@ -100,6 +179,7 @@ pub fn install_probes_deferred() { std::thread::sleep(std::time::Duration::from_millis(500)); } install_probes(); + install_listener_probe(); }); }