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
38 changes: 37 additions & 1 deletion docs/_docs/dev-guide/imix.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,42 @@ permalink: dev-guide/imix

Imix in the main bot for Realm.

## Agent protobuf

In order to communicate agent state and configuration during the claimTask request the agent sends a protobuf containing various configuration options. If any are updated agent side they're now syncronsized with the server ensuring operators can track the state of their agents.

In order to keep these configuration options in sync realm uses protobuf and code generation to ensure agent and server agree.

If you need to update these fields start with the `tavern/internal/c2/proto/c2.proto` file.

Once you've finished making your changes apply these changes across the project using `cd /workspaces/realm/ && go generater ./tavern/...`

To generate the associated agent proto's use cargo build in the `implants` direcotry. This will copy the necesarry protos from tavern and preform the code generation.

### Adding enums

Add your enum type to the `*.proto` file under the message type that will use it.
For example:
```
message ActiveTransport {
string uri = 1;
uint64 interval = 2;

enum Type {
TRANSPORT_UNSPECIFIED = 0;
TRANSPORT_GRPC = 1;
TRANSPORT_HTTP1 = 2;
TRANSPORT_DNS = 3;
}

Type type = 3;
string extra = 4;
}
```

And add a new enum definition to `tavern/internal/c2/c2pb/enum_<MESSAGE NAME>_<ENUM NAME>.go` This should be similar to other enums that exist you can likely copy and rename an existing one. See `tavern/internal/c2/c2pb/enum_beacon_active_transport_type.go`


