From 800cc9ca6698f104b8e47ea1c898ed6a5b9625cf Mon Sep 17 00:00:00 2001 From: Hulto <7121375+hulto@users.noreply.github.com> Date: Tue, 18 Mar 2025 04:31:44 +0000 Subject: [PATCH 1/3] fix shell for windows --- implants/lib/eldritch/Cargo.toml | 1 + implants/lib/eldritch/src/sys/shell_impl.rs | 142 ++++++++++++++++---- 2 files changed, 115 insertions(+), 28 deletions(-) diff --git a/implants/lib/eldritch/Cargo.toml b/implants/lib/eldritch/Cargo.toml index cfe87aa1b..914222c07 100644 --- a/implants/lib/eldritch/Cargo.toml +++ b/implants/lib/eldritch/Cargo.toml @@ -65,6 +65,7 @@ windows-sys = { workspace = true, features = [ "Win32_Security", "Win32_System_SystemInformation", "Win32_System_SystemServices", + "Win32_UI_Shell" ] } whoami = { workspace = true } network-interface = { workspace = true } diff --git a/implants/lib/eldritch/src/sys/shell_impl.rs b/implants/lib/eldritch/src/sys/shell_impl.rs index b4f1f760f..ef1c07510 100644 --- a/implants/lib/eldritch/src/sys/shell_impl.rs +++ b/implants/lib/eldritch/src/sys/shell_impl.rs @@ -5,10 +5,53 @@ use starlark::const_frozen_string; use starlark::values::dict::Dict; use starlark::values::Heap; use std::process::Command; -use std::str; + +#[cfg(target_os = "windows")] +use { + std::ffi::{OsStr, OsString}, + std::os::windows::ffi::{OsStrExt, OsStringExt}, + std::str, + windows_sys::Win32::UI::Shell::CommandLineToArgvW, +}; use super::CommandOutput; +#[cfg(target_os = "windows")] +pub fn to_wstring(str: impl AsRef) -> Vec { + OsStr::new(str.as_ref()) + .encode_wide() + .chain(once(0)) + .collect() +} + +#[cfg(target_os = "windows")] +pub unsafe fn os_string_from_wide_ptr(ptr: *const u16) -> OsString { + let mut len = 0; + while *ptr.offset(len) != 0 { + len += 1; + } + + // Push it onto the list. + let buf = slice::from_raw_parts(ptr, len as usize); + OsStringExt::from_wide(buf) +} + +#[cfg(target_os = "windows")] +pub fn to_argv(command_line: &str) -> Vec { + let mut argv: Vec = Vec::new(); + let mut argc = 0; + unsafe { + let args = CommandLineToArgvW(to_wstring(command_line).as_ptr(), &mut argc); + + for i in 0..argc { + argv.push(os_string_from_wide_ptr(*args.offset(i as isize))); + } + + // LocalFree(args as HLOCAL); + } + argv +} + pub fn shell(starlark_heap: &Heap, cmd: String) -> Result { let cmd_res = handle_shell(cmd)?; @@ -22,30 +65,36 @@ pub fn shell(starlark_heap: &Heap, cmd: String) -> Result { } fn handle_shell(cmd: String) -> Result { - let command_string: &str; - let command_args: Vec<&str>; - - if cfg!(target_os = "macos") || cfg!(target_os = "linux") { - command_string = "bash"; - command_args = ["-c", cmd.as_str()].to_vec(); - } else if cfg!(target_os = "windows") { - command_string = "cmd"; - command_args = ["/c", cmd.as_str()].to_vec(); - } else { - // linux and such - command_string = "sh"; - command_args = ["-c", cmd.as_str()].to_vec(); + #[cfg(not(target_os = "windows"))] + { + let command_string = "sh"; + let command_args = ["-c", cmd.as_str()].to_vec(); + let tmp_res = Command::new(command_string).args(command_args).output()?; + Ok(CommandOutput { + stdout: String::from_utf8_lossy(&tmp_res.stdout).to_string(), + stderr: String::from_utf8_lossy(&tmp_res.stderr).to_string(), + status: tmp_res + .status + .code() + .context("Failed to retrieve status code")?, + }) } - let tmp_res = Command::new(command_string).args(command_args).output()?; - Ok(CommandOutput { - stdout: String::from_utf8_lossy(&tmp_res.stdout).to_string(), - stderr: String::from_utf8_lossy(&tmp_res.stderr).to_string(), - status: tmp_res - .status - .code() - .context("Failed to retrieve status code")?, - }) + #[cfg(target_os = "windows")] + { + let command_string = "cmd"; + let all_together = format!("/c {}", cmd); + let new_arg = to_argv(all_together.as_str()); + let tmp_res = Command::new(command_string).args(new_arg).output()?; + Ok(CommandOutput { + stdout: String::from_utf8_lossy(&tmp_res.stdout).to_string(), + stderr: String::from_utf8_lossy(&tmp_res.stderr).to_string(), + status: tmp_res + .status + .code() + .context("Failed to retrieve status code")?, + }) + } } #[cfg(test)] @@ -85,14 +134,51 @@ mod tests { } Ok(()) } + + #[cfg(target_os = "windows")] #[test] fn test_sys_shell_complex_windows() -> anyhow::Result<()> { + let res = + handle_shell(String::from("wmic useraccount get name | findstr /i admin"))?.stdout; + assert!(res.contains("runner") || res.contains("Administrator") || res.contains("user")); + Ok(()) + } + + #[cfg(target_os = "windows")] + #[test] + fn test_argv_parse() -> anyhow::Result<()> { + let cmd = r#"cmd.exe /c cmd.exe /c cmd.exe /c dir "C:\Program Files\Windows Defender""#; + let res = to_argv(cmd); + assert_eq!(res.len(), 8); + assert_eq!(res[0], OsString::from("cmd.exe")); + assert_eq!(res[7], OsString::from(r"C:\Program Files\Windows Defender")); + let cmd = r#"cmd.exe /c reg query "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon" /v LegalNoticeCaption"#; + let res = to_argv(cmd); + assert_eq!(res.len(), 7); + assert_eq!(res[0], OsString::from("cmd.exe")); + assert_eq!( + res[4], + OsString::from(r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon") + ); + let cmd = r#"/c reg query "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon" /v LegalNoticeCaption"#; + let res = to_argv(cmd); + assert_eq!(res.len(), 6); + assert_eq!(res[0], OsString::from("/c")); + assert_eq!( + res[3], + OsString::from(r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon") + ); + Ok(()) + } + + #[test] + fn test_sys_shell_spaces_windows() -> anyhow::Result<()> { if cfg!(target_os = "windows") { - let res = - handle_shell(String::from("wmic useraccount get name | findstr /i admin"))?.stdout; - assert!( - res.contains("runner") || res.contains("Administrator") || res.contains("user") - ); + let res = handle_shell(String::from( + r#"cmd.exe /c dir "C:\Program Files\Windows Defender""#, + ))? + .stdout; + assert!(res.contains("MsMpEng.exe")); } Ok(()) } From 98bdb1016307ddfaefdb63435eabb7a0f097b67b Mon Sep 17 00:00:00 2001 From: Hulto <7121375+hulto@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:14:11 -0400 Subject: [PATCH 2/3] Update shell_impl.rs --- implants/lib/eldritch/src/sys/shell_impl.rs | 59 ++++++++++----------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/implants/lib/eldritch/src/sys/shell_impl.rs b/implants/lib/eldritch/src/sys/shell_impl.rs index ef1c07510..240e4775e 100644 --- a/implants/lib/eldritch/src/sys/shell_impl.rs +++ b/implants/lib/eldritch/src/sys/shell_impl.rs @@ -5,17 +5,31 @@ use starlark::const_frozen_string; use starlark::values::dict::Dict; use starlark::values::Heap; use std::process::Command; - #[cfg(target_os = "windows")] use { + std::{slice, str}, + std::iter::once, + std::path::Path, std::ffi::{OsStr, OsString}, std::os::windows::ffi::{OsStrExt, OsStringExt}, - std::str, - windows_sys::Win32::UI::Shell::CommandLineToArgvW, + windows_sys::Win32::System::Memory::LocalFree, + windows_sys::Win32::UI::Shell::CommandLineToArgvW }; use super::CommandOutput; +pub fn shell(starlark_heap: &Heap, cmd: String) -> Result { + let cmd_res = handle_shell(cmd)?; + + let res = SmallMap::new(); + let mut dict_res = Dict::new(res); + insert_dict_kv!(dict_res, starlark_heap, "stdout", cmd_res.stdout, String); + insert_dict_kv!(dict_res, starlark_heap, "stderr", cmd_res.stderr, String); + insert_dict_kv!(dict_res, starlark_heap, "status", cmd_res.status, i32); + + Ok(dict_res) +} + #[cfg(target_os = "windows")] pub fn to_wstring(str: impl AsRef) -> Vec { OsStr::new(str.as_ref()) @@ -47,22 +61,11 @@ pub fn to_argv(command_line: &str) -> Vec { argv.push(os_string_from_wide_ptr(*args.offset(i as isize))); } - // LocalFree(args as HLOCAL); + LocalFree(args as isize); } argv } -pub fn shell(starlark_heap: &Heap, cmd: String) -> Result { - let cmd_res = handle_shell(cmd)?; - - let res = SmallMap::new(); - let mut dict_res = Dict::new(res); - insert_dict_kv!(dict_res, starlark_heap, "stdout", cmd_res.stdout, String); - insert_dict_kv!(dict_res, starlark_heap, "stderr", cmd_res.stderr, String); - insert_dict_kv!(dict_res, starlark_heap, "status", cmd_res.status, i32); - - Ok(dict_res) -} fn handle_shell(cmd: String) -> Result { #[cfg(not(target_os = "windows"))] @@ -140,13 +143,15 @@ mod tests { fn test_sys_shell_complex_windows() -> anyhow::Result<()> { let res = handle_shell(String::from("wmic useraccount get name | findstr /i admin"))?.stdout; - assert!(res.contains("runner") || res.contains("Administrator") || res.contains("user")); + assert!( + res.contains("runner") || res.contains("Administrator") || res.contains("user") + ); Ok(()) } #[cfg(target_os = "windows")] #[test] - fn test_argv_parse() -> anyhow::Result<()> { + fn test_sys_shell_argv_parse() -> anyhow::Result<()> { let cmd = r#"cmd.exe /c cmd.exe /c cmd.exe /c dir "C:\Program Files\Windows Defender""#; let res = to_argv(cmd); assert_eq!(res.len(), 8); @@ -156,29 +161,23 @@ mod tests { let res = to_argv(cmd); assert_eq!(res.len(), 7); assert_eq!(res[0], OsString::from("cmd.exe")); - assert_eq!( - res[4], - OsString::from(r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon") - ); + assert_eq!(res[4], OsString::from(r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon")); let cmd = r#"/c reg query "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon" /v LegalNoticeCaption"#; let res = to_argv(cmd); assert_eq!(res.len(), 6); assert_eq!(res[0], OsString::from("/c")); - assert_eq!( - res[3], - OsString::from(r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon") - ); + assert_eq!(res[3], OsString::from(r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon")); Ok(()) } #[test] fn test_sys_shell_spaces_windows() -> anyhow::Result<()> { if cfg!(target_os = "windows") { - let res = handle_shell(String::from( - r#"cmd.exe /c dir "C:\Program Files\Windows Defender""#, - ))? - .stdout; - assert!(res.contains("MsMpEng.exe")); + let res = + handle_shell(String::from(r#"cmd.exe /c dir "C:\Program Files\Windows Defender""#))?; + assert!( + res.stdout.contains("MsMpEng.exe") + ); } Ok(()) } From cd34bd97db7f0e93bc7fd2c579732e1fe1a4b598 Mon Sep 17 00:00:00 2001 From: Hulto <7121375+hulto@users.noreply.github.com> Date: Tue, 18 Mar 2025 22:18:06 +0000 Subject: [PATCH 3/3] format --- implants/lib/eldritch/src/sys/shell_impl.rs | 32 +++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/implants/lib/eldritch/src/sys/shell_impl.rs b/implants/lib/eldritch/src/sys/shell_impl.rs index 240e4775e..6cd04c988 100644 --- a/implants/lib/eldritch/src/sys/shell_impl.rs +++ b/implants/lib/eldritch/src/sys/shell_impl.rs @@ -7,13 +7,13 @@ use starlark::values::Heap; use std::process::Command; #[cfg(target_os = "windows")] use { - std::{slice, str}, - std::iter::once, - std::path::Path, std::ffi::{OsStr, OsString}, + std::iter::once, std::os::windows::ffi::{OsStrExt, OsStringExt}, + std::path::Path, + std::{slice, str}, windows_sys::Win32::System::Memory::LocalFree, - windows_sys::Win32::UI::Shell::CommandLineToArgvW + windows_sys::Win32::UI::Shell::CommandLineToArgvW, }; use super::CommandOutput; @@ -66,7 +66,6 @@ pub fn to_argv(command_line: &str) -> Vec { argv } - fn handle_shell(cmd: String) -> Result { #[cfg(not(target_os = "windows"))] { @@ -143,9 +142,7 @@ mod tests { fn test_sys_shell_complex_windows() -> anyhow::Result<()> { let res = handle_shell(String::from("wmic useraccount get name | findstr /i admin"))?.stdout; - assert!( - res.contains("runner") || res.contains("Administrator") || res.contains("user") - ); + assert!(res.contains("runner") || res.contains("Administrator") || res.contains("user")); Ok(()) } @@ -161,23 +158,28 @@ mod tests { let res = to_argv(cmd); assert_eq!(res.len(), 7); assert_eq!(res[0], OsString::from("cmd.exe")); - assert_eq!(res[4], OsString::from(r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon")); + assert_eq!( + res[4], + OsString::from(r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon") + ); let cmd = r#"/c reg query "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon" /v LegalNoticeCaption"#; let res = to_argv(cmd); assert_eq!(res.len(), 6); assert_eq!(res[0], OsString::from("/c")); - assert_eq!(res[3], OsString::from(r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon")); + assert_eq!( + res[3], + OsString::from(r"HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon") + ); Ok(()) } #[test] fn test_sys_shell_spaces_windows() -> anyhow::Result<()> { if cfg!(target_os = "windows") { - let res = - handle_shell(String::from(r#"cmd.exe /c dir "C:\Program Files\Windows Defender""#))?; - assert!( - res.stdout.contains("MsMpEng.exe") - ); + let res = handle_shell(String::from( + r#"cmd.exe /c dir "C:\Program Files\Windows Defender""#, + ))?; + assert!(res.stdout.contains("MsMpEng.exe")); } Ok(()) }