diff --git a/docs/_docs/dev-guide/imix.md b/docs/_docs/dev-guide/imix.md index ff4f38206..65fed4035 100644 --- a/docs/_docs/dev-guide/imix.md +++ b/docs/_docs/dev-guide/imix.md @@ -25,6 +25,8 @@ Out of the box realm comes with two options `File` and `Env` to determine what h `Env` will read from the agent environment variables looking for `IMIX_HOST_ID` if it's set it will use the UUID4 string set there. +There is a third option available on Windows systems to store the UUID value inside a registry key. Follow the steps below to update `lib.rs` to include `Registry` as a default before `File` to enable it. On hosts that are not Windows, imix will simply skip `Registry`. + If no selectors succeed a random UUID4 ID will be generated and used for the bot. This should be avoided. ## Develop A Host Uniqueness Selector diff --git a/docs/_docs/user-guide/imix.md b/docs/_docs/user-guide/imix.md index 281a1e20d..fe9b31f8a 100644 --- a/docs/_docs/user-guide/imix.md +++ b/docs/_docs/user-guide/imix.md @@ -64,11 +64,13 @@ Imix uses the `host_unique` library under `implants/lib/host_unique` to determin We recommend that you use the `File` for the most reliability: - Exists across reboots -- Garunteed to be unique per host (because the bot creates it) +- Guaranteed to be unique per host (because the bot creates it) - Can be used by multiple instances of the beacon on the same host. If you cannot use the `File` selector we highly recommend manually setting the `Env` selector with the environment variable `IMIX_HOST_ID`. This will override the `File` one avoiding writes to disk but must be managed by the operators. +For Windows hosts, a `Registry` selector is available, but must be enabled before compilation. See the [imix dev guide](/dev-guide/imix#host-selector) on how to enable it. + If all uniqueness selectors fail imix will randomly generate a UUID to avoid crashing. This isn't ideal as in the UI each new beacon will appear as thought it were on a new host. diff --git a/implants/lib/host_unique/Cargo.toml b/implants/lib/host_unique/Cargo.toml index 08793b50e..a6d808192 100644 --- a/implants/lib/host_unique/Cargo.toml +++ b/implants/lib/host_unique/Cargo.toml @@ -9,6 +9,9 @@ edition = "2021" uuid = { workspace = true, features = ["v4", "fast-rng"] } log = { workspace = true } +[target.'cfg(target_os = "windows")'.dependencies] +winreg = { workspace = true } + [dev-dependencies] pretty_env_logger = "0.5.0" tempfile = { workspace = true } diff --git a/implants/lib/host_unique/src/lib.rs b/implants/lib/host_unique/src/lib.rs index b39060751..724434b34 100644 --- a/implants/lib/host_unique/src/lib.rs +++ b/implants/lib/host_unique/src/lib.rs @@ -4,6 +4,8 @@ mod env; pub use env::Env; mod file; pub use file::File; +mod registry; +pub use registry::Registry; pub trait HostIDSelector { fn get_name(&self) -> String; diff --git a/implants/lib/host_unique/src/registry.rs b/implants/lib/host_unique/src/registry.rs new file mode 100644 index 000000000..11bdc873e --- /dev/null +++ b/implants/lib/host_unique/src/registry.rs @@ -0,0 +1,126 @@ +use crate::HostIDSelector; +use uuid::Uuid; + +#[derive(Default)] +pub struct Registry { + subkey: Option, + value_name: Option, +} + +impl Registry { + /* + * Returns a predefined path to the host id registry key. + */ + pub fn with_subkey(mut self, path: impl Into) -> Self { + self.subkey = Some(path.into()); + self + } + + pub fn with_value_name(mut self, name: impl Into) -> Self { + self.value_name = Some(name.into()); + self + } + + #[cfg(target_os = "windows")] + fn key_path(&self) -> &str { + self.subkey.as_deref().unwrap_or("SOFTWARE\\Imix") + } + + #[cfg(target_os = "windows")] + fn val_name(&self) -> &str { + self.value_name.as_deref().unwrap_or("system-id") + } +} + +impl HostIDSelector for Registry { + fn get_name(&self) -> String { + "registry".into() + } + + fn get_host_id(&self) -> Option { + // On non‑Windows targets this selector is unavailable + #[cfg(not(target_os = "windows"))] + { + return None; + } + + #[cfg(target_os = "windows")] + { + use std::io::ErrorKind; + use winreg::{enums::HKEY_LOCAL_MACHINE, RegKey}; + + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + // Try to open the key for reading + let key = match hklm.open_subkey(self.key_path()) { + Ok(k) => k, + Err(_err) if _err.kind() == ErrorKind::NotFound => { + // If it doesn't exist, create it + match hklm.create_subkey(self.key_path()) { + Ok((k, _disp)) => k, + Err(_err) => { + #[cfg(debug_assertions)] + log::debug!("failed to create registry key: {:?}", _err); + return None; + } + } + } + Err(_err) => { + #[cfg(debug_assertions)] + log::debug!("failed to open registry key: {:?}", _err); + return None; + } + }; + + // Try to read existing value + if let Ok(stored) = key.get_value::(self.val_name()) { + if let Ok(uuid) = Uuid::parse_str(&stored) { + return Some(uuid); + } else { + #[cfg(debug_assertions)] + log::debug!("invalid UUID in registry: {:?}", stored); + } + } + + // Otherwise generate a new one and persist it + let new_uuid = Uuid::new_v4(); + let s = new_uuid.to_string(); + if let Err(_err) = key.set_value(self.val_name(), &s) { + #[cfg(debug_assertions)] + log::debug!("failed to write registry value: {:?}", _err); + } + Some(new_uuid) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_os = "windows")] + #[test] + fn test_registry() { + use winreg::enums::HKEY_LOCAL_MACHINE; + use winreg::RegKey; + + let selector = Registry::default(); + let id_one = selector.get_host_id(); + let id_two = selector.get_host_id(); + + assert!(id_one.is_some()); + assert!(id_two.is_some()); + assert_eq!(id_one, id_two); + + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let _ = hklm.delete_subkey_all(selector.key_path()); + } + + #[cfg(not(target_os = "windows"))] + #[test] + fn test_registry_non_windows() { + let selector = Registry::default(); + // on non‑Windows we expect registry lookup to be None + assert!(selector.get_host_id().is_none()); + } +}