Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e44a3792f |
Generated
+5
@@ -2,10 +2,15 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openfut-common"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openfut-hook"
|
name = "openfut-hook"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"openfut-common",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ edition = "2021"
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# Shared, dependency-free source of truth for the OpenFUT destination
|
||||||
|
# (host + ports) and the sockaddr byte-order helpers. Keeps all three hooks
|
||||||
|
# consistent and keeps this logic host-testable outside WinSock.
|
||||||
|
openfut-common = { path = "../openfut-common" }
|
||||||
windows-sys = { version = "0.59", features = [
|
windows-sys = { version = "0.59", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_System_LibraryLoader",
|
"Win32_System_LibraryLoader",
|
||||||
|
|||||||
+13
-15
@@ -1,20 +1,18 @@
|
|||||||
/// Reads openfut.cfg from the same directory as this DLL.
|
//! Loads `openfut.cfg` (next to this DLL) into a shared [`ServerConfig`].
|
||||||
///
|
//!
|
||||||
/// The file contains a single line: the IP the hook should redirect EA
|
//! There is intentionally **no loopback fallback**: if the file is missing,
|
||||||
/// hostnames to, e.g. "192.168.1.10" or "127.0.0.1".
|
//! empty, or invalid, this returns a [`ConfigError`] and the caller logs it and
|
||||||
/// Falls back to 127.0.0.1 if the file is missing or unreadable.
|
//! declines to redirect. Missing configuration is an error, never `127.0.0.1`.
|
||||||
|
use openfut_common::{ConfigError, ServerConfig};
|
||||||
use windows_sys::Win32::System::LibraryLoader::GetModuleFileNameA;
|
use windows_sys::Win32::System::LibraryLoader::GetModuleFileNameA;
|
||||||
|
|
||||||
pub fn read_redirect_ip(module: windows_sys::Win32::Foundation::HMODULE) -> String {
|
/// Read and parse `openfut.cfg` from the same directory as this DLL.
|
||||||
if let Some(cfg_path) = config_path(module) {
|
pub fn load_config(
|
||||||
if let Ok(content) = std::fs::read_to_string(&cfg_path) {
|
module: windows_sys::Win32::Foundation::HMODULE,
|
||||||
let ip = content.trim().to_string();
|
) -> Result<ServerConfig, ConfigError> {
|
||||||
if !ip.is_empty() {
|
let path = config_path(module).ok_or(ConfigError::ConfigMissing)?;
|
||||||
return ip;
|
let contents = std::fs::read_to_string(&path).map_err(|_| ConfigError::ConfigMissing)?;
|
||||||
}
|
ServerConfig::parse(&contents)
|
||||||
}
|
|
||||||
}
|
|
||||||
"127.0.0.1".to_string()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config_path(module: windows_sys::Win32::Foundation::HMODULE) -> Option<std::path::PathBuf> {
|
fn config_path(module: windows_sys::Win32::Foundation::HMODULE) -> Option<std::path::PathBuf> {
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
/// Hooks ws2_32!connect via inline detour (no iptables needed).
|
/// Hooks ws2_32!connect via inline detour (no iptables needed).
|
||||||
/// Uses unhook/rehook pattern: restores original bytes, calls real function, re-installs hook.
|
/// Uses unhook/rehook pattern: restores original bytes, calls real function, re-installs hook.
|
||||||
/// This avoids trampoline RIP-relocation issues entirely.
|
/// This avoids trampoline RIP-relocation issues entirely.
|
||||||
|
///
|
||||||
|
/// The redirect destination (IP + port) comes entirely from the shared
|
||||||
|
/// [`crate::server`] state, which is populated once from `openfut.cfg`. This
|
||||||
|
/// hook does NOT choose an address itself — no hardcoded loopback, no per-hook
|
||||||
|
/// redirect IP. EA *source* ports are recognised via `openfut-common`'s port
|
||||||
|
/// map; the matching OpenFUT *destination* port + configured IP are substituted.
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
const AF_INET: u16 = 2;
|
const AF_INET: u16 = 2;
|
||||||
const PORT_HTTPS_NBO: u16 = 0xBB01; // 443 big-endian
|
|
||||||
const PORT_BRIDGE_NBO: u16 = 0xFB20; // 8443 big-endian
|
|
||||||
const PORT_BLAZE_REDIRECTOR_NBO: u16 = 0x3927; // 10041 big-endian
|
|
||||||
const PORT_BLAZE_MAIN_NBO: u16 = 0x8FA4; // 42127 big-endian
|
|
||||||
const ADDR_LOOPBACK_NBO: u32 = 0x0100_007F; // 127.0.0.1 big-endian
|
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
struct SockaddrIn {
|
struct SockaddrIn {
|
||||||
@@ -27,18 +28,26 @@ static mut ORIGINAL_BYTES: [u8; 14] = [0u8; 14];
|
|||||||
|
|
||||||
// For WSAConnect IAT fallback
|
// For WSAConnect IAT fallback
|
||||||
type WsaConnectFn = unsafe extern "system" fn(
|
type WsaConnectFn = unsafe extern "system" fn(
|
||||||
s: usize, name: *const u8, namelen: i32,
|
s: usize,
|
||||||
caller: *const (), callee: *const (),
|
name: *const u8,
|
||||||
sqos: *const (), gqos: *const ()) -> i32;
|
namelen: i32,
|
||||||
|
caller: *const (),
|
||||||
|
callee: *const (),
|
||||||
|
sqos: *const (),
|
||||||
|
gqos: *const (),
|
||||||
|
) -> i32;
|
||||||
static REAL_WSA: OnceLock<WsaConnectFn> = OnceLock::new();
|
static REAL_WSA: OnceLock<WsaConnectFn> = OnceLock::new();
|
||||||
pub fn set_real_wsa_connect(f: WsaConnectFn) { let _ = REAL_WSA.set(f); }
|
pub fn set_real_wsa_connect(f: WsaConnectFn) {
|
||||||
|
let _ = REAL_WSA.set(f);
|
||||||
|
}
|
||||||
|
|
||||||
unsafe fn write_hook(target: *mut u8, dest: u64) {
|
unsafe fn write_hook(target: *mut u8, dest: u64) {
|
||||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||||
let mut old: u32 = 0;
|
let mut old: u32 = 0;
|
||||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||||
// FF 25 00 00 00 00 JMP [rip+0]
|
// FF 25 00 00 00 00 JMP [rip+0]
|
||||||
target.write(0xFF); target.add(1).write(0x25);
|
target.write(0xFF);
|
||||||
|
target.add(1).write(0x25);
|
||||||
(target.add(2) as *mut u32).write(0u32);
|
(target.add(2) as *mut u32).write(0u32);
|
||||||
(target.add(6) as *mut u64).write(dest);
|
(target.add(6) as *mut u64).write(dest);
|
||||||
VirtualProtect(target as _, 14, old, &mut old);
|
VirtualProtect(target as _, 14, old, &mut old);
|
||||||
@@ -53,23 +62,38 @@ unsafe fn restore_original(target: *mut u8) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn redirect_if_ea(name: *const u8, namelen: i32) -> Option<([u8; 16], i32)> {
|
unsafe fn redirect_if_ea(name: *const u8, namelen: i32) -> Option<([u8; 16], i32)> {
|
||||||
if namelen < 8 { return None; }
|
if namelen < 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let sa = &*(name as *const SockaddrIn);
|
let sa = &*(name as *const SockaddrIn);
|
||||||
if sa.sin_family != AF_INET { return None; }
|
if sa.sin_family != AF_INET {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let orig = sa.sin_addr.to_le_bytes();
|
let orig = sa.sin_addr.to_le_bytes();
|
||||||
let orig_port = u16::from_be(sa.sin_port);
|
let orig_port = u16::from_be(sa.sin_port);
|
||||||
|
|
||||||
let new_port_nbo = match sa.sin_port {
|
// Map the EA source port to an OpenFUT destination port from shared config.
|
||||||
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
// Returns None if this port isn't intercepted or no server is configured —
|
||||||
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
// in which case we leave the connection untouched (no loopback fallback).
|
||||||
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
let new_port_nbo = crate::server::dest_port_nbo_from_source_nbo(sa.sin_port)?;
|
||||||
_ => return None,
|
// The destination IP is the configured/resolved OpenFUT server — never a
|
||||||
};
|
// hardcoded address. If unset, dest_port_nbo_from_source_nbo already
|
||||||
|
// returned None above, so this is guaranteed Some here.
|
||||||
|
let new_addr = crate::server::sin_addr()?;
|
||||||
|
let ni = new_addr.to_le_bytes();
|
||||||
|
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"connect_hook: {}.{}.{}.{}:{} → 127.0.0.1:{}\n",
|
"connect_hook: {}.{}.{}.{}:{} → {}.{}.{}.{}:{} (configured OpenFUT server)\n",
|
||||||
orig[3], orig[2], orig[1], orig[0], orig_port,
|
orig[3],
|
||||||
|
orig[2],
|
||||||
|
orig[1],
|
||||||
|
orig[0],
|
||||||
|
orig_port,
|
||||||
|
ni[0],
|
||||||
|
ni[1],
|
||||||
|
ni[2],
|
||||||
|
ni[3],
|
||||||
u16::from_be(new_port_nbo)
|
u16::from_be(new_port_nbo)
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -77,7 +101,7 @@ unsafe fn redirect_if_ea(name: *const u8, namelen: i32) -> Option<([u8; 16], i32
|
|||||||
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn);
|
let out = &mut *(buf.as_mut_ptr() as *mut SockaddrIn);
|
||||||
out.sin_family = AF_INET;
|
out.sin_family = AF_INET;
|
||||||
out.sin_port = new_port_nbo;
|
out.sin_port = new_port_nbo;
|
||||||
out.sin_addr = ADDR_LOOPBACK_NBO;
|
out.sin_addr = new_addr;
|
||||||
Some((buf, 16))
|
Some((buf, 16))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +119,13 @@ pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen:
|
|||||||
use windows_sys::Win32::Networking::WinSock::{getsockopt, SOL_SOCKET, SO_TYPE};
|
use windows_sys::Win32::Networking::WinSock::{getsockopt, SOL_SOCKET, SO_TYPE};
|
||||||
let mut ty: i32 = -1;
|
let mut ty: i32 = -1;
|
||||||
let mut len: i32 = 4;
|
let mut len: i32 = 4;
|
||||||
getsockopt(s, SOL_SOCKET as i32, SO_TYPE, &mut ty as *mut i32 as *mut u8, &mut len);
|
getsockopt(
|
||||||
|
s,
|
||||||
|
SOL_SOCKET as i32,
|
||||||
|
SO_TYPE,
|
||||||
|
&mut ty as *mut i32 as *mut u8,
|
||||||
|
&mut len,
|
||||||
|
);
|
||||||
ty
|
ty
|
||||||
};
|
};
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
@@ -111,11 +141,25 @@ pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen:
|
|||||||
let (call_name, call_len) = if let Some((buf, len)) = redirect_if_ea(name, namelen) {
|
let (call_name, call_len) = if let Some((buf, len)) = redirect_if_ea(name, namelen) {
|
||||||
restore_original(addr);
|
restore_original(addr);
|
||||||
let r = {
|
let r = {
|
||||||
let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32
|
let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32 =
|
||||||
= core::mem::transmute(addr);
|
core::mem::transmute(addr);
|
||||||
f(s, buf.as_ptr(), len)
|
f(s, buf.as_ptr(), len)
|
||||||
};
|
};
|
||||||
|
// connect() communicates nonblocking progress through WSAGetLastError.
|
||||||
|
// Reinstalling the detour calls VirtualProtect, which may overwrite that
|
||||||
|
// thread-local value before FIFA reads it. Preserve the real call's value
|
||||||
|
// across all hook maintenance and logging.
|
||||||
|
let wsa_error = if r != 0 {
|
||||||
|
use windows_sys::Win32::Networking::WinSock::WSAGetLastError;
|
||||||
|
WSAGetLastError()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
write_hook(addr, hooked_connect as u64);
|
write_hook(addr, hooked_connect as u64);
|
||||||
|
if r != 0 {
|
||||||
|
use windows_sys::Win32::Networking::WinSock::WSASetLastError;
|
||||||
|
WSASetLastError(wsa_error);
|
||||||
|
}
|
||||||
return r;
|
return r;
|
||||||
} else {
|
} else {
|
||||||
(name, namelen)
|
(name, namelen)
|
||||||
@@ -123,28 +167,37 @@ pub unsafe extern "system" fn hooked_connect(s: usize, name: *const u8, namelen:
|
|||||||
|
|
||||||
restore_original(addr);
|
restore_original(addr);
|
||||||
let r = {
|
let r = {
|
||||||
let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32
|
let f: unsafe extern "system" fn(usize, *const u8, i32) -> i32 = core::mem::transmute(addr);
|
||||||
= core::mem::transmute(addr);
|
|
||||||
f(s, call_name, call_len)
|
f(s, call_name, call_len)
|
||||||
};
|
};
|
||||||
|
let wsa_error = if r != 0 {
|
||||||
|
use windows_sys::Win32::Networking::WinSock::WSAGetLastError;
|
||||||
|
WSAGetLastError()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
write_hook(addr, hooked_connect as u64);
|
write_hook(addr, hooked_connect as u64);
|
||||||
if namelen >= 8 {
|
if namelen >= 8 {
|
||||||
let sa = &*(call_name as *const SockaddrIn);
|
let sa = &*(call_name as *const SockaddrIn);
|
||||||
if sa.sin_family == AF_INET {
|
if sa.sin_family == AF_INET {
|
||||||
let err = if r != 0 {
|
crate::write_log(&format!("connect_hook: result={r} wsa_err={wsa_error}\n"));
|
||||||
use windows_sys::Win32::Networking::WinSock::WSAGetLastError;
|
|
||||||
WSAGetLastError()
|
|
||||||
} else { 0 };
|
|
||||||
crate::write_log(&format!("connect_hook: result={r} wsa_err={err}\n"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if r != 0 {
|
||||||
|
use windows_sys::Win32::Networking::WinSock::WSASetLastError;
|
||||||
|
WSASetLastError(wsa_error);
|
||||||
|
}
|
||||||
r
|
r
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe extern "system" fn hooked_wsa_connect(
|
pub unsafe extern "system" fn hooked_wsa_connect(
|
||||||
s: usize, name: *const u8, namelen: i32,
|
s: usize,
|
||||||
caller: *const (), callee: *const (),
|
name: *const u8,
|
||||||
sqos: *const (), gqos: *const (),
|
namelen: i32,
|
||||||
|
caller: *const (),
|
||||||
|
callee: *const (),
|
||||||
|
sqos: *const (),
|
||||||
|
gqos: *const (),
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
let real = REAL_WSA.get().copied().unwrap();
|
let real = REAL_WSA.get().copied().unwrap();
|
||||||
if let Some((buf, len)) = redirect_if_ea(name, namelen) {
|
if let Some((buf, len)) = redirect_if_ea(name, namelen) {
|
||||||
@@ -159,7 +212,9 @@ pub unsafe fn install_inline_connect_hook() -> bool {
|
|||||||
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
||||||
|
|
||||||
let ws2 = GetModuleHandleA(b"ws2_32.dll\0".as_ptr());
|
let ws2 = GetModuleHandleA(b"ws2_32.dll\0".as_ptr());
|
||||||
if ws2.is_null() { return false; }
|
if ws2.is_null() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let connect_fn = match GetProcAddress(ws2, b"connect\0".as_ptr()) {
|
let connect_fn = match GetProcAddress(ws2, b"connect\0".as_ptr()) {
|
||||||
Some(f) => f as *mut u8,
|
Some(f) => f as *mut u8,
|
||||||
None => return false,
|
None => return false,
|
||||||
|
|||||||
@@ -1,26 +1,19 @@
|
|||||||
|
use core::ffi::c_void;
|
||||||
/// Intercepts ConnectEx (EA/DirtySDK's preferred async connect API).
|
/// Intercepts ConnectEx (EA/DirtySDK's preferred async connect API).
|
||||||
///
|
///
|
||||||
/// DirtySDK calls WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER, WSAID_CONNECTEX) once at
|
/// 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
|
/// 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.
|
/// inline so that when it returns a ConnectEx pointer we swap it for our own wrapper.
|
||||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use core::ffi::c_void;
|
|
||||||
|
|
||||||
const AF_INET: u16 = 2;
|
const AF_INET: u16 = 2;
|
||||||
const PORT_HTTPS_NBO: u16 = 0xBB01; // 443 big-endian
|
|
||||||
const PORT_BRIDGE_NBO: u16 = 0xFB20; // 8443 big-endian
|
|
||||||
const PORT_BLAZE_REDIRECTOR_NBO: u16 = 0x3927; // 10041 big-endian
|
|
||||||
const PORT_BLAZE_MAIN_NBO: u16 = 0x8FA4; // 42127 big-endian
|
|
||||||
const ADDR_LOOPBACK_NBO: u32 = 0x0100_007F; // 127.0.0.1 big-endian
|
|
||||||
|
|
||||||
// SIO_GET_EXTENSION_FUNCTION_POINTER
|
// SIO_GET_EXTENSION_FUNCTION_POINTER
|
||||||
const SIO_GET_EXT_FN: u32 = 0xC8000006;
|
const SIO_GET_EXT_FN: u32 = 0xC8000006;
|
||||||
|
|
||||||
// WSAID_CONNECTEX = {25A207B9-DDF3-4660-8EE9-76E58C74063E}
|
// WSAID_CONNECTEX = {25A207B9-DDF3-4660-8EE9-76E58C74063E}
|
||||||
const CONNECTEX_GUID: [u8; 16] = [
|
const CONNECTEX_GUID: [u8; 16] = [
|
||||||
0xB9, 0x07, 0xA2, 0x25,
|
0xB9, 0x07, 0xA2, 0x25, 0xF3, 0xDD, 0x60, 0x46, 0x8E, 0xE9, 0x76, 0xE5, 0x8C, 0x74, 0x06, 0x3E,
|
||||||
0xF3, 0xDD, 0x60, 0x46,
|
|
||||||
0x8E, 0xE9, 0x76, 0xE5, 0x8C, 0x74, 0x06, 0x3E,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -65,7 +58,8 @@ unsafe fn write_hook(target: *mut u8, dest: u64) {
|
|||||||
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
use windows_sys::Win32::System::Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE};
|
||||||
let mut old: u32 = 0;
|
let mut old: u32 = 0;
|
||||||
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
VirtualProtect(target as _, 14, PAGE_EXECUTE_READWRITE, &mut old);
|
||||||
target.write(0xFF); target.add(1).write(0x25);
|
target.write(0xFF);
|
||||||
|
target.add(1).write(0x25);
|
||||||
(target.add(2) as *mut u32).write(0u32);
|
(target.add(2) as *mut u32).write(0u32);
|
||||||
(target.add(6) as *mut u64).write(dest);
|
(target.add(6) as *mut u64).write(dest);
|
||||||
VirtualProtect(target as _, 14, old, &mut old);
|
VirtualProtect(target as _, 14, old, &mut old);
|
||||||
@@ -79,7 +73,7 @@ unsafe fn restore_wsaioctl(target: *mut u8) {
|
|||||||
VirtualProtect(target as _, 14, old, &mut old);
|
VirtualProtect(target as _, 14, old, &mut old);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Our ConnectEx wrapper: redirects EA ports to 127.0.0.1
|
/// Our ConnectEx wrapper: redirects intercepted EA endpoints to the configured OpenFUT server
|
||||||
unsafe extern "system" fn hooked_connectex(
|
unsafe extern "system" fn hooked_connectex(
|
||||||
s: usize,
|
s: usize,
|
||||||
name: *const u8,
|
name: *const u8,
|
||||||
@@ -96,28 +90,53 @@ unsafe extern "system" fn hooked_connectex(
|
|||||||
if sa.sin_family == AF_INET {
|
if sa.sin_family == AF_INET {
|
||||||
let o = sa.sin_addr.to_le_bytes();
|
let o = sa.sin_addr.to_le_bytes();
|
||||||
let orig_port = u16::from_be(sa.sin_port);
|
let orig_port = u16::from_be(sa.sin_port);
|
||||||
let new_port_nbo = match sa.sin_port {
|
// Destination port + IP come from the shared configured server —
|
||||||
PORT_HTTPS_NBO => PORT_BRIDGE_NBO,
|
// never a hardcoded loopback. None => not intercepted / unconfigured,
|
||||||
PORT_BLAZE_REDIRECTOR_NBO => PORT_BLAZE_REDIRECTOR_NBO,
|
// so the original connection is passed through untouched.
|
||||||
PORT_BLAZE_MAIN_NBO => PORT_BLAZE_MAIN_NBO,
|
if let (Some(new_port_nbo), Some(new_addr)) = (
|
||||||
_ => 0,
|
crate::server::dest_port_nbo_from_source_nbo(sa.sin_port),
|
||||||
};
|
crate::server::sin_addr(),
|
||||||
if new_port_nbo != 0 {
|
) {
|
||||||
|
let ni = new_addr.to_le_bytes();
|
||||||
crate::write_log(&format!(
|
crate::write_log(&format!(
|
||||||
"connectex_hook: {}.{}.{}.{}:{} → 127.0.0.1:{}\n",
|
"connectex_hook: {}.{}.{}.{}:{} → {}.{}.{}.{}:{} (configured OpenFUT server)\n",
|
||||||
o[3], o[2], o[1], o[0], orig_port,
|
o[3],
|
||||||
|
o[2],
|
||||||
|
o[1],
|
||||||
|
o[0],
|
||||||
|
orig_port,
|
||||||
|
ni[0],
|
||||||
|
ni[1],
|
||||||
|
ni[2],
|
||||||
|
ni[3],
|
||||||
u16::from_be(new_port_nbo)
|
u16::from_be(new_port_nbo)
|
||||||
));
|
));
|
||||||
let mut redirect = [0u8; 16];
|
let mut redirect = [0u8; 16];
|
||||||
let out = &mut *(redirect.as_mut_ptr() as *mut SockaddrIn);
|
let out = &mut *(redirect.as_mut_ptr() as *mut SockaddrIn);
|
||||||
out.sin_family = AF_INET;
|
out.sin_family = AF_INET;
|
||||||
out.sin_port = new_port_nbo;
|
out.sin_port = new_port_nbo;
|
||||||
out.sin_addr = ADDR_LOOPBACK_NBO;
|
out.sin_addr = new_addr;
|
||||||
return real_fn(s, redirect.as_ptr(), 16, send_buf, send_data_len, bytes_sent, overlapped);
|
return real_fn(
|
||||||
|
s,
|
||||||
|
redirect.as_ptr(),
|
||||||
|
16,
|
||||||
|
send_buf,
|
||||||
|
send_data_len,
|
||||||
|
bytes_sent,
|
||||||
|
overlapped,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
real_fn(s, name, namelen, 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
|
/// Our WSAIoctl hook: when ConnectEx is requested, save the real pointer and return ours
|
||||||
@@ -138,25 +157,25 @@ pub unsafe extern "system" fn hooked_wsaioctl(
|
|||||||
restore_wsaioctl(addr);
|
restore_wsaioctl(addr);
|
||||||
let result = {
|
let result = {
|
||||||
let f: WsaIoctlFn = core::mem::transmute(addr);
|
let f: WsaIoctlFn = core::mem::transmute(addr);
|
||||||
f(s, code, in_buf, in_len, out_buf, out_len, bytes_ret, overlapped, completion)
|
f(
|
||||||
|
s, code, in_buf, in_len, out_buf, out_len, bytes_ret, overlapped, completion,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
write_hook(addr, hooked_wsaioctl as u64);
|
write_hook(addr, hooked_wsaioctl as u64);
|
||||||
|
|
||||||
// If this was a ConnectEx request that succeeded, swap the pointer
|
// If this was a ConnectEx request that succeeded, swap the pointer
|
||||||
if result == 0
|
if result == 0 && code == SIO_GET_EXT_FN && in_len == 16 && !in_buf.is_null() {
|
||||||
&& 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);
|
let guid = core::slice::from_raw_parts(in_buf as *const u8, 16);
|
||||||
if guid == CONNECTEX_GUID
|
if guid == CONNECTEX_GUID && out_len >= 8 && !out_buf.is_null() {
|
||||||
&& out_len >= 8
|
|
||||||
&& !out_buf.is_null()
|
|
||||||
{
|
|
||||||
let out_ptr = out_buf as *mut usize;
|
let out_ptr = out_buf as *mut usize;
|
||||||
let real_addr = *out_ptr;
|
let real_addr = *out_ptr;
|
||||||
if REAL_CONNECTEX.compare_exchange(0, real_addr, Ordering::Relaxed, Ordering::Relaxed).is_ok() {
|
if REAL_CONNECTEX
|
||||||
crate::write_log(&format!("connectex_hook: intercepted ConnectEx @ {real_addr:#x}\n"));
|
.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
|
// Return our hook instead
|
||||||
*out_ptr = hooked_connectex as usize;
|
*out_ptr = hooked_connectex as usize;
|
||||||
@@ -168,7 +187,9 @@ pub unsafe extern "system" fn hooked_wsaioctl(
|
|||||||
pub unsafe fn install_wsaioctl_hook() -> bool {
|
pub unsafe fn install_wsaioctl_hook() -> bool {
|
||||||
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
||||||
let ws2 = GetModuleHandleA(b"ws2_32.dll\0".as_ptr());
|
let ws2 = GetModuleHandleA(b"ws2_32.dll\0".as_ptr());
|
||||||
if ws2.is_null() { return false; }
|
if ws2.is_null() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let fn_ptr = match GetProcAddress(ws2, b"WSAIoctl\0".as_ptr()) {
|
let fn_ptr = match GetProcAddress(ws2, b"WSAIoctl\0".as_ptr()) {
|
||||||
Some(f) => f as *mut u8,
|
Some(f) => f as *mut u8,
|
||||||
None => return false,
|
None => return false,
|
||||||
|
|||||||
+34
-19
@@ -1,22 +1,23 @@
|
|||||||
use std::{
|
use std::{
|
||||||
ffi::CStr,
|
ffi::CStr,
|
||||||
sync::{
|
sync::{
|
||||||
OnceLock,
|
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
|
OnceLock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use windows_sys::Win32::Networking::WinSock::{ADDRINFOA, getaddrinfo as sys_getaddrinfo};
|
use windows_sys::Win32::Networking::WinSock::{getaddrinfo as sys_getaddrinfo, ADDRINFOA};
|
||||||
|
|
||||||
type GetaddrinfoFn = unsafe extern "system" fn(
|
type GetaddrinfoFn =
|
||||||
*const u8,
|
unsafe extern "system" fn(*const u8, *const u8, *const ADDRINFOA, *mut *mut ADDRINFOA) -> i32;
|
||||||
*const u8,
|
|
||||||
*const ADDRINFOA,
|
|
||||||
*mut *mut ADDRINFOA,
|
|
||||||
) -> i32;
|
|
||||||
|
|
||||||
static REAL: OnceLock<GetaddrinfoFn> = OnceLock::new();
|
static REAL: OnceLock<GetaddrinfoFn> = OnceLock::new();
|
||||||
static REDIRECT_IP: OnceLock<Vec<u8>> = OnceLock::new();
|
// NUL-terminated dotted-quad of the resolved OpenFUT server, built once at init
|
||||||
|
// from the SAME shared config the socket hooks use. getaddrinfo redirects EA
|
||||||
|
// hostnames here so DNS resolves to the configured server. If configuration was
|
||||||
|
// missing/invalid this stays empty and EA hostnames are NOT redirected (no
|
||||||
|
// loopback fallback).
|
||||||
|
static REDIRECT_HOST: OnceLock<Vec<u8>> = OnceLock::new();
|
||||||
|
|
||||||
// Flipped to true the first time we successfully apply the runtime cert patch.
|
// 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
|
// The patch is deferred to here (rather than DllMain) because EAWebKit.dll may
|
||||||
@@ -27,10 +28,12 @@ pub fn set_real(f: GetaddrinfoFn) {
|
|||||||
let _ = REAL.set(f);
|
let _ = REAL.set(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_redirect_ip(ip: String) {
|
/// Install the resolved redirect IPv4 (dotted-quad) getaddrinfo will hand back
|
||||||
let mut bytes = ip.into_bytes();
|
/// for EA hostnames. Called once at init from the shared resolved server.
|
||||||
|
pub fn set_redirect_ip(ip: std::net::Ipv4Addr) {
|
||||||
|
let mut bytes = ip.to_string().into_bytes();
|
||||||
bytes.push(0);
|
bytes.push(0);
|
||||||
let _ = REDIRECT_IP.set(bytes);
|
let _ = REDIRECT_HOST.set(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if `host` is an EA / EA-Sports domain that should be redirected
|
/// Returns true if `host` is an EA / EA-Sports domain that should be redirected
|
||||||
@@ -60,18 +63,30 @@ pub unsafe extern "system" fn hooked_getaddrinfo(
|
|||||||
if !CERT_PATCHED.load(Ordering::Relaxed) {
|
if !CERT_PATCHED.load(Ordering::Relaxed) {
|
||||||
if crate::ssl_patch::patch_eawebkit_cert_verify() {
|
if crate::ssl_patch::patch_eawebkit_cert_verify() {
|
||||||
CERT_PATCHED.store(true, Ordering::Relaxed);
|
CERT_PATCHED.store(true, Ordering::Relaxed);
|
||||||
crate::write_log("openfut_hook: ProtoSSL cert-verify patched (lazy, from getaddrinfo)\n");
|
crate::write_log(
|
||||||
|
"openfut_hook: ProtoSSL cert-verify patched (lazy, from getaddrinfo)\n",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
crate::write_log("openfut_hook: ProtoSSL cert-verify patch FAILED in getaddrinfo\n");
|
crate::write_log(
|
||||||
|
"openfut_hook: ProtoSSL cert-verify patch FAILED in getaddrinfo\n",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let redirect = REDIRECT_IP
|
// Only redirect when a server was configured & resolved. If not,
|
||||||
.get()
|
// fall through to the real resolver — we never invent a loopback
|
||||||
.map(|v| v.as_ptr())
|
// destination here.
|
||||||
.unwrap_or(b"127.0.0.1\0".as_ptr());
|
match REDIRECT_HOST.get() {
|
||||||
|
Some(redirect) => {
|
||||||
let real = REAL.get().copied().unwrap_or(sys_getaddrinfo);
|
let real = REAL.get().copied().unwrap_or(sys_getaddrinfo);
|
||||||
return real(redirect, service_name, hints, result);
|
return real(redirect.as_ptr(), service_name, hints, result);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
crate::write_log(
|
||||||
|
"openfut_hook: EA host seen but no OpenFUT server configured — NOT redirecting\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-7
@@ -72,7 +72,11 @@ pub unsafe fn patch_iat(original_fn: *const (), hook_fn: *const ()) -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Patch the IAT of a specific already-loaded DLL (e.g. b"EAWebKit.dll\0").
|
/// Patch the IAT of a specific already-loaded DLL (e.g. b"EAWebKit.dll\0").
|
||||||
pub unsafe fn patch_iat_in(module_name: &[u8], original_fn: *const (), hook_fn: *const ()) -> usize {
|
pub unsafe fn patch_iat_in(
|
||||||
|
module_name: &[u8],
|
||||||
|
original_fn: *const (),
|
||||||
|
hook_fn: *const (),
|
||||||
|
) -> usize {
|
||||||
let module = GetModuleHandleA(module_name.as_ptr());
|
let module = GetModuleHandleA(module_name.as_ptr());
|
||||||
if module.is_null() {
|
if module.is_null() {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -80,11 +84,7 @@ pub unsafe fn patch_iat_in(module_name: &[u8], original_fn: *const (), hook_fn:
|
|||||||
patch_module(module, original_fn, hook_fn)
|
patch_module(module, original_fn, hook_fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn patch_module(
|
unsafe fn patch_module(module: HMODULE, original_fn: *const (), hook_fn: *const ()) -> usize {
|
||||||
module: HMODULE,
|
|
||||||
original_fn: *const (),
|
|
||||||
hook_fn: *const (),
|
|
||||||
) -> usize {
|
|
||||||
if module.is_null() {
|
if module.is_null() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,12 @@ unsafe fn patch_module(
|
|||||||
if val == original_fn as usize {
|
if val == original_fn as usize {
|
||||||
let target = iat_slot.add(i) as *const std::ffi::c_void;
|
let target = iat_slot.add(i) as *const std::ffi::c_void;
|
||||||
let mut old: u32 = 0;
|
let mut old: u32 = 0;
|
||||||
VirtualProtect(target, std::mem::size_of::<usize>(), PAGE_EXECUTE_READWRITE, &mut old);
|
VirtualProtect(
|
||||||
|
target,
|
||||||
|
std::mem::size_of::<usize>(),
|
||||||
|
PAGE_EXECUTE_READWRITE,
|
||||||
|
&mut old,
|
||||||
|
);
|
||||||
*iat_slot.add(i) = hook_fn as usize;
|
*iat_slot.add(i) = hook_fn as usize;
|
||||||
VirtualProtect(target, std::mem::size_of::<usize>(), old, &mut old);
|
VirtualProtect(target, std::mem::size_of::<usize>(), old, &mut old);
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|||||||
+119
-34
@@ -4,62 +4,115 @@ mod connectex_hook;
|
|||||||
mod hooks;
|
mod hooks;
|
||||||
mod iat;
|
mod iat;
|
||||||
mod origin_spy;
|
mod origin_spy;
|
||||||
|
mod server;
|
||||||
mod ssl_patch;
|
mod ssl_patch;
|
||||||
mod tls_bypass;
|
mod tls_bypass;
|
||||||
|
|
||||||
use windows_sys::Win32::{
|
use windows_sys::Win32::{
|
||||||
Foundation::{BOOL, HMODULE, TRUE},
|
Foundation::{BOOL, HMODULE, TRUE},
|
||||||
System::SystemServices::DLL_PROCESS_ATTACH,
|
|
||||||
Networking::WinSock::ADDRINFOA,
|
Networking::WinSock::ADDRINFOA,
|
||||||
|
System::SystemServices::DLL_PROCESS_ATTACH,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) fn write_log(msg: &str) {
|
pub(crate) fn write_log(msg: &str) {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||||
.create(true).append(true)
|
.create(true)
|
||||||
|
.append(true)
|
||||||
.open(r"C:\openfut_hook.log")
|
.open(r"C:\openfut_hook.log")
|
||||||
{ let _ = f.write_all(msg.as_bytes()); }
|
{
|
||||||
|
let _ = f.write_all(msg.as_bytes());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "system" fn DllMain(module: HMODULE, reason: u32, _: *mut ()) -> BOOL {
|
pub unsafe extern "system" fn DllMain(module: HMODULE, reason: u32, _: *mut ()) -> BOOL {
|
||||||
if reason == DLL_PROCESS_ATTACH { install_hooks(module); }
|
if reason == DLL_PROCESS_ATTACH {
|
||||||
|
install_hooks(module);
|
||||||
|
}
|
||||||
TRUE
|
TRUE
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn install_hooks(module: HMODULE) {
|
unsafe fn install_hooks(module: HMODULE) {
|
||||||
write_log("openfut_hook: DllMain fired\n");
|
write_log("openfut_hook: DllMain fired\n");
|
||||||
let ip = config::read_redirect_ip(module);
|
|
||||||
hooks::set_redirect_ip(ip);
|
// Load the single source of truth for the OpenFUT destination. If it's
|
||||||
|
// missing/invalid we log and install NO redirection — traffic is left alone
|
||||||
|
// rather than silently sent to loopback.
|
||||||
|
match config::load_config(module).and_then(|c| c.resolve()) {
|
||||||
|
Ok(resolved) => {
|
||||||
|
server::set(resolved);
|
||||||
|
hooks::set_redirect_ip(resolved.redirect_ip);
|
||||||
|
let o = resolved.redirect_ip.octets();
|
||||||
|
write_log(&format!(
|
||||||
|
"openfut_hook: OpenFUT server = {}.{}.{}.{} (https={} blaze_redir={} blaze_main={})\n",
|
||||||
|
o[0], o[1], o[2], o[3],
|
||||||
|
resolved.ports.https, resolved.ports.blaze_redirector, resolved.ports.blaze_main
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
write_log(&format!(
|
||||||
|
"openfut_hook: NO OpenFUT server configured ({e}); redirection DISABLED. \
|
||||||
|
Configure a server in the launcher and relaunch.\n"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let ga = iat::resolve(b"ws2_32.dll\0", b"getaddrinfo\0");
|
let ga = iat::resolve(b"ws2_32.dll\0", b"getaddrinfo\0");
|
||||||
if !ga.is_null() {
|
if !ga.is_null() {
|
||||||
let f: unsafe extern "system" fn(*const u8,*const u8,*const ADDRINFOA,*mut *mut ADDRINFOA)->i32
|
let f: unsafe extern "system" fn(
|
||||||
= std::mem::transmute(ga);
|
*const u8,
|
||||||
|
*const u8,
|
||||||
|
*const ADDRINFOA,
|
||||||
|
*mut *mut ADDRINFOA,
|
||||||
|
) -> i32 = std::mem::transmute(ga);
|
||||||
hooks::set_real(f);
|
hooks::set_real(f);
|
||||||
let n = iat::patch_iat(ga, hooks::hooked_getaddrinfo as *const ());
|
let n = iat::patch_iat(ga, hooks::hooked_getaddrinfo as *const ());
|
||||||
let m = iat::patch_iat_in(b"EAWebKit.dll\0", ga, hooks::hooked_getaddrinfo as *const ());
|
let m = iat::patch_iat_in(
|
||||||
|
b"EAWebKit.dll\0",
|
||||||
|
ga,
|
||||||
|
hooks::hooked_getaddrinfo as *const (),
|
||||||
|
);
|
||||||
write_log(&format!("openfut_hook: getaddrinfo IAT patched {n}+{m}\n"));
|
write_log(&format!("openfut_hook: getaddrinfo IAT patched {n}+{m}\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ssl_patch::patch_main_exe_cert_verify() { write_log("ssl: main exe cert-verify patched\n"); }
|
if ssl_patch::patch_main_exe_cert_verify() {
|
||||||
else { write_log("ssl: main exe cert-verify NOT FOUND\n"); }
|
write_log("ssl: main exe cert-verify patched\n");
|
||||||
if ssl_patch::patch_eawebkit_cert_verify() { write_log("ssl: EAWebKit cert-verify patched\n"); }
|
} else {
|
||||||
else { write_log("ssl: EAWebKit cert-verify deferred\n"); }
|
write_log("ssl: main exe cert-verify NOT FOUND\n");
|
||||||
|
}
|
||||||
|
if ssl_patch::patch_eawebkit_cert_verify() {
|
||||||
|
write_log("ssl: EAWebKit cert-verify patched\n");
|
||||||
|
} else {
|
||||||
|
write_log("ssl: EAWebKit cert-verify deferred\n");
|
||||||
|
}
|
||||||
|
|
||||||
if connect_hook::install_inline_connect_hook() { write_log("connect: inline-hooked\n"); }
|
if connect_hook::install_inline_connect_hook() {
|
||||||
else { write_log("connect: hook FAILED\n"); }
|
write_log("connect: inline-hooked\n");
|
||||||
|
} else {
|
||||||
|
write_log("connect: hook FAILED\n");
|
||||||
|
}
|
||||||
let wp = iat::resolve(b"ws2_32.dll\0", b"WSAConnect\0");
|
let wp = iat::resolve(b"ws2_32.dll\0", b"WSAConnect\0");
|
||||||
if !wp.is_null() {
|
if !wp.is_null() {
|
||||||
let f: unsafe extern "system" fn(usize,*const u8,i32,*const(),*const(),*const(),*const())->i32
|
let f: unsafe extern "system" fn(
|
||||||
= std::mem::transmute(wp);
|
usize,
|
||||||
|
*const u8,
|
||||||
|
i32,
|
||||||
|
*const (),
|
||||||
|
*const (),
|
||||||
|
*const (),
|
||||||
|
*const (),
|
||||||
|
) -> i32 = std::mem::transmute(wp);
|
||||||
connect_hook::set_real_wsa_connect(f);
|
connect_hook::set_real_wsa_connect(f);
|
||||||
iat::patch_iat(wp, connect_hook::hooked_wsa_connect as *const ());
|
iat::patch_iat(wp, connect_hook::hooked_wsa_connect as *const ());
|
||||||
write_log("connect: WSAConnect IAT patched\n");
|
write_log("connect: WSAConnect IAT patched\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if connectex_hook::install_wsaioctl_hook() { write_log("connectex: WSAIoctl inline-hooked\n"); }
|
if connectex_hook::install_wsaioctl_hook() {
|
||||||
else { write_log("connectex: WSAIoctl hook FAILED\n"); }
|
write_log("connectex: WSAIoctl inline-hooked\n");
|
||||||
|
} else {
|
||||||
|
write_log("connectex: WSAIoctl hook FAILED\n");
|
||||||
|
}
|
||||||
|
|
||||||
// recv/send hooks removed — LSX is now handled by the native openfut-bridge
|
// recv/send hooks removed — LSX is now handled by the native openfut-bridge
|
||||||
// LSX server (port 3216), so in-process interception is no longer needed.
|
// LSX server (port 3216), so in-process interception is no longer needed.
|
||||||
@@ -72,31 +125,63 @@ unsafe fn install_hooks(module: HMODULE) {
|
|||||||
origin_spy::$setter(f);
|
origin_spy::$setter(f);
|
||||||
iat::patch_iat(ptr, $handler as *const ());
|
iat::patch_iat(ptr, $handler as *const ());
|
||||||
"ok"
|
"ok"
|
||||||
} else { "miss" }
|
} else {
|
||||||
|
"miss"
|
||||||
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
let ra = hook_iat!(b"advapi32.dll\0", b"RegQueryValueExA\0", set_real_reg_a,
|
let ra = hook_iat!(
|
||||||
|
b"advapi32.dll\0",
|
||||||
|
b"RegQueryValueExA\0",
|
||||||
|
set_real_reg_a,
|
||||||
origin_spy::hooked_reg_query_a,
|
origin_spy::hooked_reg_query_a,
|
||||||
unsafe extern "system" fn(isize,*const u8,*mut u32,*mut u32,*mut u8,*mut u32)->i32);
|
unsafe extern "system" fn(isize, *const u8, *mut u32, *mut u32, *mut u8, *mut u32) -> i32
|
||||||
let rw = hook_iat!(b"advapi32.dll\0", b"RegQueryValueExW\0", set_real_reg_w,
|
);
|
||||||
|
let rw = hook_iat!(
|
||||||
|
b"advapi32.dll\0",
|
||||||
|
b"RegQueryValueExW\0",
|
||||||
|
set_real_reg_w,
|
||||||
origin_spy::hooked_reg_query_w,
|
origin_spy::hooked_reg_query_w,
|
||||||
unsafe extern "system" fn(isize,*const u16,*mut u32,*mut u32,*mut u8,*mut u32)->i32);
|
unsafe extern "system" fn(isize, *const u16, *mut u32, *mut u32, *mut u8, *mut u32) -> i32
|
||||||
let ma = hook_iat!(b"kernel32.dll\0", b"OpenMutexA\0", set_real_mutex_a,
|
);
|
||||||
|
let ma = hook_iat!(
|
||||||
|
b"kernel32.dll\0",
|
||||||
|
b"OpenMutexA\0",
|
||||||
|
set_real_mutex_a,
|
||||||
origin_spy::hooked_open_mutex_a,
|
origin_spy::hooked_open_mutex_a,
|
||||||
unsafe extern "system" fn(u32,i32,*const u8)->isize);
|
unsafe extern "system" fn(u32, i32, *const u8) -> isize
|
||||||
let mw = hook_iat!(b"kernel32.dll\0", b"OpenMutexW\0", set_real_mutex_w,
|
);
|
||||||
|
let mw = hook_iat!(
|
||||||
|
b"kernel32.dll\0",
|
||||||
|
b"OpenMutexW\0",
|
||||||
|
set_real_mutex_w,
|
||||||
origin_spy::hooked_open_mutex_w,
|
origin_spy::hooked_open_mutex_w,
|
||||||
unsafe extern "system" fn(u32,i32,*const u16)->isize);
|
unsafe extern "system" fn(u32, i32, *const u16) -> isize
|
||||||
write_log(&format!("origin_spy: RegA={ra} RegW={rw} MutexA={ma} MutexW={mw}\n"));
|
);
|
||||||
|
write_log(&format!(
|
||||||
|
"origin_spy: RegA={ra} RegW={rw} MutexA={ma} MutexW={mw}\n"
|
||||||
|
));
|
||||||
|
|
||||||
let cv = iat::resolve(b"crypt32.dll\0", b"CertVerifyCertificateChainPolicy\0");
|
let cv = iat::resolve(b"crypt32.dll\0", b"CertVerifyCertificateChainPolicy\0");
|
||||||
if !cv.is_null() {
|
if !cv.is_null() {
|
||||||
let f: unsafe extern "system" fn(*const u8,*const(),*const(),*mut u32)->BOOL
|
let f: unsafe extern "system" fn(*const u8, *const (), *const (), *mut u32) -> BOOL =
|
||||||
= std::mem::transmute(cv);
|
std::mem::transmute(cv);
|
||||||
tls_bypass::set_real(f);
|
tls_bypass::set_real(f);
|
||||||
iat::patch_iat(cv, tls_bypass::hooked_cert_verify_chain_policy as *const ());
|
iat::patch_iat(cv, tls_bypass::hooked_cert_verify_chain_policy as *const ());
|
||||||
iat::patch_iat_in(b"EAWebKit.dll\0", cv, tls_bypass::hooked_cert_verify_chain_policy as *const ());
|
iat::patch_iat_in(
|
||||||
iat::patch_iat_in(b"winhttp.dll\0", cv, tls_bypass::hooked_cert_verify_chain_policy as *const ());
|
b"EAWebKit.dll\0",
|
||||||
iat::patch_iat_in(b"wininet.dll\0", cv, tls_bypass::hooked_cert_verify_chain_policy as *const ());
|
cv,
|
||||||
|
tls_bypass::hooked_cert_verify_chain_policy as *const (),
|
||||||
|
);
|
||||||
|
iat::patch_iat_in(
|
||||||
|
b"winhttp.dll\0",
|
||||||
|
cv,
|
||||||
|
tls_bypass::hooked_cert_verify_chain_policy as *const (),
|
||||||
|
);
|
||||||
|
iat::patch_iat_in(
|
||||||
|
b"wininet.dll\0",
|
||||||
|
cv,
|
||||||
|
tls_bypass::hooked_cert_verify_chain_policy as *const (),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,28 +27,49 @@ static REAL_REG_W: OnceLock<RegQueryValueExWFn> = OnceLock::new();
|
|||||||
static REAL_MUTEX_A: OnceLock<OpenMutexAFn> = OnceLock::new();
|
static REAL_MUTEX_A: OnceLock<OpenMutexAFn> = OnceLock::new();
|
||||||
static REAL_MUTEX_W: OnceLock<OpenMutexWFn> = OnceLock::new();
|
static REAL_MUTEX_W: OnceLock<OpenMutexWFn> = OnceLock::new();
|
||||||
|
|
||||||
pub fn set_real_reg_a(f: RegQueryValueExAFn) { let _ = REAL_REG_A.set(f); }
|
pub fn set_real_reg_a(f: RegQueryValueExAFn) {
|
||||||
pub fn set_real_reg_w(f: RegQueryValueExWFn) { let _ = REAL_REG_W.set(f); }
|
let _ = REAL_REG_A.set(f);
|
||||||
pub fn set_real_mutex_a(f: OpenMutexAFn) { let _ = REAL_MUTEX_A.set(f); }
|
}
|
||||||
pub fn set_real_mutex_w(f: OpenMutexWFn) { let _ = REAL_MUTEX_W.set(f); }
|
pub fn set_real_reg_w(f: RegQueryValueExWFn) {
|
||||||
|
let _ = REAL_REG_W.set(f);
|
||||||
|
}
|
||||||
|
pub fn set_real_mutex_a(f: OpenMutexAFn) {
|
||||||
|
let _ = REAL_MUTEX_A.set(f);
|
||||||
|
}
|
||||||
|
pub fn set_real_mutex_w(f: OpenMutexWFn) {
|
||||||
|
let _ = REAL_MUTEX_W.set(f);
|
||||||
|
}
|
||||||
|
|
||||||
fn narrow_to_string(p: *const u8) -> String {
|
fn narrow_to_string(p: *const u8) -> String {
|
||||||
if p.is_null() { return "(null)".into(); }
|
if p.is_null() {
|
||||||
|
return "(null)".into();
|
||||||
|
}
|
||||||
let bytes = unsafe { std::ffi::CStr::from_ptr(p as *const i8) };
|
let bytes = unsafe { std::ffi::CStr::from_ptr(p as *const i8) };
|
||||||
bytes.to_string_lossy().into_owned()
|
bytes.to_string_lossy().into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wide_to_string(p: *const u16) -> String {
|
fn wide_to_string(p: *const u16) -> String {
|
||||||
if p.is_null() { return "(null)".into(); }
|
if p.is_null() {
|
||||||
|
return "(null)".into();
|
||||||
|
}
|
||||||
let mut len = 0usize;
|
let mut len = 0usize;
|
||||||
unsafe { while *p.add(len) != 0 { len += 1; } }
|
unsafe {
|
||||||
|
while *p.add(len) != 0 {
|
||||||
|
len += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(p, len) })
|
String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(p, len) })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_interesting(name: &str) -> bool {
|
fn is_interesting(name: &str) -> bool {
|
||||||
name.contains("LSX") || name.contains("Origin") || name.contains("EAL") ||
|
name.contains("LSX")
|
||||||
name.contains("Client") || name.contains("lsx") || name.contains("Port") ||
|
|| name.contains("Origin")
|
||||||
name.contains("EA") || name.contains("Connection")
|
|| name.contains("EAL")
|
||||||
|
|| name.contains("Client")
|
||||||
|
|| name.contains("lsx")
|
||||||
|
|| name.contains("Port")
|
||||||
|
|| name.contains("EA")
|
||||||
|
|| name.contains("Connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe extern "system" fn hooked_reg_query_a(
|
pub unsafe extern "system" fn hooked_reg_query_a(
|
||||||
@@ -93,8 +114,10 @@ pub unsafe extern "system" fn hooked_open_mutex_a(
|
|||||||
let name = narrow_to_string(lpmutexname);
|
let name = narrow_to_string(lpmutexname);
|
||||||
let real = REAL_MUTEX_A.get().copied().unwrap();
|
let real = REAL_MUTEX_A.get().copied().unwrap();
|
||||||
let handle = real(dwdesiredaccess, binherithandle, lpmutexname);
|
let handle = real(dwdesiredaccess, binherithandle, lpmutexname);
|
||||||
crate::write_log(&format!("origin_spy: OpenMutexA({name}) → {}\n",
|
crate::write_log(&format!(
|
||||||
if handle == 0 { "NOT_FOUND" } else { "FOUND" }));
|
"origin_spy: OpenMutexA({name}) → {}\n",
|
||||||
|
if handle == 0 { "NOT_FOUND" } else { "FOUND" }
|
||||||
|
));
|
||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +129,9 @@ pub unsafe extern "system" fn hooked_open_mutex_w(
|
|||||||
let name = wide_to_string(lpmutexname);
|
let name = wide_to_string(lpmutexname);
|
||||||
let real = REAL_MUTEX_W.get().copied().unwrap();
|
let real = REAL_MUTEX_W.get().copied().unwrap();
|
||||||
let handle = real(dwdesiredaccess, binherithandle, lpmutexname);
|
let handle = real(dwdesiredaccess, binherithandle, lpmutexname);
|
||||||
crate::write_log(&format!("origin_spy: OpenMutexW({name}) → {}\n",
|
crate::write_log(&format!(
|
||||||
if handle == 0 { "NOT_FOUND" } else { "FOUND" }));
|
"origin_spy: OpenMutexW({name}) → {}\n",
|
||||||
|
if handle == 0 { "NOT_FOUND" } else { "FOUND" }
|
||||||
|
));
|
||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//! The single, process-wide resolved OpenFUT destination.
|
||||||
|
//!
|
||||||
|
//! All three interception layers — `getaddrinfo`, `connect`, and `ConnectEx` —
|
||||||
|
//! read the destination from HERE. There is no per-hook redirect state. The
|
||||||
|
//! value is set exactly once during DLL init (after `openfut.cfg` is parsed and
|
||||||
|
//! the host resolved) and is never mutated afterward.
|
||||||
|
//!
|
||||||
|
//! If configuration was missing/invalid, this is never populated, and every
|
||||||
|
//! hook leaves traffic untouched (no loopback fallback).
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use openfut_common::{sin_addr_from_ipv4, sin_port_nbo, ResolvedServer};
|
||||||
|
|
||||||
|
static SERVER: OnceLock<ResolvedServer> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Install the resolved destination. Called once from DLL init. Ignores
|
||||||
|
/// subsequent calls (OnceLock semantics).
|
||||||
|
pub fn set(resolved: ResolvedServer) {
|
||||||
|
let _ = SERVER.set(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The resolved destination, if configuration succeeded.
|
||||||
|
pub fn get() -> Option<ResolvedServer> {
|
||||||
|
SERVER.get().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The resolved redirect IPv4, if configured.
|
||||||
|
pub fn redirect_ip() -> Option<Ipv4Addr> {
|
||||||
|
SERVER.get().map(|s| s.redirect_ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `sockaddr_in.sin_addr` value (native-endian u32) for the configured server.
|
||||||
|
pub fn sin_addr() -> Option<u32> {
|
||||||
|
SERVER.get().map(|s| sin_addr_from_ipv4(s.redirect_ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given an EA *source* port (network byte order, as seen in `sockaddr_in`),
|
||||||
|
/// return the OpenFUT *destination* port in network byte order — or `None` if
|
||||||
|
/// this port isn't intercepted or no server is configured.
|
||||||
|
pub fn dest_port_nbo_from_source_nbo(source_port_nbo: u16) -> Option<u16> {
|
||||||
|
let s = SERVER.get()?;
|
||||||
|
let source_host = u16::from_be(source_port_nbo);
|
||||||
|
s.ports.map_source_port(source_host).map(sin_port_nbo)
|
||||||
|
}
|
||||||
@@ -9,11 +9,9 @@
|
|||||||
// server's certificate chain. Always returning 1 is equivalent to trusting all certs,
|
// server's certificate chain. Always returning 1 is equivalent to trusting all certs,
|
||||||
// which is the behaviour we want for the local self-signed bridge certificate.
|
// which is the behaviour we want for the local self-signed bridge certificate.
|
||||||
|
|
||||||
use windows_sys::Win32::{
|
use windows_sys::Win32::System::{
|
||||||
System::{
|
|
||||||
LibraryLoader::GetModuleHandleA,
|
LibraryLoader::GetModuleHandleA,
|
||||||
Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE},
|
Memory::{VirtualProtect, PAGE_EXECUTE_READWRITE},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Unique 22-byte prologue of ProtoSSL's cert-verify function.
|
// Unique 22-byte prologue of ProtoSSL's cert-verify function.
|
||||||
@@ -38,7 +36,9 @@ const PATCH: &[u8] = &[
|
|||||||
];
|
];
|
||||||
|
|
||||||
fn patch_module(module: isize, scan_bytes: usize) -> bool {
|
fn patch_module(module: isize, scan_bytes: usize) -> bool {
|
||||||
if module == 0 { return false; }
|
if module == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let base = module as usize;
|
let base = module as usize;
|
||||||
let image: &[u8] = unsafe { core::slice::from_raw_parts(base as *const u8, scan_bytes) };
|
let image: &[u8] = unsafe { core::slice::from_raw_parts(base as *const u8, scan_bytes) };
|
||||||
let offset = match image.windows(PROLOGUE.len()).position(|w| w == PROLOGUE) {
|
let offset = match image.windows(PROLOGUE.len()).position(|w| w == PROLOGUE) {
|
||||||
@@ -48,9 +48,19 @@ fn patch_module(module: isize, scan_bytes: usize) -> bool {
|
|||||||
let target = (base + offset) as *mut u8;
|
let target = (base + offset) as *mut u8;
|
||||||
let mut old_prot: u32 = 0;
|
let mut old_prot: u32 = 0;
|
||||||
unsafe {
|
unsafe {
|
||||||
VirtualProtect(target as *const core::ffi::c_void, PATCH.len(), PAGE_EXECUTE_READWRITE, &mut old_prot);
|
VirtualProtect(
|
||||||
|
target as *const core::ffi::c_void,
|
||||||
|
PATCH.len(),
|
||||||
|
PAGE_EXECUTE_READWRITE,
|
||||||
|
&mut old_prot,
|
||||||
|
);
|
||||||
core::ptr::copy_nonoverlapping(PATCH.as_ptr(), target, PATCH.len());
|
core::ptr::copy_nonoverlapping(PATCH.as_ptr(), target, PATCH.len());
|
||||||
VirtualProtect(target as *const core::ffi::c_void, PATCH.len(), old_prot, &mut old_prot);
|
VirtualProtect(
|
||||||
|
target as *const core::ffi::c_void,
|
||||||
|
PATCH.len(),
|
||||||
|
old_prot,
|
||||||
|
&mut old_prot,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,12 @@ pub unsafe extern "system" fn hooked_cert_verify_chain_policy(
|
|||||||
p_policy_status: *mut u32,
|
p_policy_status: *mut u32,
|
||||||
) -> BOOL {
|
) -> BOOL {
|
||||||
if let Some(real) = REAL.get().copied() {
|
if let Some(real) = REAL.get().copied() {
|
||||||
real(psz_policy_oid, p_chain_context, p_policy_para, p_policy_status);
|
real(
|
||||||
|
psz_policy_oid,
|
||||||
|
p_chain_context,
|
||||||
|
p_policy_para,
|
||||||
|
p_policy_status,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Clear the error field of CERT_CHAIN_POLICY_STATUS regardless
|
// Clear the error field of CERT_CHAIN_POLICY_STATUS regardless
|
||||||
if !p_policy_status.is_null() {
|
if !p_policy_status.is_null() {
|
||||||
|
|||||||
Reference in New Issue
Block a user