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..6cd04c988 100644 --- a/implants/lib/eldritch/src/sys/shell_impl.rs +++ b/implants/lib/eldritch/src/sys/shell_impl.rs @@ -5,7 +5,16 @@ 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::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, +}; use super::CommandOutput; @@ -21,31 +30,73 @@ pub fn shell(starlark_heap: &Heap, cmd: String) -> Result { Ok(dict_res) } +#[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 isize); + } + argv +} + 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 +136,50 @@ 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_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); + 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""#, + ))?; + assert!(res.stdout.contains("MsMpEng.exe")); } Ok(()) }