1use super::{DetectionCategory, RecommendedAction, ScanResult, Severity};
11use serde::{Deserialize, Serialize};
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::Arc;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct MemoryScanConfig {
18 pub scan_interval_secs: u64,
19 pub scan_suspicious_only: bool,
20 pub max_region_size: u64,
21}
22
23impl Default for MemoryScanConfig {
24 fn default() -> Self {
25 Self {
26 scan_interval_secs: 30,
27 scan_suspicious_only: true,
28 max_region_size: 10_485_760, }
30 }
31}
32
33#[derive(Debug, Clone)]
35pub struct ShellcodePattern {
36 pub name: String,
37 pub pattern: Vec<u8>,
38 pub mask: Vec<u8>, pub severity: Severity,
40}
41
42#[derive(Debug, Clone)]
44pub struct MapsEntry {
45 pub start_addr: u64,
46 pub end_addr: u64,
47 pub perms: String,
48 pub offset: u64,
49 pub path: String,
50 pub is_rwx: bool,
51 pub is_anonymous: bool,
52}
53
54pub struct MemoryScanner {
56 config: MemoryScanConfig,
57 shellcode_patterns: Vec<ShellcodePattern>,
58 running: Arc<AtomicBool>,
59}
60
61impl MemoryScanner {
62 pub fn new(config: MemoryScanConfig) -> Self {
63 Self {
64 config,
65 shellcode_patterns: builtin_patterns(),
66 running: Arc::new(AtomicBool::new(true)),
67 }
68 }
69
70 pub fn parse_maps_line(line: &str) -> Option<MapsEntry> {
72 let line = line.trim();
73 if line.is_empty() {
74 return None;
75 }
76
77 let fields: Vec<&str> = line.splitn(6, char::is_whitespace).collect();
78 if fields.len() < 5 {
79 return None;
80 }
81
82 let addr_parts: Vec<&str> = fields[0].split('-').collect();
84 if addr_parts.len() != 2 {
85 return None;
86 }
87 let start_addr = u64::from_str_radix(addr_parts[0], 16).ok()?;
88 let end_addr = u64::from_str_radix(addr_parts[1], 16).ok()?;
89
90 let perms = fields[1].to_string();
92 let is_rwx = perms.contains('r') && perms.contains('w') && perms.contains('x');
93
94 let offset = u64::from_str_radix(fields[2], 16).unwrap_or(0);
96
97 let path = if fields.len() >= 6 {
99 fields[5].trim().to_string()
100 } else {
101 String::new()
102 };
103
104 let is_anonymous = path.is_empty() || path.starts_with('[');
105
106 Some(MapsEntry {
107 start_addr,
108 end_addr,
109 perms,
110 offset,
111 path,
112 is_rwx,
113 is_anonymous,
114 })
115 }
116
117 pub fn parse_maps(content: &str) -> Vec<MapsEntry> {
119 content
120 .lines()
121 .filter_map(|line| Self::parse_maps_line(line))
122 .collect()
123 }
124
125 pub fn find_rwx_regions(pid: u32) -> Vec<MapsEntry> {
127 let maps_path = format!("/proc/{}/maps", pid);
128 let content = match std::fs::read_to_string(&maps_path) {
129 Ok(c) => c,
130 Err(_) => return Vec::new(),
131 };
132
133 Self::parse_maps(&content)
134 .into_iter()
135 .filter(|e| e.is_rwx)
136 .collect()
137 }
138
139 pub fn pattern_match(data: &[u8], pattern: &[u8], mask: &[u8]) -> Vec<usize> {
142 if pattern.is_empty() || data.len() < pattern.len() || mask.len() != pattern.len() {
143 return Vec::new();
144 }
145
146 let mut matches = Vec::new();
147 for i in 0..=(data.len() - pattern.len()) {
148 let mut matched = true;
149 for j in 0..pattern.len() {
150 if (data[i + j] & mask[j]) != (pattern[j] & mask[j]) {
151 matched = false;
152 break;
153 }
154 }
155 if matched {
156 matches.push(i);
157 }
158 }
159 matches
160 }
161
162 pub fn scan_process_memory(&self, pid: u32) -> Vec<ScanResult> {
164 let mut results = Vec::new();
165
166 let rwx_regions = Self::find_rwx_regions(pid);
167
168 for region in &rwx_regions {
170 if region.is_anonymous {
171 results.push(ScanResult::new(
172 "memory_scanner",
173 format!("pid:{} region:0x{:x}-0x{:x}", pid, region.start_addr, region.end_addr),
174 Severity::Medium,
175 DetectionCategory::MemoryAnomaly {
176 pid,
177 region: format!("0x{:x}-0x{:x}", region.start_addr, region.end_addr),
178 },
179 format!(
180 "Anonymous RWX memory region at 0x{:x}-0x{:x} ({} bytes) — uncommon in legitimate processes",
181 region.start_addr,
182 region.end_addr,
183 region.end_addr - region.start_addr
184 ),
185 0.6,
186 RecommendedAction::Alert,
187 ));
188 }
189
190 let region_size = region.end_addr - region.start_addr;
192 if region_size > self.config.max_region_size {
193 continue;
194 }
195
196 let mem_path = format!("/proc/{}/mem", pid);
197 let data = match read_proc_mem(&mem_path, region.start_addr, region_size as usize) {
198 Some(d) => d,
199 None => continue, };
201
202 for pattern in &self.shellcode_patterns {
204 let offsets = Self::pattern_match(&data, &pattern.pattern, &pattern.mask);
205 if !offsets.is_empty() {
206 results.push(ScanResult::new(
207 "memory_scanner",
208 format!("pid:{} region:0x{:x}", pid, region.start_addr + offsets[0] as u64),
209 pattern.severity,
210 DetectionCategory::FilelessMalware {
211 technique: pattern.name.clone(),
212 },
213 format!(
214 "Shellcode pattern '{}' found at {} offsets in RWX memory of PID {} — possible code injection",
215 pattern.name, offsets.len(), pid
216 ),
217 0.85,
218 RecommendedAction::KillProcess { pid },
219 ));
220 }
221 }
222 }
223
224 results
225 }
226
227 pub fn scan_all_processes(&self) -> Vec<ScanResult> {
229 let mut results = Vec::new();
230
231 let entries = match std::fs::read_dir("/proc") {
232 Ok(e) => e,
233 Err(_) => return results,
234 };
235
236 for entry in entries.flatten() {
237 let name = entry.file_name();
238 let pid: u32 = match name.to_string_lossy().parse() {
239 Ok(p) => p,
240 Err(_) => continue,
241 };
242
243 let mut r = self.scan_process_memory(pid);
244 results.append(&mut r);
245 }
246
247 results
248 }
249
250 pub fn start(
252 self: Arc<Self>,
253 detection_tx: tokio::sync::mpsc::UnboundedSender<ScanResult>,
254 ) -> tokio::task::JoinHandle<()> {
255 let running = Arc::clone(&self.running);
256 let interval_secs = self.config.scan_interval_secs;
257
258 tokio::spawn(async move {
259 let mut interval =
260 tokio::time::interval(std::time::Duration::from_secs(interval_secs));
261
262 while running.load(Ordering::Relaxed) {
263 interval.tick().await;
264 let results = self.scan_all_processes();
265 for result in results {
266 if detection_tx.send(result).is_err() {
267 return;
268 }
269 }
270 }
271 })
272 }
273
274 pub fn stop(&self) {
275 self.running.store(false, Ordering::Relaxed);
276 }
277}
278
279fn builtin_patterns() -> Vec<ShellcodePattern> {
281 vec![
282 ShellcodePattern {
283 name: "x86_64_syscall_preamble".to_string(),
284 pattern: vec![0x48, 0x31, 0xc0, 0x48, 0x31, 0xff],
285 mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
286 severity: Severity::High,
287 },
288 ShellcodePattern {
289 name: "x86_int80_shellcode".to_string(),
290 pattern: vec![0x31, 0xc0, 0x50, 0x68],
291 mask: vec![0xFF, 0xFF, 0xFF, 0xFF],
292 severity: Severity::High,
293 },
294 ShellcodePattern {
295 name: "nop_sled_16".to_string(),
296 pattern: vec![0x90; 16],
297 mask: vec![0xFF; 16],
298 severity: Severity::Medium,
299 },
300 ShellcodePattern {
301 name: "reverse_tcp_socket".to_string(),
302 pattern: vec![0x6a, 0x29, 0x58, 0x6a, 0x02],
303 mask: vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
304 severity: Severity::Critical,
305 },
306 ShellcodePattern {
307 name: "meterpreter_marker".to_string(),
308 pattern: b"meterpreter".to_vec(),
309 mask: vec![0xFF; 11],
310 severity: Severity::Critical,
311 },
312 ShellcodePattern {
313 name: "metasploit_marker".to_string(),
314 pattern: b"metasploit".to_vec(),
315 mask: vec![0xFF; 10],
316 severity: Severity::Critical,
317 },
318 ShellcodePattern {
319 name: "cobalt_strike_beacon".to_string(),
320 pattern: b"beacon.dll".to_vec(),
321 mask: vec![0xFF; 10],
322 severity: Severity::Critical,
323 },
324 ]
325}
326
327fn read_proc_mem(path: &str, offset: u64, size: usize) -> Option<Vec<u8>> {
329 use std::io::{Read, Seek, SeekFrom};
330 let mut file = std::fs::File::open(path).ok()?;
331 file.seek(SeekFrom::Start(offset)).ok()?;
332 let mut buf = vec![0u8; size];
333 let n = file.read(&mut buf).ok()?;
334 buf.truncate(n);
335 Some(buf)
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn parse_maps_line_normal() {
344 let line = "7f1234000000-7f1234001000 r-xp 00000000 08:01 12345 /usr/bin/cat";
345 let entry = MemoryScanner::parse_maps_line(line).unwrap();
346 assert_eq!(entry.start_addr, 0x7f1234000000);
347 assert_eq!(entry.end_addr, 0x7f1234001000);
348 assert_eq!(entry.perms, "r-xp");
349 assert!(!entry.is_rwx);
350 assert!(!entry.is_anonymous);
351 assert_eq!(entry.path, "/usr/bin/cat");
352 }
353
354 #[test]
355 fn parse_maps_line_rwx_anonymous() {
356 let line = "7ffc00000000-7ffc00010000 rwxp 00000000 00:00 0";
357 let entry = MemoryScanner::parse_maps_line(line).unwrap();
358 assert!(entry.is_rwx);
359 assert!(entry.is_anonymous);
360 }
361
362 #[test]
363 fn parse_maps_line_heap() {
364 let line = "55a000000000-55a000100000 rw-p 00000000 00:00 0 [heap]";
365 let entry = MemoryScanner::parse_maps_line(line).unwrap();
366 assert!(!entry.is_rwx); assert!(entry.is_anonymous); }
369
370 #[test]
371 fn pattern_match_exact() {
372 let data = vec![0x00, 0x48, 0x31, 0xc0, 0x48, 0x31, 0xff, 0x00];
373 let pattern = vec![0x48, 0x31, 0xc0, 0x48, 0x31, 0xff];
374 let mask = vec![0xFF; 6];
375 let matches = MemoryScanner::pattern_match(&data, &pattern, &mask);
376 assert_eq!(matches, vec![1]);
377 }
378
379 #[test]
380 fn pattern_match_with_wildcard() {
381 let data = vec![0x48, 0x31, 0xAA, 0x48, 0x31, 0xBB];
382 let pattern = vec![0x48, 0x31, 0x00, 0x48, 0x31, 0x00];
383 let mask = vec![0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00]; let matches = MemoryScanner::pattern_match(&data, &pattern, &mask);
385 assert_eq!(matches, vec![0]);
386 }
387
388 #[test]
389 fn nop_sled_detection() {
390 let mut data = vec![0x00; 100];
391 for i in 20..36 {
393 data[i] = 0x90;
394 }
395 let pattern = vec![0x90; 16];
396 let mask = vec![0xFF; 16];
397 let matches = MemoryScanner::pattern_match(&data, &pattern, &mask);
398 assert_eq!(matches, vec![20]);
399 }
400
401 #[test]
402 fn meterpreter_detection() {
403 let data = b"some data meterpreter session more data";
404 let pattern = b"meterpreter".to_vec();
405 let mask = vec![0xFF; 11];
406 let matches = MemoryScanner::pattern_match(data, &pattern, &mask);
407 assert!(!matches.is_empty());
408 }
409
410 #[test]
411 fn no_false_positive_on_clean_data() {
412 let data = b"This is perfectly normal program text without any shellcode.";
413 let scanner = MemoryScanner::new(MemoryScanConfig::default());
414 for pattern in &scanner.shellcode_patterns {
415 let matches =
416 MemoryScanner::pattern_match(data, &pattern.pattern, &pattern.mask);
417 assert!(
418 matches.is_empty(),
419 "False positive for pattern '{}'",
420 pattern.name
421 );
422 }
423 }
424
425 #[test]
426 fn pattern_match_empty() {
427 assert!(MemoryScanner::pattern_match(&[], &[0x90], &[0xFF]).is_empty());
428 assert!(MemoryScanner::pattern_match(&[0x90], &[], &[]).is_empty());
429 }
430
431 #[test]
432 fn config_defaults() {
433 let config = MemoryScanConfig::default();
434 assert_eq!(config.scan_interval_secs, 30);
435 assert!(config.scan_suspicious_only);
436 assert!(config.max_region_size > 0);
437 }
438}