#![allow(unsafe_code)]
use std::io;
use windows::Win32::Foundation::{
CloseHandle, ERROR_INSUFFICIENT_BUFFER, ERROR_NO_SUCH_PRIVILEGE, HANDLE, LUID,
};
use windows::Win32::Security::{
GetTokenInformation, LUID_AND_ATTRIBUTES, LookupPrivilegeNameW, LookupPrivilegeValueW,
PRIVILEGE_SET, PrivilegeCheck, SE_PRIVILEGE_ENABLED, SE_PRIVILEGE_ENABLED_BY_DEFAULT,
SE_PRIVILEGE_REMOVED, TOKEN_ELEVATION, TOKEN_PRIVILEGES, TOKEN_QUERY,
};
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
use windows::core::BOOL;
use crate::PrivilegeInfo;
use crate::error::TokenPrivilegeError;
const PRIVILEGE_SET_ALL_NECESSARY: u32 = 1;
#[allow(clippy::as_conversions)] const _: () = assert!(
std::mem::size_of::<TOKEN_ELEVATION>() <= u32::MAX as usize,
"TOKEN_ELEVATION size must fit in u32"
);
pub struct OwnedHandle(HANDLE);
impl std::fmt::Debug for OwnedHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OwnedHandle").finish_non_exhaustive()
}
}
impl Drop for OwnedHandle {
fn drop(&mut self) {
if !self.0.is_invalid() {
unsafe {
let close_result = CloseHandle(self.0);
debug_assert!(close_result.is_ok(), "CloseHandle failed: {close_result:?}");
}
}
}
}
pub fn open_current_process_token() -> Result<OwnedHandle, TokenPrivilegeError> {
let mut handle = HANDLE::default();
unsafe {
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut handle)
.map_err(|e| TokenPrivilegeError::OpenTokenFailed(io::Error::from(e)))?;
}
Ok(OwnedHandle(handle))
}
pub fn query_elevation(token: &OwnedHandle) -> Result<bool, TokenPrivilegeError> {
#[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
const ELEVATION_SIZE: u32 = std::mem::size_of::<TOKEN_ELEVATION>() as u32;
let mut elevation = TOKEN_ELEVATION::default();
let mut return_length = 0_u32;
unsafe {
GetTokenInformation(
token.0,
windows::Win32::Security::TokenElevation,
Some(std::ptr::from_mut(&mut elevation).cast()),
ELEVATION_SIZE,
&raw mut return_length,
)
.map_err(|e| TokenPrivilegeError::QueryFailed(io::Error::from(e)))?;
}
Ok(elevation.TokenIsElevated != 0)
}
pub fn lookup_privilege_value(name: &str) -> Result<LUID, TokenPrivilegeError> {
let wide_name: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
let mut luid = LUID::default();
unsafe {
LookupPrivilegeValueW(
None,
windows::core::PCWSTR(wide_name.as_ptr()),
&raw mut luid,
)
.map_err(|e| {
if e.code() == ERROR_NO_SUCH_PRIVILEGE.to_hresult() {
TokenPrivilegeError::InvalidPrivilegeName {
name: name.to_owned(),
}
} else {
TokenPrivilegeError::LookupFailed {
name: name.to_owned(),
source: io::Error::from(e),
}
}
})?;
}
Ok(luid)
}
pub fn check_privilege_enabled(
token: &OwnedHandle,
luid: LUID,
) -> Result<bool, TokenPrivilegeError> {
let mut privilege_set = PRIVILEGE_SET {
PrivilegeCount: 1,
Control: PRIVILEGE_SET_ALL_NECESSARY,
Privilege: [LUID_AND_ATTRIBUTES {
Luid: luid,
Attributes: SE_PRIVILEGE_ENABLED,
}],
};
let mut result = BOOL::default();
unsafe {
PrivilegeCheck(token.0, &raw mut privilege_set, &raw mut result)
.map_err(|e| TokenPrivilegeError::CheckFailed(io::Error::from(e)))?;
}
Ok(result.as_bool())
}
pub fn enumerate_token_privileges(
token: &OwnedHandle,
) -> Result<Vec<PrivilegeInfo>, TokenPrivilegeError> {
let mut return_length = 0_u32;
let size_result = unsafe {
GetTokenInformation(
token.0,
windows::Win32::Security::TokenPrivileges,
None,
0,
&raw mut return_length,
)
};
match size_result {
Ok(()) => {
return Err(TokenPrivilegeError::QueryFailed(io::Error::other(
"GetTokenInformation unexpectedly succeeded with null buffer",
)));
}
Err(ref e) if e.code() == ERROR_INSUFFICIENT_BUFFER.to_hresult() => {
}
Err(e) => {
return Err(TokenPrivilegeError::QueryFailed(io::Error::from(e)));
}
}
if return_length == 0 {
return Err(TokenPrivilegeError::QueryFailed(io::Error::other(
"GetTokenInformation returned zero required length",
)));
}
#[allow(clippy::as_conversions)] let byte_len = return_length as usize;
let u64_len = byte_len.div_ceil(size_of::<u64>());
let mut buffer = vec![0_u64; u64_len];
unsafe {
GetTokenInformation(
token.0,
windows::Win32::Security::TokenPrivileges,
Some(buffer.as_mut_ptr().cast()),
return_length,
&raw mut return_length,
)
.map_err(|e| TokenPrivilegeError::QueryFailed(io::Error::from(e)))?;
}
let token_privileges = unsafe { &*(buffer.as_ptr().cast::<TOKEN_PRIVILEGES>()) };
#[allow(clippy::as_conversions)] let count = token_privileges.PrivilegeCount as usize;
let privileges_slice =
unsafe { std::slice::from_raw_parts(token_privileges.Privileges.as_ptr(), count) };
let mut result = Vec::with_capacity(count);
for attr in privileges_slice {
let name = lookup_privilege_name(attr.Luid)?;
let attributes = attr.Attributes;
result.push(PrivilegeInfo {
name,
enabled: (attributes & SE_PRIVILEGE_ENABLED).0 != 0,
enabled_by_default: (attributes & SE_PRIVILEGE_ENABLED_BY_DEFAULT).0 != 0,
removed: (attributes & SE_PRIVILEGE_REMOVED).0 != 0,
});
}
Ok(result)
}
fn lookup_privilege_name(luid: LUID) -> Result<String, TokenPrivilegeError> {
let mut name_len = 0_u32;
let size_result =
unsafe { LookupPrivilegeNameW(None, &raw const luid, None, &raw mut name_len) };
if name_len == 0 {
return Err(TokenPrivilegeError::QueryFailed(
size_result.err().map_or_else(
|| io::Error::other("LookupPrivilegeNameW returned zero length without error"),
io::Error::from,
),
));
}
#[allow(clippy::as_conversions)] let mut name_buf = vec![0_u16; name_len as usize];
unsafe {
LookupPrivilegeNameW(
None,
&raw const luid,
Some(windows::core::PWSTR(name_buf.as_mut_ptr())),
&raw mut name_len,
)
.map_err(|e| TokenPrivilegeError::QueryFailed(io::Error::from(e)))?;
}
#[allow(clippy::as_conversions)] let len = name_len as usize;
let name_slice = name_buf.get(..len).ok_or_else(|| {
TokenPrivilegeError::QueryFailed(io::Error::other("name buffer indexing failed"))
})?;
String::from_utf16(name_slice).map_err(|_utf16_err| {
TokenPrivilegeError::QueryFailed(io::Error::other(
"privilege name contained invalid UTF-16",
))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_and_drop_token_handle() {
let token = open_current_process_token();
assert!(token.is_ok(), "should open current process token");
}
#[test]
fn query_elevation_returns_bool() {
let token = open_current_process_token();
assert!(token.is_ok(), "should open current process token");
if let Ok(tok) = token {
let result = query_elevation(&tok);
assert!(result.is_ok(), "should query elevation");
}
}
#[test]
fn lookup_known_privilege() {
let result = lookup_privilege_value("SeChangeNotifyPrivilege");
assert!(result.is_ok(), "SeChangeNotifyPrivilege should exist");
}
#[test]
fn lookup_invalid_privilege() {
let result = lookup_privilege_value("SeTotallyFakePrivilege");
assert!(result.is_err(), "fake privilege should fail");
}
#[test]
fn check_change_notify_enabled() {
let token = open_current_process_token();
assert!(token.is_ok(), "should open current process token");
let luid = lookup_privilege_value("SeChangeNotifyPrivilege");
assert!(luid.is_ok(), "SeChangeNotifyPrivilege should exist");
if let (Ok(tok), Ok(l)) = (token, luid) {
let result = check_privilege_enabled(&tok, l);
assert!(result.is_ok(), "check should succeed");
assert!(
matches!(result, Ok(true)),
"SeChangeNotifyPrivilege should be enabled"
);
}
}
#[test]
fn enumerate_privileges_non_empty() {
let token = open_current_process_token();
assert!(token.is_ok(), "should open current process token");
if let Ok(tok) = token {
let result = enumerate_token_privileges(&tok);
assert!(result.is_ok(), "enumeration should succeed");
if let Ok(list) = result {
assert!(!list.is_empty(), "should have at least one privilege");
}
}
}
}