2092 lines
76 KiB
Rust
2092 lines
76 KiB
Rust
//! Windows-native helpers for privileged process launch and token inspection.
|
|
|
|
use serde::Serialize;
|
|
|
|
/// Target identity used for a privileged process launch.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum LaunchIdentity {
|
|
/// Run as the current process token without UAC elevation.
|
|
CurrentProcess,
|
|
/// Run as the active interactive user.
|
|
CurrentUser,
|
|
/// Run as an elevated administrator.
|
|
Admin,
|
|
/// Run as `NT AUTHORITY\SYSTEM`.
|
|
System,
|
|
/// Run as `NT SERVICE\TrustedInstaller`.
|
|
TrustedInstaller,
|
|
}
|
|
|
|
/// Privilege policy applied before launching a process.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum PrivilegeMode {
|
|
/// Keep the duplicated token privileges unchanged.
|
|
Default,
|
|
/// Attempt to enable all privileges present on the token.
|
|
EnableAll,
|
|
/// Attempt to disable all privileges present on the duplicated token.
|
|
DisableAll,
|
|
}
|
|
|
|
/// Token integrity level used for status reports and optional launch shaping.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TokenIntegrity {
|
|
/// Untrusted integrity.
|
|
Untrusted,
|
|
/// Low integrity.
|
|
Low,
|
|
/// Medium integrity.
|
|
Medium,
|
|
/// Medium-plus integrity.
|
|
MediumPlus,
|
|
/// High integrity.
|
|
High,
|
|
/// System integrity.
|
|
System,
|
|
/// Unknown or unmapped integrity.
|
|
Unknown,
|
|
}
|
|
|
|
/// Process priority class used during launch.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ProcessPriority {
|
|
/// Idle priority.
|
|
Idle,
|
|
/// Below normal priority.
|
|
BelowNormal,
|
|
/// Normal priority.
|
|
Normal,
|
|
/// Above normal priority.
|
|
AboveNormal,
|
|
/// High priority.
|
|
High,
|
|
/// Realtime priority.
|
|
Realtime,
|
|
}
|
|
|
|
/// Window presentation mode used for a launch request.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ShowWindowMode {
|
|
/// Let Windows decide.
|
|
Default,
|
|
/// Hide the new window.
|
|
Hidden,
|
|
/// Show a normal window.
|
|
Normal,
|
|
/// Show a minimized window.
|
|
Minimized,
|
|
/// Show a maximized window.
|
|
Maximized,
|
|
}
|
|
|
|
/// Stable status report for the current process token and privileged-launch capabilities.
|
|
#[allow(clippy::struct_excessive_bools)]
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
pub struct TokenStatus {
|
|
/// Whether the current process is elevated.
|
|
pub is_elevated: bool,
|
|
/// Whether the token belongs to an administrators group member.
|
|
pub is_admin_member: bool,
|
|
/// Current integrity level.
|
|
pub integrity: TokenIntegrity,
|
|
/// Current Windows user name when available.
|
|
pub current_user: Option<String>,
|
|
/// Current process session identifier when available.
|
|
pub session_id: Option<u32>,
|
|
/// Active console session identifier when available.
|
|
pub active_session_id: Option<u32>,
|
|
/// Whether the current process can directly launch as administrator.
|
|
pub can_admin: bool,
|
|
/// Whether an active interactive user token can be used.
|
|
pub can_current_user: bool,
|
|
/// Whether the current process can directly launch as `SYSTEM`.
|
|
pub can_system: bool,
|
|
/// Whether the current process can directly launch as `TrustedInstaller`.
|
|
pub can_trustedinstaller: bool,
|
|
/// Whether the `TrustedInstaller` service exists.
|
|
pub trustedinstaller_installed: bool,
|
|
/// Whether the `TrustedInstaller` service is already running.
|
|
pub trustedinstaller_running: bool,
|
|
}
|
|
|
|
/// High-level launch request consumed by `msudo`.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct LaunchRequest {
|
|
/// Program path or command name.
|
|
pub program: String,
|
|
/// Command arguments excluding the program itself.
|
|
pub args: Vec<String>,
|
|
/// Current working directory for the launched process.
|
|
pub current_directory: Option<String>,
|
|
/// Optional stdout redirection path used by short-lived relays.
|
|
pub stdout_path: Option<String>,
|
|
/// Optional stderr redirection path used by short-lived relays.
|
|
pub stderr_path: Option<String>,
|
|
/// Target identity.
|
|
pub identity: LaunchIdentity,
|
|
/// Privilege policy.
|
|
pub privileges: PrivilegeMode,
|
|
/// Optional integrity shaping request.
|
|
pub integrity: Option<TokenIntegrity>,
|
|
/// Process priority class.
|
|
pub priority: ProcessPriority,
|
|
/// Window presentation mode.
|
|
pub show_window: ShowWindowMode,
|
|
/// Optional target session id.
|
|
pub session: Option<u32>,
|
|
/// Whether the caller explicitly wants a new window.
|
|
pub new_window: bool,
|
|
/// Whether the launched process should take over the caller's current console.
|
|
pub same_console: bool,
|
|
/// Whether to wait for completion and capture the exit code.
|
|
pub wait: bool,
|
|
}
|
|
|
|
/// Result returned after a privileged launch attempt.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
|
pub struct LaunchResult {
|
|
/// Spawned process identifier when available.
|
|
pub pid: Option<u32>,
|
|
/// Child exit code when `wait` was requested and the process exited normally.
|
|
pub exit_code: Option<i32>,
|
|
/// Identity used for the launched process.
|
|
pub identity: LaunchIdentity,
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
/// Reports the current process token status.
|
|
pub fn current_token_status() -> Result<TokenStatus, WindowsSupportError> {
|
|
Err(WindowsSupportError::Unsupported(
|
|
"msudo is only supported on Windows".to_string(),
|
|
))
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
/// Relaunches the current executable with UAC elevation.
|
|
pub fn elevate_current_process(
|
|
_arguments: &[String],
|
|
_wait: bool,
|
|
_show_window: ShowWindowMode,
|
|
) -> Result<LaunchResult, WindowsSupportError> {
|
|
Err(WindowsSupportError::Unsupported(
|
|
"msudo is only supported on Windows".to_string(),
|
|
))
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
/// Reattaches the current process to the parent's console so foreground launches can share it.
|
|
pub fn attach_parent_console(_parent_pid: u32) -> Result<(), WindowsSupportError> {
|
|
Err(WindowsSupportError::Unsupported(
|
|
"msudo is only supported on Windows".to_string(),
|
|
))
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
/// Launches a process with the requested identity.
|
|
pub fn launch_request(_request: &LaunchRequest) -> Result<LaunchResult, WindowsSupportError> {
|
|
Err(WindowsSupportError::Unsupported(
|
|
"msudo is only supported on Windows".to_string(),
|
|
))
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[allow(unsafe_code)]
|
|
mod imp {
|
|
use std::ffi::{OsStr, c_void};
|
|
use std::fs::{File, OpenOptions};
|
|
use std::mem::size_of;
|
|
use std::os::windows::ffi::OsStrExt;
|
|
use std::os::windows::fs::OpenOptionsExt;
|
|
use std::os::windows::io::AsRawHandle;
|
|
use std::ptr::{null, null_mut};
|
|
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use windows_sys::Win32::Foundation::{
|
|
CloseHandle, DUPLICATE_SAME_ACCESS, DuplicateHandle, ERROR_ACCESS_DENIED,
|
|
ERROR_NOT_ALL_ASSIGNED, FALSE, GetLastError, HANDLE, INVALID_HANDLE_VALUE, TRUE,
|
|
WAIT_FAILED, WAIT_OBJECT_0,
|
|
};
|
|
use windows_sys::Win32::Security::{
|
|
AdjustTokenPrivileges, CheckTokenMembership, CreateWellKnownSid, DuplicateTokenEx,
|
|
GetTokenInformation, ImpersonateLoggedOnUser, LookupAccountSidW, LookupPrivilegeValueW,
|
|
RevertToSelf, SE_PRIVILEGE_ENABLED, SecurityImpersonation, SetTokenInformation,
|
|
TOKEN_ACCESS_MASK, TOKEN_ADJUST_DEFAULT, TOKEN_ADJUST_PRIVILEGES, TOKEN_ADJUST_SESSIONID,
|
|
TOKEN_ASSIGN_PRIMARY, TOKEN_DUPLICATE, TOKEN_ELEVATION, TOKEN_IMPERSONATE,
|
|
TOKEN_MANDATORY_LABEL, TOKEN_PRIVILEGES, TOKEN_QUERY, TokenElevation, TokenIntegrityLevel,
|
|
TokenSessionId, WinBuiltinAdministratorsSid, WinLocalSystemSid,
|
|
};
|
|
use windows_sys::Win32::System::Console::{
|
|
AttachConsole, FreeConsole, GetStdHandle, STD_ERROR_HANDLE, STD_HANDLE, STD_INPUT_HANDLE,
|
|
STD_OUTPUT_HANDLE, SetConsoleCtrlHandler,
|
|
};
|
|
use windows_sys::Win32::System::Environment::{
|
|
CreateEnvironmentBlock, DestroyEnvironmentBlock,
|
|
};
|
|
use windows_sys::Win32::System::RemoteDesktop::{
|
|
ProcessIdToSessionId, WTSGetActiveConsoleSessionId, WTSQueryUserToken,
|
|
};
|
|
use windows_sys::Win32::System::Services::{
|
|
CloseServiceHandle, OpenSCManagerW, OpenServiceW, QueryServiceStatusEx, SC_HANDLE,
|
|
SC_MANAGER_CONNECT, SC_STATUS_PROCESS_INFO, SERVICE_QUERY_STATUS, SERVICE_RUNNING,
|
|
SERVICE_START, SERVICE_START_PENDING, SERVICE_STATUS_PROCESS, StartServiceW,
|
|
};
|
|
use windows_sys::Win32::System::SystemServices::SE_GROUP_INTEGRITY;
|
|
use windows_sys::Win32::System::Threading::{
|
|
ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, CREATE_NEW_CONSOLE,
|
|
CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, CreateProcessAsUserW, CreateProcessW,
|
|
GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, HIGH_PRIORITY_CLASS,
|
|
IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, OpenProcess, OpenProcessToken,
|
|
PROCESS_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, REALTIME_PRIORITY_CLASS,
|
|
STARTF_USESHOWWINDOW, STARTF_USESTDHANDLES, STARTUPINFOW, WaitForSingleObject,
|
|
};
|
|
use windows_sys::Win32::UI::Shell::{
|
|
SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW, ShellExecuteExW,
|
|
};
|
|
|
|
use crate::WindowsSupportError;
|
|
use crate::sudo::{
|
|
LaunchIdentity, LaunchRequest, LaunchResult, PrivilegeMode, ProcessPriority,
|
|
ShowWindowMode, TokenIntegrity, TokenStatus,
|
|
};
|
|
|
|
const SECURITY_MAX_SID_SIZE: usize = 68;
|
|
const START_TIMEOUT: Duration = Duration::from_secs(10);
|
|
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
|
|
|
|
const SW_HIDE_VALUE: u16 = 0;
|
|
const SW_NORMAL_VALUE: u16 = 1;
|
|
const SW_SHOWMINIMIZED_VALUE: u16 = 2;
|
|
const SW_SHOWMAXIMIZED_VALUE: u16 = 3;
|
|
const SW_SHOWDEFAULT_VALUE: u16 = 10;
|
|
|
|
const SECURITY_MANDATORY_UNTRUSTED_RID: u32 = 0x0000_0000;
|
|
const SECURITY_MANDATORY_LOW_RID: u32 = 0x0000_1000;
|
|
const SECURITY_MANDATORY_MEDIUM_RID: u32 = 0x0000_2000;
|
|
const SECURITY_MANDATORY_MEDIUM_PLUS_RID: u32 = SECURITY_MANDATORY_MEDIUM_RID + 0x100;
|
|
const SECURITY_MANDATORY_HIGH_RID: u32 = 0x0000_3000;
|
|
const SECURITY_MANDATORY_SYSTEM_RID: u32 = 0x0000_4000;
|
|
|
|
#[derive(Debug)]
|
|
struct OwnedHandle(HANDLE);
|
|
|
|
impl OwnedHandle {
|
|
fn new(raw: HANDLE) -> Result<Self, WindowsSupportError> {
|
|
if raw.is_null() || std::ptr::eq(raw, INVALID_HANDLE_VALUE) {
|
|
Err(last_error("acquire handle"))
|
|
} else {
|
|
Ok(Self(raw))
|
|
}
|
|
}
|
|
|
|
const fn raw(&self) -> HANDLE {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl Drop for OwnedHandle {
|
|
fn drop(&mut self) {
|
|
let _ = unsafe { CloseHandle(self.0) };
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ImpersonationGuard {
|
|
_token: OwnedHandle,
|
|
}
|
|
|
|
impl ImpersonationGuard {
|
|
fn as_system() -> Result<Self, WindowsSupportError> {
|
|
Self::as_system_with_privileges(&[])
|
|
}
|
|
|
|
fn as_system_with_privileges(privileges: &[&str]) -> Result<Self, WindowsSupportError> {
|
|
let token = open_system_impersonation_token()?;
|
|
for privilege in privileges {
|
|
enable_named_privilege(token.raw(), privilege)?;
|
|
}
|
|
#[allow(unsafe_code)]
|
|
let impersonated = unsafe { ImpersonateLoggedOnUser(token.raw()) };
|
|
if impersonated == FALSE {
|
|
return Err(last_error("ImpersonateLoggedOnUser"));
|
|
}
|
|
Ok(Self { _token: token })
|
|
}
|
|
}
|
|
|
|
impl Drop for ImpersonationGuard {
|
|
fn drop(&mut self) {
|
|
let _ = unsafe { RevertToSelf() };
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct OwnedServiceHandle(SC_HANDLE);
|
|
|
|
impl OwnedServiceHandle {
|
|
fn new(raw: SC_HANDLE, action: &'static str) -> Result<Self, WindowsSupportError> {
|
|
if raw.is_null() {
|
|
Err(last_error(action))
|
|
} else {
|
|
Ok(Self(raw))
|
|
}
|
|
}
|
|
|
|
const fn raw(&self) -> SC_HANDLE {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl Drop for OwnedServiceHandle {
|
|
fn drop(&mut self) {
|
|
let _ = unsafe { CloseServiceHandle(self.0) };
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct OwnedEnvironmentBlock(*mut c_void);
|
|
|
|
impl OwnedEnvironmentBlock {
|
|
fn for_token(token: HANDLE, inherit: bool) -> Result<Self, WindowsSupportError> {
|
|
let mut block = null_mut();
|
|
#[allow(unsafe_code)]
|
|
let created =
|
|
unsafe { CreateEnvironmentBlock(&raw mut block, token, i32::from(inherit)) };
|
|
if created == FALSE {
|
|
return Err(last_error("CreateEnvironmentBlock"));
|
|
}
|
|
Ok(Self(block))
|
|
}
|
|
|
|
const fn raw(&self) -> *const c_void {
|
|
self.0.cast::<c_void>()
|
|
}
|
|
}
|
|
|
|
impl Drop for OwnedEnvironmentBlock {
|
|
fn drop(&mut self) {
|
|
#[allow(unsafe_code)]
|
|
let _ = unsafe { DestroyEnvironmentBlock(self.0.cast::<c_void>()) };
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct InheritedStdHandles {
|
|
input: OwnedHandle,
|
|
output: OwnedHandle,
|
|
error: OwnedHandle,
|
|
_stdout_file: Option<File>,
|
|
_stderr_file: Option<File>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ConsoleCtrlGuard {
|
|
ignore_ctrl_c: bool,
|
|
}
|
|
|
|
impl ConsoleCtrlGuard {
|
|
fn ignore(ignore_ctrl_c: bool) -> Result<Self, WindowsSupportError> {
|
|
if !ignore_ctrl_c {
|
|
return Ok(Self { ignore_ctrl_c });
|
|
}
|
|
#[allow(unsafe_code)]
|
|
let installed = unsafe { SetConsoleCtrlHandler(None, 1) };
|
|
if installed == FALSE {
|
|
return Err(last_error("SetConsoleCtrlHandler"));
|
|
}
|
|
Ok(Self { ignore_ctrl_c })
|
|
}
|
|
}
|
|
|
|
impl Drop for ConsoleCtrlGuard {
|
|
fn drop(&mut self) {
|
|
if self.ignore_ctrl_c {
|
|
#[allow(unsafe_code)]
|
|
let _ = unsafe { SetConsoleCtrlHandler(None, 0) };
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reports the current process token status and privileged-launch capabilities.
|
|
/// Query the current process token state and launch capabilities.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error when the current process token cannot be inspected.
|
|
pub fn current_token_status() -> Result<TokenStatus, WindowsSupportError> {
|
|
let token = open_current_token(TOKEN_QUERY)?;
|
|
let is_elevated = token_elevation(token.raw())?;
|
|
let is_admin_member = current_token_is_admin_member()?;
|
|
let integrity = token_integrity(token.raw())?;
|
|
let current_user = token_user_name(token.raw())?;
|
|
let session_id = current_process_session_id();
|
|
let active_session_id = active_console_session_id();
|
|
let (trustedinstaller_installed, trustedinstaller_running) = trustedinstaller_presence()?;
|
|
Ok(TokenStatus {
|
|
is_elevated,
|
|
is_admin_member,
|
|
integrity,
|
|
current_user,
|
|
session_id,
|
|
active_session_id,
|
|
can_admin: true,
|
|
can_current_user: active_session_id.is_some(),
|
|
can_system: is_elevated,
|
|
can_trustedinstaller: is_elevated && trustedinstaller_installed,
|
|
trustedinstaller_installed,
|
|
trustedinstaller_running,
|
|
})
|
|
}
|
|
|
|
/// Relaunches the current executable through the Windows `runas` verb.
|
|
/// Relaunch the current executable with `runas` so it can continue privileged work.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error when the elevated helper cannot be started or waited on.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics only if a Win32 structure size unexpectedly stops fitting in `u32`.
|
|
pub fn elevate_current_process(
|
|
arguments: &[String],
|
|
wait: bool,
|
|
show_window: ShowWindowMode,
|
|
) -> Result<LaunchResult, WindowsSupportError> {
|
|
let executable = std::env::current_exe().map_err(|error| {
|
|
WindowsSupportError::Process(format!("failed to locate current executable: {error}"))
|
|
})?;
|
|
let executable_wide = wide_from_os(executable.as_os_str());
|
|
let verb = wide("runas");
|
|
let parameters = quote_command_line(arguments.iter().map(String::as_str));
|
|
let parameters_wide = wide(¶meters);
|
|
let mut execute = SHELLEXECUTEINFOW {
|
|
cbSize: u32::try_from(size_of::<SHELLEXECUTEINFOW>())
|
|
.expect("shell execute info size always fits in u32"),
|
|
fMask: SEE_MASK_NOCLOSEPROCESS,
|
|
lpVerb: verb.as_ptr(),
|
|
lpFile: executable_wide.as_ptr(),
|
|
lpParameters: parameters_wide.as_ptr(),
|
|
nShow: i32::from(show_window_value(show_window)),
|
|
..SHELLEXECUTEINFOW::default()
|
|
};
|
|
#[allow(unsafe_code)]
|
|
if unsafe { ShellExecuteExW(&raw mut execute) } == FALSE {
|
|
return Err(last_error("ShellExecuteExW"));
|
|
}
|
|
let process = if execute.hProcess.is_null() {
|
|
None
|
|
} else {
|
|
Some(OwnedHandle::new(execute.hProcess)?)
|
|
};
|
|
let (pid, exit_code) = if let Some(process) = process.as_ref() {
|
|
let pid = process_id_from_handle(process.raw())?;
|
|
let exit_code = if wait {
|
|
Some(wait_for_exit_code(process.raw())?)
|
|
} else {
|
|
None
|
|
};
|
|
(Some(pid), exit_code)
|
|
} else {
|
|
(None, None)
|
|
};
|
|
Ok(LaunchResult {
|
|
pid,
|
|
exit_code,
|
|
identity: LaunchIdentity::Admin,
|
|
})
|
|
}
|
|
|
|
/// Reattach the current process to a caller-owned console before starting a foreground child.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error when the console cannot be rebound to the requested parent process.
|
|
pub fn attach_parent_console(parent_pid: u32) -> Result<(), WindowsSupportError> {
|
|
if parent_pid == 0 {
|
|
return Err(WindowsSupportError::Process(
|
|
"same-console relay requires a non-zero parent console pid".to_string(),
|
|
));
|
|
}
|
|
#[allow(unsafe_code)]
|
|
let _ = unsafe { FreeConsole() };
|
|
#[allow(unsafe_code)]
|
|
let attached = unsafe { AttachConsole(parent_pid) };
|
|
if attached == FALSE {
|
|
return Err(last_error("AttachConsole"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Launches a process with the requested identity in the current elevated context.
|
|
/// Launch a process using the requested identity and token shaping options.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns an error when token acquisition, mutation, or process creation fails.
|
|
pub fn launch_request(request: &LaunchRequest) -> Result<LaunchResult, WindowsSupportError> {
|
|
match request.identity {
|
|
LaunchIdentity::CurrentUser => launch_with_active_user(request),
|
|
LaunchIdentity::Admin if admin_launch_requires_duplicate_token(request) => {
|
|
launch_with_duplicate_identity(request, LaunchIdentity::Admin)
|
|
}
|
|
LaunchIdentity::CurrentProcess | LaunchIdentity::Admin => {
|
|
launch_with_current_identity(request)
|
|
}
|
|
LaunchIdentity::System => {
|
|
launch_with_duplicate_identity(request, LaunchIdentity::System)
|
|
}
|
|
LaunchIdentity::TrustedInstaller => {
|
|
launch_with_duplicate_identity(request, LaunchIdentity::TrustedInstaller)
|
|
}
|
|
}
|
|
}
|
|
|
|
const fn admin_launch_requires_duplicate_token(request: &LaunchRequest) -> bool {
|
|
request.session.is_some()
|
|
|| request.integrity.is_some()
|
|
|| !matches!(request.privileges, PrivilegeMode::Default)
|
|
}
|
|
|
|
fn launch_with_current_identity(
|
|
request: &LaunchRequest,
|
|
) -> Result<LaunchResult, WindowsSupportError> {
|
|
let desktop = interactive_desktop(request);
|
|
let mut startup = build_startup_info(request.show_window, desktop.as_deref());
|
|
let std_handles = inherited_standard_handles(request)?;
|
|
apply_standard_handles(&mut startup, std_handles.as_ref());
|
|
let mut process_info = PROCESS_INFORMATION::default();
|
|
let application = application_name_wide(&request.program);
|
|
let command_line_text = build_process_command_line(&request.program, &request.args);
|
|
let mut command_line = wide(&command_line_text);
|
|
let mut environment = build_environment_block();
|
|
let current_directory = request.current_directory.as_deref().map(wide);
|
|
let creation_flags = creation_flags(request);
|
|
#[allow(unsafe_code)]
|
|
let created = unsafe {
|
|
CreateProcessW(
|
|
application.as_ref().map_or(null(), Vec::as_ptr),
|
|
command_line.as_mut_ptr(),
|
|
null(),
|
|
null(),
|
|
inherit_handles_flag(std_handles.as_ref()),
|
|
creation_flags,
|
|
environment.as_mut_ptr().cast::<c_void>(),
|
|
current_directory.as_ref().map_or(null(), Vec::as_ptr),
|
|
&raw mut startup,
|
|
&raw mut process_info,
|
|
)
|
|
};
|
|
if created == FALSE {
|
|
return Err(last_error("CreateProcessW"));
|
|
}
|
|
let thread = OwnedHandle::new(process_info.hThread)?;
|
|
drop(thread);
|
|
let process = OwnedHandle::new(process_info.hProcess)?;
|
|
let exit_code = if request.wait {
|
|
Some(wait_for_exit_code(process.raw())?)
|
|
} else {
|
|
None
|
|
};
|
|
Ok(LaunchResult {
|
|
pid: Some(process_info.dwProcessId),
|
|
exit_code,
|
|
identity: request.identity,
|
|
})
|
|
}
|
|
|
|
fn launch_with_duplicate_identity(
|
|
request: &LaunchRequest,
|
|
identity: LaunchIdentity,
|
|
) -> Result<LaunchResult, WindowsSupportError> {
|
|
let status = current_token_status()?;
|
|
if !status.is_elevated {
|
|
return Err(WindowsSupportError::Unsupported(format!(
|
|
"{identity:?} launch requires an elevated administrator context"
|
|
)));
|
|
}
|
|
enable_token_discovery_privileges()?;
|
|
let token = duplicate_primary_token(identity, None)?;
|
|
let _impersonation = ImpersonationGuard::as_system_with_privileges(&[
|
|
"SeTcbPrivilege",
|
|
"SeAssignPrimaryTokenPrivilege",
|
|
"SeIncreaseQuotaPrivilege",
|
|
"SeImpersonatePrivilege",
|
|
"SeDebugPrivilege",
|
|
])?;
|
|
if let Some(session_id) = resolve_target_session_id(request.session) {
|
|
set_token_session_id(token.raw(), session_id)?;
|
|
}
|
|
apply_token_options(&token, request.privileges, request.integrity)?;
|
|
launch_with_token_as_user(request, identity, &token)
|
|
}
|
|
|
|
fn duplicate_primary_token(
|
|
identity: LaunchIdentity,
|
|
session: Option<u32>,
|
|
) -> Result<OwnedHandle, WindowsSupportError> {
|
|
let source = match identity {
|
|
LaunchIdentity::Admin | LaunchIdentity::CurrentProcess => open_current_token(
|
|
TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_ADJUST_DEFAULT,
|
|
)?,
|
|
LaunchIdentity::CurrentUser => open_active_user_process_token(session)?,
|
|
LaunchIdentity::System => open_system_process_token()?,
|
|
LaunchIdentity::TrustedInstaller => open_trustedinstaller_process_token()?,
|
|
};
|
|
let desired_access: TOKEN_ACCESS_MASK = TOKEN_QUERY
|
|
| TOKEN_DUPLICATE
|
|
| TOKEN_ASSIGN_PRIMARY
|
|
| TOKEN_ADJUST_DEFAULT
|
|
| TOKEN_ADJUST_SESSIONID
|
|
| TOKEN_ADJUST_PRIVILEGES;
|
|
let mut duplicate: HANDLE = null_mut();
|
|
#[allow(unsafe_code)]
|
|
let duplicated = unsafe {
|
|
DuplicateTokenEx(
|
|
source.raw(),
|
|
desired_access,
|
|
null(),
|
|
SecurityImpersonation,
|
|
windows_sys::Win32::Security::TokenPrimary,
|
|
&raw mut duplicate,
|
|
)
|
|
};
|
|
if duplicated == FALSE {
|
|
return Err(last_error("DuplicateTokenEx"));
|
|
}
|
|
OwnedHandle::new(duplicate)
|
|
}
|
|
|
|
fn apply_token_options(
|
|
token: &OwnedHandle,
|
|
privileges: PrivilegeMode,
|
|
integrity: Option<TokenIntegrity>,
|
|
) -> Result<(), WindowsSupportError> {
|
|
match privileges {
|
|
PrivilegeMode::Default => {}
|
|
PrivilegeMode::EnableAll => enable_token_privileges(token.raw())?,
|
|
PrivilegeMode::DisableAll => disable_token_privileges(token.raw())?,
|
|
}
|
|
if let Some(integrity) = integrity {
|
|
set_token_integrity(token.raw(), integrity)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn launch_with_token_as_user(
|
|
request: &LaunchRequest,
|
|
identity: LaunchIdentity,
|
|
token: &OwnedHandle,
|
|
) -> Result<LaunchResult, WindowsSupportError> {
|
|
let desktop = interactive_desktop(request);
|
|
let mut startup = build_startup_info(request.show_window, desktop.as_deref());
|
|
let std_handles = inherited_standard_handles(request)?;
|
|
apply_standard_handles(&mut startup, std_handles.as_ref());
|
|
let mut process_info = PROCESS_INFORMATION::default();
|
|
let application = application_name_wide(&request.program);
|
|
let command_line_text = build_process_command_line(&request.program, &request.args);
|
|
let mut command_line = wide(&command_line_text);
|
|
let environment = OwnedEnvironmentBlock::for_token(token.raw(), false)?;
|
|
let current_directory = request.current_directory.as_deref().map(wide);
|
|
let creation_flags = creation_flags(request);
|
|
#[allow(unsafe_code)]
|
|
let created = unsafe {
|
|
CreateProcessAsUserW(
|
|
token.raw(),
|
|
application.as_ref().map_or(null(), Vec::as_ptr),
|
|
command_line.as_mut_ptr(),
|
|
null(),
|
|
null(),
|
|
inherit_handles_flag(std_handles.as_ref()),
|
|
creation_flags,
|
|
environment.raw().cast_mut(),
|
|
current_directory.as_ref().map_or(null(), Vec::as_ptr),
|
|
&raw mut startup,
|
|
&raw mut process_info,
|
|
)
|
|
};
|
|
if created == FALSE {
|
|
return Err(last_error("CreateProcessAsUserW"));
|
|
}
|
|
finalize_launch(request, identity, process_info)
|
|
}
|
|
|
|
fn launch_with_active_user(
|
|
request: &LaunchRequest,
|
|
) -> Result<LaunchResult, WindowsSupportError> {
|
|
let status = current_token_status()?;
|
|
if !request_requires_token_shaping(request) {
|
|
return launch_with_current_identity(request);
|
|
}
|
|
if !status.is_elevated {
|
|
return Err(WindowsSupportError::Unsupported(
|
|
"current-user launch requires an elevated administrator context".to_string(),
|
|
));
|
|
}
|
|
enable_token_discovery_privileges()?;
|
|
let _impersonation = ImpersonationGuard::as_system_with_privileges(&[
|
|
"SeTcbPrivilege",
|
|
"SeAssignPrimaryTokenPrivilege",
|
|
"SeIncreaseQuotaPrivilege",
|
|
"SeImpersonatePrivilege",
|
|
])?;
|
|
let token = duplicate_primary_token(LaunchIdentity::CurrentUser, request.session)?;
|
|
apply_token_options(&token, request.privileges, request.integrity)?;
|
|
launch_with_token_as_user(request, LaunchIdentity::CurrentUser, &token)
|
|
}
|
|
|
|
const fn request_requires_token_shaping(request: &LaunchRequest) -> bool {
|
|
request.session.is_some()
|
|
|| request.integrity.is_some()
|
|
|| !matches!(request.privileges, PrivilegeMode::Default)
|
|
}
|
|
|
|
fn finalize_launch(
|
|
request: &LaunchRequest,
|
|
identity: LaunchIdentity,
|
|
process_info: PROCESS_INFORMATION,
|
|
) -> Result<LaunchResult, WindowsSupportError> {
|
|
let thread = OwnedHandle::new(process_info.hThread)?;
|
|
drop(thread);
|
|
let process = OwnedHandle::new(process_info.hProcess)?;
|
|
let exit_code = if request.wait {
|
|
let _console_ctrl = ConsoleCtrlGuard::ignore(request.same_console)?;
|
|
Some(wait_for_exit_code(process.raw())?)
|
|
} else {
|
|
None
|
|
};
|
|
Ok(LaunchResult {
|
|
pid: Some(process_info.dwProcessId),
|
|
exit_code,
|
|
identity,
|
|
})
|
|
}
|
|
|
|
fn open_system_process_token() -> Result<OwnedHandle, WindowsSupportError> {
|
|
open_preferred_system_process_token(TOKEN_QUERY | TOKEN_DUPLICATE)
|
|
}
|
|
|
|
fn open_system_impersonation_token() -> Result<OwnedHandle, WindowsSupportError> {
|
|
let source = open_system_process_token()?;
|
|
duplicate_token(
|
|
source.raw(),
|
|
TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_ADJUST_PRIVILEGES,
|
|
windows_sys::Win32::Security::TokenImpersonation,
|
|
)
|
|
}
|
|
|
|
fn open_active_user_process_token(
|
|
explicit_session: Option<u32>,
|
|
) -> Result<OwnedHandle, WindowsSupportError> {
|
|
let session = explicit_session
|
|
.or_else(active_console_session_id)
|
|
.or_else(current_process_session_id)
|
|
.ok_or_else(|| {
|
|
WindowsSupportError::Process("no active user session found".to_string())
|
|
})?;
|
|
let mut token: HANDLE = null_mut();
|
|
#[allow(unsafe_code)]
|
|
let queried = unsafe { WTSQueryUserToken(session, &raw mut token) };
|
|
if queried == FALSE {
|
|
return Err(last_error("WTSQueryUserToken"));
|
|
}
|
|
OwnedHandle::new(token)
|
|
}
|
|
|
|
fn open_preferred_system_process_token(
|
|
access: TOKEN_ACCESS_MASK,
|
|
) -> Result<OwnedHandle, WindowsSupportError> {
|
|
let expected = well_known_sid(WinLocalSystemSid)?;
|
|
let processes = crate::snapshot_processes();
|
|
let current_session = current_process_session_id();
|
|
|
|
for process in processes
|
|
.iter()
|
|
.filter(|process| {
|
|
process.image_name.eq_ignore_ascii_case("winlogon.exe")
|
|
&& current_session == process_session_id(process.pid)
|
|
})
|
|
.chain(
|
|
processes
|
|
.iter()
|
|
.filter(|process| process.image_name.eq_ignore_ascii_case("lsass.exe")),
|
|
)
|
|
.chain(processes.iter())
|
|
{
|
|
let opened = open_process_token_for_pid(process.pid, access);
|
|
let Ok(token) = opened else {
|
|
continue;
|
|
};
|
|
if token_user_matches_sid(token.raw(), &expected)? {
|
|
return Ok(token);
|
|
}
|
|
}
|
|
Err(WindowsSupportError::Process(
|
|
"failed to locate a usable SYSTEM process token".to_string(),
|
|
))
|
|
}
|
|
|
|
fn open_trustedinstaller_process_token() -> Result<OwnedHandle, WindowsSupportError> {
|
|
let started = Instant::now();
|
|
let _impersonation = ImpersonationGuard::as_system()?;
|
|
loop {
|
|
let pid = ensure_trustedinstaller_running()?;
|
|
match open_process_token_for_pid(pid, TOKEN_QUERY | TOKEN_DUPLICATE) {
|
|
Ok(token) => return Ok(token),
|
|
Err(error) => {
|
|
if !should_retry_trustedinstaller_token_open(&error)
|
|
|| started.elapsed() >= START_TIMEOUT
|
|
{
|
|
return Err(error);
|
|
}
|
|
thread::sleep(Duration::from_millis(100));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn duplicate_token(
|
|
source: HANDLE,
|
|
desired_access: TOKEN_ACCESS_MASK,
|
|
token_type: windows_sys::Win32::Security::TOKEN_TYPE,
|
|
) -> Result<OwnedHandle, WindowsSupportError> {
|
|
let mut duplicate: HANDLE = null_mut();
|
|
#[allow(unsafe_code)]
|
|
let duplicated = unsafe {
|
|
DuplicateTokenEx(
|
|
source,
|
|
desired_access,
|
|
null(),
|
|
SecurityImpersonation,
|
|
token_type,
|
|
&raw mut duplicate,
|
|
)
|
|
};
|
|
if duplicated == FALSE {
|
|
return Err(last_error("DuplicateTokenEx"));
|
|
}
|
|
OwnedHandle::new(duplicate)
|
|
}
|
|
|
|
fn open_process_token_for_pid(
|
|
pid: u32,
|
|
access: TOKEN_ACCESS_MASK,
|
|
) -> Result<OwnedHandle, WindowsSupportError> {
|
|
#[allow(unsafe_code)]
|
|
let process = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid) };
|
|
let process = OwnedHandle::new(process)?;
|
|
let mut token: HANDLE = null_mut();
|
|
#[allow(unsafe_code)]
|
|
let opened = unsafe { OpenProcessToken(process.raw(), access, &raw mut token) };
|
|
if opened == FALSE {
|
|
return Err(last_error("OpenProcessToken"));
|
|
}
|
|
OwnedHandle::new(token)
|
|
}
|
|
|
|
fn process_session_id(pid: u32) -> Option<u32> {
|
|
let mut session = 0_u32;
|
|
#[allow(unsafe_code)]
|
|
let ok = unsafe { ProcessIdToSessionId(pid, &raw mut session) };
|
|
(ok != FALSE).then_some(session)
|
|
}
|
|
|
|
fn should_retry_trustedinstaller_token_open(error: &WindowsSupportError) -> bool {
|
|
matches!(
|
|
error,
|
|
WindowsSupportError::WindowsApi {
|
|
action: "OpenProcessToken" | "OpenProcess",
|
|
code: ERROR_ACCESS_DENIED,
|
|
}
|
|
)
|
|
}
|
|
|
|
fn trustedinstaller_presence() -> Result<(bool, bool), WindowsSupportError> {
|
|
let manager = unsafe { OpenSCManagerW(null(), null(), SC_MANAGER_CONNECT) };
|
|
let manager = match OwnedServiceHandle::new(manager, "OpenSCManagerW") {
|
|
Ok(value) => value,
|
|
Err(error) => {
|
|
return match error {
|
|
WindowsSupportError::WindowsApi { .. } => Ok((false, false)),
|
|
other => Err(other),
|
|
};
|
|
}
|
|
};
|
|
let service_name = wide("TrustedInstaller");
|
|
let service =
|
|
unsafe { OpenServiceW(manager.raw(), service_name.as_ptr(), SERVICE_QUERY_STATUS) };
|
|
let service = match OwnedServiceHandle::new(service, "OpenServiceW") {
|
|
Ok(value) => value,
|
|
Err(error) => {
|
|
return match error {
|
|
WindowsSupportError::WindowsApi { .. } => Ok((false, false)),
|
|
other => Err(other),
|
|
};
|
|
}
|
|
};
|
|
let running = service_running(service.raw())?;
|
|
Ok((true, running))
|
|
}
|
|
|
|
fn ensure_trustedinstaller_running() -> Result<u32, WindowsSupportError> {
|
|
let manager = OwnedServiceHandle::new(
|
|
unsafe { OpenSCManagerW(null(), null(), SC_MANAGER_CONNECT) },
|
|
"OpenSCManagerW",
|
|
)?;
|
|
let service_name = wide("TrustedInstaller");
|
|
let service = OwnedServiceHandle::new(
|
|
unsafe {
|
|
OpenServiceW(
|
|
manager.raw(),
|
|
service_name.as_ptr(),
|
|
SERVICE_QUERY_STATUS | SERVICE_START,
|
|
)
|
|
},
|
|
"OpenServiceW",
|
|
)?;
|
|
let mut deadline = Instant::now() + START_TIMEOUT;
|
|
let mut last_checkpoint = 0_u32;
|
|
let mut start_requested = false;
|
|
loop {
|
|
let status = query_service_status(service.raw())?;
|
|
if status.dwCurrentState == SERVICE_RUNNING {
|
|
return Ok(status.dwProcessId);
|
|
}
|
|
if status.dwCurrentState == SERVICE_START_PENDING {
|
|
if status.dwCheckPoint != 0 && status.dwCheckPoint != last_checkpoint {
|
|
last_checkpoint = status.dwCheckPoint;
|
|
deadline = Instant::now() + START_TIMEOUT;
|
|
}
|
|
} else if !start_requested {
|
|
#[allow(unsafe_code)]
|
|
let _ = unsafe { StartServiceW(service.raw(), 0, null()) };
|
|
start_requested = true;
|
|
}
|
|
if Instant::now() >= deadline {
|
|
return Err(WindowsSupportError::Process(
|
|
"timed out waiting for TrustedInstaller service".to_string(),
|
|
));
|
|
}
|
|
let wait_hint = if status.dwWaitHint == 0 {
|
|
100
|
|
} else {
|
|
status.dwWaitHint.saturating_div(10).clamp(100, 1_000)
|
|
};
|
|
thread::sleep(Duration::from_millis(u64::from(wait_hint)));
|
|
}
|
|
}
|
|
|
|
fn service_running(service: SC_HANDLE) -> Result<bool, WindowsSupportError> {
|
|
let status = query_service_status(service)?;
|
|
Ok(status.dwCurrentState == SERVICE_RUNNING)
|
|
}
|
|
|
|
fn query_service_status(
|
|
service: SC_HANDLE,
|
|
) -> Result<SERVICE_STATUS_PROCESS, WindowsSupportError> {
|
|
let mut status = SERVICE_STATUS_PROCESS::default();
|
|
let mut needed = 0_u32;
|
|
#[allow(unsafe_code)]
|
|
let queried = unsafe {
|
|
QueryServiceStatusEx(
|
|
service,
|
|
SC_STATUS_PROCESS_INFO,
|
|
(&raw mut status).cast::<u8>(),
|
|
u32::try_from(size_of::<SERVICE_STATUS_PROCESS>())
|
|
.expect("service status size always fits in u32"),
|
|
&raw mut needed,
|
|
)
|
|
};
|
|
if queried == FALSE {
|
|
return Err(last_error("QueryServiceStatusEx"));
|
|
}
|
|
Ok(status)
|
|
}
|
|
|
|
fn enable_token_discovery_privileges() -> Result<(), WindowsSupportError> {
|
|
let token = open_current_token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY)?;
|
|
for privilege in ["SeDebugPrivilege", "SeImpersonatePrivilege"] {
|
|
enable_named_privilege(token.raw(), privilege)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn enable_token_privileges(token: HANDLE) -> Result<(), WindowsSupportError> {
|
|
let bytes = get_token_info_bytes(token, windows_sys::Win32::Security::TokenPrivileges)?;
|
|
#[allow(unsafe_code)]
|
|
let privilege_count = unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast::<u32>()) };
|
|
let privileges_offset = std::mem::offset_of!(TOKEN_PRIVILEGES, Privileges);
|
|
for index in 0..usize::try_from(privilege_count).expect("privilege count fits usize") {
|
|
#[allow(unsafe_code)]
|
|
let privilege = unsafe {
|
|
std::ptr::read_unaligned(
|
|
bytes
|
|
.as_ptr()
|
|
.add(
|
|
privileges_offset
|
|
+ index
|
|
* size_of::<windows_sys::Win32::Security::LUID_AND_ATTRIBUTES>(
|
|
),
|
|
)
|
|
.cast::<windows_sys::Win32::Security::LUID_AND_ATTRIBUTES>(),
|
|
)
|
|
};
|
|
adjust_single_token_privilege(token, privilege.Luid, SE_PRIVILEGE_ENABLED)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn disable_token_privileges(token: HANDLE) -> Result<(), WindowsSupportError> {
|
|
#[allow(unsafe_code)]
|
|
let adjusted =
|
|
unsafe { AdjustTokenPrivileges(token, 1, null_mut(), 0, null_mut(), null_mut()) };
|
|
if adjusted == FALSE {
|
|
return Err(last_error("AdjustTokenPrivileges"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn enable_named_privilege(token: HANDLE, name: &str) -> Result<(), WindowsSupportError> {
|
|
let name_wide = wide(name);
|
|
let mut luid = windows_sys::Win32::Foundation::LUID::default();
|
|
#[allow(unsafe_code)]
|
|
let looked_up = unsafe { LookupPrivilegeValueW(null(), name_wide.as_ptr(), &raw mut luid) };
|
|
if looked_up == FALSE {
|
|
return Err(last_error("LookupPrivilegeValueW"));
|
|
}
|
|
adjust_single_token_privilege(token, luid, SE_PRIVILEGE_ENABLED)
|
|
}
|
|
|
|
fn adjust_single_token_privilege(
|
|
token: HANDLE,
|
|
luid: windows_sys::Win32::Foundation::LUID,
|
|
attributes: u32,
|
|
) -> Result<(), WindowsSupportError> {
|
|
let mut privileges = TOKEN_PRIVILEGES {
|
|
PrivilegeCount: 1,
|
|
Privileges: [windows_sys::Win32::Security::LUID_AND_ATTRIBUTES {
|
|
Luid: luid,
|
|
Attributes: attributes,
|
|
}],
|
|
};
|
|
#[allow(unsafe_code)]
|
|
let adjusted = unsafe {
|
|
AdjustTokenPrivileges(
|
|
token,
|
|
FALSE,
|
|
&raw mut privileges,
|
|
u32::try_from(size_of::<TOKEN_PRIVILEGES>())
|
|
.expect("token privileges size always fits in u32"),
|
|
null_mut(),
|
|
null_mut(),
|
|
)
|
|
};
|
|
if adjusted == FALSE {
|
|
return Err(last_error("AdjustTokenPrivileges"));
|
|
}
|
|
#[allow(unsafe_code)]
|
|
let status = unsafe { GetLastError() };
|
|
if status == ERROR_NOT_ALL_ASSIGNED {
|
|
return Err(WindowsSupportError::Process(
|
|
"token did not contain every requested privilege".to_string(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn open_current_token(access: TOKEN_ACCESS_MASK) -> Result<OwnedHandle, WindowsSupportError> {
|
|
let mut token: HANDLE = null_mut();
|
|
#[allow(unsafe_code)]
|
|
let opened = unsafe { OpenProcessToken(GetCurrentProcess(), access, &raw mut token) };
|
|
if opened == FALSE {
|
|
return Err(last_error("OpenProcessToken"));
|
|
}
|
|
OwnedHandle::new(token)
|
|
}
|
|
|
|
fn token_elevation(token: HANDLE) -> Result<bool, WindowsSupportError> {
|
|
let elevation: TOKEN_ELEVATION = get_token_info(token, TokenElevation)?;
|
|
Ok(elevation.TokenIsElevated != 0)
|
|
}
|
|
|
|
fn token_integrity(token: HANDLE) -> Result<TokenIntegrity, WindowsSupportError> {
|
|
let bytes = get_token_info_bytes(token, TokenIntegrityLevel)?;
|
|
let label: TOKEN_MANDATORY_LABEL =
|
|
unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast::<TOKEN_MANDATORY_LABEL>()) };
|
|
if label.Label.Attributes
|
|
& u32::try_from(SE_GROUP_INTEGRITY).expect("integrity flag fits u32")
|
|
== 0
|
|
{
|
|
return Ok(TokenIntegrity::Unknown);
|
|
}
|
|
let sid = label.Label.Sid;
|
|
let authority = sub_authority_count(sid)?;
|
|
let rid = sub_authority(sid, authority.saturating_sub(1))?;
|
|
Ok(match rid {
|
|
SECURITY_MANDATORY_UNTRUSTED_RID => TokenIntegrity::Untrusted,
|
|
SECURITY_MANDATORY_LOW_RID => TokenIntegrity::Low,
|
|
SECURITY_MANDATORY_MEDIUM_RID => TokenIntegrity::Medium,
|
|
SECURITY_MANDATORY_MEDIUM_PLUS_RID => TokenIntegrity::MediumPlus,
|
|
SECURITY_MANDATORY_HIGH_RID => TokenIntegrity::High,
|
|
SECURITY_MANDATORY_SYSTEM_RID => TokenIntegrity::System,
|
|
_ => TokenIntegrity::Unknown,
|
|
})
|
|
}
|
|
|
|
fn set_token_integrity(
|
|
token: HANDLE,
|
|
integrity: TokenIntegrity,
|
|
) -> Result<(), WindowsSupportError> {
|
|
let Some(rid) = integrity_rid(integrity) else {
|
|
return Ok(());
|
|
};
|
|
let mut sid = mandatory_label_sid(rid);
|
|
let mut label = TOKEN_MANDATORY_LABEL {
|
|
Label: windows_sys::Win32::Security::SID_AND_ATTRIBUTES {
|
|
Sid: sid.as_mut_ptr().cast::<c_void>(),
|
|
Attributes: u32::try_from(SE_GROUP_INTEGRITY).expect("integrity flag fits u32"),
|
|
},
|
|
};
|
|
#[allow(unsafe_code)]
|
|
let updated = unsafe {
|
|
SetTokenInformation(
|
|
token,
|
|
TokenIntegrityLevel,
|
|
(&raw mut label).cast::<c_void>(),
|
|
u32::try_from(size_of::<TOKEN_MANDATORY_LABEL>() + sid.len())
|
|
.expect("mandatory label buffer size fits in u32"),
|
|
)
|
|
};
|
|
if updated == FALSE {
|
|
return Err(last_error("SetTokenInformation"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
const fn integrity_rid(integrity: TokenIntegrity) -> Option<u32> {
|
|
match integrity {
|
|
TokenIntegrity::Unknown => None,
|
|
TokenIntegrity::Untrusted => Some(SECURITY_MANDATORY_UNTRUSTED_RID),
|
|
TokenIntegrity::Low => Some(SECURITY_MANDATORY_LOW_RID),
|
|
TokenIntegrity::Medium => Some(SECURITY_MANDATORY_MEDIUM_RID),
|
|
TokenIntegrity::MediumPlus => Some(SECURITY_MANDATORY_MEDIUM_PLUS_RID),
|
|
TokenIntegrity::High => Some(SECURITY_MANDATORY_HIGH_RID),
|
|
TokenIntegrity::System => Some(SECURITY_MANDATORY_SYSTEM_RID),
|
|
}
|
|
}
|
|
|
|
fn mandatory_label_sid(rid: u32) -> [u8; 12] {
|
|
let mut sid = [0_u8; 12];
|
|
sid[0] = 1;
|
|
sid[1] = 1;
|
|
sid[7] = 16;
|
|
sid[8..12].copy_from_slice(&rid.to_le_bytes());
|
|
sid
|
|
}
|
|
|
|
fn token_user_name(token: HANDLE) -> Result<Option<String>, WindowsSupportError> {
|
|
let bytes = get_token_info_bytes(token, windows_sys::Win32::Security::TokenUser)?;
|
|
let user: windows_sys::Win32::Security::TOKEN_USER = unsafe {
|
|
std::ptr::read_unaligned(
|
|
bytes
|
|
.as_ptr()
|
|
.cast::<windows_sys::Win32::Security::TOKEN_USER>(),
|
|
)
|
|
};
|
|
let sid = user.User.Sid;
|
|
let mut name_len = 0_u32;
|
|
let mut domain_len = 0_u32;
|
|
let mut use_type = 0_i32;
|
|
#[allow(unsafe_code)]
|
|
let _ = unsafe {
|
|
LookupAccountSidW(
|
|
null(),
|
|
sid,
|
|
null_mut(),
|
|
&raw mut name_len,
|
|
null_mut(),
|
|
&raw mut domain_len,
|
|
&raw mut use_type,
|
|
)
|
|
};
|
|
if name_len == 0 {
|
|
return Ok(None);
|
|
}
|
|
let mut name = vec![0_u16; usize::try_from(name_len).expect("name length fits usize")];
|
|
let mut domain =
|
|
vec![0_u16; usize::try_from(domain_len).expect("domain length fits usize")];
|
|
#[allow(unsafe_code)]
|
|
let looked_up = unsafe {
|
|
LookupAccountSidW(
|
|
null(),
|
|
sid,
|
|
name.as_mut_ptr(),
|
|
&raw mut name_len,
|
|
domain.as_mut_ptr(),
|
|
&raw mut domain_len,
|
|
&raw mut use_type,
|
|
)
|
|
};
|
|
if looked_up == FALSE {
|
|
return Ok(None);
|
|
}
|
|
let name =
|
|
wide_to_string(&name[..usize::try_from(name_len).expect("name length fits usize")]);
|
|
let domain = wide_to_string(
|
|
&domain[..usize::try_from(domain_len).expect("domain length fits usize")],
|
|
);
|
|
if domain.is_empty() {
|
|
Ok(Some(name))
|
|
} else {
|
|
Ok(Some(format!("{domain}\\{name}")))
|
|
}
|
|
}
|
|
|
|
fn current_token_is_admin_member() -> Result<bool, WindowsSupportError> {
|
|
let sid = well_known_sid(WinBuiltinAdministratorsSid)?;
|
|
let mut result = FALSE;
|
|
#[allow(unsafe_code)]
|
|
let checked = unsafe {
|
|
CheckTokenMembership(
|
|
null_mut(),
|
|
sid.as_ptr().cast_mut().cast::<c_void>(),
|
|
&raw mut result,
|
|
)
|
|
};
|
|
if checked == FALSE {
|
|
return Err(last_error("CheckTokenMembership"));
|
|
}
|
|
Ok(result != FALSE)
|
|
}
|
|
|
|
fn current_process_session_id() -> Option<u32> {
|
|
let mut session = 0_u32;
|
|
#[allow(unsafe_code)]
|
|
let ok = unsafe { ProcessIdToSessionId(GetCurrentProcessId(), &raw mut session) };
|
|
(ok != FALSE).then_some(session)
|
|
}
|
|
|
|
fn active_console_session_id() -> Option<u32> {
|
|
#[allow(unsafe_code)]
|
|
let session = unsafe { WTSGetActiveConsoleSessionId() };
|
|
(session != u32::MAX).then_some(session)
|
|
}
|
|
|
|
fn resolve_target_session_id(explicit_session: Option<u32>) -> Option<u32> {
|
|
explicit_session
|
|
.or_else(|| current_process_session_id().filter(|session| *session != 0))
|
|
.or_else(active_console_session_id)
|
|
.or_else(current_process_session_id)
|
|
}
|
|
|
|
fn token_user_matches_sid(token: HANDLE, expected: &[u8]) -> Result<bool, WindowsSupportError> {
|
|
let bytes = get_token_info_bytes(token, windows_sys::Win32::Security::TokenUser)?;
|
|
let user: windows_sys::Win32::Security::TOKEN_USER = unsafe {
|
|
std::ptr::read_unaligned(
|
|
bytes
|
|
.as_ptr()
|
|
.cast::<windows_sys::Win32::Security::TOKEN_USER>(),
|
|
)
|
|
};
|
|
sid_equal(user.User.Sid, expected.as_ptr().cast::<c_void>())
|
|
}
|
|
|
|
fn sid_equal(left: *mut c_void, right: *const c_void) -> Result<bool, WindowsSupportError> {
|
|
#[allow(unsafe_code)]
|
|
let left_len = unsafe { windows_sys::Win32::Security::GetLengthSid(left) };
|
|
#[allow(unsafe_code)]
|
|
let right_len = unsafe { windows_sys::Win32::Security::GetLengthSid(right.cast_mut()) };
|
|
if left_len != right_len {
|
|
return Ok(false);
|
|
}
|
|
let left = sid_bytes(left, left_len)?;
|
|
let right = sid_bytes(right.cast_mut(), right_len)?;
|
|
Ok(left == right)
|
|
}
|
|
|
|
fn sid_bytes(sid: *mut c_void, len: u32) -> Result<Vec<u8>, WindowsSupportError> {
|
|
let len = usize::try_from(len).expect("sid length fits usize");
|
|
if sid.is_null() {
|
|
return Err(WindowsSupportError::Process("null SID pointer".to_string()));
|
|
}
|
|
#[allow(unsafe_code)]
|
|
let slice = unsafe { std::slice::from_raw_parts(sid.cast::<u8>(), len) };
|
|
Ok(slice.to_vec())
|
|
}
|
|
|
|
fn well_known_sid(kind: i32) -> Result<Vec<u8>, WindowsSupportError> {
|
|
let mut bytes = vec![0_u8; SECURITY_MAX_SID_SIZE];
|
|
let mut size = u32::try_from(bytes.len()).expect("sid buffer size fits u32");
|
|
#[allow(unsafe_code)]
|
|
let created = unsafe {
|
|
CreateWellKnownSid(
|
|
kind,
|
|
null_mut(),
|
|
bytes.as_mut_ptr().cast::<c_void>(),
|
|
&raw mut size,
|
|
)
|
|
};
|
|
if created == FALSE {
|
|
return Err(last_error("CreateWellKnownSid"));
|
|
}
|
|
bytes.truncate(usize::try_from(size).expect("sid size fits usize"));
|
|
Ok(bytes)
|
|
}
|
|
|
|
fn sub_authority_count(sid: *mut c_void) -> Result<u32, WindowsSupportError> {
|
|
if sid.is_null() {
|
|
return Err(WindowsSupportError::Process("null SID pointer".to_string()));
|
|
}
|
|
#[allow(unsafe_code)]
|
|
let count = unsafe { *(sid.cast::<u8>().add(1)) };
|
|
Ok(u32::from(count))
|
|
}
|
|
|
|
fn sub_authority(sid: *mut c_void, index: u32) -> Result<u32, WindowsSupportError> {
|
|
if sid.is_null() {
|
|
return Err(WindowsSupportError::Process("null SID pointer".to_string()));
|
|
}
|
|
let offset = 8_usize + usize::try_from(index).expect("sid index fits usize") * 4;
|
|
#[allow(unsafe_code)]
|
|
let value = unsafe { std::ptr::read_unaligned(sid.cast::<u8>().add(offset).cast::<u32>()) };
|
|
Ok(value)
|
|
}
|
|
|
|
fn set_token_session_id(token: HANDLE, session: u32) -> Result<(), WindowsSupportError> {
|
|
let mut session = session;
|
|
#[allow(unsafe_code)]
|
|
let updated = unsafe {
|
|
SetTokenInformation(
|
|
token,
|
|
TokenSessionId,
|
|
(&raw mut session).cast::<c_void>(),
|
|
u32::try_from(size_of::<u32>()).expect("u32 size always fits in u32"),
|
|
)
|
|
};
|
|
if updated == FALSE {
|
|
return Err(last_error("SetTokenInformation"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn interactive_desktop(request: &LaunchRequest) -> Option<Vec<u16>> {
|
|
if request.new_window || request.same_console {
|
|
Some(wide("winsta0\\default"))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn build_startup_info(show_window: ShowWindowMode, desktop: Option<&[u16]>) -> STARTUPINFOW {
|
|
STARTUPINFOW {
|
|
cb: u32::try_from(size_of::<STARTUPINFOW>())
|
|
.expect("startup info size always fits in u32"),
|
|
dwFlags: STARTF_USESHOWWINDOW,
|
|
wShowWindow: show_window_value(show_window),
|
|
lpDesktop: desktop.map_or(null_mut(), |value| value.as_ptr().cast_mut()),
|
|
..STARTUPINFOW::default()
|
|
}
|
|
}
|
|
|
|
fn inherited_standard_handles(
|
|
request: &LaunchRequest,
|
|
) -> Result<Option<InheritedStdHandles>, WindowsSupportError> {
|
|
if request.new_window {
|
|
return Ok(None);
|
|
}
|
|
let (output, stdout_file) = if let Some(path) = &request.stdout_path {
|
|
let file = create_relay_output_file(path)?;
|
|
(
|
|
duplicate_inheritable_handle(file.as_raw_handle().cast::<c_void>(), "stdout file")?,
|
|
Some(file),
|
|
)
|
|
} else {
|
|
(duplicate_standard_handle(STD_OUTPUT_HANDLE)?, None)
|
|
};
|
|
let (error, stderr_file) = if let Some(path) = &request.stderr_path {
|
|
let file = create_relay_output_file(path)?;
|
|
(
|
|
duplicate_inheritable_handle(file.as_raw_handle().cast::<c_void>(), "stderr file")?,
|
|
Some(file),
|
|
)
|
|
} else {
|
|
(duplicate_standard_handle(STD_ERROR_HANDLE)?, None)
|
|
};
|
|
Ok(Some(InheritedStdHandles {
|
|
input: duplicate_standard_handle(STD_INPUT_HANDLE)?,
|
|
output,
|
|
error,
|
|
_stdout_file: stdout_file,
|
|
_stderr_file: stderr_file,
|
|
}))
|
|
}
|
|
|
|
fn duplicate_standard_handle(
|
|
handle_id: STD_HANDLE,
|
|
) -> Result<OwnedHandle, WindowsSupportError> {
|
|
#[allow(unsafe_code)]
|
|
let source = unsafe { GetStdHandle(handle_id) };
|
|
if source.is_null() || std::ptr::eq(source, INVALID_HANDLE_VALUE) {
|
|
return Err(last_error("GetStdHandle"));
|
|
}
|
|
duplicate_inheritable_handle(source, "standard handle")
|
|
}
|
|
|
|
fn create_relay_output_file(path: &str) -> Result<File, WindowsSupportError> {
|
|
OpenOptions::new()
|
|
.create_new(true)
|
|
.write(true)
|
|
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
|
|
.open(path)
|
|
.map_err(|error| {
|
|
WindowsSupportError::Process(format!(
|
|
"failed to create relay output file {path}: {error}"
|
|
))
|
|
})
|
|
}
|
|
|
|
fn duplicate_inheritable_handle(
|
|
source: HANDLE,
|
|
action: &'static str,
|
|
) -> Result<OwnedHandle, WindowsSupportError> {
|
|
#[allow(unsafe_code)]
|
|
let current_process = unsafe { GetCurrentProcess() };
|
|
let mut duplicate: HANDLE = null_mut();
|
|
#[allow(unsafe_code)]
|
|
let duplicated = unsafe {
|
|
DuplicateHandle(
|
|
current_process,
|
|
source,
|
|
current_process,
|
|
&raw mut duplicate,
|
|
0,
|
|
TRUE,
|
|
DUPLICATE_SAME_ACCESS,
|
|
)
|
|
};
|
|
if duplicated == FALSE {
|
|
return Err(last_error(action));
|
|
}
|
|
OwnedHandle::new(duplicate)
|
|
}
|
|
|
|
const fn apply_standard_handles(
|
|
startup: &mut STARTUPINFOW,
|
|
handles: Option<&InheritedStdHandles>,
|
|
) {
|
|
if let Some(handles) = handles {
|
|
startup.dwFlags |= STARTF_USESTDHANDLES;
|
|
startup.hStdInput = handles.input.raw();
|
|
startup.hStdOutput = handles.output.raw();
|
|
startup.hStdError = handles.error.raw();
|
|
}
|
|
}
|
|
|
|
const fn inherit_handles_flag(handles: Option<&InheritedStdHandles>) -> i32 {
|
|
if handles.is_some() { TRUE } else { FALSE }
|
|
}
|
|
|
|
const fn creation_flags(request: &LaunchRequest) -> u32 {
|
|
let mut flags = priority_flag(request.priority) | CREATE_UNICODE_ENVIRONMENT;
|
|
if request.new_window {
|
|
flags |= CREATE_NEW_CONSOLE;
|
|
} else if matches!(request.show_window, ShowWindowMode::Hidden) {
|
|
flags |= CREATE_NO_WINDOW;
|
|
}
|
|
flags
|
|
}
|
|
|
|
const fn priority_flag(priority: ProcessPriority) -> u32 {
|
|
match priority {
|
|
ProcessPriority::Idle => IDLE_PRIORITY_CLASS,
|
|
ProcessPriority::BelowNormal => BELOW_NORMAL_PRIORITY_CLASS,
|
|
ProcessPriority::Normal => NORMAL_PRIORITY_CLASS,
|
|
ProcessPriority::AboveNormal => ABOVE_NORMAL_PRIORITY_CLASS,
|
|
ProcessPriority::High => HIGH_PRIORITY_CLASS,
|
|
ProcessPriority::Realtime => REALTIME_PRIORITY_CLASS,
|
|
}
|
|
}
|
|
|
|
const fn show_window_value(mode: ShowWindowMode) -> u16 {
|
|
match mode {
|
|
ShowWindowMode::Default => SW_SHOWDEFAULT_VALUE,
|
|
ShowWindowMode::Hidden => SW_HIDE_VALUE,
|
|
ShowWindowMode::Normal => SW_NORMAL_VALUE,
|
|
ShowWindowMode::Minimized => SW_SHOWMINIMIZED_VALUE,
|
|
ShowWindowMode::Maximized => SW_SHOWMAXIMIZED_VALUE,
|
|
}
|
|
}
|
|
|
|
fn build_process_command_line(program: &str, args: &[String]) -> String {
|
|
let mut values = Vec::with_capacity(args.len() + 1);
|
|
values.push(program);
|
|
values.extend(args.iter().map(String::as_str));
|
|
quote_command_line(values)
|
|
}
|
|
|
|
fn application_name_wide(program: &str) -> Option<Vec<u16>> {
|
|
if should_use_path_search(program) {
|
|
None
|
|
} else {
|
|
Some(wide(program))
|
|
}
|
|
}
|
|
|
|
fn should_use_path_search(program: &str) -> bool {
|
|
!program.contains(['\\', '/', ':'])
|
|
}
|
|
|
|
fn quote_command_line<'a, I>(values: I) -> String
|
|
where
|
|
I: IntoIterator<Item = &'a str>,
|
|
{
|
|
let mut rendered = String::new();
|
|
for value in values {
|
|
if !rendered.is_empty() {
|
|
rendered.push(' ');
|
|
}
|
|
rendered.push_str("e_windows_arg(value));
|
|
}
|
|
rendered
|
|
}
|
|
|
|
fn quote_windows_arg(value: &str) -> String {
|
|
if value.is_empty() {
|
|
return "\"\"".to_string();
|
|
}
|
|
if !value.contains([' ', '\t', '"']) && !value.ends_with('\\') {
|
|
return value.to_string();
|
|
}
|
|
|
|
let mut rendered = String::with_capacity(value.len() + 2);
|
|
rendered.push('"');
|
|
let mut backslashes = 0;
|
|
for character in value.chars() {
|
|
if character == '\\' {
|
|
backslashes += 1;
|
|
continue;
|
|
}
|
|
if character == '"' {
|
|
rendered.push_str(&"\\".repeat(backslashes * 2 + 1));
|
|
rendered.push('"');
|
|
backslashes = 0;
|
|
continue;
|
|
}
|
|
if backslashes > 0 {
|
|
rendered.push_str(&"\\".repeat(backslashes));
|
|
backslashes = 0;
|
|
}
|
|
rendered.push(character);
|
|
}
|
|
if backslashes > 0 {
|
|
rendered.push_str(&"\\".repeat(backslashes * 2));
|
|
}
|
|
rendered.push('"');
|
|
rendered
|
|
}
|
|
|
|
fn build_environment_block() -> Vec<u16> {
|
|
let mut pairs = std::env::vars_os()
|
|
.map(|(name, value)| {
|
|
let mut rendered = String::new();
|
|
rendered.push_str(&name.to_string_lossy());
|
|
rendered.push('=');
|
|
rendered.push_str(&value.to_string_lossy());
|
|
rendered
|
|
})
|
|
.collect::<Vec<_>>();
|
|
pairs.sort_unstable();
|
|
let mut buffer = Vec::new();
|
|
for pair in pairs {
|
|
buffer.extend(OsStr::new(&pair).encode_wide());
|
|
buffer.push(0);
|
|
}
|
|
buffer.push(0);
|
|
buffer
|
|
}
|
|
|
|
fn process_id_from_handle(process: HANDLE) -> Result<u32, WindowsSupportError> {
|
|
#[allow(unsafe_code)]
|
|
let pid = unsafe { windows_sys::Win32::System::Threading::GetProcessId(process) };
|
|
if pid == 0 {
|
|
Err(last_error("GetProcessId"))
|
|
} else {
|
|
Ok(pid)
|
|
}
|
|
}
|
|
|
|
fn wait_for_exit_code(process: HANDLE) -> Result<i32, WindowsSupportError> {
|
|
#[allow(unsafe_code)]
|
|
let waited = unsafe { WaitForSingleObject(process, u32::MAX) };
|
|
if waited != WAIT_OBJECT_0 {
|
|
return if waited == WAIT_FAILED {
|
|
Err(last_error("WaitForSingleObject"))
|
|
} else {
|
|
Err(WindowsSupportError::Process(format!(
|
|
"unexpected wait result: {waited}"
|
|
)))
|
|
};
|
|
}
|
|
let mut code = 0_u32;
|
|
#[allow(unsafe_code)]
|
|
let ok = unsafe { GetExitCodeProcess(process, &raw mut code) };
|
|
if ok == FALSE {
|
|
return Err(last_error("GetExitCodeProcess"));
|
|
}
|
|
Ok(i32::try_from(code).unwrap_or(i32::MAX))
|
|
}
|
|
|
|
fn get_token_info<T: Copy>(
|
|
token: HANDLE,
|
|
class: windows_sys::Win32::Security::TOKEN_INFORMATION_CLASS,
|
|
) -> Result<T, WindowsSupportError> {
|
|
let bytes = get_token_info_bytes(token, class)?;
|
|
#[allow(unsafe_code)]
|
|
let value = unsafe { *(bytes.as_ptr().cast::<T>()) };
|
|
Ok(value)
|
|
}
|
|
|
|
fn get_token_info_bytes(
|
|
token: HANDLE,
|
|
class: windows_sys::Win32::Security::TOKEN_INFORMATION_CLASS,
|
|
) -> Result<Vec<u8>, WindowsSupportError> {
|
|
let mut needed = 0_u32;
|
|
#[allow(unsafe_code)]
|
|
let _ = unsafe { GetTokenInformation(token, class, null_mut(), 0, &raw mut needed) };
|
|
if needed == 0 {
|
|
return Err(last_error("GetTokenInformation"));
|
|
}
|
|
let mut bytes = vec![0_u8; usize::try_from(needed).expect("token info length fits usize")];
|
|
#[allow(unsafe_code)]
|
|
let ok = unsafe {
|
|
GetTokenInformation(
|
|
token,
|
|
class,
|
|
bytes.as_mut_ptr().cast::<c_void>(),
|
|
needed,
|
|
&raw mut needed,
|
|
)
|
|
};
|
|
if ok == FALSE {
|
|
return Err(last_error("GetTokenInformation"));
|
|
}
|
|
Ok(bytes)
|
|
}
|
|
|
|
fn wide(value: &str) -> Vec<u16> {
|
|
let mut wide = OsStr::new(value).encode_wide().collect::<Vec<_>>();
|
|
wide.push(0);
|
|
wide
|
|
}
|
|
|
|
fn wide_from_os(value: &OsStr) -> Vec<u16> {
|
|
let mut wide = value.encode_wide().collect::<Vec<_>>();
|
|
wide.push(0);
|
|
wide
|
|
}
|
|
|
|
fn wide_to_string(value: &[u16]) -> String {
|
|
let terminator = value
|
|
.iter()
|
|
.position(|item| *item == 0)
|
|
.unwrap_or(value.len());
|
|
String::from_utf16_lossy(&value[..terminator])
|
|
}
|
|
|
|
fn last_error(action: &'static str) -> WindowsSupportError {
|
|
#[allow(unsafe_code)]
|
|
let code = unsafe { GetLastError() };
|
|
WindowsSupportError::WindowsApi { action, code }
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{
|
|
CREATE_NEW_CONSOLE, CREATE_NO_WINDOW, ERROR_ACCESS_DENIED, ProcessPriority,
|
|
ShowWindowMode, TokenIntegrity, admin_launch_requires_duplicate_token,
|
|
application_name_wide, build_environment_block, build_process_command_line,
|
|
build_startup_info, create_relay_output_file, creation_flags, integrity_rid,
|
|
interactive_desktop, launch_request, mandatory_label_sid, priority_flag,
|
|
quote_command_line, quote_windows_arg, resolve_target_session_id,
|
|
should_retry_trustedinstaller_token_open, should_use_path_search, show_window_value,
|
|
wide, wide_from_os, wide_to_string,
|
|
};
|
|
use std::ffi::OsStr;
|
|
use std::fs;
|
|
|
|
use crate::WindowsSupportError;
|
|
use crate::sudo::{LaunchIdentity, LaunchRequest, PrivilegeMode};
|
|
|
|
#[test]
|
|
fn windows_arg_quoting_handles_spaces_quotes_and_trailing_backslashes() {
|
|
assert_eq!(quote_windows_arg("plain"), "plain");
|
|
assert_eq!(quote_windows_arg("two words"), "\"two words\"");
|
|
assert_eq!(quote_windows_arg("a\"b"), "\"a\\\"b\"");
|
|
assert_eq!(quote_windows_arg("c:\\temp\\"), "\"c:\\temp\\\\\"");
|
|
}
|
|
|
|
#[test]
|
|
fn command_line_builder_includes_program_first() {
|
|
let command = build_process_command_line("tool.exe", &["two words".to_string()]);
|
|
assert_eq!(command, "tool.exe \"two words\"");
|
|
assert_eq!(
|
|
quote_command_line(["", "tab\tvalue", "simple"]),
|
|
"\"\" \"tab\tvalue\" simple"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn bare_commands_use_windows_path_search() {
|
|
assert!(should_use_path_search("cmd"));
|
|
assert!(should_use_path_search("pwsh.exe"));
|
|
assert!(application_name_wide("cmd").is_none());
|
|
assert!(application_name_wide("pwsh.exe").is_none());
|
|
assert!(application_name_wide("C:\\Windows\\System32\\cmd.exe").is_some());
|
|
assert!(application_name_wide(".\\tool.exe").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn enum_mappings_cover_priority_and_show_window() {
|
|
assert!(priority_flag(ProcessPriority::Idle) > 0);
|
|
assert!(priority_flag(ProcessPriority::BelowNormal) > 0);
|
|
assert!(priority_flag(ProcessPriority::Normal) > 0);
|
|
assert!(priority_flag(ProcessPriority::AboveNormal) > 0);
|
|
assert!(priority_flag(ProcessPriority::High) > 0);
|
|
assert!(priority_flag(ProcessPriority::Realtime) > 0);
|
|
assert_eq!(show_window_value(ShowWindowMode::Default), 10);
|
|
assert_eq!(show_window_value(ShowWindowMode::Hidden), 0);
|
|
assert_eq!(show_window_value(ShowWindowMode::Normal), 1);
|
|
assert_eq!(show_window_value(ShowWindowMode::Minimized), 2);
|
|
assert_eq!(show_window_value(ShowWindowMode::Maximized), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn token_integrity_debug_shape_is_stable() {
|
|
assert_eq!(format!("{:?}", TokenIntegrity::MediumPlus), "MediumPlus");
|
|
}
|
|
|
|
#[test]
|
|
fn trustedinstaller_retry_only_handles_access_denied_token_open() {
|
|
let retryable = WindowsSupportError::WindowsApi {
|
|
action: "OpenProcessToken",
|
|
code: ERROR_ACCESS_DENIED,
|
|
};
|
|
let retryable_process = WindowsSupportError::WindowsApi {
|
|
action: "OpenProcess",
|
|
code: ERROR_ACCESS_DENIED,
|
|
};
|
|
assert!(should_retry_trustedinstaller_token_open(&retryable));
|
|
assert!(should_retry_trustedinstaller_token_open(&retryable_process));
|
|
}
|
|
|
|
#[test]
|
|
fn interactive_launches_use_winsta0_default_desktop() {
|
|
let request = LaunchRequest {
|
|
program: "cmd.exe".to_string(),
|
|
args: Vec::new(),
|
|
current_directory: None,
|
|
stdout_path: None,
|
|
stderr_path: None,
|
|
identity: LaunchIdentity::TrustedInstaller,
|
|
privileges: PrivilegeMode::Default,
|
|
integrity: None,
|
|
priority: ProcessPriority::Normal,
|
|
show_window: ShowWindowMode::Default,
|
|
session: None,
|
|
same_console: false,
|
|
new_window: true,
|
|
wait: false,
|
|
};
|
|
let desktop = interactive_desktop(&request).expect("interactive desktop");
|
|
let startup = build_startup_info(request.show_window, Some(&desktop));
|
|
assert_eq!(wide_to_string(&desktop), "winsta0\\default");
|
|
assert!(!startup.lpDesktop.is_null());
|
|
}
|
|
|
|
#[test]
|
|
fn same_console_launches_use_winsta0_default_desktop() {
|
|
let request = LaunchRequest {
|
|
program: "cmd.exe".to_string(),
|
|
args: Vec::new(),
|
|
current_directory: None,
|
|
stdout_path: None,
|
|
stderr_path: None,
|
|
identity: LaunchIdentity::System,
|
|
privileges: PrivilegeMode::Default,
|
|
integrity: None,
|
|
priority: ProcessPriority::Normal,
|
|
show_window: ShowWindowMode::Default,
|
|
session: None,
|
|
same_console: true,
|
|
new_window: false,
|
|
wait: true,
|
|
};
|
|
let desktop = interactive_desktop(&request).expect("interactive desktop");
|
|
assert_eq!(wide_to_string(&desktop), "winsta0\\default");
|
|
}
|
|
|
|
#[test]
|
|
fn hidden_new_window_does_not_request_no_window_flag() {
|
|
let request = LaunchRequest {
|
|
program: "cmd.exe".to_string(),
|
|
args: Vec::new(),
|
|
current_directory: None,
|
|
stdout_path: None,
|
|
stderr_path: None,
|
|
identity: LaunchIdentity::Admin,
|
|
privileges: PrivilegeMode::Default,
|
|
integrity: None,
|
|
priority: ProcessPriority::Normal,
|
|
show_window: ShowWindowMode::Hidden,
|
|
session: None,
|
|
same_console: false,
|
|
new_window: true,
|
|
wait: false,
|
|
};
|
|
let flags = creation_flags(&request);
|
|
assert_ne!(flags & CREATE_NEW_CONSOLE, 0);
|
|
assert_eq!(flags & CREATE_NO_WINDOW, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_session_wins_session_resolution() {
|
|
assert_eq!(resolve_target_session_id(Some(42)), Some(42));
|
|
}
|
|
|
|
#[test]
|
|
fn creation_flags_cover_window_and_priority_combinations() {
|
|
let mut request = LaunchRequest {
|
|
program: "cmd.exe".to_string(),
|
|
args: Vec::new(),
|
|
current_directory: None,
|
|
stdout_path: None,
|
|
stderr_path: None,
|
|
identity: LaunchIdentity::CurrentProcess,
|
|
privileges: PrivilegeMode::Default,
|
|
integrity: None,
|
|
priority: ProcessPriority::High,
|
|
show_window: ShowWindowMode::Hidden,
|
|
session: None,
|
|
same_console: false,
|
|
new_window: false,
|
|
wait: false,
|
|
};
|
|
let hidden_same_window = creation_flags(&request);
|
|
assert_ne!(hidden_same_window & CREATE_NO_WINDOW, 0);
|
|
assert_eq!(hidden_same_window & CREATE_NEW_CONSOLE, 0);
|
|
|
|
request.new_window = true;
|
|
let hidden_new_window = creation_flags(&request);
|
|
assert_ne!(hidden_new_window & CREATE_NEW_CONSOLE, 0);
|
|
assert_eq!(hidden_new_window & CREATE_NO_WINDOW, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn wide_and_environment_helpers_emit_null_terminated_buffers() {
|
|
let wide = wide("Mercury");
|
|
assert_eq!(wide_to_string(&wide), "Mercury");
|
|
assert_eq!(wide.last(), Some(&0));
|
|
|
|
let from_os = wide_from_os(OsStr::new("Toolbox"));
|
|
assert_eq!(wide_to_string(&from_os), "Toolbox");
|
|
assert_eq!(from_os.last(), Some(&0));
|
|
|
|
let environment = build_environment_block();
|
|
assert!(environment.ends_with(&[0, 0]));
|
|
}
|
|
|
|
#[test]
|
|
fn relay_output_file_refuses_existing_replacements() {
|
|
let path = std::env::temp_dir().join(format!(
|
|
"windowsupport-relay-output-{}.txt",
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("epoch")
|
|
.as_nanos()
|
|
));
|
|
fs::write(&path, "attacker").expect("replacement");
|
|
|
|
let error = create_relay_output_file(&path.display().to_string())
|
|
.expect_err("existing replacement should be refused");
|
|
|
|
assert!(
|
|
error
|
|
.to_string()
|
|
.contains("failed to create relay output file")
|
|
);
|
|
assert_eq!(
|
|
fs::read_to_string(&path).expect("replacement remains"),
|
|
"attacker"
|
|
);
|
|
let _ = fs::remove_file(path);
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn current_process_launch_waits_for_child_exit_code() {
|
|
let request = LaunchRequest {
|
|
program: "cmd.exe".to_string(),
|
|
args: vec![
|
|
"/D".to_string(),
|
|
"/S".to_string(),
|
|
"/C".to_string(),
|
|
"exit /b 7".to_string(),
|
|
],
|
|
current_directory: None,
|
|
stdout_path: None,
|
|
stderr_path: None,
|
|
identity: LaunchIdentity::CurrentProcess,
|
|
privileges: PrivilegeMode::Default,
|
|
integrity: None,
|
|
priority: ProcessPriority::Normal,
|
|
show_window: ShowWindowMode::Hidden,
|
|
session: None,
|
|
same_console: false,
|
|
new_window: true,
|
|
wait: true,
|
|
};
|
|
|
|
let result = launch_request(&request).expect("launch current process");
|
|
|
|
assert_eq!(result.identity, LaunchIdentity::CurrentProcess);
|
|
assert_eq!(result.exit_code, Some(7));
|
|
assert!(result.pid.is_some());
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn current_process_launch_can_relay_stdout_to_exclusive_file() {
|
|
let path = std::env::temp_dir().join(format!(
|
|
"windowsupport-relay-capture-{}.txt",
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.expect("epoch")
|
|
.as_nanos()
|
|
));
|
|
let request = LaunchRequest {
|
|
program: "cmd.exe".to_string(),
|
|
args: vec![
|
|
"/D".to_string(),
|
|
"/S".to_string(),
|
|
"/C".to_string(),
|
|
"echo MercuryStdout".to_string(),
|
|
],
|
|
current_directory: None,
|
|
stdout_path: Some(path.display().to_string()),
|
|
stderr_path: None,
|
|
identity: LaunchIdentity::CurrentProcess,
|
|
privileges: PrivilegeMode::Default,
|
|
integrity: None,
|
|
priority: ProcessPriority::Normal,
|
|
show_window: ShowWindowMode::Hidden,
|
|
session: None,
|
|
same_console: false,
|
|
new_window: false,
|
|
wait: true,
|
|
};
|
|
|
|
let result = launch_request(&request).expect("launch with stdout relay");
|
|
|
|
assert_eq!(result.exit_code, Some(0));
|
|
assert!(
|
|
fs::read_to_string(&path)
|
|
.expect("relay output")
|
|
.contains("MercuryStdout")
|
|
);
|
|
let _ = fs::remove_file(path);
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn current_user_launch_without_token_shaping_uses_current_identity_path() {
|
|
let status = super::current_token_status().expect("token status");
|
|
assert!(
|
|
status
|
|
.current_user
|
|
.as_deref()
|
|
.is_some_and(|user| !user.is_empty())
|
|
);
|
|
|
|
let request = LaunchRequest {
|
|
program: "cmd.exe".to_string(),
|
|
args: vec![
|
|
"/D".to_string(),
|
|
"/S".to_string(),
|
|
"/C".to_string(),
|
|
"exit /b 0".to_string(),
|
|
],
|
|
current_directory: None,
|
|
stdout_path: None,
|
|
stderr_path: None,
|
|
identity: LaunchIdentity::CurrentUser,
|
|
privileges: PrivilegeMode::Default,
|
|
integrity: None,
|
|
priority: ProcessPriority::Normal,
|
|
show_window: ShowWindowMode::Hidden,
|
|
session: None,
|
|
same_console: false,
|
|
new_window: true,
|
|
wait: true,
|
|
};
|
|
|
|
let result = launch_request(&request).expect("launch current user");
|
|
|
|
assert_eq!(result.identity, LaunchIdentity::CurrentUser);
|
|
assert_eq!(result.exit_code, Some(0));
|
|
}
|
|
|
|
#[test]
|
|
fn admin_launch_requires_duplicate_token_for_token_mutations() {
|
|
let request = LaunchRequest {
|
|
program: "cmd.exe".to_string(),
|
|
args: Vec::new(),
|
|
current_directory: None,
|
|
stdout_path: None,
|
|
stderr_path: None,
|
|
identity: LaunchIdentity::Admin,
|
|
privileges: PrivilegeMode::Default,
|
|
integrity: None,
|
|
priority: ProcessPriority::Normal,
|
|
show_window: ShowWindowMode::Default,
|
|
session: None,
|
|
same_console: false,
|
|
new_window: false,
|
|
wait: false,
|
|
};
|
|
assert!(!admin_launch_requires_duplicate_token(&request));
|
|
|
|
let mut integrity = request.clone();
|
|
integrity.integrity = Some(TokenIntegrity::High);
|
|
assert!(admin_launch_requires_duplicate_token(&integrity));
|
|
|
|
let mut privileges = request.clone();
|
|
privileges.privileges = PrivilegeMode::DisableAll;
|
|
assert!(admin_launch_requires_duplicate_token(&privileges));
|
|
|
|
let mut session = request;
|
|
session.session = Some(1);
|
|
assert!(admin_launch_requires_duplicate_token(&session));
|
|
}
|
|
|
|
#[test]
|
|
fn mandatory_label_sid_matches_integrity_authority_shape() {
|
|
let rid = integrity_rid(TokenIntegrity::MediumPlus).expect("rid");
|
|
let sid = mandatory_label_sid(rid);
|
|
assert_eq!(sid[0], 1);
|
|
assert_eq!(sid[1], 1);
|
|
assert_eq!(sid[2..8], [0, 0, 0, 0, 0, 16]);
|
|
assert_eq!(u32::from_le_bytes([sid[8], sid[9], sid[10], sid[11]]), rid);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub use imp::{
|
|
attach_parent_console, current_token_status, elevate_current_process, launch_request,
|
|
};
|