Skip to main content

ib_shell_item/hook/
mod.rs

1/*!
2## Remote processes
3There are mainly three ways to hook remote processes:
4- [`inject`]: Inject a [`DLL`](dll) directly
5  - Controllable target processes.
6  - Easily hot reload.
7  - Hard to manage for multi-process applications (Explorer).
8  - May cause antivirus false positives.
9- Register a Shell extension
10  - Require system (Registry) changes.
11  - Hard to hot reload, since the extension will be loaded into many processes.
12- DLL hijacking
13  - Only suitable for third-party applications.
14
15## Applications
16- As a performance/shittiness measure.
17  - Windows 11 24H2 Explorer: 2000 calls/folder
18  - DOpus: 250 calls/folder
19  - TC: 0?
20*/
21use std::{cell::SyncUnsafeCell, path::PathBuf, sync::RwLock};
22
23use bon::Builder;
24use serde::{Deserialize, Serialize};
25use tracing::{debug, error, info, trace, warn};
26use windows::{
27    Win32::UI::Shell::{IShellItem, IShellItem2},
28    core::{GUID, Interface},
29};
30use windows_sys::{Win32::UI::Shell::Common::ITEMIDLIST, core::HRESULT};
31
32use crate::{ShellItem, ShellItemDisplayName};
33
34pub mod display_name;
35#[cfg(feature = "hook-dll")]
36pub mod dll;
37pub mod folder;
38#[cfg(feature = "hook-dll")]
39pub mod inject;
40#[cfg(feature = "prop")]
41pub mod prop;
42
43type SHCreateItemFromIDListFn = unsafe extern "system" fn(
44    pidl: *const ITEMIDLIST,
45    riid: *const GUID,
46    ppv: *mut *mut core::ffi::c_void,
47) -> HRESULT;
48
49// shell32.dll!SHCreateItemFromIDList is actually implemented in windows.storage.dll
50windows_link::link!("windows.storage.dll" "system" "SHCreateItemFromIDList" fn SHCreateItemFromIDList_windows_storage(pidl : *const ITEMIDLIST, riid : *const GUID, ppv : *mut *mut core::ffi::c_void) -> HRESULT);
51
52static TRUE_SH_CREATE_ITEM_FROM_ID_LIST: SyncUnsafeCell<SHCreateItemFromIDListFn> =
53    SyncUnsafeCell::new(SHCreateItemFromIDList_windows_storage);
54
55/// Hook configuration for [`SHCreateItemFromIDList`].
56/// This is used to intercept shell item creation from ID lists.
57#[derive(Default, Serialize, Deserialize, Clone, Builder, Debug)]
58pub struct HookConfig {
59    /// If true, the hook will intercept all [`SHCreateItemFromIDList]` calls.
60    pub enabled: bool,
61
62    /// If `Some`, the hook will intercept [`IShellItem::GetDisplayName`] calls.
63    pub display_name: Option<display_name::DisplayNameHookConfig>,
64
65    pub folder: Option<folder::FolderHookConfig>,
66
67    #[cfg(feature = "prop")]
68    pub property: Option<prop::PropertyHookConfig>,
69
70    /// Path to the log file.
71    ///
72    /// Existing logs in the log file won't be cleared.
73    ///
74    /// Ignored if `hook-log` feature is not enabled.
75    pub log: Option<PathBuf>,
76}
77
78/// Why not `Mutex`?
79/// Besides performance, `RwLock` is also needed to avoid reentrant deadlock.
80static HOOK_CONFIG: RwLock<HookConfig> = RwLock::new(HookConfig {
81    enabled: false,
82    display_name: None,
83    folder: None,
84    #[cfg(feature = "prop")]
85    property: None,
86    log: None,
87});
88
89/// [`ShellItem::from_id_list`]
90unsafe extern "system" fn sh_create_item_from_id_list(
91    pidl: *const ITEMIDLIST,
92    riid: *const GUID,
93    ppv: *mut *mut core::ffi::c_void,
94) -> HRESULT {
95    let real = || unsafe { (*TRUE_SH_CREATE_ITEM_FROM_ID_LIST.get())(pidl, riid, ppv) };
96
97    let result = real();
98
99    let config = HOOK_CONFIG.read().unwrap();
100    if !config.enabled {
101        return result;
102    }
103
104    // If successful, get and log the display name
105    trace!(?pidl, ?riid, ?ppv, ?result, "SHCreateItemFromIDList called");
106    if result >= 0 {
107        let iid = unsafe { *riid };
108        let item = unsafe {
109            match iid {
110                IShellItem::IID => {
111                    // let ppv = ppv as *mut IShellItem;
112                    // unsafe { &*ppv }
113                    IShellItem::from_raw_borrowed(&*ppv).unwrap()
114                }
115                IShellItem2::IID => {
116                    /*
117                    match IShellItem2::from_raw_borrowed(&*ppv).unwrap().cast() {
118                        Ok(item) => item,
119                        Err(e) => {
120                            error!(?e);
121                            return result;
122                    }
123                    */
124                    return result;
125                }
126                _ => {
127                    warn!(?iid, "unknown");
128                    return result;
129                }
130            }
131        };
132        let name = item.get_display_name(ShellItemDisplayName::FileSystemPath);
133        debug!(?name, "SHCreateItemFromIDList called");
134
135        // Hook GetDisplayName if name config is some
136        if config.display_name.is_some() {
137            let get_display_name = item.vtable().GetDisplayName;
138            if let Err(e) = display_name::enable_hook(get_display_name) {
139                error!(%e, "Failed to hook GetDisplayName");
140            }
141        }
142
143        #[cfg(feature = "prop")]
144        if config.property.is_some() {
145            if let Ok(item2) = item.cast::<IShellItem2>() {
146                if let Err(e) = prop::enable_hook(&item2) {
147                    error!(%e, "Failed to hook prop");
148                }
149            }
150        }
151    } else {
152        debug!(?result, "SHCreateItemFromIDList called");
153    }
154
155    result
156}
157
158fn hook(enable: bool) -> windows::core::Result<()> {
159    let res = unsafe {
160        slim_detours_sys::SlimDetoursInlineHook(
161            enable as _,
162            TRUE_SH_CREATE_ITEM_FROM_ID_LIST.get().cast(),
163            sh_create_item_from_id_list as _,
164        )
165    };
166    windows::core::HRESULT(res).ok()
167}
168
169/// Initialize logging if log path is set in config.
170#[cfg(feature = "hook-log")]
171fn log_init(log_path: &PathBuf) {
172    // Syncly log in debug mode
173    #[cfg(debug_assertions)]
174    let writer = {
175        let log_dir = log_path.parent().unwrap();
176        let log_filename = log_path.file_name().unwrap();
177        tracing_appender::rolling::never(log_dir, log_filename)
178    };
179    #[cfg(not(debug_assertions))]
180    let (writer, _guard) = tracing_appender::non_blocking(
181        std::fs::OpenOptions::new()
182            .append(true)
183            .create(true)
184            .open(log_path)
185            .ok()
186            .unwrap(),
187    );
188
189    let _ = tracing_subscriber::fmt()
190        .with_writer(writer)
191        .with_max_level(tracing::Level::DEBUG)
192        .with_ansi(false)
193        .try_init();
194    info!("log_init");
195}
196
197/// Set the hook with optional config.
198/// If config is None or enabled is false, the hook is disabled.
199pub fn set_hook(config: Option<HookConfig>) {
200    if let Some(config) = config {
201        let mut hook_config = HOOK_CONFIG.write().unwrap();
202        *hook_config = config;
203        if hook_config.enabled {
204            #[cfg(feature = "hook-log")]
205            if let Some(ref log_path) = hook_config.log {
206                log_init(log_path);
207            }
208            info!("attach");
209            if let Err(e) = hook(true) {
210                error!(%e, "Failed to hook SHCreateItemFromIDList");
211            }
212
213            if let Err(e) = folder::apply(hook_config.folder.clone()) {
214                error!(?e, "folder");
215            }
216        }
217    } else {
218        info!("detach");
219        if let Err(e) = hook(false) {
220            error!(%e, "Failed to detach hook");
221        }
222        // Should be after hook()
223        if let Err(e) = display_name::disable_hook() {
224            error!(%e, "Failed to detach GetDisplayName");
225        }
226        #[cfg(feature = "prop")]
227        if let Err(e) = prop::disable_hook() {
228            error!(%e, "Failed to detach prop");
229        }
230
231        if let Err(e) = folder::apply(None) {
232            error!(?e, "folder");
233        }
234
235        #[cfg(feature = "everything")]
236        unsafe {
237            everything_ipc::wm::EverythingClient::shared_quit_join_thread()
238        };
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn hook_config_default() {
248        let config = HookConfig::default();
249        assert!(!config.enabled);
250    }
251
252    #[test]
253    fn set_hook_none() {
254        // Should not panic
255        set_hook(None);
256    }
257
258    #[test]
259    fn set_hook_disabled() {
260        set_hook(Some(HookConfig {
261            enabled: false,
262            ..Default::default()
263        }));
264    }
265}