From 1c94b830149228152421b652a3865631df2afa6e Mon Sep 17 00:00:00 2001 From: tianrking Date: Fri, 20 Mar 2026 18:58:38 +0800 Subject: [PATCH] feat(tree): add cross-platform --tree mode with windows descendant injection --- README.md | 4 + README.zh-CN.md | 4 + proxychains-bin/src/main.rs | 34 +++++- proxychains-injector/Cargo.toml | 1 + proxychains-injector/src/injector.rs | 163 +++++++++++++++++++++++++++ 5 files changed, 203 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f9b48bd..425866e 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,9 @@ Recommendation: - Proxy protocols: `socks5`, `socks4`/`socks4a`, `http`, `raw` - Chain modes: `dynamic_chain`, `strict_chain`, `random_chain`, `load_balance`, `failover` - Proxy groups: `[ProxyList:]` + `--group` +- Process tree mode: `--tree` + - Windows: root process + child/grandchild process injection + - Linux/macOS: inherited preload environment for child processes - Discovery/validation: - `--list-groups` - `--check` @@ -99,6 +102,7 @@ proxychains4 -f ./proxychains.conf --group jp --check proxychains4 -f ./proxychains.conf --group jp --probe proxychains4 -f ./proxychains.conf --probe --probe-timeout-ms 1200 proxychains4 -f ./proxychains.conf --probe --probe-json +proxychains4 -f ./proxychains.conf --tree curl https://ifconfig.me ``` ## Example Config diff --git a/README.zh-CN.md b/README.zh-CN.md index fbc6340..0cbeb75 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -37,6 +37,9 @@ - 代理协议:`socks5`、`socks4`/`socks4a`、`http`、`raw` - 链路模式:`dynamic_chain`、`strict_chain`、`random_chain`、`load_balance`、`failover` - 代理分组:`[ProxyList:]` + `--group` +- 进程树模式:`--tree` + - Windows:根进程 + 子/孙进程注入 + - Linux/macOS:通过 preload 环境变量继承覆盖子进程 - 配置发现/校验: - `--list-groups` - `--check` @@ -99,6 +102,7 @@ proxychains4 -f ./proxychains.conf --group jp --check proxychains4 -f ./proxychains.conf --group jp --probe proxychains4 -f ./proxychains.conf --probe --probe-timeout-ms 1200 proxychains4 -f ./proxychains.conf --probe --probe-json +proxychains4 -f ./proxychains.conf --tree curl https://ifconfig.me ``` ## 示例配置 diff --git a/proxychains-bin/src/main.rs b/proxychains-bin/src/main.rs index c31791c..93fb65d 100644 --- a/proxychains-bin/src/main.rs +++ b/proxychains-bin/src/main.rs @@ -70,6 +70,10 @@ struct Args { #[arg(long)] probe_fail_only: bool, + /// Enable process-tree mode (inject/proxy child and grandchild processes) + #[arg(long)] + tree: bool, + /// The command to run #[arg( required_unless_present_any = ["list_groups", "check", "probe"], @@ -496,6 +500,10 @@ fn execute_command(args: &Args, config: &Config) -> Result { // Set proxychains environment variables set_proxychains_env(config, args); + if args.tree { + debug!("tree mode enabled (unix): relying on environment inheritance"); + } + // Build command arguments let program = &args.command[0]; let c_args: Vec = args @@ -618,9 +626,15 @@ fn execute_command(args: &Args, config: &Config) -> Result { debug!("Spawning and injecting: {:?}", process_info); - let exit_code = injector - .spawn_inject_wait(&process_info) - .map_err(|e| format!("Failed to spawn/inject/wait: {}", e))?; + let exit_code = if args.tree { + injector + .spawn_inject_tree_wait(&process_info) + .map_err(|e| format!("Failed to spawn/inject tree/wait: {}", e))? + } else { + injector + .spawn_inject_wait(&process_info) + .map_err(|e| format!("Failed to spawn/inject/wait: {}", e))? + }; info!("Process exited with code: {}", exit_code); @@ -704,4 +718,18 @@ mod tests { let args = args.unwrap(); assert!(args.probe_fail_only); } + + #[test] + fn test_args_tree_without_command_fails() { + let args = Args::try_parse_from(["proxychains4", "--tree"]); + assert!(args.is_err()); + } + + #[test] + fn test_args_tree_with_command() { + let args = Args::try_parse_from(["proxychains4", "--tree", "curl", "https://example.com"]); + assert!(args.is_ok()); + let args = args.unwrap(); + assert!(args.tree); + } } diff --git a/proxychains-injector/Cargo.toml b/proxychains-injector/Cargo.toml index 2dd96d1..159002a 100644 --- a/proxychains-injector/Cargo.toml +++ b/proxychains-injector/Cargo.toml @@ -18,6 +18,7 @@ tracing.workspace = true [target.'cfg(windows)'.dependencies] windows = { version = "0.54", features = [ "Win32_Foundation", + "Win32_System_Diagnostics_ToolHelp", "Win32_System_Threading", "Win32_System_Memory", "Win32_System_LibraryLoader", diff --git a/proxychains-injector/src/injector.rs b/proxychains-injector/src/injector.rs index 52555d7..f78d956 100644 --- a/proxychains-injector/src/injector.rs +++ b/proxychains-injector/src/injector.rs @@ -221,6 +221,121 @@ impl ProxychainsInjector { } } + /// Create a suspended process, inject DLL, resume, and keep injecting descendants + /// (child/grandchild processes) until the root process exits. + #[cfg(windows)] + pub fn spawn_inject_tree_wait(&self, process_info: &ProcessInfo) -> Result { + use std::collections::HashSet; + use std::os::windows::ffi::OsStrExt; + use windows::core::{PCWSTR, PWSTR}; + use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_OBJECT_0}; + use windows::Win32::System::Threading::{ + CreateProcessW, GetExitCodeProcess, ResumeThread, WaitForSingleObject, + CREATE_SUSPENDED, 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("e_arg(arg)); + } + + debug!("Spawning suspended process (tree mode): {}", command_line); + + let mut cmd_wide: Vec = 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::() 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 root_pid = process_info_win.dwProcessId; + debug!("Suspended root process created with PID: {}", root_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 root process {}", root_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 mut injected_pids: HashSet = HashSet::new(); + injected_pids.insert(root_pid); + + loop { + let descendants = enumerate_descendant_pids(root_pid)?; + for pid in descendants { + if injected_pids.contains(&pid) { + continue; + } + match self.inject_by_pid(pid) { + Ok(_) => { + injected_pids.insert(pid); + } + Err(e) => { + debug!("Tree-mode injection skipped for PID {}: {}", pid, e); + } + } + } + + let wait_ret = WaitForSingleObject(process_handle, 150); + if wait_ret == WAIT_OBJECT_0 { + break; + } + } + + 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<()> { @@ -322,6 +437,54 @@ impl ProxychainsInjector { } } +#[cfg(windows)] +fn enumerate_descendant_pids(root_pid: u32) -> Result> { + use std::collections::{HashMap, HashSet, VecDeque}; + use windows::Win32::Foundation::CloseHandle; + use windows::Win32::System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, + TH32CS_SNAPPROCESS, + }; + + unsafe { + let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) + .map_err(|e| InjectorError::WindowsApi(format!("CreateToolhelp32Snapshot failed: {:?}", e)))?; + + let mut entry = PROCESSENTRY32W::default(); + entry.dwSize = std::mem::size_of::() as u32; + + let mut parent_by_pid: HashMap = HashMap::new(); + if Process32FirstW(snapshot, &mut entry).is_ok() { + loop { + parent_by_pid.insert(entry.th32ProcessID, entry.th32ParentProcessID); + if Process32NextW(snapshot, &mut entry).is_err() { + break; + } + } + } + + let _ = CloseHandle(snapshot); + + let mut seen: HashSet = HashSet::new(); + let mut queue: VecDeque = VecDeque::new(); + let mut descendants = Vec::new(); + + queue.push_back(root_pid); + seen.insert(root_pid); + + while let Some(parent) = queue.pop_front() { + for (&pid, &ppid) in parent_by_pid.iter() { + if ppid == parent && seen.insert(pid) { + descendants.push(pid); + queue.push_back(pid); + } + } + } + + Ok(descendants) + } +} + /// Find the proxychains DLL/library path pub fn find_library_path() -> Result { // Try to find the library relative to the executable