Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions malefic-crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub use runtime::{
ConfigBlobText,
RuntimeConfig,
CONFIG_BLOB_B64_LEN,
CONFIG_BLOB_PREFIX,
CONFIG_BLOB_TEXT,
CONFIG_BLOB_TEXT_LEN,
decode_runtime_config_bytes,
Expand Down
11 changes: 8 additions & 3 deletions malefic-crates/config/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ pub const CONFIG_BLOB_TEXT_LEN: usize = 16384;
const CONFIG_BLOB_PREFIX_LEN: usize = 8;
const CONFIG_BLOB_PAYLOAD_LEN: usize = CONFIG_BLOB_TEXT_LEN - CONFIG_BLOB_PREFIX_LEN;

/// Prefix marking the embedded runtime config blob slot. Shared with the patcher
/// (malefic-mutant) so both sides always agree; bump it when the slot format changes.
pub const CONFIG_BLOB_PREFIX: &[u8; 8] = b"CFGv4B64";

/// Backwards-compatible alias used by the patching CLI (this is *not* a base64 length anymore).
pub const CONFIG_BLOB_B64_LEN: usize = CONFIG_BLOB_TEXT_LEN;

Expand All @@ -32,8 +36,8 @@ pub struct ConfigBlobText {
impl ConfigBlobText {
pub const fn new() -> Self {
Self {
// Inline the prefix bytes directly to avoid creating a separate constant in the binary
prefix: *b"CFGv4B64",
// Const reference is inlined at compile time, no separate symbol in the binary
prefix: *CONFIG_BLOB_PREFIX,
payload: [PAD_BYTE; CONFIG_BLOB_PAYLOAD_LEN],
}
}
Expand Down Expand Up @@ -755,7 +759,7 @@ pub fn load_runtime_config(fallback: RuntimeConfig) -> RuntimeConfig {
}

/// Create a fixed-length ASCII string (4096 bytes) that fits exactly in the embedded buffer.
/// Format: `CFGv3B64` + base64(encrypted tinyserde bytes) + `#` padding.
/// Format: `CONFIG_BLOB_PREFIX` (currently `CFGv4B64`) + base64(encrypted tinyserde bytes) + `#` padding.
#[cfg(feature = "encoder")]
pub fn encode_runtime_config(config: &RuntimeConfig) -> Result<String, BlobError> {
let cipher = XorCipher::new(BLOB_KEY_SEED);
Expand Down Expand Up @@ -899,6 +903,7 @@ mod tests {
domain_suffix: None,
}
}],
max_packet_length: 0,
}
}

Expand Down
18 changes: 15 additions & 3 deletions malefic-mutant/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,11 +420,11 @@ pub enum Tool {
output: String,
},

