use alloc::vec::Vec;
use super::{DetectionContext, Severity};
#[derive(Debug, Clone)]
pub struct HeuristicResult {
pub id: u32,
pub name: &'static str,
pub confidence: u8,
pub severity: Severity,
pub description: &'static str,
}
pub mod cmdline {
use super::*;
pub fn check_encoded_powershell(cmdline: &[u16]) -> Option<HeuristicResult> {
let cmdline_str = String::from_utf16_lossy(cmdline);
let lower = cmdline_str.to_lowercase();
if lower.contains("-encodedcommand") || lower.contains("-enc ") || lower.contains(" -e ") {
if lower.contains("powershell") {
return Some(HeuristicResult {
id: 1001,
name: "EncodedPowerShell",
confidence: 85,
severity: Severity::High,
description: "PowerShell with encoded command detected",
});
}
}
None
}
pub fn check_download_cradle(cmdline: &[u16]) -> Option<HeuristicResult> {
let cmdline_str = String::from_utf16_lossy(cmdline);
let lower = cmdline_str.to_lowercase();
let patterns = [
"downloadstring",
"downloadfile",
"wget",
"curl",
"invoke-webrequest",
"iwr ",
"bits transfer",
"certutil -urlcache",
"bitsadmin /transfer",
];
for pattern in &patterns {
if lower.contains(pattern) {
return Some(HeuristicResult {
id: 1002,
name: "DownloadCradle",
confidence: 70,
severity: Severity::Medium,
description: "Potential download cradle detected",
});
}
}
None
}
pub fn check_obfuscation(cmdline: &[u16]) -> Option<HeuristicResult> {
let cmdline_str = String::from_utf16_lossy(cmdline);
let caret_count = cmdline_str.matches('^').count();
if caret_count > 5 {
return Some(HeuristicResult {
id: 1003,
name: "CaretObfuscation",
confidence: 75,
severity: Severity::Medium,
description: "Command line caret obfuscation detected",
});
}
if cmdline_str.contains("'+'") || cmdline_str.contains("\"+\"") {
return Some(HeuristicResult {
id: 1004,
name: "StringConcatenation",
confidence: 60,
severity: Severity::Low,
description: "Command line string concatenation detected",
});
}
None
}
pub fn check_credential_access(cmdline: &[u16]) -> Option<HeuristicResult> {
let cmdline_str = String::from_utf16_lossy(cmdline);
let lower = cmdline_str.to_lowercase();
let patterns = [
"sekurlsa",
"lsadump",
"procdump",
"comsvcs.dll",
"minidump",
"ntdsutil",
"shadowcopy",
];
for pattern in &patterns {
if lower.contains(pattern) {
return Some(HeuristicResult {
id: 1005,
name: "CredentialAccess",
confidence: 90,
severity: Severity::Critical,
description: "Potential credential dumping detected",
});
}
}
None
}
use alloc::string::String;
}
pub mod filepath {
use super::*;
pub fn check_suspicious_location(path: &[u16]) -> Option<HeuristicResult> {
let path_str = String::from_utf16_lossy(path);
let lower = path_str.to_lowercase();
if (lower.contains("\\temp\\") || lower.contains("\\tmp\\"))
&& (lower.ends_with(".exe") || lower.ends_with(".dll") || lower.ends_with(".scr"))
{
return Some(HeuristicResult {
id: 2001,
name: "TempExecutable",
confidence: 65,
severity: Severity::Medium,
description: "Executable in temporary directory",
});
}
let suspicious_doubles = [".doc.exe", ".pdf.exe", ".txt.exe", ".jpg.exe"];
for ext in &suspicious_doubles {
if lower.ends_with(ext) {
return Some(HeuristicResult {
id: 2002,
name: "DoubleExtension",
confidence: 85,
severity: Severity::High,
description: "Suspicious double file extension",
});
}
}
if lower.contains("\\appdata\\") && lower.ends_with(".exe") {
return Some(HeuristicResult {
id: 2003,
name: "AppDataExecutable",
confidence: 50,
severity: Severity::Low,
description: "Executable in AppData directory",
});
}
None
}
pub fn check_suspicious_name(path: &[u16]) -> Option<HeuristicResult> {
let path_str = String::from_utf16_lossy(path);
let lower = path_str.to_lowercase();
let filename = lower.rsplit('\\').next().unwrap_or(&lower);
if filename.len() > 8 {
let alpha_count = filename.chars().filter(|c| c.is_alphabetic()).count();
let digit_count = filename.chars().filter(|c| c.is_numeric()).count();
if digit_count > alpha_count && digit_count > 4 {
return Some(HeuristicResult {
id: 2004,
name: "RandomFileName",
confidence: 55,
severity: Severity::Low,
description: "Potentially randomly generated filename",
});
}
}
let known_bad = ["mimikatz", "lazagne", "rubeus", "seatbelt"];
for name in &known_bad {
if filename.contains(name) {
return Some(HeuristicResult {
id: 2005,
name: "KnownMalwareName",
confidence: 95,
severity: Severity::Critical,
description: "Known malware tool name detected",
});
}
}
None
}
use alloc::string::String;
}
pub mod registry {
use super::*;
pub fn check_persistence_key(key_path: &[u16]) -> Option<HeuristicResult> {
let key_str = String::from_utf16_lossy(key_path);
let lower = key_str.to_lowercase();
let persistence_keys = [
"\\software\\microsoft\\windows\\currentversion\\run",
"\\software\\microsoft\\windows\\currentversion\\runonce",
"\\software\\microsoft\\windows\\currentversion\\policies\\explorer\\run",
"\\software\\microsoft\\windows nt\\currentversion\\winlogon",
"\\software\\microsoft\\windows nt\\currentversion\\image file execution options",
"\\system\\currentcontrolset\\services",
"\\software\\classes\\clsid",
"\\software\\classes\\\\shell\\open\\command",
];
for key in &persistence_keys {
if lower.contains(key) {
return Some(HeuristicResult {
id: 3001,
name: "PersistenceRegistry",
confidence: 75,
severity: Severity::Medium,
description: "Modification to persistence registry key",
});
}
}
None
}
pub fn check_security_tampering(key_path: &[u16], _value: &[u8]) -> Option<HeuristicResult> {
let key_str = String::from_utf16_lossy(key_path);
let lower = key_str.to_lowercase();
let security_keys = [
"\\software\\policies\\microsoft\\windows defender",
"\\software\\microsoft\\amsi",
"\\system\\currentcontrolset\\control\\lsa",
];
for key in &security_keys {
if lower.contains(key) {
return Some(HeuristicResult {
id: 3002,
name: "SecurityTampering",
confidence: 85,
severity: Severity::High,
description: "Potential security product tampering",
});
}
}
None
}
use alloc::string::String;
}
pub mod network {
use super::*;
pub fn check_suspicious_ip(ip: u32) -> Option<HeuristicResult> {
let _is_private = (ip & 0xFF000000 == 0x0A000000) || (ip & 0xFFF00000 == 0xAC100000) || (ip & 0xFFFF0000 == 0xC0A80000);
None
}
pub fn check_suspicious_port(port: u16) -> Option<HeuristicResult> {
let suspicious_ports = [
4444, 5555, 1337, 31337, 6666, 6667, 8080, 8443, ];
if suspicious_ports.contains(&port) {
return Some(HeuristicResult {
id: 4001,
name: "SuspiciousPort",
confidence: 60,
severity: Severity::Medium,
description: "Connection to commonly malicious port",
});
}
None
}
pub fn check_beaconing(
connection_times: &[u64],
_threshold_variance: f64,
) -> Option<HeuristicResult> {
if connection_times.len() < 5 {
return None;
}
let mut intervals: Vec<i64> = Vec::new();
for i in 1..connection_times.len() {
intervals.push((connection_times[i] - connection_times[i - 1]) as i64);
}
if intervals.len() > 2 {
let mean: f64 = intervals.iter().sum::<i64>() as f64 / intervals.len() as f64;
let variance: f64 = intervals
.iter()
.map(|&x| {
let diff = x as f64 - mean;
diff * diff
})
.sum::<f64>()
/ intervals.len() as f64;
let cv = libm::sqrt(variance) / mean;
if cv < 0.1 && intervals.len() >= 5 {
return Some(HeuristicResult {
id: 4002,
name: "BeaconingBehavior",
confidence: 80,
severity: Severity::High,
description: "Regular network beaconing pattern detected",
});
}
}
None
}
}
pub fn run_all_heuristics(context: &DetectionContext) -> Vec<HeuristicResult> {
let mut results = Vec::new();
if let Some(r) = cmdline::check_encoded_powershell(&context.command_line) {
results.push(r);
}
if let Some(r) = cmdline::check_download_cradle(&context.command_line) {
results.push(r);
}
if let Some(r) = cmdline::check_obfuscation(&context.command_line) {
results.push(r);
}
if let Some(r) = cmdline::check_credential_access(&context.command_line) {
results.push(r);
}
if let Some(r) = filepath::check_suspicious_location(&context.image_path) {
results.push(r);
}
if let Some(r) = filepath::check_suspicious_name(&context.image_path) {
results.push(r);
}
results
}