use std::collections::BTreeSet;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Decoded {
pub key: Vec<u8>,
pub offset: usize,
pub text: String,
}
impl Decoded {
pub fn key_hex(&self) -> String {
self.key.iter().map(|b| format!("{:02x}", b)).collect()
}
}
const KNOWN_MULTI_BYTE_KEYS: &[&[u8]] = &[
&[0xDE, 0xAD, 0xBE, 0xEF],
&[0xDE, 0xDE, 0xFF, 0xBA],
&[0xBA, 0xAD, 0xF0, 0x0D],
&[0x54, 0x76, 0x12, 0x9D],
&[0x37, 0x37, 0x37, 0x37],
&[0x22, 0x22, 0x22, 0x22],
];
#[inline]
fn is_printable(b: u8) -> bool {
(0x20..0x7f).contains(&b) || b == b'\t'
}
const SEED_WORDS: &[&str] = &[
"/bin", "/etc", "/proc", "/tmp", "/usr", "/var", "/dev", "/sys",
"/lib", "/run", "/home", "/mnt", "/opt", "/sbin",
"open", "read", "write", "fork", "exec", "kill", "connect",
"socket", "send", "recv", "fcntl", "ioctl", "ptrace", "getpid",
"shell", "telnet", "scan", "attack", "flood", "kworker",
"ksoftirqd", "ngrok", "router", "bot", "cnc", "loader", "crypt",
"http", "https", "ftp", "tcp", "udp", "irc", "smtp", "ssh",
"tor", "onion", "dns",
"admin", "root", "pass", "user", "login", "guest", "default",
"init", "systemd", "rc.d", "cron", "reboot", "shutdown",
"iptables", "busybox", "wget", "curl", "tftp",
"echo", "cat", "rm ", "ls ", "cp ", "mv ", "chmod", "chattr",
];
fn contains_seed_word(decoded: &[u8]) -> bool {
let lower: Vec<u8> = decoded.iter().map(|b| b.to_ascii_lowercase()).collect();
SEED_WORDS
.iter()
.any(|word| word_boundary_search(&lower, word.as_bytes()))
}
fn word_boundary_search(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || needle.len() > haystack.len() {
return false;
}
let first_alpha = needle[0].is_ascii_alphabetic();
let last_alpha = needle[needle.len() - 1].is_ascii_alphabetic();
haystack
.windows(needle.len())
.enumerate()
.any(|(i, w)| {
if w != needle {
return false;
}
let before_ok = !first_alpha
|| i == 0
|| !haystack[i - 1].is_ascii_alphabetic();
let after_idx = i + needle.len();
let after_ok = !last_alpha
|| after_idx >= haystack.len()
|| !haystack[after_idx].is_ascii_alphabetic();
before_ok && after_ok
})
}
pub fn brute_decode(data: &[u8], min_run: usize) -> Vec<Decoded> {
let mut hits: BTreeSet<(String, Vec<u8>)> = BTreeSet::new();
let mut out = Vec::new();
for key in 1u8..=255 {
scan_with_key(data, &[key], min_run, &mut hits, &mut out);
}
for k in KNOWN_MULTI_BYTE_KEYS {
scan_with_key(data, k, min_run, &mut hits, &mut out);
}
out.sort();
out
}
pub fn decode_with_key(data: &[u8], key: &[u8], min_run: usize) -> Vec<Decoded> {
let mut hits: BTreeSet<(String, Vec<u8>)> = BTreeSet::new();
let mut out = Vec::new();
scan_with_key(data, key, min_run, &mut hits, &mut out);
out.sort();
out
}
fn scan_with_key(
data: &[u8],
key: &[u8],
min_run: usize,
hits: &mut BTreeSet<(String, Vec<u8>)>,
out: &mut Vec<Decoded>,
) {
if key.is_empty() {
return;
}
let mut run_start: Option<usize> = None;
let mut i = 0;
while i < data.len() {
let dec = data[i] ^ key[i % key.len()];
if is_printable(dec) {
if run_start.is_none() {
run_start = Some(i);
}
} else if let Some(start) = run_start.take() {
if i - start >= min_run {
emit_run(data, start, i, key, hits, out);
}
}
i += 1;
}
if let Some(start) = run_start {
if data.len() - start >= min_run {
emit_run(data, start, data.len(), key, hits, out);
}
}
}
fn emit_run(
data: &[u8],
start: usize,
end: usize,
key: &[u8],
hits: &mut BTreeSet<(String, Vec<u8>)>,
out: &mut Vec<Decoded>,
) {
let printable_src = data[start..end].iter().filter(|&&b| is_printable(b)).count();
if printable_src * 2 >= end - start {
return;
}
let decoded: Vec<u8> = data[start..end]
.iter()
.enumerate()
.map(|(i, b)| b ^ key[(start + i) % key.len()])
.collect();
let letters = decoded
.iter()
.filter(|&&b| b.is_ascii_alphabetic())
.count();
if letters * 10 < decoded.len() * 7 {
return;
}
let mut seen = [false; 26];
for &b in &decoded {
if b.is_ascii_alphabetic() {
seen[(b.to_ascii_lowercase() - b'a') as usize] = true;
}
}
if seen.iter().filter(|x| **x).count() < 5 {
return;
}
if !contains_seed_word(&decoded) {
return;
}
let s = match std::str::from_utf8(&decoded) {
Ok(s) => s.to_string(),
Err(_) => return,
};
let dedupe_key = (s.clone(), key.to_vec());
if hits.insert(dedupe_key) {
out.push(Decoded {
key: key.to_vec(),
offset: start,
text: s,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_input_yields_nothing() {
assert!(brute_decode(&[], 8).is_empty());
}
#[test]
fn recovers_xor_string_substring() {
let plain = b"/usr/bin/wget http_payload";
let key = 0x77u8;
let encoded: Vec<u8> = plain.iter().map(|b| b ^ key).collect();
let hits = brute_decode(&encoded, 8);
let found = hits
.iter()
.any(|d| d.key == vec![key] && d.text.contains("/usr/bin/wget"));
assert!(found, "missed XOR=0x77 plaintext: {:?}", hits);
}
#[test]
fn skips_plaintext_input() {
let plain = b"this is just plaintext that any strings tool finds";
let hits = brute_decode(plain, 8);
assert!(
hits.is_empty(),
"emitted decode for plaintext source: {:?}",
hits
);
}
#[test]
fn min_run_floor_enforced() {
let plain = b"foo";
let key = 0x55u8;
let encoded: Vec<u8> = plain.iter().map(|b| b ^ key).collect();
let mut buf = vec![0x00];
buf.extend_from_slice(&encoded);
buf.push(0x00);
let hits = brute_decode(&buf, 8);
assert!(hits.iter().all(|d| d.text != "foo"));
}
#[test]
fn dedupe_avoids_duplicate_emit_per_key() {
let plain = b"shell_attack_handler_v1";
let key = 0xAAu8;
let sep = 0xAAu8;
let encoded: Vec<u8> = plain.iter().map(|b| b ^ key).collect();
let mut buf = Vec::new();
buf.extend_from_slice(&encoded);
buf.push(sep);
buf.extend_from_slice(&encoded);
let hits = brute_decode(&buf, 8);
let n = hits
.iter()
.filter(|d| d.key == vec![key] && d.text.contains("shell_attack_handler_v1"))
.count();
assert_eq!(n, 1, "expected single dedupe entry, got {}: {:?}", n, hits);
}
#[test]
fn recovers_mirai_4byte_xor() {
let plain = b"command_handler_attack_table_v1xxx";
let key = [0xDEu8, 0xDE, 0xFF, 0xBA];
let encoded: Vec<u8> = plain
.iter()
.enumerate()
.map(|(i, b)| b ^ key[i % 4])
.collect();
let hits = decode_with_key(&encoded, &key, 8);
let found = hits.iter().any(|d| d.text.contains("command_handler"));
assert!(found, "missed Mirai 4-byte decode: {:?}", hits);
assert_eq!(hits[0].key_hex(), "dedeffba");
}
#[test]
fn dictionary_filter_rejects_random_letters() {
let plain = b"abcdefghijklmnopqrstuvwxyz"; let key = 0xAAu8;
let encoded: Vec<u8> = plain.iter().map(|b| b ^ key).collect();
let hits = brute_decode(&encoded, 8);
let leaked = hits.iter().any(|d| {
d.text.contains("abcdefghij")
});
assert!(!leaked, "dictionary filter passed noise: {:?}", hits);
}
#[test]
fn known_keys_in_brute_decode() {
let plain = b"shell_command_killer_v2_payload";
let key = [0xBA, 0xAD, 0xF0, 0x0D];
let encoded: Vec<u8> = plain
.iter()
.enumerate()
.map(|(i, b)| b ^ key[i % 4])
.collect();
let hits = brute_decode(&encoded, 8);
let found = hits
.iter()
.any(|d| d.key == key.to_vec() && d.text.contains("shell_command"));
assert!(found, "brute_decode missed multi-byte key: {:?}", hits);
}
}