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}
28
29// ── Classification ────────────────────────────────────────────────────────────
30
31/// Classify a HKCU COM server path.
32///
33/// Returns `(is_suspicious, reason)`.
34/// Suspicious when the path is in a user-writable directory (`\temp\`,
35/// `\appdata\`, `\downloads\`, `\public\`, `\programdata\`), or when it
36/// overrides a non-empty HKCR registration with a different path.
37pub fn classify_com_hijack(hkcr_server: &str, hkcu_server: &str) -> (bool, Option<String>) {
38    if hkcu_server.is_empty() {
39        return (false, None);
40    }
41    let lower = hkcu_server.to_ascii_lowercase();
42
43    if lower.contains("\\temp\\") {
44        return (true, Some("DLL in \\temp\\".to_string()));
45    }
46    if lower.contains("\\appdata\\") {
47        return (true, Some("DLL in \\appdata\\".to_string()));
48    }
49    if lower.contains("\\downloads\\") {
50        return (true, Some("DLL in \\downloads\\".to_string()));
51    }
52    if lower.contains("\\public\\") {
53        return (true, Some("DLL in \\public\\".to_string()));
54    }
55    if lower.contains("\\programdata\\") {
56        return (true, Some("DLL in \\programdata\\".to_string()));
57    }
58    if !hkcr_server.is_empty() && !hkcu_server.eq_ignore_ascii_case(hkcr_server) {
59        return (true, Some(format!("HKCU overrides HKCR ({hkcr_server})")));
60    }
61    (false, None)
62}
63
64// ── Public API ────────────────────────────────────────────────────────────────
65
66/// Parse COM hijacking candidates from a pair of hives.
67///
68/// `hku_hive`: NTUSER.DAT — contains `Software\Classes\CLSID` user overrides.
69/// `hkcr_hive`: SOFTWARE or USRCLASS.DAT — contains the system-wide CLSID registrations.
70pub fn parse_pair(
71    hku_hive: &Hive<Cursor<Vec<u8>>>,
72    hkcr_hive: &Hive<Cursor<Vec<u8>>>,
73) -> Vec<ComHijackInfo> {
74    let mut results = Vec::new();
75
76    let clsid_key = match hku_hive.open_key("Software\\Classes\\CLSID") {
77        Ok(Some(k)) => k,
78        _ => return results,
79    };
80
81    let guids = match clsid_key.subkeys() {
82        Ok(v) => v,
83        Err(_) => return results,
84    };
85
86    for guid_key in guids {
87        let clsid = guid_key.name();
88
89        // Find InprocServer32 under this GUID key in HKCU
90        let inproc = match guid_key.subkey("InprocServer32") {
91            Ok(Some(k)) => k,
92            _ => continue,
93        };
94
95        let hkcu_server = read_default_value(&inproc);
96        if hkcu_server.is_empty() {
97            continue;
98        }
99
100        // Look up the same CLSID in HKCR
101        let hkcr_server = read_hkcr_server(hkcr_hive, &clsid);
102
103        let (is_suspicious, suspicious_reason) = classify_com_hijack(&hkcr_server, &hkcu_server);
104
105        results.push(ComHijackInfo {
106            clsid,
107            hkcu_server,
108            hkcr_server,
109            is_suspicious,
110            suspicious_reason,
111        });
112    }
113
114    results
115}
116
117/// Parse user-side COM registrations from a single NTUSER.DAT hive.
118///
119/// Returns entries without HKCR comparison (`hkcr_server` will be empty).
120pub fn parse_hkcu_only(hku_hive: &Hive<Cursor<Vec<u8>>>) -> Vec<ComHijackInfo> {
121    let mut results = Vec::new();
122
123    let clsid_key = match hku_hive.open_key("Software\\Classes\\CLSID") {
124        Ok(Some(k)) => k,
125        _ => return results,
126    };
127
128    let guids = match clsid_key.subkeys() {
129        Ok(v) => v,
130        Err(_) => return results,
131    };
132
133    for guid_key in guids {
134        let clsid = guid_key.name();
135
136        let inproc = match guid_key.subkey("InprocServer32") {
137            Ok(Some(k)) => k,
138            _ => continue,
139        };
140
141        let hkcu_server = read_default_value(&inproc);
142        if hkcu_server.is_empty() {
143            continue;
144        }
145
146        let (is_suspicious, suspicious_reason) = classify_com_hijack("", &hkcu_server);
147
148        results.push(ComHijackInfo {
149            clsid,
150            hkcu_server,
151            hkcr_server: String::new(),
152            is_suspicious,
153            suspicious_reason,
154        });
155    }
156
157    results
158}
159
160// ── Helpers ───────────────────────────────────────────────────────────────────
161
162/// Read the default (empty-name) value from a key as a string.
163fn read_default_value(key: &winreg_core::key::Key<'_>) -> String {
164    let vals = match key.values() {
165        Ok(v) => v,
166        Err(_) => return String::new(),
167    };
168    for val in vals {
169        if val.name().is_empty() {
170            return val.as_string().unwrap_or_default();
171        }
172    }
173    String::new()
174}
175
176/// Try to look up the CLSID InprocServer32 default value in the HKCR hive.
177///
178/// Tries multiple path prefixes to handle both SOFTWARE hives and USRCLASS.DAT.
179fn read_hkcr_server(hkcr_hive: &Hive<Cursor<Vec<u8>>>, clsid: &str) -> String {
180    let paths = [
181        format!("SOFTWARE\\Classes\\CLSID\\{clsid}\\InprocServer32"),
182        format!("Classes\\CLSID\\{clsid}\\InprocServer32"),
183        format!("CLSID\\{clsid}\\InprocServer32"),
184    ];
185    for path in &paths {
186        if let Ok(Some(k)) = hkcr_hive.open_key(path) {
187            let s = read_default_value(&k);
188            if !s.is_empty() {
189                return s;
190            }
191        }
192    }
193    String::new()
194}