use std::{ffi::c_void, time::Duration};
use windows::{
Win32::System::RemoteDesktop::{
WTS_CURRENT_SERVER_HANDLE, WTS_SESSION_INFOW, WTSActive, WTSEnumerateSessionsW,
WTSFreeMemory, WTSGetActiveConsoleSessionId, WTSQuerySessionInformationW, WTSUserName,
},
core::{PWSTR, w},
};
use crate::{
error::ElevateError,
security::{Privilege, ProcessSnapshot, ServiceManager, Token, TokenType, win32},
strategy::ElevationStrategy,
};
const LOCAL_SYSTEM_SID: &str = "S-1-5-18";
struct WtsMemoryGuard(*mut c_void);
impl Drop for WtsMemoryGuard {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
WTSFreeMemory(self.0);
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum SessionTarget {
#[default]
CallerSession,
Explicit(u32),
AnyInteractive,
NoSession,
}
pub struct TrustedInstallerStrategy {
service_timeout: Duration,
session_target: SessionTarget,
}
impl Default for TrustedInstallerStrategy {
fn default() -> Self {
Self {
service_timeout: Duration::from_secs(30),
session_target: SessionTarget::default(),
}
}
}
impl TrustedInstallerStrategy {
pub const fn with_timeout(service_timeout: Duration) -> Self {
Self {
service_timeout,
session_target: SessionTarget::CallerSession,
}
}
pub fn with_session_target(session_target: SessionTarget) -> Self {
Self {
service_timeout: Duration::from_secs(30),
session_target,
}
}
pub fn new(service_timeout: Duration, session_target: SessionTarget) -> Self {
Self {
service_timeout,
session_target,
}
}
}
impl ElevationStrategy for TrustedInstallerStrategy {
fn acquire_primary_token(&self) -> Result<Token, ElevateError> {
let current_token = Token::current()?;
current_token.enable_privileges(&[Privilege::Debug, Privilege::Impersonate])?;
let target_session = resolve_session(¤t_token, &self.session_target)?;
let ti_pid = ServiceManager::open()?
.service(w!("TrustedInstaller"))?
.start_and_wait("TrustedInstaller", self.service_timeout)?;
let system_pid = match target_session {
Some(session_id) => find_system_process("winlogon.exe", session_id)?,
None => find_any_system_process()?,
};
let _impersonation_guard = Token::from_pid(system_pid)?
.duplicate(TokenType::Impersonation)?
.impersonate()?;
let ti_primary_token = Token::from_pid(ti_pid)?.duplicate(TokenType::Primary)?;
if let Some(session_id) = target_session {
ti_primary_token.set_session_id(session_id)?;
}
Ok(ti_primary_token)
}
}
fn resolve_session(
caller_token: &Token,
target: &SessionTarget,
) -> Result<Option<u32>, ElevateError> {
match target {
SessionTarget::CallerSession => {
let id = caller_token.session_id()?;
if id == 0 {
return Err(ElevateError::SessionZeroNotSupported);
}
Ok(Some(id))
}
SessionTarget::Explicit(id) => Ok(Some(*id)),
SessionTarget::AnyInteractive => find_any_interactive_session().map(Some),
SessionTarget::NoSession => Ok(None),
}
}
fn find_any_interactive_session() -> Result<u32, ElevateError> {
let mut p_info: *mut WTS_SESSION_INFOW = std::ptr::null_mut();
let mut count = 0u32;
win32!(WTSEnumerateSessionsW(
WTS_CURRENT_SERVER_HANDLE,
0,
1,
&raw mut p_info,
&raw mut count,
))?;
let _session_list_guard = WtsMemoryGuard(p_info.cast());
if p_info.is_null() || count == 0 {
return Err(ElevateError::NoInteractiveSession);
}
let sessions = unsafe { std::slice::from_raw_parts(p_info, count as usize) };
let console_session_id = unsafe { WTSGetActiveConsoleSessionId() };
let mut candidate: Option<u32> = None;
for s in sessions {
if s.State != WTSActive {
continue;
}
if !has_logged_on_user(s.SessionId) {
continue;
}
if s.SessionId == console_session_id {
return Ok(s.SessionId);
}
if candidate.is_none() {
candidate = Some(s.SessionId);
}
}
candidate.ok_or(ElevateError::NoInteractiveSession)
}
fn has_logged_on_user(session_id: u32) -> bool {
let mut raw_ptr = PWSTR::null();
let mut bytes = 0u32;
let res = win32!(WTSQuerySessionInformationW(
WTS_CURRENT_SERVER_HANDLE,
session_id,
WTSUserName,
&raw mut raw_ptr,
&raw mut bytes,
));
if res.is_err() || raw_ptr.is_null() {
return false;
}
let _username_buf_guard = WtsMemoryGuard(raw_ptr.0.cast());
unsafe { *raw_ptr.0 != 0 }
}
fn find_system_process(process_name: &str, session_id: u32) -> Result<u32, ElevateError> {
for (pid, name) in ProcessSnapshot::new()? {
if !name.eq_ignore_ascii_case(process_name) {
continue;
}
if let Ok(token) = Token::from_pid(pid) {
let is_matching_session = token.session_id().is_ok_and(|s| s == session_id);
let is_system_user = token.user_sid().is_ok_and(|sid| sid == LOCAL_SYSTEM_SID);
if is_matching_session && is_system_user {
return Ok(pid);
}
}
}
Err(ElevateError::ProcessNotFound(process_name.to_string()))
}
fn find_any_system_process() -> Result<u32, ElevateError> {
let current_pid = std::process::id();
let mut best: Option<u32> = None;
for (pid, _) in ProcessSnapshot::new()? {
if pid == current_pid || pid <= 4 {
continue;
}
let Ok(token) = Token::from_pid(pid) else {
continue;
};
let is_system = token.user_sid().is_ok_and(|sid| sid == LOCAL_SYSTEM_SID);
let is_session0 = token.session_id().is_ok_and(|s| s == 0);
if is_system && is_session0 {
best = Some(best.map_or(pid, |prev| prev.min(pid)));
}
}
best.ok_or_else(|| {
ElevateError::ProcessNotFound("accessible Session 0 SYSTEM process".to_string())
})
}