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    /// The Run key's `LastWriteTime` — approximately when this autorun entry
58    /// was last written. `None` when the key carries no timestamp.
59    pub last_written: Option<chrono::DateTime<chrono::Utc>>,
60}
61
62// ── Classification ────────────────────────────────────────────────────────────
63
64/// Classify a run-key command string for suspicious `LOLBin` abuse patterns.
65///
66/// Returns `Some(reason)` when suspicious, `None` when benign.
67///
68/// Patterns detected:
69/// - `powershell` with `-enc` or `-encodedcommand`
70/// - `cmd` with `/c` and (`http`, `ftp`, or `\\`)
71/// - `mshta` anywhere in the command
72/// - `regsvr32` with `/s /n` or `/u /s`
73/// - `certutil` with `-decode` or `-urlcache`
74/// - `bitsadmin` with `/transfer`
75/// - `wscript` or `cscript` launched from `\temp\` or `\appdata\`
76/// - `rundll32` with a path containing `\temp\` or `\appdata\`
77/// - path contains `\temp\` or `\appdata\local\temp\`
78/// - `msiexec` with `/q` and `http`
79pub fn classify_run_entry(command: &str) -> Option<String> {
80    if command.is_empty() {
81        return None;
82    }
83
84    let lower = command.to_ascii_lowercase();
85
86    // PowerShell encoded command
87    if lower.contains("powershell") && (lower.contains("-enc") || lower.contains("-encodedcommand"))
88    {
89        return Some("powershell encoded command (-enc / -encodedcommand)".to_string());
90    }
91
92    // cmd /c with network or UNC path
93    if lower.contains("cmd")
94        && lower.contains("/c")
95        && (lower.contains("http") || lower.contains("ftp") || lower.contains("\\\\"))
96    {
97        return Some("cmd /c with remote resource (http/ftp/UNC)".to_string());
98    }
99
100    // mshta
101    if lower.contains("mshta") {
102        return Some("mshta execution (HTML Application host abuse)".to_string());
103    }
104
105    // regsvr32 squiblydoo / bypass
106    if lower.contains("regsvr32") && (lower.contains("/s") && lower.contains("/n"))
107        || (lower.contains("regsvr32") && lower.contains("/u") && lower.contains("/s"))
108    {
109        return Some("regsvr32 /s /n or /u /s (AppLocker bypass / squiblydoo)".to_string());
110    }
111
112    // certutil download cradle or decode
113    if lower.contains("certutil") && (lower.contains("-decode") || lower.contains("-urlcache")) {
114        return Some("certutil -decode or -urlcache (download cradle / obfuscation)".to_string());
115    }
116
117    // bitsadmin
118    if lower.contains("bitsadmin") && lower.contains("/transfer") {
119        return Some("bitsadmin /transfer (BITS download abuse)".to_string());
120    }
121
122    // wscript/cscript from temp or appdata
123    if (lower.contains("wscript") || lower.contains("cscript"))
124        && (lower.contains("\\temp\\") || lower.contains("\\appdata\\"))
125    {
126        return Some("wscript/cscript launched from \\temp\\ or \\appdata\\ path".to_string());
127    }
128
129    // rundll32 from temp or appdata
130    if lower.contains("rundll32") && (lower.contains("\\temp\\") || lower.contains("\\appdata\\")) {
131        return Some("rundll32 with DLL in \\temp\\ or \\appdata\\ path".to_string());
132    }
133
134    // path itself is in temp or appdata\local\temp
135    if lower.contains("\\appdata\\local\\temp\\") || lower.starts_with("\\temp\\") {
136        return Some("executable path is in \\temp\\ or \\appdata\\local\\temp\\".to_string());
137    }
138
139    // msiexec silent with HTTP URL
140    if lower.contains("msiexec") && lower.contains("/q") && lower.contains("http") {
141        return Some("msiexec /q with HTTP URL (silent remote install)".to_string());
142    }
143
144    None
145}
146
147// ── Public parse function ─────────────────────────────────────────────────────
148
149/// Extract all Run-key entries from a hive.
150///
151/// Auto-detects whether the hive is a SOFTWARE (HKLM) or NTUSER.DAT (HKCU)
152/// hive and selects the appropriate key paths accordingly.  Winlogon
153/// `Userinit` and `Shell` values are also collected.
154pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<RunKeyEntry> {
155    let hive_type = hive.detect_hive_type();
156
157    let (hive_label, run_paths, winlogon_path) = match hive_type {
158        HiveType::Software => ("HKLM", SOFTWARE_RUN_PATHS, WINLOGON_PATH_SOFTWARE),
159        HiveType::NtUser => ("HKCU", NTUSER_RUN_PATHS, WINLOGON_PATH_NTUSER),
160        // For unknown hive types, try SOFTWARE paths as a best-effort.
161        _ => ("UNKNOWN", SOFTWARE_RUN_PATHS, WINLOGON_PATH_SOFTWARE),
162    };
163
164    let mut entries: Vec<RunKeyEntry> = Vec::new();
165
166    // Enumerate standard Run/RunOnce/… key paths.
167    for &key_path in run_paths {
168        let Ok(Some(key)) = hive.open_key(key_path) else {
169            continue;
170        };
171
172        let Ok(values) = key.values() else {
173            continue;
174        };
175
176        let last_written = key.last_written();
177
178        for val in values {
179            let command = val.as_string().unwrap_or_default();
180            let suspicious_reason = classify_run_entry(&command);
181            let is_suspicious = suspicious_reason.is_some();
182            entries.push(RunKeyEntry {
183                hive: hive_label.to_string(),
184                key_path: key_path.to_string(),
185                value_name: val.name(),
186                command,
187                is_suspicious,
188                suspicious_reason,
189                last_written,
190            });
191        }
192    }
193
194    // Enumerate Winlogon persistence values.
195    if let Ok(Some(winlogon)) = hive.open_key(winlogon_path) {
196        let last_written = winlogon.last_written();
197        for &vname in WINLOGON_VALUES {
198            let Ok(Some(val)) = winlogon.value(vname) else {
199                continue;
200            };
201            let command = val.as_string().unwrap_or_default();
202            let suspicious_reason = classify_run_entry(&command);
203            let is_suspicious = suspicious_reason.is_some();
204            entries.push(RunKeyEntry {
205                hive: hive_label.to_string(),
206                key_path: winlogon_path.to_string(),
207                value_name: vname.to_string(),
208                command,
209                is_suspicious,
210                suspicious_reason,
211                last_written,
212            });
213        }
214    }
215
216    entries
217}