Skip to main content

winreg_artifacts/
com_hijacking.rs

1//! COM object hijacking detection from offline registry hives.
2//!
3//! Detects when a CLSID has a user-side `Software\Classes\CLSID\{guid}\InprocServer32`
4//! registration (from NTUSER.DAT) that overrides the system-wide HKCR entry
5//! (from SOFTWARE or USRCLASS.DAT), a technique used by malware to load
6//! arbitrary DLLs into COM clients without admin privileges.
7
8use std::io::Cursor;
9
10use winreg_core::hive::Hive;
11
12// ── Output type ───────────────────────────────────────────────────────────────
13
14/// A COM class registration where HKCU may override HKCR (potential hijack).
15#[derive(Debug, Clone, serde::Serialize)]
16pub struct ComHijackInfo {
17    /// The CLSID string, e.g. `{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}`.
18    pub clsid: String,
19    /// DLL path registered under HKCU (the user-side override).
20    pub hkcu_server: String,
21    /// DLL path registered under HKCR (empty if no HKCR hive or not found).
22    pub hkcr_server: String,
23    /// `true` when the HKCU server path is in an unusual/writable location.
24    pub is_suspicious: bool,
25    /// Human-readable explanation when `is_suspicious` is `true`.
26    pub suspicious_reason: Option<String>,
27    /// The CLSID GUID key's `LastWriteTime` — approximately when this COM
28    /// registration was written. `None` when the key carries no timestamp.
29    pub last_written: Option<chrono::DateTime<chrono::Utc>>,
30}
31
32// ── Classification ────────────────────────────────────────────────────────────
33
34/// Classify a HKCU COM server path.
35///
36/// Returns `(is_suspicious, reason)`.
37/// Suspicious when the path is in a user-writable directory (`\temp\`,
38/// `\appdata\`, `\downloads\`, `\public\`, `\programdata\`), or when it
39/// overrides a non-empty HKCR registration with a different path.
40// `hkcr_server`/`hkcu_server` differ only by the hive root (HKCR vs HKCU);
41// that distinction is the whole point of the comparison, so keep both names.
42#[allow(clippy::similar_names)]
43pub fn classify_com_hijack(hkcr_server: &str, hkcu_server: &str) -> (bool, Option<String>) {
44    if hkcu_server.is_empty() {
45        return (false, None);
46    }
47    let lower = hkcu_server.to_ascii_lowercase();
48
49    if lower.contains("\\temp\\") {
50        return (true, Some("DLL in \\temp\\".to_string()));
51    }
52    if lower.contains("\\appdata\\") {
53        return (true, Some("DLL in \\appdata\\".to_string()));
54    }
55    if lower.contains("\\downloads\\") {
56        return (true, Some("DLL in \\downloads\\".to_string()));
57    }
58    if lower.contains("\\public\\") {
59        return (true, Some("DLL in \\public\\".to_string()));
60    }
61    if lower.contains("\\programdata\\") {
62        return (true, Some("DLL in \\programdata\\".to_string()));
63    }
64    if !hkcr_server.is_empty() && !hkcu_server.eq_ignore_ascii_case(hkcr_server) {
65        return (true, Some(format!("HKCU overrides HKCR ({hkcr_server})")));
66    }
67    (false, None)
68}
69
70// ── Public API ────────────────────────────────────────────────────────────────
71
72/// Parse COM hijacking candidates from a pair of hives.
73///
74/// `hku_hive`: NTUSER.DAT — contains `Software\Classes\CLSID` user overrides.
75/// `hkcr_hive`: SOFTWARE or USRCLASS.DAT — contains the system-wide CLSID registrations.
76// `hkcu_server`/`hkcr_server` differ only by the hive root, which is the point.
77#[allow(clippy::similar_names)]
78pub fn parse_pair(
79    hku_hive: &Hive<Cursor<Vec<u8>>>,
80    hkcr_hive: &Hive<Cursor<Vec<u8>>>,
81) -> Vec<ComHijackInfo> {
82    let mut results = Vec::new();
83
84    let Some(clsid_key) = open_user_clsid_key(hku_hive) else {
85        return results;
86    };
87
88    let Ok(guids) = clsid_key.subkeys() else {
89        return results;
90    };
91
92    for guid_key in guids {
93        let clsid = guid_key.name();
94
95        // Find InprocServer32 under this GUID key in HKCU
96        let Ok(Some(inproc)) = guid_key.subkey("InprocServer32") else {
97            continue;
98        };
99
100        let hkcu_server = read_default_value(&inproc);
101        if hkcu_server.is_empty() {
102            continue;
103        }
104
105        // Look up the same CLSID in HKCR
106        let hkcr_server = read_hkcr_server(hkcr_hive, &clsid);
107
108        let (is_suspicious, suspicious_reason) = classify_com_hijack(&hkcr_server, &hkcu_server);
109
110        results.push(ComHijackInfo {
111            clsid,
112            hkcu_server,
113            hkcr_server,
114            is_suspicious,
115            suspicious_reason,
116            last_written: guid_key.last_written(),
117        });
118    }
119
120    results
121}
122
123/// Parse user-side COM registrations from a single NTUSER.DAT hive.
124///
125/// Returns entries without HKCR comparison (`hkcr_server` will be empty).
126pub fn parse_hkcu_only(hku_hive: &Hive<Cursor<Vec<u8>>>) -> Vec<ComHijackInfo> {
127    let mut results = Vec::new();
128
129    let Some(clsid_key) = open_user_clsid_key(hku_hive) else {
130        return results;
131    };
132
133    let Ok(guids) = clsid_key.subkeys() else {
134        return results;
135    };
136
137    for guid_key in guids {
138        let clsid = guid_key.name();
139
140        let Ok(Some(inproc)) = guid_key.subkey("InprocServer32") else {
141            continue;
142        };
143
144        let hkcu_server = read_default_value(&inproc);
145        if hkcu_server.is_empty() {
146            continue;
147        }
148
149        let (is_suspicious, suspicious_reason) = classify_com_hijack("", &hkcu_server);
150
151        results.push(ComHijackInfo {
152            clsid,
153            hkcu_server,
154            hkcr_server: String::new(),
155            is_suspicious,
156            suspicious_reason,
157            last_written: guid_key.last_written(),
158        });
159    }
160
161    results
162}
163
164// ── Helpers ───────────────────────────────────────────────────────────────────
165
166/// Open the per-user CLSID enumeration key, trying each hive layout in turn:
167/// NTUSER.DAT `Software\Classes\CLSID` (rare), SOFTWARE/HKCR `Classes\CLSID`,
168/// and UsrClass.dat root `CLSID` — the real Win10 per-user COM home.
169fn open_user_clsid_key(hive: &Hive<Cursor<Vec<u8>>>) -> Option<winreg_core::key::Key<'_>> {
170    ["Software\\Classes\\CLSID", "Classes\\CLSID", "CLSID"]
171        .iter()
172        .find_map(|path| hive.open_key(path).ok().flatten())
173}
174
175/// Read the default (empty-name) value from a key as a string.
176fn read_default_value(key: &winreg_core::key::Key<'_>) -> String {
177    let Ok(vals) = key.values() else {
178        return String::new();
179    };
180    for val in vals {
181        if val.name().is_empty() {
182            return val.as_string().unwrap_or_default();
183        }
184    }
185    String::new()
186}
187
188/// Try to look up the CLSID `InprocServer32` default value in the HKCR hive.
189///
190/// Tries multiple path prefixes to handle both SOFTWARE hives and USRCLASS.DAT.
191fn read_hkcr_server(hkcr_hive: &Hive<Cursor<Vec<u8>>>, clsid: &str) -> String {
192    let paths = [
193        format!("SOFTWARE\\Classes\\CLSID\\{clsid}\\InprocServer32"),
194        format!("Classes\\CLSID\\{clsid}\\InprocServer32"),
195        format!("CLSID\\{clsid}\\InprocServer32"),
196    ];
197    for path in &paths {
198        if let Ok(Some(k)) = hkcr_hive.open_key(path) {
199            let s = read_default_value(&k);
200            if !s.is_empty() {
201                return s;
202            }
203        }
204    }
205    String::new()
206}