Skip to main content

winreg_artifacts/
run_keys.rs

1//! Windows autostart (Run/RunOnce) registry key artifact extractor.
2//!
3//! Enumerates all standard persistence-related Run keys from a REGF hive and
4//! classifies each entry against known LOLBin / living-off-the-land abuse
5//! patterns (MITRE ATT&CK T1547.001).
6
7use std::io::Cursor;
8
9use winreg_core::detect::HiveType;
10use winreg_core::hive::Hive;
11
12// ── Key paths to enumerate ────────────────────────────────────────────────────
13
14/// Run key paths stored in a SOFTWARE hive (HKLM) — relative to hive root.
15const SOFTWARE_RUN_PATHS: &[&str] = &[
16    "Microsoft\\Windows\\CurrentVersion\\Run",
17    "Microsoft\\Windows\\CurrentVersion\\RunOnce",
18    "Microsoft\\Windows\\CurrentVersion\\RunServices",
19    "Microsoft\\Windows\\CurrentVersion\\RunServicesOnce",
20];
21
22/// Run key paths stored in an NTUSER.DAT hive (HKCU) — relative to hive root.
23const NTUSER_RUN_PATHS: &[&str] = &[
24    "Software\\Microsoft\\Windows\\CurrentVersion\\Run",
25    "Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce",
26    "Software\\Microsoft\\Windows\\CurrentVersion\\RunServices",
27    "Software\\Microsoft\\Windows\\CurrentVersion\\RunServicesOnce",
28];
29
30/// Winlogon key path for a SOFTWARE hive.
31const WINLOGON_PATH_SOFTWARE: &str = "Microsoft\\Windows NT\\CurrentVersion\\Winlogon";
32
33/// Winlogon key path for an NTUSER.DAT hive.
34const WINLOGON_PATH_NTUSER: &str = "Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon";
35
36/// Winlogon values that can hold persistence commands.
37const WINLOGON_VALUES: &[&str] = &["Userinit", "Shell"];
38
39// ── Output type ───────────────────────────────────────────────────────────────
40
41/// A single autorun entry extracted from a registry hive.
42#[derive(Debug, Clone, serde::Serialize)]
43pub struct RunKeyEntry {
44    /// Hive origin: `"HKLM"` for SOFTWARE, `"HKCU"` for NTUSER.DAT,
45    /// or `"UNKNOWN"` for unrecognised hive types.
46    pub hive: String,
47    /// Full registry key path (relative to hive root).
48    pub key_path: String,
49    /// Value name (the persistence entry identifier).
50    pub value_name: String,
51    /// Value data: the command or path that runs at startup.
52    pub command: String,
53    /// `true` if the command matches a known LOLBin abuse pattern.
54    pub is_suspicious: bool,
55    /// Human-readable explanation when `is_suspicious` is `true`.
56    pub suspicious_reason: Option<String>,
57}
58
59// ── Classification ────────────────────────────────────────────────────────────
60
61/// Classify a run-key command string for suspicious LOLBin abuse patterns.
62///
63/// Returns `Some(reason)` when suspicious, `None` when benign.
64///
65/// Patterns detected:
66/// - `powershell` with `-enc` or `-encodedcommand`
67/// - `cmd` with `/c` and (`http`, `ftp`, or `\\`)
68/// - `mshta` anywhere in the command
69/// - `regsvr32` with `/s /n` or `/u /s`
70/// - `certutil` with `-decode` or `-urlcache`
71/// - `bitsadmin` with `/transfer`
72/// - `wscript` or `cscript` launched from `\temp\` or `\appdata\`
73/// - `rundll32` with a path containing `\temp\` or `\appdata\`
74/// - path contains `\temp\` or `\appdata\local\temp\`
75/// - `msiexec` with `/q` and `http`
76pub fn classify_run_entry(command: &str) -> Option<String> {
77    if command.is_empty() {
78        return None;
79    }
80
81    let lower = command.to_ascii_lowercase();
82
83    // PowerShell encoded command
84    if lower.contains("powershell") && (lower.contains("-enc") || lower.contains("-encodedcommand"))
85    {
86        return Some("powershell encoded command (-enc / -encodedcommand)".to_string());
87    }
88
89    // cmd /c with network or UNC path
90    if lower.contains("cmd") && lower.contains("/c") {
91        if lower.contains("http") || lower.contains("ftp") || lower.contains("\\\\") {
92            return Some("cmd /c with remote resource (http/ftp/UNC)".to_string());
93        }
94    }
95
96    // mshta
97    if lower.contains("mshta") {
98        return Some("mshta execution (HTML Application host abuse)".to_string());
99    }
100
101    // regsvr32 squiblydoo / bypass
102    if lower.contains("regsvr32") && (lower.contains("/s") && lower.contains("/n"))
103        || (lower.contains("regsvr32") && lower.contains("/u") && lower.contains("/s"))
104    {
105        return Some("regsvr32 /s /n or /u /s (AppLocker bypass / squiblydoo)".to_string());
106    }
107
108    // certutil download cradle or decode
109    if lower.contains("certutil") && (lower.contains("-decode") || lower.contains("-urlcache")) {
110        return Some("certutil -decode or -urlcache (download cradle / obfuscation)".to_string());
111    }
112
113    // bitsadmin
114    if lower.contains("bitsadmin") && lower.contains("/transfer") {
115        return Some("bitsadmin /transfer (BITS download abuse)".to_string());
116    }
117
118    // wscript/cscript from temp or appdata
119    if (lower.contains("wscript") || lower.contains("cscript"))
120        && (lower.contains("\\temp\\") || lower.contains("\\appdata\\"))
121    {
122        return Some("wscript/cscript launched from \\temp\\ or \\appdata\\ path".to_string());
123    }
124
125    // rundll32 from temp or appdata
126    if lower.contains("rundll32") && (lower.contains("\\temp\\") || lower.contains("\\appdata\\")) {
127        return Some("rundll32 with DLL in \\temp\\ or \\appdata\\ path".to_string());
128    }
129
130    // path itself is in temp or appdata\local\temp
131    if lower.contains("\\appdata\\local\\temp\\") || lower.starts_with("\\temp\\") {
132        return Some("executable path is in \\temp\\ or \\appdata\\local\\temp\\".to_string());
133    }
134
135    // msiexec silent with HTTP URL
136    if lower.contains("msiexec") && lower.contains("/q") && lower.contains("http") {
137        return Some("msiexec /q with HTTP URL (silent remote install)".to_string());
138    }
139
140    None
141}
142
143// ── Public parse function ─────────────────────────────────────────────────────
144
145/// Extract all Run-key entries from a hive.
146///
147/// Auto-detects whether the hive is a SOFTWARE (HKLM) or NTUSER.DAT (HKCU)
148/// hive and selects the appropriate key paths accordingly.  Winlogon
149/// `Userinit` and `Shell` values are also collected.
150pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<RunKeyEntry> {
151    let hive_type = hive.detect_hive_type();
152
153    let (hive_label, run_paths, winlogon_path) = match hive_type {
154        HiveType::Software => ("HKLM", SOFTWARE_RUN_PATHS, WINLOGON_PATH_SOFTWARE),
155        HiveType::NtUser => ("HKCU", NTUSER_RUN_PATHS, WINLOGON_PATH_NTUSER),
156        // For unknown hive types, try SOFTWARE paths as a best-effort.
157        _ => ("UNKNOWN", SOFTWARE_RUN_PATHS, WINLOGON_PATH_SOFTWARE),
158    };
159
160    let mut entries: Vec<RunKeyEntry> = Vec::new();
161
162    // Enumerate standard Run/RunOnce/… key paths.
163    for &key_path in run_paths {
164        let key = match hive.open_key(key_path) {
165            Ok(Some(k)) => k,
166            _ => continue,
167        };
168
169        let values = match key.values() {
170            Ok(v) => v,
171            Err(_) => continue,
172        };
173
174        for val in values {
175            let command = val.as_string().unwrap_or_default();
176            let suspicious_reason = classify_run_entry(&command);
177            let is_suspicious = suspicious_reason.is_some();
178            entries.push(RunKeyEntry {
179                hive: hive_label.to_string(),
180                key_path: key_path.to_string(),
181                value_name: val.name(),
182                command,
183                is_suspicious,
184                suspicious_reason,
185            });
186        }
187    }
188
189    // Enumerate Winlogon persistence values.
190    if let Ok(Some(winlogon)) = hive.open_key(winlogon_path) {
191        for &vname in WINLOGON_VALUES {
192            let val = match winlogon.value(vname) {
193                Ok(Some(v)) => v,
194                _ => continue,
195            };
196            let command = val.as_string().unwrap_or_default();
197            let suspicious_reason = classify_run_entry(&command);
198            let is_suspicious = suspicious_reason.is_some();
199            entries.push(RunKeyEntry {
200                hive: hive_label.to_string(),
201                key_path: winlogon_path.to_string(),
202                value_name: vname.to_string(),
203                command,
204                is_suspicious,
205                suspicious_reason,
206            });
207        }
208    }
209
210    entries
211}