Skip to main content

elevate_ti/security/
command.rs

1//! Command-style process launcher with bound security tokens.
2
3use std::{
4    env,
5    ffi::{OsString, c_void},
6    mem,
7    os::windows::ffi::OsStringExt,
8    path::{Path, PathBuf},
9    ptr,
10};
11
12use windows::{
13    Win32::{
14        System::{
15            Environment::{
16                CreateEnvironmentBlock, DestroyEnvironmentBlock, ExpandEnvironmentStringsW,
17            },
18            Threading::{
19                CREATE_PROCESS_LOGON_FLAGS, CREATE_UNICODE_ENVIRONMENT, CreateProcessWithTokenW,
20                PROCESS_INFORMATION, STARTF_USESHOWWINDOW, STARTUPINFOW,
21            },
22        },
23        UI::WindowsAndMessaging::SW_SHOWNORMAL,
24    },
25    core::{HSTRING, PCWSTR, PWSTR},
26};
27
28use super::{HandleGuard, token::Token, win32};
29use crate::error::ElevateError;
30
31/// Target interactive desktop window station.
32#[derive(Debug, Clone, PartialEq, Eq, Default)]
33pub enum InteractiveDesktop {
34    /// The standard interactive user desktop (`WinSta0\Default`).
35    #[default]
36    Default,
37
38    /// The secure desktop (`WinSta0\Winlogon`), active during UAC prompts and lock screens.
39    Winlogon,
40
41    /// The screensaver desktop (`WinSta0\Screen-Saver`).
42    Screensaver,
43
44    /// A custom Window Station and Desktop object path (e.g. "`MyStation\\MyDesktop`").
45    Custom(String),
46}
47
48impl InteractiveDesktop {
49    /// Create a custom window station and desktop target path.
50    pub fn custom(station_and_desktop: impl Into<String>) -> Self {
51        Self::Custom(station_and_desktop.into())
52    }
53
54    pub(crate) fn as_station_str(&self) -> &str {
55        match self {
56            Self::Default => "WinSta0\\Default",
57            Self::Winlogon => "WinSta0\\Winlogon",
58            Self::Screensaver => "WinSta0\\Screen-Saver",
59            Self::Custom(station) => station.as_str(),
60        }
61    }
62}
63
64/// Process host environment context.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum ExecutionContext {
67    /// Run interactively attached to a specific desktop.
68    Interactive(InteractiveDesktop),
69
70    /// Run as a headless Session 0 process with no window station or desktop attached.
71    Headless,
72}
73
74impl Default for ExecutionContext {
75    fn default() -> Self {
76        Self::Interactive(InteractiveDesktop::default())
77    }
78}
79
80/// RAII guard for destroying the Win32 user environment block.
81struct EnvBlockGuard(*mut c_void);
82
83impl Drop for EnvBlockGuard {
84    fn drop(&mut self) {
85        if !self.0.is_null() {
86            unsafe {
87                let _ = DestroyEnvironmentBlock(self.0);
88            }
89        }
90    }
91}
92
93/// Builder for launching processes under a specified primary security token.
94#[must_use = "commands do nothing unless spawned"]
95pub struct TokenCommand {
96    token: Token,
97    exe: Option<PathBuf>,
98    args: Option<String>,
99    desktop_str: Option<HSTRING>,
100}
101
102impl TokenCommand {
103    /// Create a new command builder bound to an elevated token and its target desktop context.
104    pub fn new(token: Token, desktop: Option<InteractiveDesktop>) -> Self {
105        let desktop_str = desktop.map(|d| HSTRING::from(d.as_station_str()));
106        Self {
107            token,
108            exe: None,
109            args: None,
110            desktop_str,
111        }
112    }
113
114    /// Set the target executable to the path of the current binary.
115    pub fn current_exe(mut self) -> Result<Self, ElevateError> {
116        self.exe = Some(env::current_exe()?);
117        Ok(self)
118    }
119
120    /// Set a custom target executable path. Supports environment variables like `%SystemRoot%`.
121    pub fn exe(mut self, path: impl AsRef<Path>) -> Self {
122        self.exe = Some(expand_env_path(path.as_ref()));
123        self
124    }
125
126    /// Pass a raw command-line string to the process.
127    pub fn args(mut self, args: impl Into<String>) -> Self {
128        self.args = Some(args.into());
129        self
130    }
131
132    /// Spawn the process under the configured token context.
133    pub fn spawn(self) -> Result<(), ElevateError> {
134        let exe_hstr = self.exe.as_ref().map(|p| HSTRING::from(p.as_os_str()));
135
136        let cmd = match (&self.args, &self.exe) {
137            (Some(args), _) => args.clone(),
138            (None, Some(exe)) => format!("\"{}\"", exe.display()),
139            (None, None) => return Err(ElevateError::MissingExePath),
140        };
141
142        let mut cmd_utf16: Vec<u16> = cmd.encode_utf16().chain(std::iter::once(0)).collect();
143
144        let mut raw_env_block: *mut c_void = ptr::null_mut();
145        win32!(CreateEnvironmentBlock(
146            &raw mut raw_env_block,
147            self.token.as_raw(),
148            false
149        ))?;
150        let _env_guard = EnvBlockGuard(raw_env_block);
151
152        let cwd = env::current_dir()?;
153        let cwd_hstr = HSTRING::from(cwd.as_os_str());
154
155        let lp_desktop = match &self.desktop_str {
156            Some(d) => PWSTR(d.as_ptr().cast_mut()),
157            None => PWSTR::null(),
158        };
159
160        let si = STARTUPINFOW {
161            cb: mem::size_of::<STARTUPINFOW>() as u32,
162            lpDesktop: lp_desktop,
163            dwFlags: STARTF_USESHOWWINDOW,
164            wShowWindow: SW_SHOWNORMAL.0 as u16,
165            ..Default::default()
166        };
167        let mut pi = PROCESS_INFORMATION::default();
168
169        let app_name = match &exe_hstr {
170            Some(h) => PCWSTR(h.as_ptr()),
171            None => PCWSTR::null(),
172        };
173
174        let spawn_res = win32!(CreateProcessWithTokenW(
175            self.token.as_raw(),
176            CREATE_PROCESS_LOGON_FLAGS(0),
177            app_name,
178            PWSTR(cmd_utf16.as_mut_ptr()),
179            CREATE_UNICODE_ENVIRONMENT,
180            Some(raw_env_block),
181            &cwd_hstr,
182            &raw const si,
183            &raw mut pi,
184        ));
185
186        let _process_guard = HandleGuard(pi.hProcess);
187        let _thread_guard = HandleGuard(pi.hThread);
188
189        spawn_res?;
190        Ok(())
191    }
192}
193
194fn expand_env_path(path: &Path) -> PathBuf {
195    let input = HSTRING::from(path.as_os_str());
196
197    let needed = unsafe { ExpandEnvironmentStringsW(&input, None) };
198    if needed == 0 {
199        return path.to_path_buf();
200    }
201
202    let mut buf: Vec<u16> = vec![0; needed as usize];
203    let written = unsafe { ExpandEnvironmentStringsW(&input, Some(&mut buf)) };
204    if written == 0 || written as usize > buf.len() {
205        return path.to_path_buf();
206    }
207
208    let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
209    PathBuf::from(OsString::from_wide(&buf[..len]))
210}