/// Patch compiled binaries (NAME / KEY / SERVER address)
/// Patch compiled binaries (NAME / KEY / SERVER address, or runtime config blob)
#[command(name = "patch")]
Patch {
/// Target binary file (e.g. malefic.exe)
#[arg(long, short = 'f')]
#[arg(long, short = 'f', visible_alias = "input", visible_short_alias = 'i')]
file: String,

/// New NAME value (beacon name)
Expand All @@ -439,6 +439,18 @@ pub enum Tool {
#[arg(long)]
server_address: Option<String>,

/// Runtime config file (json/yaml) matching RuntimeConfig
#[arg(long, short = 'c')]
config: Option<String>,

/// Generate runtime config from implant.yaml instead of raw RuntimeConfig
#[arg(long = "from-implant")]
from_implant: Option<String>,

/// Pre-encoded blob string (length must equal CONFIG_BLOB_B64_LEN)
#[arg(long)]
blob: Option<String>,

/// Output file path (defaults to <file>.patched)
#[arg(long, short = 'o')]
output: Option<String>,
Expand All @@ -448,7 +460,7 @@ pub enum Tool {
xor_key: Option<String>,
},

/// Patch runtime config blob (CFGv3B64 prefix + fixed payload)
/// Patch runtime config blob (CFG prefix + fixed payload)
#[command(name = "patch-config")]
PatchConfig {
/// Target binary file (e.g. malefic.exe)
Expand Down
137 changes: 87 additions & 50 deletions malefic-mutant/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,9 +607,28 @@ fn parse_tool(tool: &Tool) -> anyhow::Result<()> {
name,
key,
server_address,
config,
from_implant,
blob,
output,
xor_key,
} => {
let config_mode = config.is_some() || from_implant.is_some() || blob.is_some();
if config_mode {
if name.is_some() || key.is_some() || server_address.is_some() {
return Err(anyhow::anyhow!(
"--name/--key/--server-address cannot be combined with --config/--from-implant/--blob"
));
}
return patch_runtime_config_blob(
file,
config.as_ref(),
from_implant.as_ref(),
blob.as_ref(),
output.as_ref(),
);
}

use crate::tool::patch::{
batch_patch_binary, BatchPatchOptions, FieldPatch, PatchField,
};
Expand Down Expand Up @@ -648,7 +667,7 @@ fn parse_tool(tool: &Tool) -> anyhow::Result<()> {
}

if patches.is_empty() {
log_error!("No fields specified to patch. Use --name, --key, or --server-address");
log_error!("No fields specified to patch. Use --name, --key, --server-address, --config, --from-implant, or --blob");
return Err(anyhow::anyhow!("no fields specified for patching"));
}

Expand Down Expand Up @@ -693,51 +712,13 @@ fn parse_tool(tool: &Tool) -> anyhow::Result<()> {
from_implant,
blob,
output,
} => {
use crate::tool::patch::{patch_config_blob, ConfigBlobPatchOptions};
use malefic_config::{encode_runtime_config, CONFIG_BLOB_B64_LEN};

log_step!("Patching runtime config blob in {}", file);

let blob_str = if let Some(b64) = blob {
normalize_blob_string(b64, CONFIG_BLOB_B64_LEN)?
} else {
let cfg = if let Some(path) = from_implant.as_ref() {
let implant = load_yaml_config(path)?;
convert_implant_to_runtime_config(&implant)?
} else {
let cfg_path = config
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--config or --blob must be provided"))?;
load_runtime_config_from_file(cfg_path)?
};
let encoded = encode_runtime_config(&cfg)
.map_err(|e| anyhow::anyhow!("encode_runtime_config failed: {:?}", e))?;
encoded
};

let opts = ConfigBlobPatchOptions {
file: file.clone(),
blob_b64: blob_str,
blob_len: CONFIG_BLOB_B64_LEN,
output: output.clone(),
};

let outcome = patch_config_blob(&opts)?;
let overwritten = outcome.output_path == std::path::PathBuf::from(file);

log_success!(
"Patched runtime config blob at offset 0x{:X} ({} decimal)",
outcome.offset,
outcome.offset
);
if overwritten {
log_info!("Original file overwritten in-place");
} else {
log_info!("Patched file written to {}", outcome.output_path.display());
}
Ok(())
}
} => patch_runtime_config_blob(
file,
config.as_ref(),
from_implant.as_ref(),
blob.as_ref(),
output.as_ref(),
),
Tool::Encode {
input,
encoding,
Expand Down Expand Up @@ -1630,15 +1611,71 @@ fn convert_implant_to_runtime_config(
})
}

/// Patch the fixed-length runtime config blob slot of a compiled binary.
///
/// Shared by `tool patch` (when a config source is given) and `tool patch-config`.
fn patch_runtime_config_blob(
file: &str,
config: Option<&String>,
from_implant: Option<&String>,
blob: Option<&String>,
output: Option<&String>,
) -> anyhow::Result<()> {
use crate::tool::patch::{patch_config_blob, ConfigBlobPatchOptions};
use malefic_config::{encode_runtime_config, CONFIG_BLOB_B64_LEN};

log_step!("Patching runtime config blob in {}", file);

let blob_str = if let Some(b64) = blob {
normalize_blob_string(b64, CONFIG_BLOB_B64_LEN)?
} else {
let cfg = if let Some(path) = from_implant {
let implant = load_yaml_config(path)?;
convert_implant_to_runtime_config(&implant)?
} else {
let cfg_path = config.ok_or_else(|| {
anyhow::anyhow!("--config, --from-implant or --blob must be provided")
})?;
load_runtime_config_from_file(cfg_path)?
};
encode_runtime_config(&cfg)
.map_err(|e| anyhow::anyhow!("encode_runtime_config failed: {:?}", e))?
};

let opts = ConfigBlobPatchOptions {
file: file.to_string(),
blob_b64: blob_str,
blob_len: CONFIG_BLOB_B64_LEN,
output: output.cloned(),
};

let outcome = patch_config_blob(&opts)?;
let overwritten = outcome.output_path == std::path::PathBuf::from(file);

log_success!(
"Patched runtime config blob at offset 0x{:X} ({} decimal)",
outcome.offset,
outcome.offset
);
if overwritten {
log_info!("Original file overwritten in-place");
} else {
log_info!("Patched file written to {}", outcome.output_path.display());
}
Ok(())
}

fn normalize_blob_string(raw: &str, expected_len: usize) -> anyhow::Result<String> {
const PREFIX: &str = "CFGv3B64";
use malefic_config::CONFIG_BLOB_PREFIX;

let prefix = std::str::from_utf8(CONFIG_BLOB_PREFIX).expect("config blob prefix is utf8");

let cleaned: String = raw.chars().filter(|c| !c.is_whitespace()).collect();
let mut normalized = if cleaned.starts_with(PREFIX) {
let mut normalized = if cleaned.starts_with(prefix) {
cleaned
} else {
let mut out = String::with_capacity(PREFIX.len() + cleaned.len());
out.push_str(PREFIX);
let mut out = String::with_capacity(prefix.len() + cleaned.len());
out.push_str(prefix);
out.push_str(&cleaned);
out
};
Expand Down
9 changes: 7 additions & 2 deletions malefic-mutant/src/tool/patch.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use anyhow::{anyhow, bail, Context, Result};
use clap::ValueEnum;
use malefic_config::CONFIG_BLOB_PREFIX;
use std::fs;
use std::path::{Path, PathBuf};

const BLOCK_LEN: usize = 64;
const CONFIG_BLOB_PREFIX: &[u8] = b"CFGv3B64";

#[derive(Clone, Debug, ValueEnum)]
pub enum PatchField {
Expand Down Expand Up @@ -419,7 +419,12 @@ pub fn patch_config_blob(opts: &ConfigBlobPatchOptions) -> Result<ConfigBlobPatc
}
})
.next()
.ok_or_else(|| anyhow!("could not find valid config slot (CFGv3B64 + padding/base64)"))?;
.ok_or_else(|| {
anyhow!(
"could not find valid config slot ({} + padding/base64)",
std::str::from_utf8(CONFIG_BLOB_PREFIX).unwrap()
)
})?;

data[start..end].copy_from_slice(opts.blob_b64.as_bytes());

Expand Down
Loading