use std::{
ffi::c_void,
path::{Path, PathBuf},
time::Duration,
};
use windows::{
Win32::{
Foundation::MAX_PATH,
System::{
RemoteDesktop::{
WTS_CURRENT_SERVER_HANDLE, WTS_SESSION_INFOW, WTSActive, WTSEnumerateSessionsW,
WTSFreeMemory, WTSGetActiveConsoleSessionId, WTSQuerySessionInformationW,
WTSUserName,
},
SystemInformation::GetSystemDirectoryW,
SystemServices::SECURITY_MANDATORY_SYSTEM_RID,
Threading::{
OpenProcess, PROCESS_NAME_FORMAT, PROCESS_QUERY_LIMITED_INFORMATION,
QueryFullProcessImageNameW,
},
},
},
core::{PWSTR, w},
};
use crate::{
error::ElevateError,
security::{
ExecutionContext, HandleGuard, Privilege, ProcessSnapshot, ServiceManager, Token,
TokenType, win32,
},
strategy::ElevationStrategy,
};
const LOCAL_SYSTEM_SID: &str = "S-1-5-18";
const SESSION0_PREFERRED_SYSTEM_PROCESSES: &[&str] = &["services.exe", "wininit.exe"];
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, PartialEq, Eq, Default)]
pub enum SessionTarget {
#[default]
CallerSession,
Explicit(u32),
AnyInteractive,
}
pub struct TrustedInstallerStrategy {
service_timeout: Duration,
context: ExecutionContext,
session_target: SessionTarget,
}
impl Default for TrustedInstallerStrategy {
fn default() -> Self {
Self {
service_timeout: Duration::from_secs(30),
context: ExecutionContext::default(),
session_target: SessionTarget::default(),
}
}
}
impl TrustedInstallerStrategy {
pub fn new(service_timeout: Duration, context: ExecutionContext) -> Self {
Self {
service_timeout,
context,
session_target: SessionTarget::CallerSession,
}
}
#[must_use]
pub fn with_session_target(mut self, session_target: SessionTarget) -> Self {
self.session_target = session_target;
self
}
#[must_use]
pub const fn context(&self) -> &ExecutionContext {
&self.context
}
}
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 = match self.context {
ExecutionContext::Headless => None,
ExecutionContext::Interactive(_) => {
Some(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<u32, ElevateError> {
match target {
SessionTarget::CallerSession => {
let id = caller_token.session_id()?;
if id == 0 {
return Err(ElevateError::SessionZeroNotSupported);
}
Ok(id)
}
SessionTarget::Explicit(id) => Ok(*id),
SessionTarget::AnyInteractive => find_any_interactive_session(),
}
}
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 || !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 get_system32_directory() -> Option<PathBuf> {
let mut buf = [0u16; MAX_PATH as usize];
let len = unsafe { GetSystemDirectoryW(Some(&mut buf)) } as usize;
if len == 0 || len > buf.len() {
return None;
}
Some(PathBuf::from(String::from_utf16_lossy(&buf[..len])))
}
fn is_system32_binary(pid: u32, system32: &Path) -> bool {
let Ok(process) = (unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) })
else {
return false;
};
let _guard = HandleGuard(process);
let mut buf = [0u16; 1024];
let mut size = buf.len() as u32;
let res = unsafe {
QueryFullProcessImageNameW(
process,
PROCESS_NAME_FORMAT(0),
PWSTR(buf.as_mut_ptr()),
&raw mut size,
)
};
if res.is_err() || size == 0 {
return false;
}
let exe_path = PathBuf::from(String::from_utf16_lossy(&buf[..size as usize]));
exe_path
.parent()
.and_then(|p| p.to_str())
.zip(system32.to_str())
.is_some_and(|(parent, sys)| parent.eq_ignore_ascii_case(sys))
}
fn is_viable_system_token(token: &Token, target_session: Option<u32>) -> bool {
let is_system = token.user_sid().is_ok_and(|sid| sid == LOCAL_SYSTEM_SID);
if !is_system {
return false;
}
if target_session.is_some_and(|expected| !token.session_id().is_ok_and(|s| s == expected)) {
return false;
}
if token.is_restricted().unwrap_or(true) {
return false;
}
let is_system_il = token
.integrity_level()
.is_ok_and(|il| il >= SECURITY_MANDATORY_SYSTEM_RID as u32);
if !is_system_il {
return false;
}
token
.has_privilege_present(Privilege::Impersonate)
.unwrap_or(false)
}
fn find_system_process(process_name: &str, session_id: u32) -> Result<u32, ElevateError> {
let system32 = get_system32_directory().ok_or_else(|| {
ElevateError::ProcessNotFound("system32 directory unresolvable".to_string())
})?;
for (pid, name) in ProcessSnapshot::new()? {
if !name.eq_ignore_ascii_case(process_name) {
continue;
}
if is_system32_binary(pid, &system32)
&& Token::from_pid(pid).is_ok_and(|t| is_viable_system_token(&t, Some(session_id)))
{
return Ok(pid);
}
}
Err(ElevateError::ProcessNotFound(process_name.to_string()))
}
fn find_any_system_process() -> Result<u32, ElevateError> {
if let Some(system32) = get_system32_directory() {
for &target_name in SESSION0_PREFERRED_SYSTEM_PROCESSES {
for (pid, name) in ProcessSnapshot::new()? {
if name.eq_ignore_ascii_case(target_name)
&& is_system32_binary(pid, &system32)
&& Token::from_pid(pid).is_ok_and(|t| is_viable_system_token(&t, Some(0)))
{
return Ok(pid);
}
}
}
}
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;
};
if is_viable_system_token(&token, Some(0)) {
best = Some(best.map_or(pid, |prev| prev.min(pid)));
}
}
best.ok_or_else(|| {
ElevateError::ProcessNotFound("viable unrestricted Session 0 SYSTEM process".to_string())
})
}