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}
58
59pub 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 if lower.contains("powershell") && (lower.contains("-enc") || lower.contains("-encodedcommand"))
85 {
86 return Some("powershell encoded command (-enc / -encodedcommand)".to_string());
87 }
88
89 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 if lower.contains("mshta") {
98 return Some("mshta execution (HTML Application host abuse)".to_string());
99 }
100
101 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 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 if lower.contains("bitsadmin") && lower.contains("/transfer") {
115 return Some("bitsadmin /transfer (BITS download abuse)".to_string());
116 }
117
118 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 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 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 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
143pub 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 _ => ("UNKNOWN", SOFTWARE_RUN_PATHS, WINLOGON_PATH_SOFTWARE),
158 };
159
160 let mut entries: Vec<RunKeyEntry> = Vec::new();
161
162 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 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}