winreg_artifacts/
run_keys.rs1use std::io::Cursor;
8
9use winreg_core::detect::HiveType;
10use winreg_core::hive::Hive;
11
12const 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
22const 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
30const WINLOGON_PATH_SOFTWARE: &str = "Microsoft\\Windows NT\\CurrentVersion\\Winlogon";
32
33const WINLOGON_PATH_NTUSER: &str = "Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon";
35
36const WINLOGON_VALUES: &[&str] = &["Userinit", "Shell"];
38
39#[derive(Debug, Clone, serde::Serialize)]
43pub struct RunKeyEntry {
44 pub hive: String,
47 pub key_path: String,
49 pub value_name: String,
51 pub command: String,
53 pub is_suspicious: bool,
55 pub suspicious_reason: Option<String>,
57 pub last_written: Option<chrono::DateTime<chrono::Utc>>,
60}
61
62pub 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 if lower.contains("powershell") && (lower.contains("-enc") || lower.contains("-encodedcommand"))
88 {
89 return Some("powershell encoded command (-enc / -encodedcommand)".to_string());
90 }
91
92 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 if lower.contains("mshta") {
102 return Some("mshta execution (HTML Application host abuse)".to_string());
103 }
104
105 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 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 if lower.contains("bitsadmin") && lower.contains("/transfer") {
119 return Some("bitsadmin /transfer (BITS download abuse)".to_string());
120 }
121
122 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 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 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 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
147pub 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 _ => ("UNKNOWN", SOFTWARE_RUN_PATHS, WINLOGON_PATH_SOFTWARE),
162 };
163
164 let mut entries: Vec<RunKeyEntry> = Vec::new();
165
166 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 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}