use std::{
path::PathBuf,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use anyhow::{Context as _, Error};
use clap::Args;
use isr::Profile;
use vmi::{
VmiSession,
arch::amd64::Amd64,
driver::VmiFullDriver,
os::windows::WindowsOs,
utils::{injector::UserInjectorHandler, shellcode::StatusKind},
};
use super::{
bridge::{DeployBridge, DeployPolicy, DeployStatus, ExecuteResponse},
parameters::DeployParameters,
recipe::deploy_recipe,
};
use crate::{bridge::BridgeOutput, common, monitor::Monitor};
#[derive(Debug, Args)]
pub struct DeployArguments {
#[arg(long, default_value = "explorer.exe")]
process: String,
#[arg(long, requires = "download_path")]
url: Option<String>,
#[arg(long, requires = "url")]
download_path: Option<String>,
#[arg(long = "extract-to", requires = "url")]
extraction_directory: Option<String>,
#[arg(long)]
execute: Option<String>,
#[arg(long, requires = "execute")]
arguments: Option<String>,
#[arg(long, requires = "execute")]
working_directory: Option<String>,
#[arg(long, requires = "execute")]
show_window: Option<i32>,
#[arg(long, requires = "execute")]
monitor: bool,
#[arg(long, default_value = "artifacts")]
output_directory: PathBuf,
#[arg(long, default_value_t = 0)]
max_download_retries: u64,
}
#[derive(Debug)]
struct DeployMonitorRequest {
output_directory: PathBuf,
}
#[derive(Debug)]
struct DeployRequest {
process: String,
parameters: DeployParameters,
policy: DeployPolicy,
monitor: Option<DeployMonitorRequest>,
}
impl DeployArguments {
fn into_request(self) -> DeployRequest {
let Self {
process,
url,
download_path,
extraction_directory,
execute,
arguments,
working_directory,
show_window,
monitor,
max_download_retries,
output_directory,
} = self;
let policy = DeployPolicy::default().max_download_retries(max_download_retries);
let policy = if monitor {
policy.execute_response(ExecuteResponse::Wait)
}
else {
policy.maybe_allow_execute(execute.is_some())
};
let monitor_request = if monitor {
Some(DeployMonitorRequest { output_directory })
}
else {
None
};
let parameters = match (url, execute) {
(None, None) => DeployParameters::builder().build(),
(None, Some(executable)) => DeployParameters::builder()
.execute(executable)
.maybe_arguments(arguments)
.maybe_working_directory(working_directory)
.maybe_show_window(show_window)
.build(),
(Some(url), None) => {
let download_path =
download_path.expect("--download-path required by clap when --url is set");
DeployParameters::builder()
.download(url)
.download_path(download_path)
.maybe_extraction_directory(extraction_directory)
.build()
}
(Some(url), Some(executable)) => {
let download_path =
download_path.expect("--download-path required by clap when --url is set");
DeployParameters::builder()
.download(url)
.download_path(download_path)
.maybe_extraction_directory(extraction_directory)
.execute(executable)
.maybe_arguments(arguments)
.maybe_working_directory(working_directory)
.maybe_show_window(show_window)
.build()
}
};
DeployRequest {
process,
parameters,
policy,
monitor: monitor_request,
}
}
}
fn validate_deploy_output(output: BridgeOutput) -> Result<DeployStatus, Error> {
let status = match output {
BridgeOutput::DeployFinished(status) => status,
BridgeOutput::DeployWaiting | BridgeOutput::DeployExecuting => {
anyhow::bail!("unexpected deploy output: {output:?}")
}
};
anyhow::ensure!(
status.kind() == StatusKind::SUCCESS,
"deploy failed: {status:?}"
);
Ok(status)
}
fn validate_deploy_waiting_output(output: BridgeOutput) -> Result<(), Error> {
anyhow::ensure!(
output == BridgeOutput::DeployWaiting,
"deploy waiting failed: {output:?}"
);
Ok(())
}
pub fn run<Driver>(
session: &VmiSession<'_, WindowsOs<Driver>>,
profile: &Profile,
terminate_flag: Arc<AtomicBool>,
arguments: DeployArguments,
) -> Result<(), Error>
where
Driver: VmiFullDriver<Architecture = Amd64>,
{
let DeployRequest {
process,
parameters,
policy,
monitor,
} = arguments.into_request();
let process_id = common::find_process_id(session, &process)?;
let output = session
.handle(|session| {
UserInjectorHandler::new(session, deploy_recipe(¶meters))?
.with_bridge(DeployBridge::new(policy))?
.with_pid(process_id)
})?
.context("deploy injection interrupted")?
.context("deploy bridge completed without an output")?
.map_err(|packet| anyhow::anyhow!("unhandled deploy bridge packet: {packet:?}"))?;
let monitor = match monitor {
Some(monitor) => monitor,
None => {
let status = validate_deploy_output(output)?;
tracing::info!(?status, "deploy completed");
return Ok(());
}
};
validate_deploy_waiting_output(output)?;
tracing::info!("deploy injector parked at execute gate");
let monitor_terminate_flag = terminate_flag.clone();
let output = session.handle(|session| {
Monitor::new(
session,
profile,
monitor_terminate_flag,
monitor.output_directory,
)
})?;
match output {
Some(Ok(Some(pid))) => tracing::info!(%pid, "deploy monitoring completed"),
Some(Ok(None)) => tracing::info!("deploy monitoring cancelled"),
Some(Err(err)) => anyhow::bail!("deploy failed during monitoring: {err:?}"),
None if terminate_flag.load(Ordering::Relaxed) => {
tracing::info!("deploy monitoring cancelled")
}
None => anyhow::bail!("deploy monitoring interrupted"),
}
Ok(())
}