Skip to main content

win_text_inject/
target.rs

1//! Identify the injection target and decide, before injecting, whether injection can work at all.
2//!
3//! `SendInput` into a higher-integrity window fails silently. Per MSDN:
4//!
5//! > This function fails when it is blocked by UIPI. Note that neither `GetLastError` nor the
6//! > return value will indicate the failure was caused by UIPI blocking.
7//!
8//! So the text simply vanishes. Checking the target's integrity level costs microseconds and turns
9//! a silent loss into an honest "press Ctrl+V here" message. This is the root cause of the
10//! elevated-window failures reported against every tool in this category.
11
12use std::path::Path;
13
14use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND};
15use windows::Win32::Security::{
16    GetTokenInformation, TokenIntegrityLevel, TOKEN_MANDATORY_LABEL, TOKEN_QUERY,
17};
18use windows::Win32::System::Threading::{
19    GetCurrentProcess, OpenProcess, OpenProcessToken, QueryFullProcessImageNameW,
20    PROCESS_NAME_FORMAT, PROCESS_QUERY_LIMITED_INFORMATION,
21};
22use windows::Win32::UI::WindowsAndMessaging::{
23    GetForegroundWindow, GetWindowThreadProcessId, RealGetWindowClassW,
24};
25
26use crate::Error;
27
28/// Windows integrity levels, ordered. Comparison is what matters, not the raw RID.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
30pub enum Integrity {
31    /// Below Low. Used by heavily sandboxed processes.
32    Untrusted,
33    /// Sandboxed processes, e.g. browser renderers and protected-mode content.
34    Low,
35    /// Ordinary user processes. Where a dictation app normally runs.
36    Medium,
37    /// Between Medium and High; used by some UAC-aware processes.
38    MediumPlus,
39    /// Elevated (administrator) processes. UIPI blocks input from Medium into these.
40    High,
41    /// Service and kernel-adjacent processes.
42    System,
43    /// Protected-process level, above anything a desktop app can reach.
44    Protected,
45}
46
47impl Integrity {
48    /// Map the RID from a mandatory-label SID to a level.
49    ///
50    /// Ranges rather than equality: Windows defines the in-between values as belonging to the next
51    /// lower named level.
52    fn from_rid(rid: u32) -> Self {
53        match rid {
54            0..=0x0FFF => Integrity::Untrusted,
55            0x1000..=0x1FFF => Integrity::Low,
56            0x2000..=0x2FFF => Integrity::Medium,
57            0x3000..=0x3FFF => Integrity::MediumPlus,
58            0x4000..=0x4FFF => Integrity::High,
59            0x5000..=0x5FFF => Integrity::System,
60            _ => Integrity::Protected,
61        }
62    }
63}
64
65/// The window that will receive injected text, captured as a snapshot.
66///
67/// Capture this at hotkey **press**, not at injection time: between press and release the user may
68/// have moved focus, and injecting into whatever happens to be foreground later is how text ends up
69/// in the wrong application.
70#[derive(Debug, Clone)]
71pub struct Target {
72    /// Raw `HWND` of the captured foreground window.
73    pub hwnd: isize,
74    /// Process that owns the window.
75    pub pid: u32,
76    /// Lowercased executable file name, e.g. `code.exe`. Empty when it could not be read.
77    pub exe: String,
78    /// Real window class of the foreground window, e.g. `Chrome_RenderWidgetHostHWND`.
79    pub class: String,
80    /// Integrity level of the owning process. Determines whether `SendInput` can reach it.
81    pub integrity: Integrity,
82}
83
84impl Target {
85    /// Snapshot the current foreground window.
86    pub fn foreground() -> Result<Self, Error> {
87        let hwnd = unsafe { GetForegroundWindow() };
88        if hwnd.0.is_null() {
89            return Err(Error::NoForegroundWindow);
90        }
91
92        let mut pid: u32 = 0;
93        unsafe { GetWindowThreadProcessId(hwnd, Some(&mut pid)) };
94        if pid == 0 {
95            return Err(Error::NoForegroundWindow);
96        }
97
98        Ok(Self {
99            hwnd: hwnd.0 as isize,
100            pid,
101            exe: process_exe_name(pid).unwrap_or_default(),
102            class: window_class(hwnd),
103            integrity: process_integrity(pid).unwrap_or(Integrity::Medium),
104        })
105    }
106
107    /// True when this target still holds the foreground. Injection must be aborted otherwise.
108    pub fn still_foreground(&self) -> bool {
109        let current = unsafe { GetForegroundWindow() };
110        current.0 as isize == self.hwnd
111    }
112
113    /// Whether synthesized input can reach this target.
114    ///
115    /// UIPI permits injection only into processes at an equal or lower integrity level.
116    pub fn accepts_injection(&self) -> bool {
117        match our_integrity() {
118            Some(ours) => self.integrity <= ours,
119            // Unable to determine our own level: assume the optimistic case and let the caller's
120            // verification step catch a failure, rather than refusing to work at all.
121            None => true,
122        }
123    }
124}
125
126fn window_class(hwnd: HWND) -> String {
127    let mut buf = [0u16; 256];
128    let len = unsafe { RealGetWindowClassW(hwnd, &mut buf) };
129    String::from_utf16_lossy(&buf[..len as usize])
130}
131
132fn process_exe_name(pid: u32) -> Option<String> {
133    unsafe {
134        let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid).ok()?;
135        let mut buf = [0u16; 512];
136        let mut len = buf.len() as u32;
137        let result = QueryFullProcessImageNameW(
138            handle,
139            PROCESS_NAME_FORMAT(0),
140            windows::core::PWSTR(buf.as_mut_ptr()),
141            &mut len,
142        );
143        let _ = CloseHandle(handle);
144        result.ok()?;
145
146        let full = String::from_utf16_lossy(&buf[..len as usize]);
147        Some(
148            Path::new(&full)
149                .file_name()?
150                .to_string_lossy()
151                .to_lowercase(),
152        )
153    }
154}
155
156fn process_integrity(pid: u32) -> Option<Integrity> {
157    unsafe {
158        let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid).ok()?;
159        let level = token_integrity(handle);
160        let _ = CloseHandle(handle);
161        level
162    }
163}
164
165fn our_integrity() -> Option<Integrity> {
166    unsafe { token_integrity(GetCurrentProcess()) }
167}
168
169unsafe fn token_integrity(process: HANDLE) -> Option<Integrity> {
170    let mut token = HANDLE::default();
171    OpenProcessToken(process, TOKEN_QUERY, &mut token).ok()?;
172
173    let mut needed: u32 = 0;
174    // First call always fails with ERROR_INSUFFICIENT_BUFFER; it exists to report the size.
175    let _ = GetTokenInformation(token, TokenIntegrityLevel, None, 0, &mut needed);
176    if needed == 0 {
177        let _ = CloseHandle(token);
178        return None;
179    }
180
181    let mut buf = vec![0u8; needed as usize];
182    let result = GetTokenInformation(
183        token,
184        TokenIntegrityLevel,
185        Some(buf.as_mut_ptr() as *mut _),
186        needed,
187        &mut needed,
188    );
189    let _ = CloseHandle(token);
190    result.ok()?;
191
192    let label = &*(buf.as_ptr() as *const TOKEN_MANDATORY_LABEL);
193    let sid = label.Label.Sid;
194    if sid.is_invalid() {
195        return None;
196    }
197
198    let count_ptr = windows::Win32::Security::GetSidSubAuthorityCount(sid);
199    if count_ptr.is_null() {
200        return None;
201    }
202    let last = (*count_ptr).saturating_sub(1) as u32;
203    let rid_ptr = windows::Win32::Security::GetSidSubAuthority(sid, last);
204    if rid_ptr.is_null() {
205        return None;
206    }
207    Some(Integrity::from_rid(*rid_ptr))
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn integrity_is_ordered() {
216        assert!(Integrity::Low < Integrity::Medium);
217        assert!(Integrity::Medium < Integrity::High);
218        assert!(Integrity::High < Integrity::System);
219    }
220
221    #[test]
222    fn rid_ranges_map_to_named_levels() {
223        assert_eq!(Integrity::from_rid(0x0000), Integrity::Untrusted);
224        assert_eq!(Integrity::from_rid(0x1000), Integrity::Low);
225        assert_eq!(Integrity::from_rid(0x2000), Integrity::Medium);
226        assert_eq!(Integrity::from_rid(0x3000), Integrity::MediumPlus);
227        assert_eq!(Integrity::from_rid(0x4000), Integrity::High);
228        assert_eq!(Integrity::from_rid(0x5000), Integrity::System);
229    }
230
231    #[test]
232    fn in_between_rids_fall_to_the_lower_named_level() {
233        // Windows treats values between named levels as the lower level, not the higher one.
234        assert_eq!(Integrity::from_rid(0x2100), Integrity::Medium);
235        assert_eq!(Integrity::from_rid(0x4FFF), Integrity::High);
236    }
237
238    #[test]
239    fn our_own_integrity_is_readable() {
240        // The test process must be able to read its own token; failure means the SID walk is wrong.
241        assert!(our_integrity().is_some());
242    }
243
244    #[test]
245    fn a_normal_test_process_runs_at_medium_or_high() {
246        let level = our_integrity().unwrap();
247        assert!(level >= Integrity::Medium, "unexpected level {level:?}");
248    }
249}