diff --git a/ghost-core/Cargo.toml b/ghost-core/Cargo.toml index 8184d34..5c48782 100644 --- a/ghost-core/Cargo.toml +++ b/ghost-core/Cargo.toml @@ -15,6 +15,7 @@ serde_json = "1.0" uuid = { version = "1.0", features = ["v4"] } toml = "0.8" chrono = "0.4" +yara = "0.28" [target.'cfg(windows)'.dependencies] windows = { version = "0.58", features = [ diff --git a/ghost-core/src/yara_engine.rs b/ghost-core/src/yara_engine.rs index 1f9dd70..9302e14 100644 --- a/ghost-core/src/yara_engine.rs +++ b/ghost-core/src/yara_engine.rs @@ -1,33 +1,28 @@ use crate::{GhostError, MemoryRegion, ProcessInfo}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; use std::time::SystemTime; +use yara::{Compiler, Rules, Scanner}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DynamicYaraEngine { - rules: Vec, - sources: Vec, + rules_path: Option, + #[serde(skip)] + compiled_rules: Option, + rule_metadata: Vec, scan_cache: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct YaraRule { +pub struct YaraRuleMetadata { pub name: String, - pub content: String, - pub source: String, + pub file_path: String, pub threat_level: ThreatLevel, pub last_updated: SystemTime, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct YaraRuleSource { - pub name: String, - pub url: String, - pub enabled: bool, - pub rule_count: usize, - pub last_update: SystemTime, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct YaraScanResult { pub matches: Vec, @@ -38,13 +33,15 @@ pub struct YaraScanResult { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RuleMatch { pub rule_name: String, + pub namespace: String, pub threat_level: ThreatLevel, pub offset: u64, pub length: u32, pub metadata: HashMap, + pub matched_strings: Vec, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum ThreatLevel { Info = 1, Low = 2, @@ -53,6 +50,22 @@ pub enum ThreatLevel { Critical = 5, } +impl ThreatLevel { + pub fn from_metadata(metadata: &HashMap) -> Self { + metadata + .get("threat_level") + .and_then(|s| match s.to_lowercase().as_str() { + "info" => Some(ThreatLevel::Info), + "low" => Some(ThreatLevel::Low), + "medium" => Some(ThreatLevel::Medium), + "high" => Some(ThreatLevel::High), + "critical" => Some(ThreatLevel::Critical), + _ => None, + }) + .unwrap_or(ThreatLevel::Medium) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] struct CachedScanResult { result: YaraScanResult, @@ -60,96 +73,334 @@ struct CachedScanResult { } impl DynamicYaraEngine { - pub fn new(_config_path: Option<&str>) -> Result { - let sources = vec![ - YaraRuleSource { - name: "Malware Bazaar".to_string(), - url: "https://bazaar.abuse.ch/browse/".to_string(), - enabled: true, - rule_count: 0, - last_update: SystemTime::now(), - }, - YaraRuleSource { - name: "VX-Underground".to_string(), - url: "https://vx-underground.org/yara".to_string(), - enabled: true, - rule_count: 0, - last_update: SystemTime::now(), - }, - ]; + /// Create a new YARA engine with optional custom rules directory + pub fn new(rules_path: Option<&str>) -> Result { + let rules_path = rules_path.map(PathBuf::from); - Ok(DynamicYaraEngine { - rules: Vec::new(), - sources, + let mut engine = DynamicYaraEngine { + rules_path, + compiled_rules: None, + rule_metadata: Vec::new(), scan_cache: HashMap::new(), - }) - } + }; - pub async fn update_rules(&mut self) -> Result { - let mut updated_count = 0; - - for source in &mut self.sources { - if !source.enabled { - continue; + // Attempt to load rules if path is provided + if engine.rules_path.is_some() { + if let Err(e) = engine.compile_rules() { + log::warn!("Failed to compile YARA rules: {:?}", e); } - - // Simulate rule download - let new_rules = vec![YaraRule { - name: format!("generic_malware_{}", updated_count + 1), - content: "rule generic_malware { condition: true }".to_string(), - source: source.name.clone(), - threat_level: ThreatLevel::Medium, - last_updated: SystemTime::now(), - }]; - - self.rules.extend(new_rules); - source.rule_count = self.rules.len(); - source.last_update = SystemTime::now(); - updated_count += 1; } - Ok(updated_count) + Ok(engine) } + /// Compile all YARA rules from the rules directory + pub fn compile_rules(&mut self) -> Result { + let rules_dir = self.rules_path.as_ref().ok_or_else(|| { + GhostError::ConfigurationError("No rules directory configured".to_string()) + })?; + + if !rules_dir.exists() { + return Err(GhostError::ConfigurationError(format!( + "Rules directory does not exist: {}", + rules_dir.display() + ))); + } + + let mut compiler = Compiler::new() + .map_err(|e| GhostError::ConfigurationError(format!("YARA compiler error: {}", e)))?; + + let mut rule_count = 0; + self.rule_metadata.clear(); + + // Recursively find and compile all .yar and .yara files + let rule_files = Self::find_rule_files(rules_dir)?; + + for rule_file in &rule_files { + match fs::read_to_string(rule_file) { + Ok(content) => { + let namespace = rule_file + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("default"); + + match compiler.add_rules_str_with_namespace(&content, namespace) { + Ok(_) => { + log::info!("Compiled YARA rule: {}", rule_file.display()); + + self.rule_metadata.push(YaraRuleMetadata { + name: namespace.to_string(), + file_path: rule_file.display().to_string(), + threat_level: ThreatLevel::Medium, + last_updated: SystemTime::now(), + }); + + rule_count += 1; + } + Err(e) => { + log::error!("Failed to compile {}: {}", rule_file.display(), e); + } + } + } + Err(e) => { + log::error!("Failed to read {}: {}", rule_file.display(), e); + } + } + } + + if rule_count == 0 { + return Err(GhostError::ConfigurationError( + "No YARA rules were successfully compiled".to_string(), + )); + } + + self.compiled_rules = Some( + compiler + .compile_rules() + .map_err(|e| GhostError::ConfigurationError(format!("Rule compilation failed: {}", e)))?, + ); + + log::info!("Successfully compiled {} YARA rules", rule_count); + Ok(rule_count) + } + + /// Find all YARA rule files in the given directory + fn find_rule_files(dir: &Path) -> Result, GhostError> { + let mut rule_files = Vec::new(); + + if !dir.is_dir() { + return Ok(rule_files); + } + + let entries = fs::read_dir(dir).map_err(|e| { + GhostError::ConfigurationError(format!("Failed to read rules directory: {}", e)) + })?; + + for entry in entries.flatten() { + let path = entry.path(); + + if path.is_file() { + if let Some(ext) = path.extension() { + if ext == "yar" || ext == "yara" { + rule_files.push(path); + } + } + } else if path.is_dir() { + // Recursively search subdirectories + rule_files.extend(Self::find_rule_files(&path)?); + } + } + + Ok(rule_files) + } + + /// Scan process memory regions with compiled YARA rules pub async fn scan_process( &self, - _process: &ProcessInfo, + process: &ProcessInfo, memory_regions: &[MemoryRegion], ) -> Result { let start_time = SystemTime::now(); - let mut matches = Vec::new(); - let mut bytes_scanned = 0; - // Simulate YARA scanning + let rules = self.compiled_rules.as_ref().ok_or_else(|| { + GhostError::ConfigurationError("YARA rules not compiled".to_string()) + })?; + + let mut all_matches = Vec::new(); + let mut bytes_scanned = 0u64; + + // Scan each executable memory region for region in memory_regions.iter() { - bytes_scanned += region.size; + // Only scan executable regions with reasonable size + if !region.protection.is_executable() { + continue; + } - // Simulate finding suspicious patterns - if region.protection.is_executable() && region.protection.is_writable() { - matches.push(RuleMatch { - rule_name: "suspicious_rwx_memory".to_string(), - threat_level: ThreatLevel::High, - offset: region.base_address as u64, - length: 1024, - metadata: HashMap::new(), - }); + if region.size > 100 * 1024 * 1024 { + log::debug!( + "Skipping large region at {:#x} (size: {} MB)", + region.base_address, + region.size / (1024 * 1024) + ); + continue; + } + + // Read memory content + let memory_content = match Self::read_process_memory(process.pid, region) { + Ok(data) => data, + Err(e) => { + log::debug!( + "Failed to read memory at {:#x}: {:?}", + region.base_address, + e + ); + continue; + } + }; + + bytes_scanned += memory_content.len() as u64; + + // Perform YARA scan on this memory region + match Self::scan_memory_with_yara(rules, &memory_content, region.base_address) { + Ok(mut matches) => { + all_matches.append(&mut matches); + } + Err(e) => { + log::debug!("YARA scan error at {:#x}: {:?}", region.base_address, e); + } } } let scan_time_ms = start_time.elapsed().unwrap_or_default().as_millis() as u64; Ok(YaraScanResult { - matches, + matches: all_matches, scan_time_ms, - bytes_scanned: bytes_scanned as u64, + bytes_scanned, }) } - pub fn get_rule_count(&self) -> usize { - self.rules.len() + /// Scan a memory buffer with YARA rules + fn scan_memory_with_yara( + rules: &Rules, + data: &[u8], + base_address: usize, + ) -> Result, GhostError> { + let mut scanner = Scanner::new(rules) + .map_err(|e| GhostError::ScanError(format!("Scanner creation failed: {}", e)))?; + + let scan_results = scanner + .scan_mem(data) + .map_err(|e| GhostError::ScanError(format!("Scan failed: {}", e)))?; + + let mut matches = Vec::new(); + + for rule in scan_results { + let rule_name = rule.identifier.to_string(); + let namespace = rule.namespace.to_string(); + + // Extract metadata + let mut metadata = HashMap::new(); + for meta in rule.metadatas { + let value = match meta.value { + yara::MetadataValue::Integer(i) => i.to_string(), + yara::MetadataValue::String(ref s) => s.clone(), + yara::MetadataValue::Boolean(b) => b.to_string(), + }; + metadata.insert(meta.identifier.to_string(), value); + } + + let threat_level = ThreatLevel::from_metadata(&metadata); + + // Extract matched strings + let mut matched_strings = Vec::new(); + for string in rule.strings { + let identifier = string.identifier.to_string(); + for m in string.matches { + matched_strings.push(format!( + "{} at offset {:#x}", + identifier, + base_address + m.offset + )); + + // Create a match entry for each string match + matches.push(RuleMatch { + rule_name: rule_name.clone(), + namespace: namespace.clone(), + threat_level, + offset: (base_address + m.offset) as u64, + length: m.length as u32, + metadata: metadata.clone(), + matched_strings: vec![identifier.clone()], + }); + } + } + } + + Ok(matches) } - pub fn get_sources(&self) -> &[YaraRuleSource] { - &self.sources + /// Read memory from a specific process and region + #[cfg(target_os = "windows")] + fn read_process_memory( + pid: u32, + region: &MemoryRegion, + ) -> Result, GhostError> { + use windows::Win32::Foundation::{CloseHandle, HANDLE}; + use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; + use windows::Win32::System::Threading::{OpenProcess, PROCESS_VM_READ}; + + unsafe { + let handle = OpenProcess(PROCESS_VM_READ, false, pid) + .map_err(|e| GhostError::MemoryReadError(format!("OpenProcess failed: {}", e)))?; + + let mut buffer = vec![0u8; region.size]; + let mut bytes_read = 0; + + let result = ReadProcessMemory( + handle, + region.base_address as *const _, + buffer.as_mut_ptr() as *mut _, + region.size, + Some(&mut bytes_read), + ); + + let _ = CloseHandle(handle); + + if result.is_ok() && bytes_read > 0 { + buffer.truncate(bytes_read); + Ok(buffer) + } else { + Err(GhostError::MemoryReadError( + "ReadProcessMemory failed".to_string(), + )) + } + } + } + + /// Read memory from a specific process and region (Linux implementation) + #[cfg(target_os = "linux")] + fn read_process_memory( + pid: u32, + region: &MemoryRegion, + ) -> Result, GhostError> { + use std::fs::File; + use std::io::{Read, Seek, SeekFrom}; + + let mem_path = format!("/proc/{}/mem", pid); + let mut file = File::open(&mem_path) + .map_err(|e| GhostError::MemoryReadError(format!("Failed to open {}: {}", mem_path, e)))?; + + file.seek(SeekFrom::Start(region.base_address as u64)) + .map_err(|e| GhostError::MemoryReadError(format!("Seek failed: {}", e)))?; + + let mut buffer = vec![0u8; region.size]; + file.read_exact(&mut buffer) + .map_err(|e| GhostError::MemoryReadError(format!("Read failed: {}", e)))?; + + Ok(buffer) + } + + /// Read memory from a specific process and region (macOS implementation) + #[cfg(target_os = "macos")] + fn read_process_memory( + _pid: u32, + _region: &MemoryRegion, + ) -> Result, GhostError> { + Err(GhostError::NotImplemented( + "Memory reading not implemented for macOS".to_string(), + )) + } + + pub fn get_rule_count(&self) -> usize { + self.rule_metadata.len() + } + + pub fn get_rule_metadata(&self) -> &[YaraRuleMetadata] { + &self.rule_metadata + } + + pub fn is_compiled(&self) -> bool { + self.compiled_rules.is_some() } }