## 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.
Expand Down Expand Up @@ -127,7 +163,7 @@ impl Transport for Custom {
// e.g., client: None
}
}
fn new(callback: String, proxy_uri: Option<String>) -> Result<Self> {
fn new(callback: String, config: Config) -> Result<Self> {
// TODO: setup connection/client hook in proxy, anything else needed
// before functions get called.
Err(anyhow!("Unimplemented!"))
Expand Down
12 changes: 4 additions & 8 deletions docs/_docs/user-guide/imix.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ Building in the dev container limits variables that might cause issues and is th
| IMIX_SERVER_PUBKEY | The public key for the tavern server (obtain from server using `curl $IMIX_CALLBACK_URI/status`). | automatic | Yes |
| 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 |
| IMIX_RUN_ONCE | Imix will only do one callback and execution of queued tasks (may want to pair with runtime environment variable `IMIX_BEACON_ID`) | false | No |
| IMIX_TRANSPORT_EXTRA_HTTP_PROXY | Overide system settings for proxy URI over HTTP(S) (must specify a scheme, e.g. `https://`) | No proxy | No |
| IMIX_TRANSPORT_EXTRA_DOH | Enable DoH, eventually specify which DoH service to use. Requires the grpc-doh flag. | No DoH. | No |


Imix has run-time configuration, that may be specified using environment variables during execution.

Expand Down Expand Up @@ -118,13 +120,7 @@ Every callback interval imix will query each active thread for new output and re

## Proxy support

Imix's default `grpc` transport supports http and https proxies for outbound communication.
By default imix will try to determine the systems proxy settings:

- On Linux reading the environment variables `http_proxy` and then `https_proxy`
- On Windows - we cannot automatically determine the default proxy
- On MacOS - we cannot automatically determine the default proxy
- On FreeBSD - we cannot automatically determine the default proxy
Imix's default `grpc` transport supports http and https proxies for outbound communication. These must be set at compile time.

## Identifying unique hosts

Expand Down
19 changes: 16 additions & 3 deletions implants/imix/src/agent.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::task::TaskHandle;
use anyhow::Result;
use anyhow::{Context, Result};
use pb::{c2::ClaimTasksRequest, config::Config};
use std::time::{Duration, Instant};
use transport::Transport;
Expand Down Expand Up @@ -88,7 +88,17 @@ impl<T: Transport + 'static> Agent<T> {
* Callback once using the configured client to claim new tasks and report available output.
*/
pub async fn callback(&mut self) -> Result<()> {
self.t = T::new(self.cfg.callback_uri.clone(), self.cfg.proxy_uri.clone())?;
//TODO: De-dupe this fields - probably just need to pass the config not the callback URI.
self.t = T::new(
self.cfg
.info
.clone()
.context("failed to get info")?
.active_transport
.context("failed to get transport")?
.uri,
self.cfg.clone(),
)?;
self.claim_tasks(self.t.clone()).await?;
self.report(self.t.clone()).await?;
self.t = T::init(); // re-init to make sure no active connections during sleep
Expand Down Expand Up @@ -121,7 +131,10 @@ impl<T: Transport + 'static> Agent<T> {
}

let interval = match self.cfg.info.clone() {
Some(b) => Ok(b.interval),
Some(b) => Ok(b
.active_transport
.context("failed to get transport")?
.interval),
None => Err(anyhow::anyhow!("beacon info is missing from agent")),
}?;
let delay = match interval.checked_sub(start.elapsed().as_secs()) {
Expand Down
12 changes: 10 additions & 2 deletions implants/imix/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pub use pb::config::Config;

use transport::{ActiveTransport, Transport};

const DEFAULT_RETRY_INTERVAL: u64 = 5;

pub async fn handle_main() {
if let Some(("install", _)) = Command::new("imix")
.subcommand(Command::new("install").about("Install imix"))
Expand All @@ -19,8 +21,14 @@ pub async fn handle_main() {
}

loop {
let cfg = Config::default_with_imix_verison(VERSION);
let retry_interval = cfg.retry_interval;
let cfg = Config::default_with_imix_version(VERSION);
let retry_interval = cfg
.info
.as_ref()
.and_then(|beacon| beacon.active_transport.as_ref())
.map(|transport| transport.interval)
.unwrap_or(DEFAULT_RETRY_INTERVAL);

#[cfg(debug_assertions)]
log::info!("agent config initialized {:#?}", cfg.clone());

Expand Down
66 changes: 52 additions & 14 deletions implants/imixv2/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ impl<T: Transport + Sync + 'static> ImixAgent<T> {
runtime_handle: tokio::runtime::Handle,
task_registry: Arc<TaskRegistry>,
) -> Self {
let uri = config.callback_uri.clone();
//TODO: simplyify this section transport, callback_uris, active_uri_idx, and config seem to duplicate information.
let c = config.clone();
let active_transport = c
.info
.as_ref()
.and_then(|info| info.active_transport.as_ref());
let uri = active_transport.map(|t| t.uri.clone()).unwrap_or_default();

Self {
config: Arc::new(RwLock::new(config)),
transport: Arc::new(RwLock::new(transport)),
Expand All @@ -52,7 +59,13 @@ impl<T: Transport + Sync + 'static> ImixAgent<T> {
.info
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No beacon info in config"))?;
Ok(info.interval)
let interval = info
.active_transport
.as_ref()
.ok_or_else(|| anyhow::anyhow!("no active transport set"))?
.interval;

Ok(interval)
}

// Triggers config.refresh_primary_ip() in a write lock
Expand Down Expand Up @@ -100,7 +113,8 @@ impl<T: Transport + Sync + 'static> ImixAgent<T> {
}

// Helper to get config URIs for creating new transport
pub async fn get_transport_config(&self) -> (String, Option<String>) {
pub async fn get_transport_config(&self) -> (String, Config) {
let config = self.config.read().await.clone();
let uris = self.callback_uris.read().await;
let idx = *self.active_uri_idx.read().await;
let callback_uri = if idx < uris.len() {
Expand All @@ -109,8 +123,7 @@ impl<T: Transport + Sync + 'static> ImixAgent<T> {
// Fallback, should not happen unless empty
uris.first().cloned().unwrap_or_default()
};
let cfg = self.config.read().await;
(callback_uri, cfg.proxy_uri.clone())
(callback_uri, config)
}

pub async fn rotate_callback_uri(&self) {
Expand All @@ -134,8 +147,8 @@ impl<T: Transport + Sync + 'static> ImixAgent<T> {
}

// 2. Create new transport from config
let (callback_uri, proxy_uri) = self.get_transport_config().await;
let t = T::new(callback_uri, proxy_uri).context("Failed to create on-demand transport")?;
let (callback_uri, config) = self.get_transport_config().await;
let t = T::new(callback_uri, config).context("Failed to create on-demand transport")?;

#[cfg(debug_assertions)]
log::debug!("Created on-demand transport for background task");
Expand Down Expand Up @@ -302,22 +315,45 @@ impl<T: Transport + Send + Sync + 'static> Agent for ImixAgent<T> {
.map_err(|e: String| e)?;

let active_uri = self.get_active_callback_uri().unwrap_or_default();
let config = cfg.clone();

let active_transport = config
.info
.as_ref()
.and_then(|info| info.active_transport.as_ref())
.context("failed to get active transport")
.map_err(|e| e.to_string())?;

map.insert("callback_uri".to_string(), active_uri);
if let Some(proxy) = &cfg.proxy_uri {
map.insert("proxy_uri".to_string(), proxy.clone());
}
map.insert("retry_interval".to_string(), cfg.retry_interval.to_string());
map.insert(
"retry_interval".to_string(),
active_transport.interval.to_string(),
);
map.insert("run_once".to_string(), cfg.run_once.to_string());

if let Some(info) = &cfg.info {
map.insert("beacon_id".to_string(), info.identifier.clone());
map.insert("principal".to_string(), info.principal.clone());
map.insert("interval".to_string(), info.interval.to_string());
map.insert(
"interval".to_string(),
active_transport.interval.to_string(),
);
if let Some(host) = &info.host {
map.insert("hostname".to_string(), host.name.clone());
map.insert("platform".to_string(), host.platform.to_string());
map.insert("primary_ip".to_string(), host.primary_ip.clone());
}
if let Some(active_transport) = &info.active_transport {
map.insert("uri".to_string(), active_transport.uri.clone());
map.insert(
"type".to_string(),
active_transport.r#type.clone().to_string(),
);
map.insert(
"extra".to_string(),
active_transport.extra.clone().to_string(),
);
}
}
Ok(map)
}
Expand Down Expand Up @@ -370,8 +406,10 @@ impl<T: Transport + Send + Sync + 'static> Agent for ImixAgent<T> {
self.block_on(async {
{
let mut cfg = self.config.write().await;
if let Some(info) = &mut cfg.info {
info.interval = interval;
if let Some(info) = &mut cfg.info
&& let Some(active_transport) = &mut info.active_transport
{
active_transport.interval = interval;
}
}
// We force a check-in to update the server with the new interval
Expand Down
9 changes: 5 additions & 4 deletions implants/imixv2/src/run.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::Result;
use anyhow::{Context, Result};
use eldritch_agent::Agent;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
Expand All @@ -15,7 +16,7 @@ pub async fn run_agent() -> Result<()> {
init_logger();

// Load config / defaults
let config = Config::default_with_imix_verison(VERSION);
let config = Config::default_with_imix_version(VERSION);
#[cfg(debug_assertions)]
log::info!("Loaded config: {config:#?}");

Expand Down Expand Up @@ -88,9 +89,9 @@ async fn run_agent_cycle(agent: Arc<ImixAgent<ActiveTransport>>, registry: Arc<T
agent.refresh_ip().await;

// Create new active transport
let (callback_uri, proxy_uri) = agent.get_transport_config().await;
let (callback_uri, config) = agent.get_transport_config().await;

let transport = match ActiveTransport::new(callback_uri, proxy_uri) {
let transport = match ActiveTransport::new(callback_uri, config) {
Ok(t) => t,
Err(_e) => {
#[cfg(debug_assertions)]
Expand Down
7 changes: 6 additions & 1 deletion implants/imixv2/src/tests/agent_trait_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,14 @@ async fn test_imix_agent_report_file() {
#[allow(clippy::field_reassign_with_default)]
async fn test_imix_agent_config_access() {
let mut config = Config::default();
config.callback_uri = "http://localhost:8080".to_string();

config.info = Some(pb::c2::Beacon {
identifier: "agent1".to_string(),
active_transport: Some(pb::c2::ActiveTransport {
uri: "http://localhost:8080".to_string(),
interval: 5,
..Default::default()
}),
..Default::default()
});

Expand Down
6 changes: 5 additions & 1 deletion implants/imixv2/src/tests/callback_interval_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ async fn test_imix_agent_get_callback_interval_error() {
async fn test_imix_agent_get_callback_interval_success() {
let mut config = Config::default();
config.info = Some(pb::c2::Beacon {
interval: 10,
active_transport: Some(pb::c2::ActiveTransport {
uri: "http://example.com/callback".to_string(),
interval: 10,
..Default::default()
}),
..Default::default()
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,24 @@ impl SyncDispatcher for SetCallbackIntervalMessage {
"SetCallbackIntervalMessage: beacon is missing from config"
)),
}?;
// TODO: we can probably just modify the interval not rebuild the entire beacon see set_callback_uri.rs
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is 1v we let it die 🫡

c.info = Some(Beacon {
identifier: b.identifier,
principal: b.principal,
host: b.host,
agent: b.agent,
interval: self.new_interval,
transport: transport.get_type() as i32,
active_transport: Some(pb::c2::ActiveTransport {
uri: b
.active_transport
.as_ref()
.map_or(String::new(), |at| at.uri.clone()),
interval: self.new_interval,
r#type: transport.get_type() as i32,
extra: b
.active_transport
.as_ref()
.map_or(String::new(), |at| at.extra.clone()),
}),
});
Ok(c)
}
Expand Down
11 changes: 9 additions & 2 deletions implants/lib/eldritch/src/runtime/messages/set_callback_uri.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{SyncDispatcher, Transport};
use anyhow::Result;
use anyhow::{Context, Result};
use pb::config::Config;

/*
Expand All @@ -16,7 +16,14 @@ pub struct SetCallbackUriMessage {
impl SyncDispatcher for SetCallbackUriMessage {
fn dispatch(self, _transport: &mut impl Transport, cfg: Config) -> Result<Config> {
let mut c = cfg.clone();
c.callback_uri = self.new_uri;
c.info
.as_mut()
.context("missing config info")?
.active_transport
.as_mut()
.context("missing active transport")?
.uri = self.new_uri;

Ok(c)
}
}
1 change: 1 addition & 0 deletions implants/lib/pb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ host_unique = { workspace = true }
log = { workspace = true }
netdev = { workspace = true }
prost = { workspace = true }
serde_json = { workspace = true }
prost-types = { workspace = true }
rand_chacha = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
Expand Down
Loading
Loading