Files
OpenFUT/openfut-hook/src/connectex_hook.rs
T
funman300 7dcf610b71 openfut-hook: RE instrumentation for the Blaze dial-gate investigation
In-process, read-only probes and transport observation built while closing
the online/FUT route from both the memory and network sides.

- probe.rs / dial_notification.rs: menu-time ctx dump, connMgr enumerator,
  synthetic dial-notification + direct-call dial trigger, and the
  [element+0x40] container write-watchpoint. All env-gated, one-shot,
  VirtualQuery-guarded; none alter game state by default.
- transport_watch.rs + connect/connectex/hooks/lib: M0 transport observation
  (grep-friendly TRANSPORT_WATCH logging on the existing getaddrinfo/connect/
  WSAConnect/ConnectEx detours) and an IPv6 (v4-mapped) EA-redirect so the
  game's IPv6 :443 dials land on the bridge instead of the dead servers.

Findings: the game never initiates a Blaze connection offline; the dial
handler is registered by a self-registering, message-driven state machine
whose container stays empty with no Blaze exchange. See openfut-bridge
docs/closure-and-preservation.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:58:17 -07:00

151 lines
5.3 KiB
Rust

/// Intercepts ConnectEx (EA/DirtySDK's preferred async connect API).
///
/// DirtySDK calls WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER, WSAID_CONNECTEX) once at
/// startup to get a ConnectEx function pointer, bypassing all IAT hooks. We hook WSAIoctl
/// inline so that when it returns a ConnectEx pointer we swap it for our own wrapper.
use core::sync::atomic::{AtomicUsize, Ordering};
use core::ffi::c_void;
// Address rewriting (v4 + v6) is shared from connect_hook::redirect_if_ea, so the port
// constants and sockaddr structs no longer live here.
// SIO_GET_EXTENSION_FUNCTION_POINTER
const SIO_GET_EXT_FN: u32 = 0xC8000006;
// WSAID_CONNECTEX = {25A207B9-DDF3-4660-8EE9-76E58C74063E}
const CONNECTEX_GUID: [u8; 16] = [
0xB9, 0x07, 0xA2, 0x25,
0xF3, 0xDD, 0x60, 0x46,
0x8E, 0xE9, 0x76, 0xE5, 0x8C, 0x74, 0x06, 0x3E,
];
// The real ConnectEx pointer, saved after WSAIoctl returns it
static REAL_CONNECTEX: AtomicUsize = AtomicUsize::new(0);
// ConnectEx function signature
type ConnectExFn = unsafe extern "system" fn(
s: usize,
name: *const u8,
namelen: i32,
send_buf: *const c_void,
send_data_len: u32,
bytes_sent: *mut u32,
overlapped: *mut c_void,
) -> i32;
// WSAIoctl function address (for inline unhook/rehook)
static WSAIOCTL_ADDR: AtomicUsize = AtomicUsize::new(0);
static mut WSAIOCTL_ORIG: [u8; 14] = [0u8; 14];
type WsaIoctlFn = unsafe extern "system" fn(
s: usize,
code: u32,
in_buf: *const c_void,
in_len: u32,
out_buf: *mut c_void,
out_len: u32,
bytes_ret: *mut u32,
overlapped: *mut c_void,
completion: *const c_void,
) -> i32;
unsafe fn write_hook(target: *mut u8, dest: u64) {
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
let mut old: u32 = 0;
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
target.write(0xFF); target.add(1).write(0x25);
(target.add(2) as *mut u32).write(0u32);
(target.add(6) as *mut u64).write(dest);
VirtualProtect(target as _, 14, old, &mut old);
}
unsafe fn restore_wsaioctl(target: *mut u8) {
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
let mut old: u32 = 0;
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
core::ptr::copy_nonoverlapping(WSAIOCTL_ORIG.as_ptr(), target, 14);
VirtualProtect(target as _, 14, old, &mut old);
}
/// Our ConnectEx wrapper: redirects EA ports to 127.0.0.1
unsafe extern "system" fn hooked_connectex(
s: usize,
name: *const u8,
namelen: i32,
send_buf: *const c_void,
send_data_len: u32,
bytes_sent: *mut u32,
overlapped: *mut c_void,
) -> i32 {
let real_fn: ConnectExFn = core::mem::transmute(REAL_CONNECTEX.load(Ordering::Relaxed));
// Milestone-0 transport watch (self-gates on OPENFUT_TRANSPORT_WATCH).
crate::transport_watch::note_connect("ConnectEx", name, namelen, s);
// Share the one redirect implementation (v4 + v6) with connect_hook, so ConnectEx
// dials get the same IPv6 handling as plain connect().
if let Some((buf, len)) = crate::connect_hook::redirect_if_ea(name, namelen) {
return real_fn(s, buf.as_ptr(), len, send_buf, send_data_len, bytes_sent, overlapped);
}
real_fn(s, name, namelen, send_buf, send_data_len, bytes_sent, overlapped)
}
/// Our WSAIoctl hook: when ConnectEx is requested, save the real pointer and return ours
pub unsafe extern "system" fn hooked_wsaioctl(
s: usize,
code: u32,
in_buf: *const c_void,
in_len: u32,
out_buf: *mut c_void,
out_len: u32,
bytes_ret: *mut u32,
overlapped: *mut c_void,
completion: *const c_void,
) -> i32 {
let addr = WSAIOCTL_ADDR.load(Ordering::Relaxed) as *mut u8;
// Call the real WSAIoctl via unhook/rehook
restore_wsaioctl(addr);
let result = {
let f: WsaIoctlFn = core::mem::transmute(addr);
f(s, code, in_buf, in_len, out_buf, out_len, bytes_ret, overlapped, completion)
};
write_hook(addr, hooked_wsaioctl as u64);
// If this was a ConnectEx request that succeeded, swap the pointer
if result == 0
&& code == SIO_GET_EXT_FN
&& in_len == 16
&& !in_buf.is_null()
{
let guid = core::slice::from_raw_parts(in_buf as *const u8, 16);
if guid == CONNECTEX_GUID
&& out_len >= 8
&& !out_buf.is_null()
{
let out_ptr = out_buf as *mut usize;
let real_addr = *out_ptr;
if REAL_CONNECTEX.compare_exchange(0, real_addr, Ordering::Relaxed, Ordering::Relaxed).is_ok() {
crate::write_log(&format!("connectex_hook: intercepted ConnectEx @ {real_addr:#x}\n"));
}
// Return our hook instead
*out_ptr = hooked_connectex as usize;
}
}
result
}
pub unsafe fn install_wsaioctl_hook() -> bool {
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
let ws2 = GetModuleHandleA(b"ws2_32.dll\0".as_ptr());
if ws2.is_null() { return false; }
let fn_ptr = match GetProcAddress(ws2, b"WSAIoctl\0".as_ptr()) {
Some(f) => f as *mut u8,
None => return false,
};
core::ptr::copy_nonoverlapping(fn_ptr, WSAIOCTL_ORIG.as_mut_ptr(), 14);
WSAIOCTL_ADDR.store(fn_ptr as usize, Ordering::Relaxed);
write_hook(fn_ptr, hooked_wsaioctl as u64);
true
}