forensicnomicon/heuristics/
paths.rs1pub const ZONE_LOCAL: u32 = 0;
6pub const ZONE_INTRANET: u32 = 1;
7pub const ZONE_TRUSTED: u32 = 2;
8pub const ZONE_INTERNET: u32 = 3;
9pub const ZONE_RESTRICTED: u32 = 4;
10
11#[must_use]
15pub fn is_internet_download(zone_id: u32) -> bool {
16 zone_id >= ZONE_INTERNET
17}
18
19#[must_use]
27pub fn is_double_extension(filename: &str) -> bool {
28 let name = filename.rsplit(['/', '\\']).next().unwrap_or(filename);
30 let parts: Vec<&str> = name.splitn(3, '.').collect();
31 parts.len() == 3 && parts.iter().all(|p| !p.is_empty())
33}
34
35#[must_use]
39pub fn is_alternate_data_stream(path: &str) -> bool {
40 path.chars().skip(2).any(|c| c == ':')
41}
42
43#[must_use]
45pub fn is_linux_hidden_name(name: &str) -> bool {
46 name.starts_with('.') && name.len() > 1
47}
48
49#[must_use]
52pub fn is_unc_path(path: &str) -> bool {
53 path.starts_with("\\\\") || path.starts_with("//")
54}
55
56pub const SUSPICIOUS_EXEC_PREFIXES: &[&str] = &[
59 "\\Temp\\",
60 "\\tmp\\",
61 "\\AppData\\Local\\Temp\\",
62 "\\Users\\Public\\",
63 "\\ProgramData\\",
64 "\\Downloads\\",
65 "\\$Recycle.Bin\\",
66 "\\PerfLogs\\",
67 "/tmp/",
68 "/dev/shm/",
69 "/run/shm/",
70 "/var/tmp/",
71];
72
73#[must_use]
79pub fn is_suspicious_exec_path(path: &str) -> bool {
80 let lower = path.to_ascii_lowercase();
81 SUSPICIOUS_EXEC_PREFIXES
82 .iter()
83 .any(|p| lower.contains(&p.to_ascii_lowercase()))
84}
85
86pub const CONSOLE_KNOWN_VALUE_NAMES: &[&str] = &[
103 "ColorTable00",
104 "ColorTable01",
105 "ColorTable02",
106 "ColorTable03",
107 "ColorTable04",
108 "ColorTable05",
109 "ColorTable06",
110 "ColorTable07",
111 "ColorTable08",
112 "ColorTable09",
113 "ColorTable10",
114 "ColorTable11",
115 "ColorTable12",
116 "ColorTable13",
117 "ColorTable14",
118 "ColorTable15",
119 "CtrlKeyShortcutsDisabled",
120 "CursorColor",
121 "CursorSize",
122 "CursorType",
123 "DefaultBackground",
124 "DefaultForeground",
125 "EnableColorSelection",
126 "ExtendedEditKey",
127 "ExtendedEditKeyCustom",
128 "FaceName",
129 "FilterOnPaste",
130 "FontFamily",
131 "FontSize",
132 "FontWeight",
133 "ForceV2",
134 "HistoryBufferSize",
135 "HistoryNoDup",
136 "InsertMode",
137 "LineSelection",
138 "LineWrap",
139 "LoadConIme",
140 "NumberOfHistoryBuffers",
141 "PopupColors",
142 "QuickEdit",
143 "ScreenBufferSize",
144 "ScreenColors",
145 "TerminalScrolling",
146 "TrimLeadingZeros",
147 "WindowAlpha",
148 "WindowPosition",
149 "WindowSize",
150 "WordDelimiters",
151];
152
153#[must_use]
160pub fn is_suspicious_console_value_name(name: &str) -> bool {
161 if name.is_empty() {
162 return true;
163 }
164 !CONSOLE_KNOWN_VALUE_NAMES
165 .iter()
166 .any(|known| known.eq_ignore_ascii_case(name))
167}
168
169#[must_use]
179pub fn is_suspicious_console_subkey(key_path: &str) -> bool {
180 const PREFIX: &str = "HKCU\\Console\\";
181 if key_path.len() <= PREFIX.len() {
182 return false;
183 }
184 if !key_path
185 .get(..PREFIX.len())
186 .is_some_and(|p| p.eq_ignore_ascii_case(PREFIX))
187 {
188 return false;
189 }
190 let tail = &key_path[PREFIX.len()..];
191 let first_segment = tail.split('\\').next().unwrap_or(tail);
192 !first_segment.is_empty() && first_segment.chars().all(|c| c.is_ascii_digit())
193}
194
195#[must_use]
213pub fn is_ntuser_man_path(path: &str) -> bool {
214 let base = path.rsplit(['/', '\\']).next().unwrap_or(path);
215 base.eq_ignore_ascii_case("NTUSER.MAN")
216}
217
218#[must_use]
232pub fn is_task_com_handler_dll_suspicious(dll_path: &str) -> bool {
233 if dll_path.is_empty() {
234 return false;
235 }
236 let lower = dll_path.to_ascii_lowercase();
237 let safe_prefixes = [
238 r"%systemroot%\system32",
239 r"%windir%\system32",
240 r"c:\windows\system32",
241 r"%systemroot%\syswow64",
242 r"%windir%\syswow64",
243 r"c:\windows\syswow64",
244 ];
245 !safe_prefixes.iter().any(|p| lower.starts_with(p))
246}
247
248#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn zone_internet_is_internet_download() {
255 assert!(is_internet_download(ZONE_INTERNET));
256 }
257
258 #[test]
259 fn zone_restricted_is_internet_download() {
260 assert!(is_internet_download(ZONE_RESTRICTED));
261 }
262
263 #[test]
264 fn zone_local_is_not_internet_download() {
265 assert!(!is_internet_download(ZONE_LOCAL));
266 }
267
268 #[test]
269 fn zone_trusted_is_not_internet_download() {
270 assert!(!is_internet_download(ZONE_TRUSTED));
271 }
272
273 #[test]
274 fn double_extension_pdf_exe() {
275 assert!(is_double_extension("invoice.pdf.exe"));
276 }
277
278 #[test]
279 fn double_extension_doc_exe() {
280 assert!(is_double_extension("report.doc.exe"));
281 }
282
283 #[test]
284 fn single_extension_not_double() {
285 assert!(!is_double_extension("program.exe"));
286 }
287
288 #[test]
289 fn no_extension_not_double() {
290 assert!(!is_double_extension("makefile"));
291 }
292
293 #[test]
294 fn double_extension_empty_part_not_flagged() {
295 assert!(!is_double_extension(".hidden.exe"));
297 }
298
299 #[test]
300 fn ads_path_detected() {
301 assert!(is_alternate_data_stream(r"C:\file.txt:stream"));
302 }
303
304 #[test]
305 fn normal_path_not_ads() {
306 assert!(!is_alternate_data_stream(r"C:\file.txt"));
307 }
308
309 #[test]
310 fn drive_colon_not_ads() {
311 assert!(!is_alternate_data_stream(r"C:\dir\file.txt"));
313 }
314
315 #[test]
316 fn linux_hidden_dot_file() {
317 assert!(is_linux_hidden_name(".bashrc"));
318 }
319
320 #[test]
321 fn linux_hidden_double_dot() {
322 assert!(is_linux_hidden_name("..file"));
323 }
324
325 #[test]
326 fn linux_non_hidden() {
327 assert!(!is_linux_hidden_name("bashrc"));
328 }
329
330 #[test]
331 fn linux_single_dot_not_hidden() {
332 assert!(!is_linux_hidden_name("."));
334 }
335
336 #[test]
337 fn unc_path_backslash() {
338 assert!(is_unc_path(r"\\server\share"));
339 }
340
341 #[test]
342 fn unc_path_forward_slash() {
343 assert!(is_unc_path("//server/share"));
344 }
345
346 #[test]
347 fn normal_path_not_unc() {
348 assert!(!is_unc_path(r"C:\Windows"));
349 }
350
351 #[test]
352 fn suspicious_exec_tmp_path() {
353 assert!(is_suspicious_exec_path(
354 r"C:\Users\bob\AppData\Local\Temp\evil.exe"
355 ));
356 }
357
358 #[test]
359 fn suspicious_exec_dev_shm() {
360 assert!(is_suspicious_exec_path("/dev/shm/payload"));
361 }
362
363 #[test]
364 fn normal_exec_path_not_suspicious() {
365 assert!(!is_suspicious_exec_path(r"C:\Windows\System32\calc.exe"));
366 }
367
368 #[test]
369 fn suspicious_exec_path_is_case_insensitive() {
370 assert!(is_suspicious_exec_path(
372 r"\VOLUME{X}\USERS\BOB\DOWNLOADS\INVOICE.EXE"
373 ));
374 assert!(is_suspicious_exec_path(r"\VOLUME{X}\WINDOWS\TEMP\X.EXE"));
375 assert!(is_suspicious_exec_path(r"\VOLUME{X}\$RECYCLE.BIN\A.EXE"));
376 assert!(is_suspicious_exec_path(r"\VOLUME{X}\PERFLOGS\B.EXE"));
377 }
378
379 #[test]
384 fn console_facename_is_known() {
385 assert!(!is_suspicious_console_value_name("FaceName"));
386 }
387
388 #[test]
389 fn console_fontsize_is_known() {
390 assert!(!is_suspicious_console_value_name("FontSize"));
391 }
392
393 #[test]
394 fn console_colortable00_is_known() {
395 assert!(!is_suspicious_console_value_name("ColorTable00"));
396 }
397
398 #[test]
399 fn console_known_value_case_insensitive() {
400 assert!(!is_suspicious_console_value_name("facename"));
402 assert!(!is_suspicious_console_value_name("FACENAME"));
403 }
404
405 #[test]
406 fn console_arbitrary_blob_name_is_suspicious() {
407 assert!(is_suspicious_console_value_name("config"));
409 assert!(is_suspicious_console_value_name(
410 "d33f351a4aeea5e608853d1a56661059"
411 ));
412 }
413
414 #[test]
415 fn console_empty_value_name_is_suspicious() {
416 assert!(is_suspicious_console_value_name(""));
418 }
419
420 #[test]
421 fn console_numeric_subkey_is_suspicious() {
422 assert!(is_suspicious_console_subkey(r"HKCU\Console\0"));
424 assert!(is_suspicious_console_subkey(
425 r"HKCU\Console\0\d33f351a4aeea5e608853d1a56661059"
426 ));
427 }
428
429 #[test]
430 fn console_app_subkey_not_suspicious() {
431 assert!(!is_suspicious_console_subkey(r"HKCU\Console\cmd.exe"));
433 assert!(!is_suspicious_console_subkey(
434 r"HKCU\Console\%SystemRoot%_System32_cmd.exe"
435 ));
436 }
437
438 #[test]
439 fn console_root_key_not_flagged_as_subkey() {
440 assert!(!is_suspicious_console_subkey(r"HKCU\Console"));
442 assert!(!is_suspicious_console_subkey(r"HKCU\Console\"));
443 }
444
445 #[test]
446 fn console_subkey_check_case_insensitive() {
447 assert!(is_suspicious_console_subkey(r"hkcu\console\0"));
449 }
450
451 #[test]
452 fn non_console_key_not_flagged() {
453 assert!(!is_suspicious_console_subkey(
454 r"HKCU\Software\Microsoft\Windows"
455 ));
456 }
457
458 #[test]
463 fn ntuser_man_in_userprofile_detected() {
464 assert!(is_ntuser_man_path(r"C:\Users\bob\NTUSER.MAN"));
465 }
466
467 #[test]
468 fn ntuser_man_case_insensitive() {
469 assert!(is_ntuser_man_path(r"C:\Users\bob\ntuser.man"));
471 assert!(is_ntuser_man_path(r"C:\Users\bob\NtUser.Man"));
472 }
473
474 #[test]
475 fn ntuser_dat_not_flagged() {
476 assert!(!is_ntuser_man_path(r"C:\Users\bob\NTUSER.DAT"));
478 }
479
480 #[test]
481 fn ntuser_man_on_unc_share_detected() {
482 assert!(is_ntuser_man_path(r"\\server\share\profile.v6\NTUSER.MAN"));
484 }
485
486 #[test]
487 fn ntuser_man_substring_in_other_filename_not_flagged() {
488 assert!(!is_ntuser_man_path(
490 r"C:\Users\bob\notes\ntuser.man.backup.txt"
491 ));
492 }
493
494 #[test]
497 fn system32_dll_is_not_suspicious() {
498 assert!(!is_task_com_handler_dll_suspicious(
499 r"%SystemRoot%\System32\regidle.dll"
500 ));
501 }
502
503 #[test]
504 fn system32_dll_case_insensitive() {
505 assert!(!is_task_com_handler_dll_suspicious(
506 r"C:\WINDOWS\system32\DeviceDirectoryClient.dll"
507 ));
508 }
509
510 #[test]
511 fn syswow64_dll_is_not_suspicious() {
512 assert!(!is_task_com_handler_dll_suspicious(
513 r"%SystemRoot%\SysWOW64\example.dll"
514 ));
515 }
516
517 #[test]
518 fn temp_dir_dll_is_suspicious() {
519 assert!(is_task_com_handler_dll_suspicious(
520 r"C:\Users\bob\AppData\Local\Temp\evil.dll"
521 ));
522 }
523
524 #[test]
525 fn programdata_dll_is_suspicious() {
526 assert!(is_task_com_handler_dll_suspicious(
527 r"C:\ProgramData\payload.dll"
528 ));
529 }
530
531 #[test]
532 fn empty_dll_path_is_not_flagged() {
533 assert!(!is_task_com_handler_dll_suspicious(""));
534 }
535}
536
537#[must_use]
548pub fn is_pam_module_path_suspicious(module_path: &str) -> bool {
549 if module_path.is_empty() {
550 return false;
551 }
552 let lower = module_path.to_ascii_lowercase();
553 let safe_prefixes = [
554 "/lib/security/",
555 "/lib/x86_64-linux-gnu/security/",
556 "/lib/aarch64-linux-gnu/security/",
557 "/usr/lib/security/",
558 "/usr/lib/x86_64-linux-gnu/security/",
559 "/usr/lib64/security/",
560 ];
561 !safe_prefixes.iter().any(|p| lower.starts_with(p))
562}
563
564#[must_use]
574pub fn is_pam_exec_script_suspicious(script_path: &str) -> bool {
575 if script_path.is_empty() {
576 return false;
577 }
578 let lower = script_path.to_ascii_lowercase();
579 let suspicious_prefixes = [
580 "/tmp/",
581 "/var/tmp/",
582 "/dev/shm/",
583 "/home/",
584 "/root/",
585 "/run/user/",
586 ];
587 suspicious_prefixes.iter().any(|p| lower.starts_with(p))
588}
589
590#[cfg(test)]
591mod tests_pam_heuristics {
592 use super::*;
593
594 #[test]
597 fn standard_lib_security_so_is_safe() {
598 assert!(!is_pam_module_path_suspicious("/lib/security/pam_unix.so"));
599 }
600
601 #[test]
602 fn debian_security_path_is_safe() {
603 assert!(!is_pam_module_path_suspicious(
604 "/lib/x86_64-linux-gnu/security/pam_sss.so"
605 ));
606 }
607
608 #[test]
609 fn usr_lib_security_is_safe() {
610 assert!(!is_pam_module_path_suspicious(
611 "/usr/lib/x86_64-linux-gnu/security/pam_unix.so"
612 ));
613 }
614
615 #[test]
616 fn tmp_pam_so_is_suspicious() {
617 assert!(is_pam_module_path_suspicious("/tmp/pam_linux.so"));
618 }
619
620 #[test]
621 fn home_dir_pam_so_is_suspicious() {
622 assert!(is_pam_module_path_suspicious("/home/attacker/pam_linux.so"));
623 }
624
625 #[test]
626 fn var_tmp_pam_so_is_suspicious() {
627 assert!(is_pam_module_path_suspicious("/var/tmp/pam_evil.so"));
628 }
629
630 #[test]
631 fn empty_pam_module_path_is_not_suspicious() {
632 assert!(!is_pam_module_path_suspicious(""));
633 }
634
635 #[test]
638 fn etc_script_is_not_suspicious() {
639 assert!(!is_pam_exec_script_suspicious("/etc/pam_scripts/notify.sh"));
640 }
641
642 #[test]
643 fn usr_local_script_is_not_suspicious() {
644 assert!(!is_pam_exec_script_suspicious(
645 "/usr/local/bin/pam_check.sh"
646 ));
647 }
648
649 #[test]
650 fn tmp_capture_script_is_suspicious() {
651 assert!(is_pam_exec_script_suspicious("/tmp/capture.sh"));
652 }
653
654 #[test]
655 fn dev_shm_script_is_suspicious() {
656 assert!(is_pam_exec_script_suspicious("/dev/shm/exfil.sh"));
657 }
658
659 #[test]
660 fn home_dir_script_is_suspicious() {
661 assert!(is_pam_exec_script_suspicious("/home/user/.config/run.sh"));
662 }
663
664 #[test]
665 fn var_tmp_script_is_suspicious() {
666 assert!(is_pam_exec_script_suspicious("/var/tmp/backdoor.sh"));
667 }
668
669 #[test]
670 fn root_home_script_is_suspicious() {
671 assert!(is_pam_exec_script_suspicious("/root/.bashrc_extra.sh"));
672 }
673
674 #[test]
675 fn empty_script_path_is_not_suspicious() {
676 assert!(!is_pam_exec_script_suspicious(""));
677 }
678}