Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/_docs/dev-guide/imix.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/_docs/user-guide/imix.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 3 additions & 0 deletions implants/lib/host_unique/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
2 changes: 2 additions & 0 deletions implants/lib/host_unique/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
126 changes: 126 additions & 0 deletions implants/lib/host_unique/src/registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use crate::HostIDSelector;
use uuid::Uuid;

#[derive(Default)]
pub struct Registry {
subkey: Option<String>,
value_name: Option<String>,
}

impl Registry {
/*
* Returns a predefined path to the host id registry key.
*/
pub fn with_subkey(mut self, path: impl Into<String>) -> Self {
self.subkey = Some(path.into());
self
}

pub fn with_value_name(mut self, name: impl Into<String>) -> 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<Uuid> {
// 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::<String, _>(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());
}
}
Loading