mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-06-30 12:24:58 +03:00
fix(ipc): harden local IPC authorization and portable-service bootstrap flow (#14671)
* fix(ipc): harden ipc access Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): full cmd path, comments, simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): portable service, ipc exit Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): Remove unused logs Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): Use SetEntriesInAclW instead of icacls Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): Comments Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): check is_reparse_point Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): shmem name, no fallback Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): better exit and clear Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): portable service, better exit Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): comments, id -u Signed-off-by: fufesou <linlong1266@gmail.com> * fix: comments linux headless, rx desktop ready Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): magic number Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): update deps Signed-off-by: fufesou <linlong1266@gmail.com> * Update Cargo.lock * Update Cargo.lock * fix(ipc): harden ipc, test `identity_unavailable` Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): portable service, check dir of shmem Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): macos, better check exe allowed Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): update hbb_common Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): update hbb_common Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): harden ipc, better active uid for uinput Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): harden portable service token validation Compare portable service IPC tokens in constant time and document the CSPRNG source used for one-time token generation. Clarify Windows IPC authorization comments around canonical path matching and partial peer identity lookup. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): harden portable service token handling Generate the portable service IPC token directly from OsRng, keep token comparison in the IPC layer as a fixed-length byte-wise check, and document the malformed-frame behavior for protected service IPC. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(ipc): comments Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
@@ -29,6 +29,12 @@ use wallpaper;
|
||||
pub const PA_SAMPLE_RATE: u32 = 48000;
|
||||
static mut UNMODIFIED: bool = true;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ActiveUserLookupCache {
|
||||
uid: String,
|
||||
username: String,
|
||||
}
|
||||
|
||||
const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"];
|
||||
const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"];
|
||||
|
||||
@@ -50,6 +56,8 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
}
|
||||
};
|
||||
static ref ACTIVE_USER_LOOKUP_CACHE: std::sync::Mutex<Option<ActiveUserLookupCache>> =
|
||||
std::sync::Mutex::new(None);
|
||||
// https://github.com/rustdesk/rustdesk/issues/13705
|
||||
// Check if `sudo -E` actually preserves environment.
|
||||
//
|
||||
@@ -82,6 +90,27 @@ lazy_static::lazy_static! {
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn update_active_user_lookup_cache(desktop: &Desktop) {
|
||||
if let Ok(mut cache) = ACTIVE_USER_LOOKUP_CACHE.lock() {
|
||||
if desktop.uid.is_empty() || desktop.username.is_empty() {
|
||||
*cache = None;
|
||||
} else {
|
||||
*cache = Some(ActiveUserLookupCache {
|
||||
uid: desktop.uid.clone(),
|
||||
username: desktop.username.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_active_user_id_name_from_cache() -> Option<(String, String)> {
|
||||
let cache = ACTIVE_USER_LOOKUP_CACHE.lock().ok()?;
|
||||
let entry = cache.as_ref()?;
|
||||
Some((entry.uid.clone(), entry.username.clone()))
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
// XDO context - created via libxdo-sys (which uses dynamic loading stub).
|
||||
// If libxdo is not available, xdo will be null and xdo-based functions become no-ops.
|
||||
@@ -789,6 +818,7 @@ pub fn start_os_service() {
|
||||
let mut last_restart = Instant::now();
|
||||
while running.load(Ordering::SeqCst) {
|
||||
desktop.refresh();
|
||||
update_active_user_lookup_cache(&desktop);
|
||||
|
||||
// Duplicate logic here with should_start_server
|
||||
// Login wayland will try to start a headless --server.
|
||||
@@ -861,13 +891,29 @@ pub fn start_os_service() {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Returns the cached active `(uid, username)` snapshot when available.
|
||||
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
|
||||
pub fn get_active_user_id_name() -> (String, String) {
|
||||
if let Some(id_name) = get_active_user_id_name_from_cache() {
|
||||
return id_name;
|
||||
}
|
||||
let vec_id_name = get_values_of_seat0(&[1, 2]);
|
||||
(vec_id_name[0].clone(), vec_id_name[1].clone())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Returns the cached active uid when available.
|
||||
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
|
||||
pub fn get_active_userid() -> String {
|
||||
if let Some((uid, _)) = get_active_user_id_name_from_cache() {
|
||||
return uid;
|
||||
}
|
||||
get_values_of_seat0(&[1])[0].clone()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Returns the active uid from a fresh seat0 lookup, bypassing the service-loop cache.
|
||||
pub fn get_active_userid_fresh() -> String {
|
||||
get_values_of_seat0(&[1])[0].clone()
|
||||
}
|
||||
|
||||
@@ -922,7 +968,12 @@ fn _get_display_manager() -> String {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Returns the cached active username when available.
|
||||
/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly.
|
||||
pub fn get_active_username() -> String {
|
||||
if let Some((_, username)) = get_active_user_id_name_from_cache() {
|
||||
return username;
|
||||
}
|
||||
get_values_of_seat0(&[2])[0].clone()
|
||||
}
|
||||
|
||||
|
||||
@@ -73,10 +73,19 @@ use winapi::{
|
||||
};
|
||||
use windows::Win32::{
|
||||
Foundation::{CloseHandle as WinCloseHandle, HANDLE as WinHANDLE},
|
||||
Security::{
|
||||
GetTokenInformation as WinGetTokenInformation, IsWellKnownSid, TokenUser,
|
||||
WinLocalSystemSid, TOKEN_QUERY as WIN_TOKEN_QUERY, TOKEN_USER,
|
||||
},
|
||||
System::Diagnostics::ToolHelp::{
|
||||
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
|
||||
TH32CS_SNAPPROCESS,
|
||||
},
|
||||
System::Threading::{
|
||||
OpenProcess as WinOpenProcess, OpenProcessToken as WinOpenProcessToken,
|
||||
QueryFullProcessImageNameW as WinQueryFullProcessImageNameW,
|
||||
PROCESS_QUERY_LIMITED_INFORMATION as WIN_PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
},
|
||||
};
|
||||
use windows_service::{
|
||||
define_windows_service,
|
||||
@@ -88,6 +97,14 @@ use windows_service::{
|
||||
};
|
||||
use winreg::{enums::*, RegKey};
|
||||
|
||||
mod acl;
|
||||
pub(crate) use acl::current_process_user_sid_string;
|
||||
pub use acl::{
|
||||
set_path_permission, set_path_permission_for_portable_service_shmem_dir,
|
||||
set_path_permission_for_portable_service_shmem_file,
|
||||
validate_path_for_portable_service_shmem_dir,
|
||||
};
|
||||
|
||||
pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window
|
||||
pub const EXPLORER_EXE: &'static str = "explorer.exe";
|
||||
pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW";
|
||||
@@ -565,6 +582,55 @@ pub fn get_current_session_id(share_rdp: bool) -> DWORD {
|
||||
unsafe { get_current_session(if share_rdp { TRUE } else { FALSE }) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn resolve_expected_active_session_id_for_service(session_id: u32) -> Option<u32> {
|
||||
let share_rdp_enabled = is_share_rdp();
|
||||
if get_available_sessions(false)
|
||||
.iter()
|
||||
.any(|e| e.sid == session_id)
|
||||
{
|
||||
return Some(session_id);
|
||||
}
|
||||
let current_active_session =
|
||||
unsafe { get_current_session(if share_rdp_enabled { TRUE } else { FALSE }) };
|
||||
if current_active_session == u32::MAX {
|
||||
None
|
||||
} else {
|
||||
Some(current_active_session)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn authorize_service_scoped_ipc_connection(
|
||||
stream: &ipc::Connection,
|
||||
expected_active_session_id: Option<u32>,
|
||||
) -> bool {
|
||||
let (authorized, peer_pid, peer_session_id, peer_is_system) =
|
||||
stream.service_authorization_status_for_session(expected_active_session_id);
|
||||
if !authorized {
|
||||
ipc::log_rejected_windows_ipc_connection(
|
||||
crate::POSTFIX_SERVICE,
|
||||
peer_pid,
|
||||
peer_session_id,
|
||||
expected_active_session_id,
|
||||
peer_is_system,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if let Err(err) =
|
||||
ipc::ensure_peer_executable_matches_current_by_pid_opt(peer_pid, crate::POSTFIX_SERVICE)
|
||||
{
|
||||
log::warn!(
|
||||
"Rejected unauthorized connection on protected service-scoped IPC channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}",
|
||||
crate::POSTFIX_SERVICE,
|
||||
peer_pid,
|
||||
err
|
||||
);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
extern "system" {
|
||||
fn BlockInput(v: BOOL) -> BOOL;
|
||||
}
|
||||
@@ -631,6 +697,15 @@ async fn run_service(_arguments: Vec<OsString>) -> ResultType<()> {
|
||||
Ok(res) => match res {
|
||||
Some(Ok(stream)) => {
|
||||
let mut stream = ipc::Connection::new(stream);
|
||||
// Keep IPC authorization consistent with the session we are currently serving.
|
||||
// Recompute expected session right before authorization to avoid using a stale
|
||||
// session_id after awaiting incoming.next().
|
||||
let expected_active_session_id =
|
||||
resolve_expected_active_session_id_for_service(session_id);
|
||||
if !authorize_service_scoped_ipc_connection(&stream, expected_active_session_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Ok(Some(data)) = stream.next_timeout(1000).await {
|
||||
match data {
|
||||
ipc::Data::Close => {
|
||||
@@ -1141,6 +1216,22 @@ pub fn get_active_user_home() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "flutter"))]
|
||||
#[inline]
|
||||
pub fn portable_service_logon_helper_paths() -> Option<(PathBuf, PathBuf)> {
|
||||
// Keep parity with history for now: derive LocalAppData from user profile path.
|
||||
// If users report redirected/non-standard LocalAppData issues, switch to:
|
||||
// `BaseDirs::new()?.data_local_dir()` for Known Folder-based resolution.
|
||||
let user_dir = hbb_common::directories_next::UserDirs::new()?;
|
||||
let dir = user_dir
|
||||
.home_dir()
|
||||
.join("AppData")
|
||||
.join("Local")
|
||||
.join("rustdesk-sciter");
|
||||
let dst = dir.join("rustdesk.exe");
|
||||
Some((dir, dst))
|
||||
}
|
||||
|
||||
pub fn is_prelogin() -> bool {
|
||||
let Some(username) = get_current_session_username() else {
|
||||
return false;
|
||||
@@ -2327,16 +2418,33 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
|
||||
is_run_as_system,
|
||||
crate::username(),
|
||||
);
|
||||
let arg_elevate = if is_setup {
|
||||
let mut arg_elevate = if is_setup {
|
||||
"--noinstall --elevate"
|
||||
} else {
|
||||
"--elevate"
|
||||
};
|
||||
let arg_run_as_system = if is_setup {
|
||||
}
|
||||
.to_owned();
|
||||
let mut arg_run_as_system = if is_setup {
|
||||
"--noinstall --run-as-system"
|
||||
} else {
|
||||
"--run-as-system"
|
||||
};
|
||||
}
|
||||
.to_owned();
|
||||
let shmem_name_from_args = crate::portable_service::portable_service_shmem_name_from_args();
|
||||
if shmem_name_from_args.is_none() && crate::portable_service::has_portable_service_shmem_arg() {
|
||||
log::error!("Invalid portable service shared memory argument, aborting elevation flow");
|
||||
// This is a malformed bootstrap argument in a privilege-sensitive path.
|
||||
// Keep fail-closed process termination here to avoid continuing elevation
|
||||
// with inconsistent shared-memory contract.
|
||||
std::process::exit(1);
|
||||
}
|
||||
if let Some(shmem_name) = shmem_name_from_args {
|
||||
let shmem_arg = crate::portable_service::portable_service_shmem_arg(&shmem_name);
|
||||
arg_elevate.push(' ');
|
||||
arg_elevate.push_str(&shmem_arg);
|
||||
arg_run_as_system.push(' ');
|
||||
arg_run_as_system.push_str(&shmem_arg);
|
||||
}
|
||||
if is_root() {
|
||||
if is_run_as_system {
|
||||
log::info!("run portable service");
|
||||
@@ -2347,7 +2455,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
|
||||
Ok(elevated) => {
|
||||
if elevated {
|
||||
if !is_run_as_system {
|
||||
if run_as_system(arg_run_as_system).is_ok() {
|
||||
if run_as_system(arg_run_as_system.as_str()).is_ok() {
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
log::error!(
|
||||
@@ -2358,7 +2466,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
|
||||
}
|
||||
} else {
|
||||
if !is_elevate {
|
||||
if let Ok(true) = elevate(arg_elevate) {
|
||||
if let Ok(true) = elevate(arg_elevate.as_str()) {
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
log::error!("Failed to elevate, error {}", io::Error::last_os_error());
|
||||
@@ -2416,6 +2524,115 @@ pub fn is_elevated(process_id: Option<DWORD>) -> ResultType<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
unsafe fn read_token_user_buffer(token: WinHANDLE, subject: &str) -> ResultType<Vec<u8>> {
|
||||
let mut token_user_size = 0u32;
|
||||
let get_info_result = WinGetTokenInformation(token, TokenUser, None, 0, &mut token_user_size);
|
||||
match get_info_result {
|
||||
Ok(()) => {
|
||||
if token_user_size == 0 {
|
||||
bail!(
|
||||
"Failed to get {} token user size: unexpected zero buffer size",
|
||||
subject
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Allow expected size-probe failures if Windows still returns required size.
|
||||
let is_insufficient_buffer =
|
||||
e.code() == windows::core::HRESULT::from_win32(ERROR_INSUFFICIENT_BUFFER as u32);
|
||||
let is_bad_length =
|
||||
e.code() == windows::core::HRESULT::from_win32(ERROR_BAD_LENGTH as u32);
|
||||
if (!is_insufficient_buffer && !is_bad_length) || token_user_size == 0 {
|
||||
bail!("Failed to get {} token user size: {}", subject, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut buffer = vec![0u8; token_user_size as usize];
|
||||
WinGetTokenInformation(
|
||||
token,
|
||||
TokenUser,
|
||||
Some(buffer.as_mut_ptr() as *mut core::ffi::c_void),
|
||||
token_user_size,
|
||||
&mut token_user_size,
|
||||
)
|
||||
.map_err(|e| anyhow!("Failed to get {} token user: {}", subject, e))?;
|
||||
|
||||
let min_size = std::mem::size_of::<TOKEN_USER>();
|
||||
if buffer.len() < min_size {
|
||||
bail!(
|
||||
"Failed to parse {} token user: buffer too small (got {}, need >= {})",
|
||||
subject,
|
||||
buffer.len(),
|
||||
min_size
|
||||
);
|
||||
}
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// Similar to `is_root()` / `is_local_system()` but for an arbitrary process.
|
||||
///
|
||||
/// Returns `true` if the target process is running as LocalSystem (SID: S-1-5-18).
|
||||
///
|
||||
/// TODO: After a few releases of real-world validation, consider replacing
|
||||
/// the legacy `is_local_system()` with this implementation.
|
||||
pub fn is_process_running_as_system(process_id: DWORD) -> ResultType<bool> {
|
||||
unsafe {
|
||||
let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id)
|
||||
.map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?;
|
||||
|
||||
let mut token = WinHANDLE::default();
|
||||
let result = (|| -> ResultType<bool> {
|
||||
WinOpenProcessToken(process, WIN_TOKEN_QUERY, &mut token)
|
||||
.map_err(|e| anyhow!("Failed to open process {} token: {}", process_id, e))?;
|
||||
|
||||
let token_subject = format!("process {}", process_id);
|
||||
let buffer = read_token_user_buffer(token, token_subject.as_str())?;
|
||||
let token_user: TOKEN_USER =
|
||||
std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER);
|
||||
Ok(IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool())
|
||||
})();
|
||||
|
||||
if !token.is_invalid() {
|
||||
let _ = WinCloseHandle(token);
|
||||
}
|
||||
let _ = WinCloseHandle(process);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_process_executable_path(process_id: DWORD) -> ResultType<PathBuf> {
|
||||
const PROCESS_IMAGE_PATH_BUFFER_LEN: usize = 32 * 1024;
|
||||
unsafe {
|
||||
let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id)
|
||||
.map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?;
|
||||
|
||||
let result = (|| -> ResultType<PathBuf> {
|
||||
let mut buffer = vec![0u16; PROCESS_IMAGE_PATH_BUFFER_LEN];
|
||||
let mut length = PROCESS_IMAGE_PATH_BUFFER_LEN as u32;
|
||||
WinQueryFullProcessImageNameW(
|
||||
process,
|
||||
windows::Win32::System::Threading::PROCESS_NAME_FORMAT(0),
|
||||
windows::core::PWSTR(buffer.as_mut_ptr()),
|
||||
&mut length,
|
||||
)
|
||||
.map_err(|e| anyhow!("Failed to query process {} image path: {}", process_id, e))?;
|
||||
if length == 0 {
|
||||
bail!(
|
||||
"Failed to query process {} image path: empty result",
|
||||
process_id
|
||||
);
|
||||
}
|
||||
buffer.truncate(length as usize);
|
||||
Ok(PathBuf::from(OsString::from_wide(&buffer)))
|
||||
})();
|
||||
|
||||
let _ = WinCloseHandle(process);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_foreground_window_elevated() -> ResultType<bool> {
|
||||
unsafe {
|
||||
let mut process_id: DWORD = 0;
|
||||
@@ -2708,16 +2925,6 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) ->
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
pub fn set_path_permission(dir: &Path, permission: &str) -> ResultType<()> {
|
||||
std::process::Command::new("icacls")
|
||||
.arg(dir.as_os_str())
|
||||
.arg("/grant")
|
||||
.arg(format!("*S-1-1-0:(OI)(CI){}", permission))
|
||||
.arg("/T")
|
||||
.spawn()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn str_to_device_name(name: &str) -> [u16; 32] {
|
||||
let mut device_name: Vec<u16> = wide_string(name);
|
||||
@@ -4281,6 +4488,87 @@ pub(super) fn get_pids_with_first_arg_by_wmic<S1: AsRef<str>, S2: AsRef<str>>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Test-only reusable Win32 HANDLE RAII helper.
|
||||
// If a future non-test path needs the same pattern, move it out of this test module.
|
||||
//
|
||||
// This struct is similar to `hbb_common::platform::windows::RAIIHandle`,
|
||||
// but `RAIIHandle` depends on `WinApi` crate, while this `HandleGuard` only depends on `windows` crate.
|
||||
struct HandleGuard(WinHANDLE);
|
||||
|
||||
impl HandleGuard {
|
||||
#[inline]
|
||||
fn new(handle: WinHANDLE) -> Self {
|
||||
Self(handle)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get(&self) -> WinHANDLE {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HandleGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
if !self.0.is_invalid() {
|
||||
let _ = WinCloseHandle(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_process_running_as_system_invalid_pid_errors() {
|
||||
assert!(is_process_running_as_system(u32::MAX).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_process_running_as_system_matches_current_process_token_user() {
|
||||
let pid = unsafe { windows::Win32::System::Threading::GetCurrentProcessId() };
|
||||
let actual = is_process_running_as_system(pid).unwrap();
|
||||
|
||||
let expected = unsafe {
|
||||
// Keep this test consistent: use only the `windows` crate APIs/types.
|
||||
let process = HandleGuard::new(
|
||||
WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
|
||||
.expect("WinOpenProcess should succeed for current process"),
|
||||
);
|
||||
let mut token = WinHANDLE::default();
|
||||
WinOpenProcessToken(process.get(), WIN_TOKEN_QUERY, &mut token)
|
||||
.expect("WinOpenProcessToken should succeed for current process");
|
||||
let token = HandleGuard::new(token);
|
||||
|
||||
let mut token_user_size = 0u32;
|
||||
let _ = WinGetTokenInformation(token.get(), TokenUser, None, 0, &mut token_user_size);
|
||||
assert_ne!(token_user_size, 0, "TokenUser size should be non-zero");
|
||||
|
||||
let mut buffer = vec![0u8; token_user_size as usize];
|
||||
WinGetTokenInformation(
|
||||
token.get(),
|
||||
TokenUser,
|
||||
Some(buffer.as_mut_ptr() as *mut core::ffi::c_void),
|
||||
token_user_size,
|
||||
&mut token_user_size,
|
||||
)
|
||||
.expect("WinGetTokenInformation(TokenUser) should succeed for current process");
|
||||
|
||||
let min_size = std::mem::size_of::<TOKEN_USER>();
|
||||
assert!(
|
||||
buffer.len() >= min_size,
|
||||
"TokenUser buffer too small (got {}, need >= {})",
|
||||
buffer.len(),
|
||||
min_size
|
||||
);
|
||||
let token_user: TOKEN_USER =
|
||||
std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER);
|
||||
let expected = IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool();
|
||||
expected
|
||||
};
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uninstall_cert() {
|
||||
println!("uninstall driver certs: {:?}", cert::uninstall_cert());
|
||||
|
||||
903
src/platform/windows/acl.rs
Normal file
903
src/platform/windows/acl.rs
Normal file
@@ -0,0 +1,903 @@
|
||||
// https://learn.microsoft.com/en-us/windows/win32/secgloss/security-glossary
|
||||
|
||||
use super::{read_token_user_buffer, wide_string, ResultType};
|
||||
use hbb_common::{anyhow::anyhow, bail};
|
||||
use std::{
|
||||
fs, io,
|
||||
os::windows::{ffi::OsStrExt, fs::MetadataExt},
|
||||
path::Path,
|
||||
};
|
||||
use windows::{
|
||||
core::{PCWSTR, PWSTR},
|
||||
Win32::{
|
||||
Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL},
|
||||
Security::{
|
||||
Authorization::{
|
||||
ConvertSidToStringSidW, ConvertStringSidToSidW, GetNamedSecurityInfoW,
|
||||
SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W, SET_ACCESS,
|
||||
SE_FILE_OBJECT, TRUSTEE_IS_GROUP, TRUSTEE_IS_SID, TRUSTEE_IS_USER, TRUSTEE_W,
|
||||
},
|
||||
ACE_FLAGS, ACL, CONTAINER_INHERIT_ACE, DACL_SECURITY_INFORMATION, NO_INHERITANCE,
|
||||
OBJECT_INHERIT_ACE, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID,
|
||||
TOKEN_QUERY, TOKEN_USER,
|
||||
},
|
||||
Storage::FileSystem::{FILE_ALL_ACCESS, FILE_GENERIC_WRITE},
|
||||
System::Threading::{GetCurrentProcess, OpenProcessToken},
|
||||
},
|
||||
};
|
||||
|
||||
const FILE_ATTRIBUTE_REPARSE_POINT_U32: u32 = 0x400;
|
||||
|
||||
#[inline]
|
||||
fn is_reparse_point(metadata: &fs::Metadata) -> bool {
|
||||
(metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT_U32) != 0
|
||||
}
|
||||
|
||||
fn apply_grant_sid_allow_ace_to_path(
|
||||
path: &Path,
|
||||
sid_ptr: *mut std::ffi::c_void,
|
||||
access_mask: u32,
|
||||
is_group: bool,
|
||||
is_dir: bool,
|
||||
) -> ResultType<()> {
|
||||
// Merge mode: read existing DACL and append/replace ACE via SetEntriesInAclW.
|
||||
// https://learn.microsoft.com/en-us/windows/win32/secauthz/modifying-the-acls-of-an-object-in-c--
|
||||
let mut old_dacl: *mut ACL = std::ptr::null_mut();
|
||||
let mut security_descriptor = PSECURITY_DESCRIPTOR::default();
|
||||
let path_utf16: Vec<u16> = path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let get_named_result = unsafe {
|
||||
GetNamedSecurityInfoW(
|
||||
PCWSTR::from_raw(path_utf16.as_ptr()),
|
||||
SE_FILE_OBJECT,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
None,
|
||||
None,
|
||||
Some(&mut old_dacl),
|
||||
None,
|
||||
&mut security_descriptor,
|
||||
)
|
||||
};
|
||||
if get_named_result.0 != 0 {
|
||||
bail!(
|
||||
"GetNamedSecurityInfoW failed for '{}': win32_error={}",
|
||||
path.display(),
|
||||
get_named_result.0
|
||||
);
|
||||
}
|
||||
let _sd_guard = LocalAllocGuard(security_descriptor.0);
|
||||
|
||||
let inherit_flags = if is_dir {
|
||||
ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0)
|
||||
} else {
|
||||
NO_INHERITANCE
|
||||
};
|
||||
let explicit_access = [make_sid_trustee_entry(
|
||||
sid_ptr,
|
||||
access_mask,
|
||||
inherit_flags,
|
||||
is_group,
|
||||
)];
|
||||
let old_acl_option = if old_dacl.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(old_dacl as *const ACL)
|
||||
};
|
||||
let mut new_acl: *mut ACL = std::ptr::null_mut();
|
||||
let set_entries_result = unsafe {
|
||||
SetEntriesInAclW(
|
||||
Some(explicit_access.as_slice()),
|
||||
old_acl_option,
|
||||
&mut new_acl,
|
||||
)
|
||||
};
|
||||
if set_entries_result.0 != 0 {
|
||||
bail!(
|
||||
"SetEntriesInAclW failed for '{}': win32_error={}",
|
||||
path.display(),
|
||||
set_entries_result.0
|
||||
);
|
||||
}
|
||||
if new_acl.is_null() {
|
||||
bail!(
|
||||
"SetEntriesInAclW returned null ACL for '{}'",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void);
|
||||
|
||||
let set_named_result = unsafe {
|
||||
SetNamedSecurityInfoW(
|
||||
PCWSTR::from_raw(path_utf16.as_ptr()),
|
||||
SE_FILE_OBJECT,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
None,
|
||||
None,
|
||||
Some(new_acl),
|
||||
None,
|
||||
)
|
||||
};
|
||||
if set_named_result.0 != 0 {
|
||||
bail!(
|
||||
"SetNamedSecurityInfoW failed for '{}': win32_error={}",
|
||||
path.display(),
|
||||
set_named_result.0
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Grants `Everyone` on `dir` recursively for helper/runtime files that must be
|
||||
/// readable/executable across user contexts.
|
||||
///
|
||||
/// `access_mask` is the Win32 file access mask to grant recursively.
|
||||
pub fn set_path_permission(dir: &Path, access_mask: u32) -> ResultType<()> {
|
||||
let metadata = fs::symlink_metadata(dir).map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to inspect ACL target directory '{}': {}",
|
||||
dir.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
if is_reparse_point(&metadata) {
|
||||
bail!(
|
||||
"ACL target directory is a reparse point and is rejected: '{}'",
|
||||
dir.display()
|
||||
);
|
||||
}
|
||||
if !metadata.file_type().is_dir() {
|
||||
bail!("ACL target is not a directory: '{}'", dir.display());
|
||||
}
|
||||
|
||||
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0")?;
|
||||
let mut stack = vec![dir.to_path_buf()];
|
||||
while let Some(path) = stack.pop() {
|
||||
let metadata = fs::symlink_metadata(&path)
|
||||
.map_err(|e| anyhow!("Failed to inspect ACL target '{}': {}", path.display(), e))?;
|
||||
if is_reparse_point(&metadata) {
|
||||
continue;
|
||||
}
|
||||
let is_dir = metadata.file_type().is_dir();
|
||||
apply_grant_sid_allow_ace_to_path(
|
||||
&path,
|
||||
everyone_sid.as_sid_ptr(),
|
||||
access_mask,
|
||||
true,
|
||||
is_dir,
|
||||
)?;
|
||||
if !is_dir {
|
||||
continue;
|
||||
}
|
||||
for entry in fs::read_dir(&path)
|
||||
.map_err(|e| anyhow!("Failed to list ACL target dir '{}': {}", path.display(), e))?
|
||||
{
|
||||
let entry = entry.map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to read ACL target dir entry under '{}': {}",
|
||||
path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
stack.push(entry.path());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the current process user SID as a standard SID string
|
||||
/// (for example: `S-1-5-18`).
|
||||
///
|
||||
/// Source:
|
||||
/// - Official SID-to-string API (`ConvertSidToStringSidW`):
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/sddl/nf-sddl-convertsidtostringsidw
|
||||
pub(crate) fn current_process_user_sid_string() -> ResultType<String> {
|
||||
let mut token = HANDLE::default();
|
||||
let result = (|| -> ResultType<String> {
|
||||
unsafe {
|
||||
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token)
|
||||
.map_err(|e| anyhow!("Failed to open current process token: {}", e))?;
|
||||
}
|
||||
|
||||
let buffer = unsafe { read_token_user_buffer(token, "current process")? };
|
||||
let token_user: TOKEN_USER =
|
||||
unsafe { std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER) };
|
||||
if token_user.User.Sid.0.is_null() {
|
||||
bail!("Token SID is null");
|
||||
}
|
||||
|
||||
let mut sid_string_ptr = PWSTR::null();
|
||||
unsafe {
|
||||
ConvertSidToStringSidW(token_user.User.Sid, &mut sid_string_ptr).map_err(|e| {
|
||||
anyhow!(
|
||||
"ConvertSidToStringSidW failed for current process token SID: {}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
}
|
||||
if sid_string_ptr.is_null() {
|
||||
bail!("ConvertSidToStringSidW returned null SID string pointer");
|
||||
}
|
||||
let _sid_string_guard = LocalAllocGuard(sid_string_ptr.0 as *mut std::ffi::c_void);
|
||||
unsafe {
|
||||
sid_string_ptr
|
||||
.to_string()
|
||||
.map_err(|e| anyhow!("Failed to decode SID string as UTF-16: {}", e))
|
||||
}
|
||||
})();
|
||||
|
||||
if !token.is_invalid() {
|
||||
unsafe {
|
||||
let _ = CloseHandle(token);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Hardens ACLs for portable-service shared-memory path (directory or file).
|
||||
///
|
||||
/// Why:
|
||||
/// - Shared memory used by portable service carries runtime control/data and must not inherit
|
||||
/// broad/default ACLs.
|
||||
/// - We explicitly grant only trusted principals and remove broad groups to reduce local
|
||||
/// privilege-boundary bypass risk.
|
||||
///
|
||||
/// ACL policy applied via Win32 ACL APIs (`SetEntriesInAclW` + `SetNamedSecurityInfoW`):
|
||||
/// - common (directory + file):
|
||||
/// - `S-1-5-18` (LocalSystem): full control
|
||||
/// - `S-1-5-32-544` (Built-in Administrators): full control
|
||||
/// - `current_process_user_sid_string()` result: full control
|
||||
/// - directory (`portable_service_shmem` parent):
|
||||
/// - keep `Authenticated Users` directory-level write so other local accounts can
|
||||
/// create their own runtime shmem files after account switching
|
||||
/// - `FILE_GENERIC_WRITE + NO_INHERITANCE` means write/create on this directory itself;
|
||||
/// it is intentionally not inherited by children.
|
||||
/// Reference:
|
||||
/// - File access rights:
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants
|
||||
/// - ACE inheritance rules:
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-inheritance-rules
|
||||
/// - remove `Everyone` and `Users` grants
|
||||
/// - file (`shared_memory*` flink):
|
||||
/// - remove broad grants:
|
||||
/// - `S-1-1-0` (Everyone)
|
||||
/// - `S-1-5-11` (Authenticated Users)
|
||||
/// - `S-1-5-32-545` (Users)
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
|
||||
pub fn set_path_permission_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> {
|
||||
set_path_permission_for_portable_service_shmem_impl(path, true)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn validate_path_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> {
|
||||
validate_portable_service_shmem_dir_target(path)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn set_path_permission_for_portable_service_shmem_file(path: &Path) -> ResultType<()> {
|
||||
set_path_permission_for_portable_service_shmem_impl(path, false)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct LocalAllocGuard(*mut std::ffi::c_void);
|
||||
|
||||
impl LocalAllocGuard {
|
||||
#[inline]
|
||||
pub(super) fn as_sid_ptr(&self) -> *mut std::ffi::c_void {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LocalAllocGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.0.is_null() {
|
||||
return;
|
||||
}
|
||||
// Buffers returned by ConvertStringSidToSidW / SetEntriesInAclW /
|
||||
// ConvertSidToStringSidW are LocalAlloc-owned and must be LocalFree'ed.
|
||||
unsafe {
|
||||
let _ = LocalFree(Some(HLOCAL(self.0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn sid_string_to_local_alloc_guard(sid: &str) -> ResultType<LocalAllocGuard> {
|
||||
let sid_utf16 = wide_string(sid);
|
||||
let mut sid_ptr = PSID::default();
|
||||
unsafe {
|
||||
ConvertStringSidToSidW(PCWSTR::from_raw(sid_utf16.as_ptr()), &mut sid_ptr)
|
||||
.map_err(|e| anyhow!("ConvertStringSidToSidW failed for '{}': {}", sid, e))?;
|
||||
}
|
||||
if sid_ptr.0.is_null() {
|
||||
bail!("ConvertStringSidToSidW returned null SID for '{}'", sid);
|
||||
}
|
||||
Ok(LocalAllocGuard(sid_ptr.0))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn make_sid_trustee_entry(
|
||||
sid_ptr: *mut std::ffi::c_void,
|
||||
access_permissions: u32,
|
||||
inheritance: ACE_FLAGS,
|
||||
is_group: bool,
|
||||
) -> EXPLICIT_ACCESS_W {
|
||||
// `is_group` is explicitly provided by the caller from the concrete SID semantic
|
||||
// (e.g. Administrators/Authenticated Users => group, LocalSystem/current user => user).
|
||||
EXPLICIT_ACCESS_W {
|
||||
grfAccessPermissions: access_permissions,
|
||||
grfAccessMode: SET_ACCESS,
|
||||
grfInheritance: inheritance,
|
||||
Trustee: TRUSTEE_W {
|
||||
pMultipleTrustee: std::ptr::null_mut(),
|
||||
MultipleTrusteeOperation: Default::default(),
|
||||
TrusteeForm: TRUSTEE_IS_SID,
|
||||
TrusteeType: if is_group {
|
||||
TRUSTEE_IS_GROUP
|
||||
} else {
|
||||
TRUSTEE_IS_USER
|
||||
},
|
||||
// SAFETY: With TrusteeForm=TRUSTEE_IS_SID, ptstrName is interpreted as PSID.
|
||||
ptstrName: PWSTR::from_raw(sid_ptr as *mut u16),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_portable_service_shmem_dir_target(path: &Path) -> ResultType<()> {
|
||||
let metadata = fs::symlink_metadata(path).map_err(|e| {
|
||||
anyhow!(
|
||||
"Failed to inspect portable service shared-memory ACL directory '{}': {}",
|
||||
path.display(),
|
||||
e
|
||||
)
|
||||
})?;
|
||||
if is_reparse_point(&metadata) {
|
||||
bail!(
|
||||
"Portable service shared-memory ACL directory target is a reparse point and is rejected: '{}'",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
if !metadata.file_type().is_dir() {
|
||||
bail!(
|
||||
"Portable service shared-memory ACL target is not a directory: '{}'",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_path_permission_for_portable_service_shmem_impl(
|
||||
path: &Path,
|
||||
expect_dir: bool,
|
||||
) -> ResultType<()> {
|
||||
if expect_dir {
|
||||
validate_portable_service_shmem_dir_target(path)?;
|
||||
} else {
|
||||
let metadata_result = fs::symlink_metadata(path);
|
||||
match metadata_result {
|
||||
Ok(metadata) => {
|
||||
if metadata.file_type().is_dir() {
|
||||
bail!(
|
||||
"Portable service shared-memory ACL target is a directory, expected file-like path: '{}'",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
if is_reparse_point(&metadata) {
|
||||
bail!(
|
||||
"Portable service shared-memory ACL file target is a reparse point and is rejected: '{}'",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e)
|
||||
if e.kind() == io::ErrorKind::NotFound
|
||||
|| e.kind() == io::ErrorKind::PermissionDenied =>
|
||||
{
|
||||
// Keep going and let Win32 ACL APIs return the final OS error.
|
||||
// `Path::exists()/is_file()` and metadata can collapse ACL-denied paths into
|
||||
// a false "not found" signal under restricted directory ACLs.
|
||||
}
|
||||
Err(e) => {
|
||||
bail!(
|
||||
"Failed to inspect portable service shared-memory ACL target '{}': {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let user_sid = current_process_user_sid_string()?;
|
||||
let local_system_sid = sid_string_to_local_alloc_guard("S-1-5-18")?;
|
||||
let administrators_sid = sid_string_to_local_alloc_guard("S-1-5-32-544")?;
|
||||
let current_user_sid = sid_string_to_local_alloc_guard(&user_sid)?;
|
||||
let authenticated_users_sid = if expect_dir {
|
||||
Some(sid_string_to_local_alloc_guard("S-1-5-11")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let inherit_flags = if expect_dir {
|
||||
ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0)
|
||||
} else {
|
||||
NO_INHERITANCE
|
||||
};
|
||||
let mut entries = vec![
|
||||
make_sid_trustee_entry(
|
||||
local_system_sid.as_sid_ptr(),
|
||||
FILE_ALL_ACCESS.0,
|
||||
inherit_flags,
|
||||
false,
|
||||
),
|
||||
make_sid_trustee_entry(
|
||||
administrators_sid.as_sid_ptr(),
|
||||
FILE_ALL_ACCESS.0,
|
||||
inherit_flags,
|
||||
true,
|
||||
),
|
||||
make_sid_trustee_entry(
|
||||
current_user_sid.as_sid_ptr(),
|
||||
FILE_ALL_ACCESS.0,
|
||||
inherit_flags,
|
||||
false,
|
||||
),
|
||||
];
|
||||
if let Some(auth_sid) = authenticated_users_sid.as_ref() {
|
||||
// Keep the shared parent directory multi-user writable at directory level.
|
||||
entries.push(make_sid_trustee_entry(
|
||||
auth_sid.as_sid_ptr(),
|
||||
FILE_GENERIC_WRITE.0,
|
||||
NO_INHERITANCE,
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
// Rebuild mode: build a fresh DACL (old ACL not merged) and apply as protected.
|
||||
// This avoids carrying over broad legacy ACEs from inherited/default ACLs.
|
||||
// Reference:
|
||||
// - SetEntriesInAclW:
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setentriesinaclw
|
||||
// - SetNamedSecurityInfoW (PROTECTED_DACL_SECURITY_INFORMATION):
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setnamedsecurityinfow
|
||||
let mut new_acl: *mut ACL = std::ptr::null_mut();
|
||||
let set_entries_result =
|
||||
unsafe { SetEntriesInAclW(Some(entries.as_slice()), None, &mut new_acl) };
|
||||
if set_entries_result.0 != 0 {
|
||||
bail!(
|
||||
"SetEntriesInAclW failed for '{}': win32_error={}",
|
||||
path.display(),
|
||||
set_entries_result.0
|
||||
);
|
||||
}
|
||||
if new_acl.is_null() {
|
||||
bail!(
|
||||
"SetEntriesInAclW returned null ACL for '{}'",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void);
|
||||
|
||||
let path_utf16: Vec<u16> = path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let security_info = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION;
|
||||
let set_named_result = unsafe {
|
||||
SetNamedSecurityInfoW(
|
||||
PCWSTR::from_raw(path_utf16.as_ptr()),
|
||||
SE_FILE_OBJECT,
|
||||
security_info,
|
||||
None,
|
||||
None,
|
||||
Some(new_acl),
|
||||
None,
|
||||
)
|
||||
};
|
||||
if set_named_result.0 != 0 {
|
||||
bail!(
|
||||
"SetNamedSecurityInfoW failed for '{}': win32_error={}",
|
||||
path.display(),
|
||||
set_named_result.0
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
current_process_user_sid_string, set_path_permission,
|
||||
set_path_permission_for_portable_service_shmem_dir,
|
||||
set_path_permission_for_portable_service_shmem_file, sid_string_to_local_alloc_guard,
|
||||
LocalAllocGuard, ResultType,
|
||||
};
|
||||
use hbb_common::bail;
|
||||
use std::{
|
||||
fs,
|
||||
os::windows::{ffi::OsStrExt, fs::symlink_dir, fs::symlink_file},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use windows::{
|
||||
core::PCWSTR,
|
||||
Win32::{
|
||||
Security::{
|
||||
AclSizeInformation,
|
||||
Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT},
|
||||
EqualSid as WinEqualSid, GetAce, GetAclInformation, GetSecurityDescriptorControl,
|
||||
ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_SIZE_INFORMATION,
|
||||
DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SE_DACL_PROTECTED,
|
||||
},
|
||||
Storage::FileSystem::{
|
||||
FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ACCESS_ALLOWED_ACE_TYPE_U8: u8 = 0;
|
||||
|
||||
fn unique_acl_test_path(prefix: &str) -> PathBuf {
|
||||
std::env::temp_dir().join(format!(
|
||||
"rustdesk_acl_{}_{}_{}",
|
||||
prefix,
|
||||
std::process::id(),
|
||||
hbb_common::rand::random::<u32>()
|
||||
))
|
||||
}
|
||||
|
||||
fn try_create_dir_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool {
|
||||
match symlink_dir(target, link) {
|
||||
Ok(()) => true,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"skip {}: failed to create directory reparse point (symlink): {}",
|
||||
test_name, err
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_create_file_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool {
|
||||
match symlink_file(target, link) {
|
||||
Ok(()) => true,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"skip {}: failed to create file reparse point (symlink): {}",
|
||||
test_name, err
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_dacl(path: &Path) -> ResultType<(*mut ACL, LocalAllocGuard)> {
|
||||
let mut dacl: *mut ACL = std::ptr::null_mut();
|
||||
let mut sd = PSECURITY_DESCRIPTOR::default();
|
||||
let path_utf16: Vec<u16> = path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let result = unsafe {
|
||||
GetNamedSecurityInfoW(
|
||||
PCWSTR::from_raw(path_utf16.as_ptr()),
|
||||
SE_FILE_OBJECT,
|
||||
DACL_SECURITY_INFORMATION,
|
||||
None,
|
||||
None,
|
||||
Some(&mut dacl),
|
||||
None,
|
||||
&mut sd,
|
||||
)
|
||||
};
|
||||
if result.0 != 0 {
|
||||
bail!(
|
||||
"GetNamedSecurityInfoW failed for '{}': win32_error={}",
|
||||
path.display(),
|
||||
result.0
|
||||
);
|
||||
}
|
||||
if dacl.is_null() || sd.0.is_null() {
|
||||
bail!("DACL/security descriptor missing for '{}'", path.display());
|
||||
}
|
||||
Ok((dacl, LocalAllocGuard(sd.0)))
|
||||
}
|
||||
|
||||
fn has_allow_ace_with_mask(
|
||||
dacl: *const ACL,
|
||||
sid_ptr: *mut std::ffi::c_void,
|
||||
mask: u32,
|
||||
) -> bool {
|
||||
let mut info = ACL_SIZE_INFORMATION::default();
|
||||
if unsafe {
|
||||
GetAclInformation(
|
||||
dacl,
|
||||
&mut info as *mut _ as *mut std::ffi::c_void,
|
||||
std::mem::size_of::<ACL_SIZE_INFORMATION>() as u32,
|
||||
AclSizeInformation,
|
||||
)
|
||||
}
|
||||
.is_err()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for index in 0..info.AceCount {
|
||||
let mut ace_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
|
||||
if unsafe { GetAce(dacl, index, &mut ace_ptr) }.is_err() || ace_ptr.is_null() {
|
||||
continue;
|
||||
}
|
||||
let header = unsafe { &*(ace_ptr as *const ACE_HEADER) };
|
||||
if header.AceType != ACCESS_ALLOWED_ACE_TYPE_U8 {
|
||||
continue;
|
||||
}
|
||||
let allowed = unsafe { &*(ace_ptr as *const ACCESS_ALLOWED_ACE) };
|
||||
let ace_sid = PSID((&allowed.SidStart as *const u32) as *mut std::ffi::c_void);
|
||||
if unsafe { WinEqualSid(PSID(sid_ptr), ace_sid) }.is_ok()
|
||||
&& (allowed.Mask & mask) == mask
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn has_any_allow_ace_for_sid(dacl: *const ACL, sid_ptr: *mut std::ffi::c_void) -> bool {
|
||||
has_allow_ace_with_mask(dacl, sid_ptr, 0)
|
||||
}
|
||||
|
||||
fn is_dacl_protected(sd: PSECURITY_DESCRIPTOR) -> bool {
|
||||
let mut control: u16 = 0;
|
||||
let mut revision: u32 = 0;
|
||||
if unsafe { GetSecurityDescriptorControl(sd, &mut control, &mut revision) }.is_err() {
|
||||
return false;
|
||||
}
|
||||
(control & SE_DACL_PROTECTED.0) != 0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_portable_service_shmem_dir_acl_policy() {
|
||||
let dir = unique_acl_test_path("dir");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
set_path_permission_for_portable_service_shmem_dir(&dir).unwrap();
|
||||
|
||||
let (dacl, sd_guard) = get_file_dacl(&dir).unwrap();
|
||||
let current_user_sid =
|
||||
sid_string_to_local_alloc_guard(¤t_process_user_sid_string().unwrap()).unwrap();
|
||||
let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap();
|
||||
let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap();
|
||||
let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap();
|
||||
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
|
||||
let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap();
|
||||
|
||||
assert!(has_allow_ace_with_mask(
|
||||
dacl,
|
||||
system_sid.as_sid_ptr(),
|
||||
FILE_ALL_ACCESS.0
|
||||
));
|
||||
assert!(has_allow_ace_with_mask(
|
||||
dacl,
|
||||
admin_sid.as_sid_ptr(),
|
||||
FILE_ALL_ACCESS.0
|
||||
));
|
||||
assert!(has_allow_ace_with_mask(
|
||||
dacl,
|
||||
current_user_sid.as_sid_ptr(),
|
||||
FILE_ALL_ACCESS.0
|
||||
));
|
||||
assert!(has_allow_ace_with_mask(
|
||||
dacl,
|
||||
auth_users_sid.as_sid_ptr(),
|
||||
FILE_GENERIC_WRITE.0
|
||||
));
|
||||
assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr()));
|
||||
assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr()));
|
||||
assert!(is_dacl_protected(PSECURITY_DESCRIPTOR(
|
||||
sd_guard.as_sid_ptr()
|
||||
)));
|
||||
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_portable_service_shmem_file_acl_policy() {
|
||||
let dir = unique_acl_test_path("file");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
let file = dir.join("shared_memory_portable_service_test");
|
||||
fs::write(&file, b"x").unwrap();
|
||||
set_path_permission_for_portable_service_shmem_file(&file).unwrap();
|
||||
|
||||
let (dacl, sd_guard) = get_file_dacl(&file).unwrap();
|
||||
let current_user_sid =
|
||||
sid_string_to_local_alloc_guard(¤t_process_user_sid_string().unwrap()).unwrap();
|
||||
let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap();
|
||||
let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap();
|
||||
let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap();
|
||||
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
|
||||
let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap();
|
||||
|
||||
assert!(has_allow_ace_with_mask(
|
||||
dacl,
|
||||
system_sid.as_sid_ptr(),
|
||||
FILE_ALL_ACCESS.0
|
||||
));
|
||||
assert!(has_allow_ace_with_mask(
|
||||
dacl,
|
||||
admin_sid.as_sid_ptr(),
|
||||
FILE_ALL_ACCESS.0
|
||||
));
|
||||
assert!(has_allow_ace_with_mask(
|
||||
dacl,
|
||||
current_user_sid.as_sid_ptr(),
|
||||
FILE_ALL_ACCESS.0
|
||||
));
|
||||
assert!(!has_any_allow_ace_for_sid(
|
||||
dacl,
|
||||
auth_users_sid.as_sid_ptr()
|
||||
));
|
||||
assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr()));
|
||||
assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr()));
|
||||
assert!(is_dacl_protected(PSECURITY_DESCRIPTOR(
|
||||
sd_guard.as_sid_ptr()
|
||||
)));
|
||||
|
||||
let _ = fs::remove_file(&file);
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_path_permission_rx_applies_recursively() {
|
||||
let root = unique_acl_test_path("set_path_permission");
|
||||
let child_dir = root.join("child");
|
||||
let child_file = child_dir.join("helper.exe");
|
||||
fs::create_dir_all(&child_dir).unwrap();
|
||||
fs::write(&child_file, b"x").unwrap();
|
||||
|
||||
if let Err(err) = set_path_permission(&root, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0) {
|
||||
let text = err.to_string();
|
||||
let _ = fs::remove_file(&child_file);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
if text.contains("win32_error=5") || text.contains("Access is denied") {
|
||||
eprintln!(
|
||||
"skip test_set_path_permission_rx_applies_recursively: insufficient WRITE_DAC in current environment: {}",
|
||||
text
|
||||
);
|
||||
return;
|
||||
}
|
||||
panic!("set_path_permission failed unexpectedly: {}", text);
|
||||
}
|
||||
|
||||
let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap();
|
||||
let rx_mask = FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0;
|
||||
for target in [&root, &child_dir, &child_file] {
|
||||
let (dacl, _sd_guard) = get_file_dacl(target).unwrap();
|
||||
assert!(
|
||||
has_allow_ace_with_mask(dacl, everyone_sid.as_sid_ptr(), rx_mask),
|
||||
"Everyone RX grant missing on '{}'",
|
||||
target.display()
|
||||
);
|
||||
}
|
||||
|
||||
let _ = fs::remove_file(&child_file);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_portable_service_shmem_dir_acl_rejects_file_target() {
|
||||
let dir = unique_acl_test_path("dir_target_file");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
let file = dir.join("target.txt");
|
||||
fs::write(&file, b"x").unwrap();
|
||||
let result = set_path_permission_for_portable_service_shmem_dir(&file);
|
||||
assert!(result.is_err());
|
||||
let _ = fs::remove_file(&file);
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_portable_service_shmem_file_acl_rejects_dir_target() {
|
||||
let dir = unique_acl_test_path("file_target_dir");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
let result = set_path_permission_for_portable_service_shmem_file(&dir);
|
||||
assert!(result.is_err());
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_portable_service_shmem_file_acl_rejects_missing_target() {
|
||||
let path = unique_acl_test_path("missing").join("shared_memory_missing");
|
||||
let result = set_path_permission_for_portable_service_shmem_file(&path);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_path_permission_rejects_reparse_entrypoint() {
|
||||
let root = unique_acl_test_path("reparse_entry");
|
||||
let real_dir = root.join("real");
|
||||
let link_dir = root.join("link");
|
||||
fs::create_dir_all(&real_dir).unwrap();
|
||||
if !try_create_dir_reparse_point(
|
||||
&real_dir,
|
||||
&link_dir,
|
||||
"test_set_path_permission_rejects_reparse_entrypoint",
|
||||
) {
|
||||
let _ = fs::remove_dir_all(&real_dir);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
return;
|
||||
}
|
||||
|
||||
let result = set_path_permission(&link_dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0);
|
||||
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(
|
||||
text.contains("reparse point"),
|
||||
"expected reparse-point rejection, got '{}'",
|
||||
text
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir(&link_dir);
|
||||
let _ = fs::remove_dir_all(&real_dir);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_portable_service_shmem_dir_acl_rejects_reparse_target() {
|
||||
let root = unique_acl_test_path("reparse_shmem_dir");
|
||||
let real_dir = root.join("real");
|
||||
let link_dir = root.join("link");
|
||||
fs::create_dir_all(&real_dir).unwrap();
|
||||
if !try_create_dir_reparse_point(
|
||||
&real_dir,
|
||||
&link_dir,
|
||||
"test_portable_service_shmem_dir_acl_rejects_reparse_target",
|
||||
) {
|
||||
let _ = fs::remove_dir_all(&real_dir);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
return;
|
||||
}
|
||||
|
||||
let result = set_path_permission_for_portable_service_shmem_dir(&link_dir);
|
||||
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(
|
||||
text.contains("reparse point"),
|
||||
"expected reparse-point rejection, got '{}'",
|
||||
text
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir(&link_dir);
|
||||
let _ = fs::remove_dir_all(&real_dir);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_portable_service_shmem_file_acl_rejects_reparse_target() {
|
||||
let root = unique_acl_test_path("reparse_shmem_file");
|
||||
let real_file = root.join("real.txt");
|
||||
let link_file = root.join("link.txt");
|
||||
fs::create_dir_all(&root).unwrap();
|
||||
fs::write(&real_file, b"x").unwrap();
|
||||
if !try_create_file_reparse_point(
|
||||
&real_file,
|
||||
&link_file,
|
||||
"test_portable_service_shmem_file_acl_rejects_reparse_target",
|
||||
) {
|
||||
let _ = fs::remove_file(&real_file);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
return;
|
||||
}
|
||||
|
||||
let result = set_path_permission_for_portable_service_shmem_file(&link_file);
|
||||
let text = result.err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(
|
||||
text.contains("reparse point"),
|
||||
"expected reparse-point rejection, got '{}'",
|
||||
text
|
||||
);
|
||||
|
||||
let _ = fs::remove_file(&link_file);
|
||||
let _ = fs::remove_file(&real_file);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user