diff --git a/docs/_docs/user-guide/imix.md b/docs/_docs/user-guide/imix.md index 764368c62..678321753 100644 --- a/docs/_docs/user-guide/imix.md +++ b/docs/_docs/user-guide/imix.md @@ -83,6 +83,8 @@ RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --lib --target=x cd realm/implants/imix/ # Build imix.exe RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --target=x86_64-pc-windows-gnu +# Build imix.svc.exe +RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --features win_service --target=x86_64-pc-windows-gnu # Build imix.dll RUSTFLAGS="-C target-feature=+crt-static" cargo build --release --lib --target=x86_64-pc-windows-gnu ``` diff --git a/implants/Cargo.toml b/implants/Cargo.toml index 9fd992446..d6cda753d 100644 --- a/implants/Cargo.toml +++ b/implants/Cargo.toml @@ -74,9 +74,11 @@ trait-variant = "0.1.1" uuid = "1.5.0" which = "4.4.2" whoami = "1.3.0" +windows-service = "0.6.0" windows-sys = "0.45.0" winreg = "0.51.0" + [profile.release] strip = true # Automatically strip symbols from the binary. opt-level = "z" # Optimize for size. diff --git a/implants/imix/Cargo.toml b/implants/imix/Cargo.toml index d74febc20..c45f37e2b 100644 --- a/implants/imix/Cargo.toml +++ b/implants/imix/Cargo.toml @@ -3,12 +3,18 @@ name = "imix" version = "0.0.5" edition = "2021" +[features] +# Check if compiled by imix +win_service = [] +default = [] + [dependencies] eldritch = { workspace = true, features = ["imix"] } pb = {workspace = true } transport = { workspace = true } anyhow = { workspace = true } +env_logger = "0.11.2" chrono = { workspace = true , features = ["serde"] } clap = { workspace = true } default-net = { workspace = true } @@ -27,6 +33,9 @@ tokio = { workspace = true, features = ["full"] } uuid = { workspace = true, features = ["v4","fast-rng"] } whoami = { workspace = true } +[target.'cfg(target_os = "windows")'.dependencies] +windows-service = "0.6.0" + [dev-dependencies] httptest = { workspace = true } tempfile = { workspace = true } diff --git a/implants/imix/src/lib.rs b/implants/imix/src/lib.rs index 3d99fb08c..af2abdb27 100644 --- a/implants/imix/src/lib.rs +++ b/implants/imix/src/lib.rs @@ -3,7 +3,58 @@ mod config; mod install; mod task; mod version; +#[cfg(feature = "win_service")] +pub mod win_service; + +use std::time::Duration; pub use agent::Agent; +use clap::Command; pub use config::Config; pub use install::install; + + +pub async fn handle_main(){ + #[cfg(debug_assertions)] + init_logging(); + + if let Some(("install", _)) = Command::new("imix") + .subcommand(Command::new("install").about("Install imix")) + .get_matches() + .subcommand() + { + install().await; + return; + } + + loop { + let cfg = Config::default(); + let retry_interval = cfg.retry_interval; + #[cfg(debug_assertions)] + log::info!("agent config initialized {:#?}", cfg.clone()); + + match run(cfg).await { + Ok(_) => {} + Err(_err) => { + #[cfg(debug_assertions)] + log::error!("callback loop fatal: {_err}"); + + tokio::time::sleep(Duration::from_secs(retry_interval)).await; + } + } + } +} + +async fn run(cfg: Config) -> anyhow::Result<()> { + let mut agent = Agent::new(cfg)?; + agent.callback_loop().await?; + Ok(()) +} + +#[cfg(debug_assertions)] +fn init_logging() { + pretty_env_logger::formatted_timed_builder() + .filter_level(log::LevelFilter::Info) + .parse_env("IMIX_LOG") + .init(); +} diff --git a/implants/imix/src/main.rs b/implants/imix/src/main.rs index e530f864c..c8827a78e 100644 --- a/implants/imix/src/main.rs +++ b/implants/imix/src/main.rs @@ -1,52 +1,42 @@ -#![windows_subsystem = "windows"] +// #![windows_subsystem = "windows"] +#[cfg(all(feature = "win_service", windows))] +#[macro_use] +extern crate windows_service; -use anyhow::Result; -use clap::Command; -use imix::{Agent, Config}; -use std::time::Duration; +use imix::handle_main; + +// ============= Standard =============== + +#[cfg(not(feature = "win_service"))] #[tokio::main(flavor = "multi_thread", worker_threads = 128)] async fn main() { - #[cfg(debug_assertions)] - init_logging(); - - if let Some(("install", _)) = Command::new("imix") - .subcommand(Command::new("install").about("Install imix")) - .get_matches() - .subcommand() - { - imix::install().await; - return; - } - - loop { - let cfg = Config::default(); - let retry_interval = cfg.retry_interval; - #[cfg(debug_assertions)] - log::info!("agent config initialized {:#?}", cfg.clone()); - - match run(cfg).await { - Ok(_) => {} - Err(_err) => { - #[cfg(debug_assertions)] - log::error!("callback loop fatal: {_err}"); - - tokio::time::sleep(Duration::from_secs(retry_interval)).await; - } - } - } + + handle_main().await } -async fn run(cfg: Config) -> Result<()> { - let mut agent = Agent::new(cfg)?; - agent.callback_loop().await?; - Ok(()) + +// ============ Windows Service ============= + +#[cfg(all(feature = "win_service", not(target_os = "windows")))] +compile_error!("Feature win_service is only available on windows targets"); + +#[cfg(feature = "win_service")] +define_windows_service!(ffi_service_main, service_main); + +#[cfg(feature = "win_service")] +fn main() { + use windows_service::service_dispatcher; + service_dispatcher::start("imix", ffi_service_main).unwrap(); } -#[cfg(debug_assertions)] -fn init_logging() { - pretty_env_logger::formatted_timed_builder() - .filter_level(log::LevelFilter::Info) - .parse_env("IMIX_LOG") - .init(); +#[cfg(feature = "win_service")] +#[tokio::main(flavor = "multi_thread", worker_threads = 128)] +async fn service_main(arguments: Vec) { + use imix::win_service::handle_service_main; + + handle_service_main(arguments); + + handle_main().await; } + diff --git a/implants/imix/src/win_service.rs b/implants/imix/src/win_service.rs new file mode 100644 index 000000000..d9673120c --- /dev/null +++ b/implants/imix/src/win_service.rs @@ -0,0 +1,45 @@ +use std::{ffi::OsString, time::Duration}; +use windows_service::{ + service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, + ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, +}; + +pub fn handle_service_main(_arguments: Vec) { + let event_handler = move |control_event| -> ServiceControlHandlerResult { + match control_event { + ServiceControl::Stop => { + // Handle stop event and return control back to the system. + ServiceControlHandlerResult::NoError + } + // All services must accept Interrogate even if it's a no-op. + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + // Register system service event handler + let status_handle = service_control_handler::register("myservice", event_handler).unwrap(); + + let next_status = ServiceStatus { + // Should match the one from system service registry + service_type: ServiceType::OWN_PROCESS, + // The new state + current_state: ServiceState::Running, + // Accept stop events when running + controls_accepted: ServiceControlAccept::STOP, + // Used to report an error when starting or stopping only, otherwise must be zero + exit_code: ServiceExitCode::Win32(0), + // Only used for pending states, otherwise must be zero + checkpoint: 0, + // Only used for pending states, otherwise must be zero + wait_hint: Duration::default(), + // Process ID of the service This is only retrieved when querying the service status + process_id: None, + }; + + // Tell the system that the service is running now + status_handle.set_service_status(next_status).unwrap(); +} diff --git a/implants/lib/transport/src/grpc.rs b/implants/lib/transport/src/grpc.rs index 0ac68a7f8..975f9d1af 100644 --- a/implants/lib/transport/src/grpc.rs +++ b/implants/lib/transport/src/grpc.rs @@ -6,7 +6,6 @@ use tonic::codec::ProstCodec; use tonic::GrpcMethod; use tonic::Request; -#[cfg(debug_assertions)] use std::time::Duration; static CLAIM_TASKS_PATH: &str = "/c2.C2/ClaimTasks";