feat(windows): add connectex-overlapped and dns api hardening with ci e2e
This commit is contained in:
1 parent
04a548ca23
commit
38806cbbb1
7 files changed
+406
-4
No files matched your search
@@ -31,6 +31,7 @@ tracing-subscriber.workspace = true
|
||||
windows = { version = "0.54", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_Threading",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_LibraryLoader",
|
||||
|
||||
@@ -22,6 +22,8 @@ use windows::Win32::Networking::WinSock::{
|
||||
WSASetLastError,
|
||||
};
|
||||
use windows::core::GUID;
|
||||
use windows::Win32::System::IO::OVERLAPPED;
|
||||
use windows::Win32::System::Threading::SetEvent;
|
||||
|
||||
use crate::config::{ChainType, Config, ProxyData, ProxyState};
|
||||
use crate::dns::{is_fake_ip, DnsResolver};
|
||||
@@ -32,7 +34,8 @@ use crate::ConfigParser;
|
||||
|
||||
use super::interpose_windows::{
|
||||
init_original_functions, original_connect, original_freeaddrinfo, original_getaddrinfo,
|
||||
original_getaddrinfow, original_gethostbyname, original_getnameinfo, original_wsa_ioctl,
|
||||
original_getaddrinfoexw, original_getaddrinfow, original_gethostbyname, original_getnameinfo,
|
||||
original_dns_query_a, original_dns_query_w, original_wsa_ioctl,
|
||||
};
|
||||
use super::reload::config_reload_interval;
|
||||
|
||||
@@ -461,7 +464,6 @@ const WSAID_CONNECTEX: GUID = GUID::from_u128(0x25a207b9_ddf3_4660_8ee9_76e58c74
|
||||
/// ConnectEx replacement used when applications query extension function pointers via WSAIoctl.
|
||||
///
|
||||
/// This implementation is intentionally synchronous:
|
||||
/// - `lpOverlapped != NULL` is rejected (`WSAEOPNOTSUPP`) to avoid silent bypass.
|
||||
/// - `dwSendDataLength > 0` is rejected (`WSAEOPNOTSUPP`) because initial-send semantics are
|
||||
/// not yet mirrored.
|
||||
#[cfg(windows)]
|
||||
@@ -474,7 +476,7 @@ pub unsafe extern "system" fn hook_connect_ex_impl(
|
||||
bytes_sent: *mut u32,
|
||||
overlapped: *mut c_void,
|
||||
) -> i32 {
|
||||
if !overlapped.is_null() || (!send_buf.is_null() && send_len > 0) {
|
||||
if !send_buf.is_null() && send_len > 0 {
|
||||
WSASetLastError(WSAEOPNOTSUPP.0);
|
||||
return 0;
|
||||
}
|
||||
@@ -487,6 +489,12 @@ pub unsafe extern "system" fn hook_connect_ex_impl(
|
||||
if ret == SOCKET_ERROR {
|
||||
0
|
||||
} else {
|
||||
if !overlapped.is_null() {
|
||||
let ov = &mut *(overlapped as *mut OVERLAPPED);
|
||||
if !ov.hEvent.is_invalid() {
|
||||
let _ = SetEvent(ov.hEvent);
|
||||
}
|
||||
}
|
||||
1
|
||||
}
|
||||
}
|
||||
@@ -759,6 +767,166 @@ pub unsafe extern "system" fn hook_getaddrinfow_impl(
|
||||
0
|
||||
}
|
||||
|
||||
/// Windows GetAddrInfoExW hook implementation.
|
||||
#[cfg(windows)]
|
||||
pub unsafe extern "system" fn hook_getaddrinfoexw_impl(
|
||||
pname: *const u16,
|
||||
pservice: *const u16,
|
||||
namespace: u32,
|
||||
pnspid: *mut c_void,
|
||||
hints: *const c_void,
|
||||
ppresult: *mut *mut c_void,
|
||||
timeout: *mut c_void,
|
||||
overlapped: *mut c_void,
|
||||
completion_routine: *mut c_void,
|
||||
pname_handle: *mut c_void,
|
||||
) -> i32 {
|
||||
let state = match get_hook_state() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return original_getaddrinfoexw(
|
||||
pname,
|
||||
pservice,
|
||||
namespace,
|
||||
pnspid,
|
||||
hints,
|
||||
ppresult,
|
||||
timeout,
|
||||
overlapped,
|
||||
completion_routine,
|
||||
pname_handle,
|
||||
)
|
||||
}
|
||||
};
|
||||
maybe_reload_config(state);
|
||||
let config = state.config.lock().clone();
|
||||
if !config.proxy_dns {
|
||||
return original_getaddrinfoexw(
|
||||
pname,
|
||||
pservice,
|
||||
namespace,
|
||||
pnspid,
|
||||
hints,
|
||||
ppresult,
|
||||
timeout,
|
||||
overlapped,
|
||||
completion_routine,
|
||||
pname_handle,
|
||||
);
|
||||
}
|
||||
let dns_resolver = DnsResolver::new(config.proxy_dns, config.remote_dns_subnet);
|
||||
|
||||
if pname.is_null() {
|
||||
return WSAEINVAL.0;
|
||||
}
|
||||
|
||||
let hostname = match parse_wide_string(pname) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return WSAEINVAL.0,
|
||||
};
|
||||
|
||||
if hostname.parse::<IpAddr>().is_ok() || crate::dns::lookup_in_hosts(&hostname).is_some() {
|
||||
return original_getaddrinfoexw(
|
||||
pname,
|
||||
pservice,
|
||||
namespace,
|
||||
pnspid,
|
||||
hints,
|
||||
ppresult,
|
||||
timeout,
|
||||
overlapped,
|
||||
completion_routine,
|
||||
pname_handle,
|
||||
);
|
||||
}
|
||||
|
||||
let fake_ip = match dns_resolver.resolve(&hostname) {
|
||||
Ok(ip) => ip,
|
||||
Err(_) => return WSAHOST_NOT_FOUND.0,
|
||||
};
|
||||
|
||||
let fake_wide: Vec<u16> = fake_ip
|
||||
.to_string()
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
|
||||
original_getaddrinfoexw(
|
||||
fake_wide.as_ptr(),
|
||||
pservice,
|
||||
namespace,
|
||||
pnspid,
|
||||
hints,
|
||||
ppresult,
|
||||
timeout,
|
||||
overlapped,
|
||||
completion_routine,
|
||||
pname_handle,
|
||||
)
|
||||
}
|
||||
|
||||
const DNS_ERROR_RCODE_NAME_ERROR: i32 = 9003;
|
||||
|
||||
/// Windows DnsQuery_A hook implementation.
|
||||
#[cfg(windows)]
|
||||
pub unsafe extern "system" fn hook_dns_query_a_impl(
|
||||
name: *const i8,
|
||||
query_type: u16,
|
||||
options: u32,
|
||||
extra: *mut c_void,
|
||||
result: *mut *mut c_void,
|
||||
reserved: *mut c_void,
|
||||
) -> i32 {
|
||||
let state = match get_hook_state() {
|
||||
Some(s) => s,
|
||||
None => return original_dns_query_a(name, query_type, options, extra, result, reserved),
|
||||
};
|
||||
maybe_reload_config(state);
|
||||
let config = state.config.lock().clone();
|
||||
if !config.proxy_dns || name.is_null() {
|
||||
return original_dns_query_a(name, query_type, options, extra, result, reserved);
|
||||
}
|
||||
|
||||
let hostname = match CStr::from_ptr(name).to_str() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return DNS_ERROR_RCODE_NAME_ERROR,
|
||||
};
|
||||
if hostname.parse::<IpAddr>().is_ok() || crate::dns::lookup_in_hosts(hostname).is_some() {
|
||||
return original_dns_query_a(name, query_type, options, extra, result, reserved);
|
||||
}
|
||||
DNS_ERROR_RCODE_NAME_ERROR
|
||||
}
|
||||
|
||||
/// Windows DnsQuery_W hook implementation.
|
||||
#[cfg(windows)]
|
||||
pub unsafe extern "system" fn hook_dns_query_w_impl(
|
||||
name: *const u16,
|
||||
query_type: u16,
|
||||
options: u32,
|
||||
extra: *mut c_void,
|
||||
result: *mut *mut c_void,
|
||||
reserved: *mut c_void,
|
||||
) -> i32 {
|
||||
let state = match get_hook_state() {
|
||||
Some(s) => s,
|
||||
None => return original_dns_query_w(name, query_type, options, extra, result, reserved),
|
||||
};
|
||||
maybe_reload_config(state);
|
||||
let config = state.config.lock().clone();
|
||||
if !config.proxy_dns || name.is_null() {
|
||||
return original_dns_query_w(name, query_type, options, extra, result, reserved);
|
||||
}
|
||||
|
||||
let hostname = match parse_wide_string(name) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return DNS_ERROR_RCODE_NAME_ERROR,
|
||||
};
|
||||
if hostname.parse::<IpAddr>().is_ok() || crate::dns::lookup_in_hosts(&hostname).is_some() {
|
||||
return original_dns_query_w(name, query_type, options, extra, result, reserved);
|
||||
}
|
||||
DNS_ERROR_RCODE_NAME_ERROR
|
||||
}
|
||||
|
||||
/// Windows freeaddrinfo hook implementation.
|
||||
#[cfg(windows)]
|
||||
pub unsafe extern "system" fn hook_freeaddrinfo_impl(pres: *mut c_void) {
|
||||
|
||||
@@ -15,7 +15,8 @@ use crate::error::{Error, Result};
|
||||
|
||||
use super::hooks_windows::{
|
||||
hook_connect_impl, hook_freeaddrinfo_impl, hook_getaddrinfo_impl, hook_getaddrinfow_impl,
|
||||
hook_gethostbyname_impl, hook_getnameinfo_impl, hook_wsa_connect_impl, hook_wsa_ioctl_impl,
|
||||
hook_getaddrinfoexw_impl, hook_gethostbyname_impl, hook_getnameinfo_impl,
|
||||
hook_dns_query_a_impl, hook_dns_query_w_impl, hook_wsa_connect_impl, hook_wsa_ioctl_impl,
|
||||
};
|
||||
|
||||
type ConnectFn = unsafe extern "system" fn(usize, *const c_void, i32) -> i32;
|
||||
@@ -32,6 +33,18 @@ type GetAddrInfoFn =
|
||||
unsafe extern "system" fn(*const i8, *const i8, *const c_void, *mut *mut c_void) -> i32;
|
||||
type GetAddrInfoWFn =
|
||||
unsafe extern "system" fn(*const u16, *const u16, *const c_void, *mut *mut c_void) -> i32;
|
||||
type GetAddrInfoExWFn = unsafe extern "system" fn(
|
||||
*const u16,
|
||||
*const u16,
|
||||
u32,
|
||||
*mut c_void,
|
||||
*const c_void,
|
||||
*mut *mut c_void,
|
||||
*mut c_void,
|
||||
*mut c_void,
|
||||
*mut c_void,
|
||||
*mut c_void,
|
||||
) -> i32;
|
||||
type FreeAddrInfoFn = unsafe extern "system" fn(*mut c_void);
|
||||
type GetHostByNameFn = unsafe extern "system" fn(*const i8) -> *mut c_void;
|
||||
type GetNameInfoFn =
|
||||
@@ -47,15 +60,34 @@ type WsaIoctlFn = unsafe extern "system" fn(
|
||||
*mut c_void,
|
||||
*mut c_void,
|
||||
) -> i32;
|
||||
type DnsQueryAFn = unsafe extern "system" fn(
|
||||
*const i8,
|
||||
u16,
|
||||
u32,
|
||||
*mut c_void,
|
||||
*mut *mut c_void,
|
||||
*mut c_void,
|
||||
) -> i32;
|
||||
type DnsQueryWFn = unsafe extern "system" fn(
|
||||
*const u16,
|
||||
u16,
|
||||
u32,
|
||||
*mut c_void,
|
||||
*mut *mut c_void,
|
||||
*mut c_void,
|
||||
) -> i32;
|
||||
|
||||
static ORIGINAL_CONNECT: OnceLock<ConnectFn> = OnceLock::new();
|
||||
static ORIGINAL_WSA_CONNECT: OnceLock<WsaConnectFn> = OnceLock::new();
|
||||
static ORIGINAL_GETADDRINFO: OnceLock<GetAddrInfoFn> = OnceLock::new();
|
||||
static ORIGINAL_GETADDRINFOW: OnceLock<GetAddrInfoWFn> = OnceLock::new();
|
||||
static ORIGINAL_GETADDRINFOEXW: OnceLock<GetAddrInfoExWFn> = OnceLock::new();
|
||||
static ORIGINAL_FREEADDRINFO: OnceLock<FreeAddrInfoFn> = OnceLock::new();
|
||||
static ORIGINAL_GETHOSTBYNAME: OnceLock<GetHostByNameFn> = OnceLock::new();
|
||||
static ORIGINAL_GETNAMEINFO: OnceLock<GetNameInfoFn> = OnceLock::new();
|
||||
static ORIGINAL_WSA_IOCTL: OnceLock<WsaIoctlFn> = OnceLock::new();
|
||||
static ORIGINAL_DNS_QUERY_A: OnceLock<DnsQueryAFn> = OnceLock::new();
|
||||
static ORIGINAL_DNS_QUERY_W: OnceLock<DnsQueryWFn> = OnceLock::new();
|
||||
|
||||
static HOOKS_READY: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
@@ -69,6 +101,16 @@ unsafe fn install_api_hook<F>(proc_name: &str, detour: *mut c_void) -> Result<F>
|
||||
Ok(std::mem::transmute_copy::<*mut c_void, F>(&trampoline))
|
||||
}
|
||||
|
||||
unsafe fn install_api_hook_from_module<F>(
|
||||
module: &str,
|
||||
proc_name: &str,
|
||||
detour: *mut c_void,
|
||||
) -> Result<F> {
|
||||
let trampoline = MinHook::create_hook_api(module, proc_name, detour)
|
||||
.map_err(|s| mh_to_error("create_hook_api", s))?;
|
||||
Ok(std::mem::transmute_copy::<*mut c_void, F>(&trampoline))
|
||||
}
|
||||
|
||||
/// Store for original function pointers.
|
||||
pub struct OriginalFunctions;
|
||||
|
||||
@@ -114,6 +156,32 @@ impl OriginalFunctions {
|
||||
let _ = ORIGINAL_FREEADDRINFO.set(freeaddrinfo_fn);
|
||||
let _ = ORIGINAL_GETHOSTBYNAME.set(gethostbyname_fn);
|
||||
let _ = ORIGINAL_GETNAMEINFO.set(getnameinfo_fn);
|
||||
if let Ok(getaddrinfoexw_fn) = install_api_hook(
|
||||
"GetAddrInfoExW",
|
||||
hook_getaddrinfoexw_impl as *const () as *mut c_void,
|
||||
) {
|
||||
let _ = ORIGINAL_GETADDRINFOEXW.set(getaddrinfoexw_fn);
|
||||
} else {
|
||||
debug!("GetAddrInfoExW hook not installed (symbol unavailable)");
|
||||
}
|
||||
if let Ok(dns_query_a_fn) = install_api_hook_from_module(
|
||||
"dnsapi.dll",
|
||||
"DnsQuery_A",
|
||||
hook_dns_query_a_impl as *const () as *mut c_void,
|
||||
) {
|
||||
let _ = ORIGINAL_DNS_QUERY_A.set(dns_query_a_fn);
|
||||
} else {
|
||||
debug!("DnsQuery_A hook not installed (dnsapi.dll unavailable)");
|
||||
}
|
||||
if let Ok(dns_query_w_fn) = install_api_hook_from_module(
|
||||
"dnsapi.dll",
|
||||
"DnsQuery_W",
|
||||
hook_dns_query_w_impl as *const () as *mut c_void,
|
||||
) {
|
||||
let _ = ORIGINAL_DNS_QUERY_W.set(dns_query_w_fn);
|
||||
} else {
|
||||
debug!("DnsQuery_W hook not installed (dnsapi.dll unavailable)");
|
||||
}
|
||||
|
||||
MinHook::enable_all_hooks().map_err(|s| mh_to_error("enable_all_hooks", s))?;
|
||||
}
|
||||
@@ -237,6 +305,38 @@ pub unsafe fn original_getaddrinfow(
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the original GetAddrInfoExW function.
|
||||
pub unsafe fn original_getaddrinfoexw(
|
||||
node: *const u16,
|
||||
service: *const u16,
|
||||
namespace: u32,
|
||||
nspid: *mut c_void,
|
||||
hints: *const c_void,
|
||||
res: *mut *mut c_void,
|
||||
timeout: *mut c_void,
|
||||
overlapped: *mut c_void,
|
||||
completion_routine: *mut c_void,
|
||||
name_handle: *mut c_void,
|
||||
) -> i32 {
|
||||
if let Some(f) = ORIGINAL_GETADDRINFOEXW.get() {
|
||||
f(
|
||||
node,
|
||||
service,
|
||||
namespace,
|
||||
nspid,
|
||||
hints,
|
||||
res,
|
||||
timeout,
|
||||
overlapped,
|
||||
completion_routine,
|
||||
name_handle,
|
||||
)
|
||||
} else {
|
||||
WSASetLastError(WSAHOST_NOT_FOUND.0);
|
||||
WSAHOST_NOT_FOUND.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the original freeaddrinfo function.
|
||||
pub unsafe fn original_freeaddrinfo(res: *mut c_void) {
|
||||
if let Some(f) = ORIGINAL_FREEADDRINFO.get() {
|
||||
@@ -270,6 +370,38 @@ pub unsafe fn original_getnameinfo(
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the original DnsQuery_A function.
|
||||
pub unsafe fn original_dns_query_a(
|
||||
name: *const i8,
|
||||
query_type: u16,
|
||||
options: u32,
|
||||
extra: *mut c_void,
|
||||
result: *mut *mut c_void,
|
||||
reserved: *mut c_void,
|
||||
) -> i32 {
|
||||
if let Some(f) = ORIGINAL_DNS_QUERY_A.get() {
|
||||
f(name, query_type, options, extra, result, reserved)
|
||||
} else {
|
||||
9003
|
||||
}
|
||||
}
|
||||
|
||||
/// Call the original DnsQuery_W function.
|
||||
pub unsafe fn original_dns_query_w(
|
||||
name: *const u16,
|
||||
query_type: u16,
|
||||
options: u32,
|
||||
extra: *mut c_void,
|
||||
result: *mut *mut c_void,
|
||||
reserved: *mut c_void,
|
||||
) -> i32 {
|
||||
if let Some(f) = ORIGINAL_DNS_QUERY_W.get() {
|
||||
f(name, query_type, options, extra, result, reserved)
|
||||
} else {
|
||||
9003
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in new issue
Block a user