fix(windows): avoid startup bypass via suspended injection; add probe fail-only

This commit is contained in:
tianrking committed 2026-03-20 16:54:21 +08:00
1 parent 301b69a480
commit c452b0ea42
3 files changed
+114 -15

No files matched your search

+20 -15
View File
@@ -66,6 +66,10 @@ struct Args {
#[arg(long)]
probe_json: bool,
/// In text probe output, print only failed nodes
#[arg(long)]
probe_fail_only: bool,
/// The command to run
#[arg(
required_unless_present_any = ["list_groups", "check", "probe"],
@@ -265,7 +269,7 @@ fn run_probe(config: &Config, args: &Args) -> usize {
serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string())
);
} else {
print_probe_report(&report);
print_probe_report(&report, args.probe_fail_only);
}
failed
}
@@ -345,11 +349,14 @@ fn format_unix_ts_iso8601(ts: u64) -> String {
.unwrap_or_else(|| "1970-01-01T00:00:00Z".to_string())
}
fn print_probe_report(report: &ProbeReport) {
fn print_probe_report(report: &ProbeReport, fail_only: bool) {
println!("Proxy probe:");
println!(" timeout_ms={}", report.timeout_ms);
println!(" group={}", report.selected_group);
for n in &report.nodes {
if fail_only && n.ok {
continue;
}
if n.ok {
println!(
" [{}] OK {:<8} {} {} ms",
@@ -597,19 +604,9 @@ fn execute_command(args: &Args, config: &Config) -> Result<i32, String> {
debug!("Spawning and injecting: {:?}", process_info);
// Spawn the process and inject DLL
let mut child = injector
.spawn_and_inject(&process_info)
.map_err(|e| format!("Failed to spawn and inject: {}", e))?;
// Wait for the process to complete
let status = child
.wait()
.map_err(|e| format!("Failed to wait for process: {}", e))?;
let exit_code = status
.code()
.unwrap_or(1);
let exit_code = injector
.spawn_inject_wait(&process_info)
.map_err(|e| format!("Failed to spawn/inject/wait: {}", e))?;
info!("Process exited with code: {}", exit_code);
@@ -685,4 +682,12 @@ mod tests {
let args = args.unwrap();
assert!(args.probe_json);
}
#[test]
fn test_args_probe_fail_only() {
let args = Args::try_parse_from(["proxychains4", "--probe", "--probe-fail-only"]);
assert!(args.is_ok());
let args = args.unwrap();
assert!(args.probe_fail_only);
}
}
+88
View File
@@ -133,6 +133,94 @@ impl ProxychainsInjector {
Ok(child)
}
/// Create a suspended process, inject DLL, resume, and wait for completion (Windows).
#[cfg(windows)]
pub fn spawn_inject_wait(&self, process_info: &ProcessInfo) -> Result<i32> {
use std::os::windows::ffi::OsStrExt;
use windows::core::{PCWSTR, PWSTR};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::Threading::{
CreateProcessW, GetExitCodeProcess, ResumeThread, WaitForSingleObject,
CREATE_SUSPENDED, INFINITE, PROCESS_INFORMATION, STARTUPINFOW,
};
fn quote_arg(arg: &str) -> String {
if arg.is_empty()
|| arg.contains(' ')
|| arg.contains('\t')
|| arg.contains('"')
|| arg.contains('\\')
{
let escaped = arg.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{}\"", escaped)
} else {
arg.to_string()
}
}
let mut command_line = quote_arg(&process_info.command);
for arg in &process_info.args {
command_line.push(' ');
command_line.push_str(&quote_arg(arg));
}
debug!("Spawning suspended process: {}", command_line);
let mut cmd_wide: Vec<u16> = std::ffi::OsStr::new(&command_line)
.encode_wide()
.chain(std::iter::once(0))
.collect();
let mut startup_info = STARTUPINFOW::default();
startup_info.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
let mut process_info_win = PROCESS_INFORMATION::default();
unsafe {
CreateProcessW(
PCWSTR::null(),
PWSTR(cmd_wide.as_mut_ptr()),
None,
None,
false,
CREATE_SUSPENDED,
None,
PCWSTR::null(),
&startup_info,
&mut process_info_win,
)
.map_err(|e| InjectorError::ProcessCreationFailed(format!("{:?}", e)))?;
let process_handle: HANDLE = process_info_win.hProcess;
let thread_handle: HANDLE = process_info_win.hThread;
let pid = process_info_win.dwProcessId;
debug!("Suspended process created with PID: {}", pid);
if let Err(e) = self.inject_dll(process_handle) {
let _ = CloseHandle(thread_handle);
let _ = CloseHandle(process_handle);
return Err(e);
}
info!("Successfully injected DLL into suspended process {}", pid);
let resume_ret = ResumeThread(thread_handle);
if resume_ret == u32::MAX {
let _ = CloseHandle(thread_handle);
let _ = CloseHandle(process_handle);
return Err(InjectorError::WindowsApi("ResumeThread failed".into()));
}
let _ = WaitForSingleObject(process_handle, INFINITE);
let mut exit_code = 1u32;
GetExitCodeProcess(process_handle, &mut exit_code)
.map_err(|e| InjectorError::WindowsApi(format!("GetExitCodeProcess failed: {:?}", e)))?;
let _ = CloseHandle(thread_handle);
let _ = CloseHandle(process_handle);
Ok(exit_code as i32)
}
}
/// Internal DLL injection method (Windows)
#[cfg(windows)]
unsafe fn inject_dll(&self, process: windows::Win32::Foundation::HANDLE) -> Result<()> {
+6
View File
@@ -331,6 +331,7 @@ pub unsafe extern "system" fn hook_connect_impl(
None => return original_connect(sock, addr, len),
};
let target_port = get_port_from_sockaddr(addr);
debug!("hook_connect_impl intercepted target {}:{}", target_ip, target_port);
if config.should_bypass_ip(&target_ip) {
return original_connect(sock, addr, len);
@@ -414,6 +415,11 @@ pub unsafe extern "system" fn hook_wsa_connect_impl(
_sqos: *const c_void,
_gqos: *const c_void,
) -> i32 {
if !name.is_null() && namelen > 0 {
if let (Some(ip), port) = (get_ipaddr_from_sockaddr(name), get_port_from_sockaddr(name)) {
debug!("hook_wsa_connect_impl intercepted target {}:{}", ip, port);
}
}
hook_connect_impl(sock, name, namelen)
}