probe: add behavior-preserving mid-function detour to capture the live OnlineStatusEvent listener
The OnlineStatusEventT::HandleMessage dispatch resolves its game-side listener only at runtime (call [rax+0x28]). openfut_listener_stub patches FIFA23.exe+0x274d4d7 to replicate the four dispatch instructions while logging the resolved vtable/fn, then resumes. Alignment-safe (saves/rounds rsp before the log call). Result: listener = FIFA23.exe+0x2751060 = ret 0, a no-op default vtable slot -> the online->auth transition is state-polled, not callback-driven. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user