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
8 changes: 8 additions & 0 deletions docs/_data/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,13 @@
url: "dev-guide/eldritch#using-dict"
- title: "Using Async"
url: "dev-guide/eldritch#using-async"
- title: "Imix"
url: "dev-guide/imix"
children:
- title: "Overview"
url: "dev-guide/eldritch#overview"
- title: "Developing a host uniqueness engine"
url: "dev-guide/eldritch#develop-a-host-uniqueness-engine"

- title: "About"
url: "" # Index
88 changes: 88 additions & 0 deletions docs/_docs/dev-guide/imix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
title: Imix
tags:
- Dev Guide
description: Want to implement new functionality in the agent? Start here!
permalink: dev-guide/imix
---

## Overview

Imix in the main bot for Realm.

## Host Selector

The host selector defined in `implants/lib/host_selector` allow imix to reliably identify which host it's running on. This is helpful for operators when creating tasking across multiple beacons as well as when searching for command results. Uniqueness is stored as a UUID4 value.

Out of the box realm comes with two options `File` and `Env` to determine what host it's on.

`File` will create a file on disk that stores the UUID4 Eg. Linux:

```bash
[~]$ cat /etc/system-id
36b3c472-d19b-46cc-b3e6-ee6fd8da5b9c
```

`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.

If no selectors succeed a random UUID4 ID will be generated and used for the bot. This should be avoided.

## Develop A Host Uniqueness Engine

To create your own:

- Navigate to `implants/lib/host_unique`
- Create a file for your selector `touch mac_address.rs`
- Create an implementation of the `HostUniqueEngine`

```rust
use uuid::Uuid;

use crate::HostIDSelector;

pub struct MacAddress {}

impl Default for MacAddress {
fn default() -> Self {
MacAddress {}
}
}

impl HostIDSelector for MacAddress {
fn get_name(&self) -> String {
"mac_address".to_string()
}

fn get_host_id(&self) -> Option<uuid::Uuid> {
// Get the mac address
// Generate a UUID using it
// Return the UUID
// Return None if anything fails
}
}

#[cfg(test)]
mod tests {
use uuid::uuid;

use super::*;

#[test]
fn test_id_mac_consistent() {
let selector = MacAddress {};
let id_one = selector.get_host_id();
let id_two = selector.get_host_id();

assert_eq!(id_one, id_two);
}
}
```

- Update `lib.rs` to re-export your implementation

```rust
mod mac_address;
pub use mac_address::MacAddress;
```

- Update the `defaults()` function to include your implementation. N.B. The order from left to right is the order engines will be evaluated.
18 changes: 18 additions & 0 deletions docs/_docs/user-guide/imix.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Imix has compile-time configuration, that may be specified using environment var
| IMIX_CALLBACK_INTERVAL | Duration between callbacks, in seconds. | `5` | No |
| IMIX_RETRY_INTERVAL | Duration to wait before restarting the agent loop if an error occurs, in seconds. | `5` | No |
| IMIX_PROXY_URI | Overide system settings for proxy URI over HTTP(S) (must specify a scheme, e.g. `https://`) | No proxy | No |
| IMIX_HOST_ID | Manually specify the host ID for this beacon. Supersedes the file on disk. | - | No |

## Logging

Expand Down Expand Up @@ -54,6 +55,23 @@ By default imix will try to determine the systems proxy settings:
- On MacOS - we cannot automatically determine the default proxy
- On FreeBSD - we cannot automatically determine the default proxy

## Identifying unique hosts

Imix communicates which host it's on to Tavern enabling operators to reliably perform per host actions. The default way that imix does this is through a file on disk. We recognize that this may be un-ideal for many situations so we've also provided an environment override and made it easy for admins managing a realm deployment to change how the bot determines uniqueness.

Imix uses the `host_unique` library under `implants/lib/host_unique` to determine which host it's on. The `id` function will fail over all available options returning the first successful ID. If a method is unable to determine the uniqueness of a host it should return `None`.

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)
- Can be used by multiple instances of the beacon on the same host.

If you cannot use the `File` engine we highly recommend manually setting the `Env` engine with the environment variable `IMIX_HOST_ID`. This will override the `File` one avoiding writes to disk but must be managed by the operators.

If all uniqueness engines 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.

## Static cross compilation

### Linux
Expand Down
3 changes: 2 additions & 1 deletion implants/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
[workspace]
members = ["imix", "golem", "lib/eldritch", "lib/transport", "lib/pb"]
members = ["imix", "golem", "lib/eldritch", "lib/transport", "lib/pb", "lib/host_unique"]
resolver = "2"

