feat(tree): add cross-platform --tree mode with windows descendant injection
This commit is contained in:
1 parent
f72cf87b3a
commit
1c94b83014
5 files changed
+203
-3
No files matched your search
@@ -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>]` + `--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
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
- 代理协议:`socks5`、`socks4`/`socks4a`、`http`、`raw`
|
||||
- 链路模式:`dynamic_chain`、`strict_chain`、`random_chain`、`load_balance`、`failover`
|
||||
- 代理分组:`[ProxyList:<group>]` + `--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
|
||||
```
|
||||
|
||||
## 示例配置
|
||||
|
||||
@@ -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<i32, String> {
|
||||
// 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<CString> = args
|
||||
@@ -618,9 +626,15 @@ fn execute_command(args: &Args, config: &Config) -> Result<i32, String> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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<i32> {
|
||||
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<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 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<u32> = 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<Vec<u32>> {
|
||||
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::<PROCESSENTRY32W>() as u32;
|
||||
|
||||
let mut parent_by_pid: HashMap<u32, u32> = 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<u32> = HashSet::new();
|
||||
let mut queue: VecDeque<u32> = 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<std::path::PathBuf> {
|
||||
// Try to find the library relative to the executable
|
||||
|
||||
Reference in new issue
Block a user