#[derive(Debug, Clone)]
pub struct CmdWindowsMutation {
pub payload: String,
pub description: String,
pub rules_applied: Vec<&'static str>,
}
#[must_use]
pub fn detect_type(payload: &str) -> bool {
let lower = payload.to_ascii_lowercase();
lower.contains("cmd ")
|| lower.contains("cmd.exe")
|| lower.contains('^')
|| lower.contains("for /f")
|| lower.contains("set /p")
|| lower.contains("%comspec%")
|| (lower.contains("type ") && !lower.contains("typeof"))
|| lower.contains("dir ")
|| lower.contains("findstr ")
|| lower.contains("powershell")
|| lower.contains("certutil")
}
fn caret_obfuscate(cmd: &str) -> String {
cmd.chars()
.map(|c| format!("{c}^"))
.collect::<String>()
.trim_end_matches('^')
.to_string()
}
#[must_use]
pub fn mutate(payload: &str, max_mutations: usize) -> Vec<CmdWindowsMutation> {
if payload.is_empty() || max_mutations == 0 || !detect_type(payload) {
return Vec::new();
}
let mut results = Vec::new();
let lower = payload.to_ascii_lowercase();
let cmds = ["type", "dir", "findstr", "certutil", "powershell", "cmd"];
for cmd in &cmds {
if lower.contains(cmd) {
let obf = caret_obfuscate(cmd);
let mutated = payload
.replace(cmd, &obf)
.replace(&cmd.to_ascii_uppercase(), &obf);
if mutated != payload {
results.push(CmdWindowsMutation {
payload: mutated,
description: format!("caret escape: {cmd} → {obf}"),
rules_applied: vec!["caret_escape"],
});
}
}
}
results.push(CmdWindowsMutation {
payload: format!("cmd /c \"{payload}\""),
description: "cmd /c wrapper".into(),
rules_applied: vec!["cmd_wrapper"],
});
results.push(CmdWindowsMutation {
payload: format!("cmd.exe /c {payload}"),
description: "cmd.exe /c wrapper".into(),
rules_applied: vec!["cmd_wrapper"],
});
results.push(CmdWindowsMutation {
payload: format!("%comspec% /c {payload}"),
description: "%COMSPEC% indirection".into(),
rules_applied: vec!["comspec"],
});
results.push(CmdWindowsMutation {
payload: format!("for /f \"tokens=*\" %a in ('{payload}') do %a"),
description: "for /f loop indirection".into(),
rules_applied: vec!["for_loop"],
});
results.push(CmdWindowsMutation {
payload: format!("set /p ={payload}<nul"),
description: "set /p redirection trick".into(),
rules_applied: vec!["set_p"],
});
results.push(CmdWindowsMutation {
payload: format!("{payload}\"\"\" "),
description: "quote escape padding".into(),
rules_applied: vec!["quote_escape"],
});
if !crate::grammar::cmd::is_structured_cmd(payload) {
let var_expansions = [
("%pATh%", "%PATH% expansion case-mixed"),
("%tMp%", "%TMP% expansion"),
("%wiNDir%", "%WINDIR% expansion"),
];
for (var, desc) in &var_expansions {
results.push(CmdWindowsMutation {
payload: format!("echo {var}"),
description: (*desc).into(),
rules_applied: vec!["var_expansion"],
});
}
}
results.push(CmdWindowsMutation {
payload: format!("powershell -nop -c \"{payload}\""),
description: "powershell -nop -c wrapper".into(),
rules_applied: vec!["ps_wrapper"],
});
results.truncate(max_mutations);
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_cmd_signals() {
assert!(detect_type("cmd /c dir"));
assert!(detect_type("t^y^p^e file.txt"));
assert!(detect_type("for /f \"tokens=*\" %a in ('dir') do %a"));
}
#[test]
fn caret_obfuscation_works() {
assert_eq!(caret_obfuscate("type"), "t^y^p^e");
assert_eq!(caret_obfuscate("dir"), "d^i^r");
}
#[test]
fn generates_cmd_wrapper() {
let mutations = mutate("dir C:\\", 10);
assert!(mutations.iter().any(|m| m.payload.contains("cmd /c")));
}
#[test]
fn generates_for_loop() {
let mutations = mutate("dir C:\\", 10);
assert!(mutations.iter().any(|m| m.payload.contains("for /f")));
}
#[test]
fn rejects_non_windows_cmd() {
assert!(!detect_type("; cat /etc/passwd"));
assert!(mutate("hello world", 10).is_empty());
}
#[test]
fn generates_comspec_variant() {
let mutations = mutate("dir C:\\", 10);
assert!(mutations.iter().any(|m| m.payload.contains("%comspec%")));
}
#[test]
fn generates_powershell_wrapper() {
let mutations = mutate("powershell Get-Process", 15);
assert!(
mutations
.iter()
.any(|m| m.payload.contains("powershell -nop"))
);
}
#[test]
fn max_mutations_respected() {
let mutations = mutate("dir C:\\", 3);
assert!(mutations.len() <= 3);
}
#[test]
fn set_p_trick_generated() {
let mutations = mutate("dir C:\\", 10);
assert!(mutations.iter().any(|m| m.payload.contains("set /p")));
}
#[test]
fn empty_payload_returns_empty() {
assert!(mutate("", 10).is_empty());
}
}