[workspace.dependencies]
transport = { path = "./lib/transport" }
eldritch = { path = "./lib/eldritch" }
host_unique = { path = "./lib/host_unique" }
pb = { path = "./lib/pb" }

aes = "0.8.3"
Expand Down
1 change: 1 addition & 0 deletions implants/imix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ default = []
eldritch = { workspace = true, features = ["imix"] }
pb = { workspace = true }
transport = { workspace = true }
host_unique = { workspace = true }

anyhow = { workspace = true }
env_logger = "0.11.2"
Expand Down
62 changes: 3 additions & 59 deletions implants/imix/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
use crate::version::VERSION;
use pb::c2::host::Platform;
use std::{
fs::{self, File},
io::Write,
path::Path,
};
use uuid::Uuid;

macro_rules! callback_uri {
Expand Down Expand Up @@ -78,9 +73,11 @@ impl Default for Config {
identifier: format!("imix-v{}", VERSION),
};

let selectors = host_unique::defaults();

let host = pb::c2::Host {
name: whoami::fallible::hostname().unwrap_or(String::from("")),
identifier: get_host_id(get_host_id_path()),
identifier: host_unique::get_id_with_selectors(selectors).to_string(),
platform: get_host_platform() as i32,
primary_ip: get_primary_ip(),
};
Expand Down Expand Up @@ -208,59 +205,6 @@ fn get_host_platform() -> Platform {
return Platform::Unspecified;
}

/*
* Returns a predefined path to the host id file based on the current platform.
*/
fn get_host_id_path() -> String {
#[cfg(target_os = "windows")]
return String::from("C:\\ProgramData\\system-id");

#[cfg(target_os = "linux")]
return String::from("/etc/system-id");

#[cfg(target_os = "macos")]
return String::from("/Users/Shared/system-id");

#[cfg(target_os = "freebsd")]
return String::from("/etc/systemd-id");
}

/*
* Attempt to read a host-id from a predefined path on disk.
* If the file exist, it's value will be returned as the identifier.
* If the file does not exist, a new value will be generated and written to the file.
* If there is any failure reading / writing the file, the generated id is still returned.
*/
fn get_host_id(file_path: String) -> String {
// Read Existing Host ID
let path = Path::new(file_path.as_str());
if path.exists() {
if let Ok(host_id) = fs::read_to_string(path) {
return host_id.trim().to_string();
}
}

// Generate New
let host_id = Uuid::new_v4().to_string();

// Save to file
match File::create(path) {
Ok(mut f) => match f.write_all(host_id.as_bytes()) {
Ok(_) => {}
Err(_err) => {
#[cfg(debug_assertions)]
log::error!("failed to write host id file: {_err}");
}
},
Err(_err) => {
#[cfg(debug_assertions)]
log::error!("failed to create host id file: {_err}");
}
};

host_id
}

/*
* Return the first IPv4 address of the default interface as a string.
* Returns the empty string otherwise.
Expand Down
14 changes: 14 additions & 0 deletions implants/lib/host_unique/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "host_unique"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
uuid = { workspace = true, features = ["v4", "fast-rng"] }
log = { workspace = true }

[dev-dependencies]
pretty_env_logger = "0.5.0"
tempfile = { workspace = true }
46 changes: 46 additions & 0 deletions implants/lib/host_unique/src/env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::env;
use uuid::Uuid;

use crate::HostIDSelector;

pub struct Env {}

impl Default for Env {
fn default() -> Self {
Env {}
}
}

impl HostIDSelector for Env {
fn get_name(&self) -> String {
"env".to_string()
}

fn get_host_id(&self) -> Option<uuid::Uuid> {
let host_id_env = env::var("IMIX_HOST_ID").unwrap();
match Uuid::parse_str(&host_id_env) {
Ok(res) => Some(res),
Err(_err) => {
#[cfg(debug_assertions)]
log::debug!("Failed to deploy {:?}", _err);
None
}
}
}
}

#[cfg(test)]
mod tests {
use uuid::uuid;

use super::*;

#[test]
fn test_id_env() {
std::env::set_var("IMIX_HOST_ID", "f17b92c0-e383-4328-9017-952e5d9fd53d");
let engine = Env {};
let id = engine.get_host_id().unwrap();

assert_eq!(id, uuid!("f17b92c0-e383-4328-9017-952e5d9fd53d"));
}
}
Loading