1use memf_core::object_reader::ObjectReader;
13use memf_format::PhysicalMemoryProvider;
14
15use crate::{ProcessInfo, Result};
16
17const MAX_ENV_SIZE: u64 = 64 * 1024;
19
20#[derive(Debug, Clone, serde::Serialize)]
22pub struct LdPreloadInfo {
23 pub pid: u32,
25 pub process_name: String,
27 pub ld_preload_value: String,
29 pub preloaded_libraries: Vec<String>,
31 pub is_suspicious: bool,
33}
34
35fn parse_ld_preload(value: &str) -> Vec<String> {
40 value
41 .split(|c: char| c == ':' || c.is_ascii_whitespace())
42 .filter(|s| !s.is_empty())
43 .map(String::from)
44 .collect()
45}
46
47pub use crate::heuristics::classify_ld_preload;
55
56pub fn scan_ld_preload<P: PhysicalMemoryProvider>(
66 reader: &ObjectReader<P>,
67 processes: &[ProcessInfo],
68) -> Result<Vec<LdPreloadInfo>> {
69 if processes.is_empty() {
70 return Ok(Vec::new());
71 }
72
73 let mut results = Vec::new();
74
75 for proc in processes {
76 if let Some(info) = scan_process_ld_preload(reader, proc) {
77 results.push(info);
78 }
79 }
80
81 Ok(results)
82}
83
84fn scan_process_ld_preload<P: PhysicalMemoryProvider>(
89 reader: &ObjectReader<P>,
90 proc: &ProcessInfo,
91) -> Option<LdPreloadInfo> {
92 let mm_ptr: u64 = reader.read_field(proc.vaddr, "task_struct", "mm").ok()?;
94 if mm_ptr == 0 {
95 return None; }
97
98 let env_start: u64 = reader.read_field(mm_ptr, "mm_struct", "env_start").ok()?;
100 let env_end: u64 = reader.read_field(mm_ptr, "mm_struct", "env_end").ok()?;
101
102 if env_start == 0 || env_end <= env_start {
103 return None;
104 }
105
106 let size = (env_end - env_start).min(MAX_ENV_SIZE);
107 let data = reader.read_bytes(env_start, size as usize).ok()?;
108
109 let ld_preload_value = extract_ld_preload(&data)?;
111
112 let preloaded_libraries = parse_ld_preload(&ld_preload_value);
113 let is_suspicious = classify_ld_preload(&ld_preload_value);
114
115 Some(LdPreloadInfo {
116 pid: proc.pid as u32,
117 process_name: proc.comm.clone(),
118 ld_preload_value,
119 preloaded_libraries,
120 is_suspicious,
121 })
122}
123
124fn extract_ld_preload(data: &[u8]) -> Option<String> {
129 const PREFIX: &[u8] = b"LD_PRELOAD=";
130
131 for chunk in data.split(|&b| b == 0) {
132 if chunk.starts_with(PREFIX) {
133 let value = String::from_utf8_lossy(&chunk[PREFIX.len()..]);
134 let trimmed = value.trim();
135 if !trimmed.is_empty() {
136 return Some(trimmed.to_string());
137 }
138 }
139 }
140
141 None
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn is_suspicious_path(path: &str, safe_prefixes: &[&str]) -> bool {
150 if path.starts_with("/tmp/") || path == "/tmp" {
151 return true;
152 }
153 if path.starts_with("/dev/shm/") || path == "/dev/shm" {
154 return true;
155 }
156 if path
157 .split('/')
158 .any(|component| !component.is_empty() && component.starts_with('.'))
159 {
160 return true;
161 }
162 if !safe_prefixes.iter().any(|prefix| path.starts_with(prefix)) {
163 return true;
164 }
165 false
166 }
167
168 #[test]
173 fn parse_ld_preload_single() {
174 let result = parse_ld_preload("/usr/lib/libfoo.so");
175 assert_eq!(result, vec!["/usr/lib/libfoo.so"]);
176 }
177
178 #[test]
179 fn parse_ld_preload_multiple_colon() {
180 let result = parse_ld_preload("/lib/a.so:/lib/b.so");
181 assert_eq!(result, vec!["/lib/a.so", "/lib/b.so"]);
182 }
183
184 #[test]
185 fn parse_ld_preload_multiple_space() {
186 let result = parse_ld_preload("/lib/a.so /lib/b.so");
187 assert_eq!(result, vec!["/lib/a.so", "/lib/b.so"]);
188 }
189
190 #[test]
191 fn parse_ld_preload_mixed_delimiters() {
192 let result = parse_ld_preload("/lib/a.so:/lib/b.so /lib/c.so");
193 assert_eq!(result, vec!["/lib/a.so", "/lib/b.so", "/lib/c.so"]);
194 }
195
196 #[test]
197 fn parse_ld_preload_empty_string() {
198 let result = parse_ld_preload("");
199 assert!(result.is_empty());
200 }
201
202 #[test]
207 fn classify_benign_preload() {
208 assert!(
210 !classify_ld_preload("/usr/lib/libasan.so"),
211 "standard library path should not be suspicious"
212 );
213 }
214
215 #[test]
216 fn classify_benign_lib64() {
217 assert!(
218 !classify_ld_preload("/usr/lib64/libjemalloc.so"),
219 "/usr/lib64 should not be suspicious"
220 );
221 }
222
223 #[test]
224 fn classify_suspicious_tmp() {
225 assert!(
226 classify_ld_preload("/tmp/.hidden/rootkit.so"),
227 "/tmp path should be suspicious"
228 );
229 }
230
231 #[test]
232 fn classify_suspicious_devshm() {
233 assert!(
234 classify_ld_preload("/dev/shm/inject.so"),
235 "/dev/shm path should be suspicious"
236 );
237 }
238
239 #[test]
240 fn classify_suspicious_hidden_path() {
241 assert!(
242 classify_ld_preload("/home/user/.config/.evil/hook.so"),
243 "hidden path component should be suspicious"
244 );
245 }
246
247 #[test]
248 fn classify_suspicious_uncommon_location() {
249 assert!(
250 classify_ld_preload("/var/run/payload.so"),
251 "uncommon location should be suspicious"
252 );
253 }
254
255 #[test]
256 fn classify_multiple_with_one_suspicious() {
257 assert!(
259 classify_ld_preload("/usr/lib/libasan.so:/tmp/evil.so"),
260 "one suspicious library should flag the whole value"
261 );
262 }
263
264 #[test]
269 fn scan_ld_preload_empty() {
270 use memf_core::test_builders::PageTableBuilder;
272 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
273 use memf_symbols::isf::IsfResolver;
274 use memf_symbols::test_builders::IsfBuilder;
275
276 let json = IsfBuilder::new().build_json();
277 let resolver = IsfResolver::from_value(&json).unwrap();
278 let ptb = PageTableBuilder::new();
279 let (cr3, mem) = ptb.build();
280 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
281 let reader = ObjectReader::new(vas, Box::new(resolver));
282
283 let result = scan_ld_preload(&reader, &[]).unwrap();
284 assert!(
285 result.is_empty(),
286 "expected empty vec for empty process list"
287 );
288 }
289
290 #[test]
295 fn extract_ld_preload_finds_value() {
296 let env = b"PATH=/usr/bin\0LD_PRELOAD=/tmp/evil.so\0HOME=/root\0";
297 let result = extract_ld_preload(env);
298 assert_eq!(result.unwrap(), "/tmp/evil.so");
299 }
300
301 #[test]
302 fn extract_ld_preload_not_present_returns_none() {
303 let env = b"PATH=/usr/bin\0HOME=/root\0";
304 assert!(extract_ld_preload(env).is_none());
305 }
306
307 #[test]
308 fn extract_ld_preload_empty_value_returns_none() {
309 let env = b"LD_PRELOAD= \0OTHER=val\0";
311 assert!(
312 extract_ld_preload(env).is_none(),
313 "whitespace-only value must return None"
314 );
315 }
316
317 #[test]
318 fn extract_ld_preload_trims_whitespace() {
319 let env = b"LD_PRELOAD= /usr/lib/lib.so \0";
320 let result = extract_ld_preload(env);
321 assert_eq!(result.unwrap(), "/usr/lib/lib.so");
322 }
323
324 #[test]
329 fn is_suspicious_path_tmp_exact_is_suspicious() {
330 const SAFE: &[&str] = &["/usr/lib/"];
331 assert!(
332 is_suspicious_path("/tmp", SAFE),
333 "/tmp itself must be suspicious"
334 );
335 }
336
337 #[test]
338 fn is_suspicious_path_devshm_exact_is_suspicious() {
339 const SAFE: &[&str] = &["/usr/lib/"];
340 assert!(
341 is_suspicious_path("/dev/shm", SAFE),
342 "/dev/shm itself must be suspicious"
343 );
344 }
345
346 #[test]
347 fn is_suspicious_path_hidden_dotfile_is_suspicious() {
348 const SAFE: &[&str] = &["/usr/lib/"];
349 assert!(
350 is_suspicious_path("/home/user/.hidden.so", SAFE),
351 "dotfile must be suspicious"
352 );
353 }
354
355 #[test]
356 fn is_suspicious_path_safe_prefix_not_suspicious() {
357 const SAFE: &[&str] = &["/usr/lib/"];
358 assert!(!is_suspicious_path("/usr/lib/libasan.so", SAFE));
359 }
360
361 #[test]
362 fn is_suspicious_path_non_safe_non_tmp_non_hidden_is_suspicious() {
363 const SAFE: &[&str] = &["/usr/lib/"];
364 assert!(is_suspicious_path("/var/run/payload.so", SAFE));
366 }
367
368 #[test]
373 fn classify_lib_not_suspicious() {
374 assert!(!classify_ld_preload("/lib/libasan.so"));
375 }
376
377 #[test]
378 fn classify_lib64_not_suspicious() {
379 assert!(!classify_ld_preload("/lib64/libasan.so"));
380 }
381
382 #[test]
383 fn classify_lib32_not_suspicious() {
384 assert!(!classify_ld_preload("/lib32/libasan.so"));
385 }
386
387 #[test]
388 fn classify_usr_local_lib_not_suspicious() {
389 assert!(!classify_ld_preload("/usr/local/lib/libfoo.so"));
390 }
391
392 #[test]
393 fn classify_usr_local_lib64_not_suspicious() {
394 assert!(!classify_ld_preload("/usr/local/lib64/libfoo.so"));
395 }
396
397 #[test]
398 fn classify_usr_lib32_not_suspicious() {
399 assert!(!classify_ld_preload("/usr/lib32/libfoo.so"));
400 }
401
402 #[test]
407 fn scan_ld_preload_unreadable_task_skips_silently() {
408 use memf_core::test_builders::PageTableBuilder;
409 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
410 use memf_symbols::isf::IsfResolver;
411 use memf_symbols::test_builders::IsfBuilder;
412
413 let isf = IsfBuilder::new()
414 .add_struct("task_struct", 256)
415 .add_field("task_struct", "pid", 0, "int")
416 .add_field("task_struct", "mm", 8, "pointer")
417 .add_struct("mm_struct", 128)
418 .add_field("mm_struct", "env_start", 0, "unsigned long")
419 .add_field("mm_struct", "env_end", 8, "unsigned long")
420 .build_json();
421
422 let resolver = IsfResolver::from_value(&isf).unwrap();
423 let (cr3, mem) = PageTableBuilder::new().build();
424 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
425 let reader = ObjectReader::new(vas, Box::new(resolver));
426
427 let proc = ProcessInfo {
429 pid: 500,
430 ppid: 1,
431 comm: "bash".to_string(),
432 state: crate::types::ProcessState::Running,
433 vaddr: 0xDEAD_0000_0000_0000,
434 cr3: None,
435 start_time: 0,
436 };
437
438 let result = scan_ld_preload(&reader, &[proc]).unwrap();
439 assert!(
440 result.is_empty(),
441 "unreadable process must be silently skipped"
442 );
443 }
444
445 #[test]
446 fn ld_preload_info_serializes() {
447 let info = LdPreloadInfo {
448 pid: 42,
449 process_name: "bash".to_string(),
450 ld_preload_value: "/tmp/evil.so".to_string(),
451 preloaded_libraries: vec!["/tmp/evil.so".to_string()],
452 is_suspicious: true,
453 };
454 let json = serde_json::to_string(&info).unwrap();
455 assert!(json.contains("\"pid\":42"));
456 assert!(json.contains("\"is_suspicious\":true"));
457 }
458
459 #[test]
464 fn parse_ld_preload_consecutive_delimiters_filtered() {
465 let result = parse_ld_preload("/lib/a.so::/lib/b.so");
467 assert_eq!(result, vec!["/lib/a.so", "/lib/b.so"]);
468 }
469
470 #[test]
471 fn parse_ld_preload_tab_delimiter() {
472 let result = parse_ld_preload("/lib/a.so\t/lib/b.so");
473 assert_eq!(result, vec!["/lib/a.so", "/lib/b.so"]);
474 }
475
476 #[test]
481 fn scan_ld_preload_mm_null_skipped() {
482 use memf_core::test_builders::{flags as ptf, PageTableBuilder};
483 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
484 use memf_symbols::isf::IsfResolver;
485 use memf_symbols::test_builders::IsfBuilder;
486
487 let task_vaddr: u64 = 0xFFFF_8800_00D0_0000;
488 let task_paddr: u64 = 0x00D0_0000;
489
490 let isf = IsfBuilder::new()
491 .add_struct("task_struct", 0x200)
492 .add_field("task_struct", "pid", 0x00, "unsigned int")
493 .add_field("task_struct", "mm", 0x08, "pointer")
494 .add_struct("mm_struct", 0x100)
495 .add_field("mm_struct", "env_start", 0x00, "unsigned long")
496 .add_field("mm_struct", "env_end", 0x08, "unsigned long")
497 .build_json();
498 let resolver = IsfResolver::from_value(&isf).unwrap();
499
500 let mut task_page = [0u8; 4096];
502 task_page[0..4].copy_from_slice(&77u32.to_le_bytes()); let (cr3, mem) = PageTableBuilder::new()
506 .map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
507 .write_phys(task_paddr, &task_page)
508 .build();
509 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
510 let reader = ObjectReader::new(vas, Box::new(resolver));
511
512 let proc = ProcessInfo {
513 pid: 77,
514 ppid: 1,
515 comm: "kworker".to_string(),
516 state: crate::types::ProcessState::Running,
517 vaddr: task_vaddr,
518 cr3: None,
519 start_time: 0,
520 };
521
522 let result = scan_ld_preload(&reader, &[proc]).unwrap();
523 assert!(result.is_empty(), "kernel thread with mm=0 must be skipped");
524 }
525
526 #[test]
531 fn scan_ld_preload_env_block_with_ld_preload_produces_entry() {
532 use memf_core::test_builders::{flags as ptf, PageTableBuilder};
533 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
534 use memf_symbols::isf::IsfResolver;
535 use memf_symbols::test_builders::IsfBuilder;
536
537 let task_vaddr: u64 = 0xFFFF_8800_00D1_0000;
543 let task_paddr: u64 = 0x00D1_0000;
544 let mm_vaddr: u64 = 0xFFFF_8800_00D2_0000;
545 let mm_paddr: u64 = 0x00D2_0000;
546 let env_vaddr: u64 = 0xFFFF_8800_00D3_0000;
547 let env_paddr: u64 = 0x00D3_0000;
548
549 let env_data: &[u8] = b"PATH=/usr/bin\0LD_PRELOAD=/tmp/evil.so\0HOME=/root\0";
550 let env_end_vaddr = env_vaddr + env_data.len() as u64;
551
552 let isf = IsfBuilder::new()
553 .add_struct("task_struct", 0x200)
554 .add_field("task_struct", "pid", 0x00, "unsigned int")
555 .add_field("task_struct", "mm", 0x08, "pointer")
556 .add_struct("mm_struct", 0x100)
557 .add_field("mm_struct", "env_start", 0x00, "unsigned long")
558 .add_field("mm_struct", "env_end", 0x08, "unsigned long")
559 .build_json();
560 let resolver = IsfResolver::from_value(&isf).unwrap();
561
562 let mut task_page = [0u8; 4096];
564 task_page[0..4].copy_from_slice(&123u32.to_le_bytes()); task_page[8..16].copy_from_slice(&mm_vaddr.to_le_bytes());
566
567 let mut mm_page = [0u8; 4096];
569 mm_page[0..8].copy_from_slice(&env_vaddr.to_le_bytes());
570 mm_page[8..16].copy_from_slice(&env_end_vaddr.to_le_bytes());
571
572 let mut env_page = [0u8; 4096];
574 env_page[..env_data.len()].copy_from_slice(env_data);
575
576 let (cr3, mem) = PageTableBuilder::new()
577 .map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
578 .write_phys(task_paddr, &task_page)
579 .map_4k(mm_vaddr, mm_paddr, ptf::WRITABLE)
580 .write_phys(mm_paddr, &mm_page)
581 .map_4k(env_vaddr, env_paddr, ptf::WRITABLE)
582 .write_phys(env_paddr, &env_page)
583 .build();
584 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
585 let reader = ObjectReader::new(vas, Box::new(resolver));
586
587 let proc = ProcessInfo {
588 pid: 123,
589 ppid: 1,
590 comm: "evil_proc".to_string(),
591 state: crate::types::ProcessState::Running,
592 vaddr: task_vaddr,
593 cr3: None,
594 start_time: 0,
595 };
596
597 let result = scan_ld_preload(&reader, &[proc]).unwrap();
598 assert_eq!(result.len(), 1, "one LD_PRELOAD entry should be produced");
599 assert_eq!(result[0].ld_preload_value, "/tmp/evil.so");
600 assert_eq!(result[0].preloaded_libraries, vec!["/tmp/evil.so"]);
601 assert!(result[0].is_suspicious, "/tmp/ path must be suspicious");
602 assert_eq!(result[0].pid, 123);
603 }
604
605 #[test]
610 fn scan_ld_preload_empty_env_region_skipped() {
611 use memf_core::test_builders::{flags as ptf, PageTableBuilder};
612 use memf_core::vas::{TranslationMode, VirtualAddressSpace};
613 use memf_symbols::isf::IsfResolver;
614 use memf_symbols::test_builders::IsfBuilder;
615
616 let task_vaddr: u64 = 0xFFFF_8800_00D4_0000;
617 let task_paddr: u64 = 0x00D4_0000;
618 let mm_vaddr: u64 = 0xFFFF_8800_00D5_0000;
619 let mm_paddr: u64 = 0x00D5_0000;
620
621 let isf = IsfBuilder::new()
622 .add_struct("task_struct", 0x200)
623 .add_field("task_struct", "pid", 0x00, "unsigned int")
624 .add_field("task_struct", "mm", 0x08, "pointer")
625 .add_struct("mm_struct", 0x100)
626 .add_field("mm_struct", "env_start", 0x00, "unsigned long")
627 .add_field("mm_struct", "env_end", 0x08, "unsigned long")
628 .build_json();
629 let resolver = IsfResolver::from_value(&isf).unwrap();
630
631 let mut task_page = [0u8; 4096];
632 task_page[8..16].copy_from_slice(&mm_vaddr.to_le_bytes());
633
634 let mut mm_page = [0u8; 4096];
636 let same_addr: u64 = 0xFFFF_8800_00D6_0000;
637 mm_page[0..8].copy_from_slice(&same_addr.to_le_bytes()); mm_page[8..16].copy_from_slice(&same_addr.to_le_bytes()); let (cr3, mem) = PageTableBuilder::new()
641 .map_4k(task_vaddr, task_paddr, ptf::WRITABLE)
642 .write_phys(task_paddr, &task_page)
643 .map_4k(mm_vaddr, mm_paddr, ptf::WRITABLE)
644 .write_phys(mm_paddr, &mm_page)
645 .build();
646 let vas = VirtualAddressSpace::new(mem, cr3, TranslationMode::X86_64FourLevel);
647 let reader = ObjectReader::new(vas, Box::new(resolver));
648
649 let proc = ProcessInfo {
650 pid: 88,
651 ppid: 1,
652 comm: "proc88".to_string(),
653 state: crate::types::ProcessState::Running,
654 vaddr: task_vaddr,
655 cr3: None,
656 start_time: 0,
657 };
658
659 let result = scan_ld_preload(&reader, &[proc]).unwrap();
660 assert!(
661 result.is_empty(),
662 "env_start == env_end → empty env region → no entry"
663 );
664 }
665}