feat(hook): expand IAT hook coverage with TLS bypass, connect/recv hooks, and logging

Adds connect_hook, connectex_hook, recv_hook, ssl_patch, tls_bypass, lsx, ea_stub,
and origin_spy modules to intercept EA's TLS and socket layers in addition to
getaddrinfo. Adds DLL-level logging to C:\openfut_hook.log for debugging. Also
patches windows-sys feature flags to include Cryptography and Threading APIs needed
by the new hooks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-30 10:36:17 -07:00
parent 3b965f1f3f
commit 87241acc1a
13 changed files with 1633 additions and 40 deletions
+43 -17
View File
@@ -1,13 +1,13 @@
use std::{ffi::CStr, sync::OnceLock};
use std::{
ffi::CStr,
sync::{
OnceLock,
atomic::{AtomicBool, Ordering},
},
};
use windows_sys::Win32::Networking::WinSock::{ADDRINFOA, getaddrinfo as sys_getaddrinfo};
const INTERCEPT: &[&str] = &[
"fut.ea.com",
"utas.mob.v4.fut.ea.com",
"utas.s2.fut.ea.com",
];
type GetaddrinfoFn = unsafe extern "system" fn(
*const u8,
*const u8,
@@ -16,19 +16,35 @@ type GetaddrinfoFn = unsafe extern "system" fn(
) -> i32;
static REAL: OnceLock<GetaddrinfoFn> = OnceLock::new();
// Stored as a NUL-terminated byte string so the hook can pass it to getaddrinfo.
static REDIRECT_IP: OnceLock<Vec<u8>> = OnceLock::new();
// Flipped to true the first time we successfully apply the runtime cert patch.
// The patch is deferred to here (rather than DllMain) because EAWebKit.dll may
// not be loaded yet when the hook DLL is injected.
static CERT_PATCHED: AtomicBool = AtomicBool::new(false);
pub fn set_real(f: GetaddrinfoFn) {
let _ = REAL.set(f);
}
pub fn set_redirect_ip(ip: String) {
let mut bytes = ip.into_bytes();
bytes.push(0); // NUL-terminate for passing to getaddrinfo
bytes.push(0);
let _ = REDIRECT_IP.set(bytes);
}
/// Returns true if `host` is an EA / EA-Sports domain that should be redirected
/// to the local OpenFUT bridge.
fn is_ea_host(host: &str) -> bool {
let h = host.to_ascii_lowercase();
h.ends_with(".ea.com")
|| h == "ea.com"
|| h.ends_with(".easports.com")
|| h == "easports.com"
|| h.ends_with(".ugc.footapi.com")
|| h.ends_with(".footapi.com")
}
pub unsafe extern "system" fn hooked_getaddrinfo(
node_name: *const u8,
service_name: *const u8,
@@ -37,15 +53,25 @@ pub unsafe extern "system" fn hooked_getaddrinfo(
) -> i32 {
if !node_name.is_null() {
if let Ok(host) = CStr::from_ptr(node_name as *const i8).to_str() {
for target in INTERCEPT {
if host.eq_ignore_ascii_case(target) {
let redirect = REDIRECT_IP
.get()
.map(|v| v.as_ptr())
.unwrap_or(b"127.0.0.1\0".as_ptr());
let real = REAL.get().copied().unwrap_or(sys_getaddrinfo);
return real(redirect, service_name, hints, result);
crate::write_log(&format!("openfut_hook: getaddrinfo({host})\n"));
if is_ea_host(host) {
// Apply the ProtoSSL cert-verify bypass the first time we see an EA
// hostname — EAWebKit.dll must be loaded by now because it's calling us.
if !CERT_PATCHED.load(Ordering::Relaxed) {
if crate::ssl_patch::patch_eawebkit_cert_verify() {
CERT_PATCHED.store(true, Ordering::Relaxed);
crate::write_log("openfut_hook: ProtoSSL cert-verify patched (lazy, from getaddrinfo)\n");
} else {
crate::write_log("openfut_hook: ProtoSSL cert-verify patch FAILED in getaddrinfo\n");
}
}
let redirect = REDIRECT_IP
.get()
.map(|v| v.as_ptr())
.unwrap_or(b"127.0.0.1\0".as_ptr());
let real = REAL.get().copied().unwrap_or(sys_getaddrinfo);
return real(redirect, service_name, hints, result);
}
}
}