mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-08-25 07:36:41 +00:00
fix: Harden Windows installer temp command scripts (#15634)
* fix: Harden Windows installer temp command scripts Signed-off-by: fufesou <linlong1266@gmail.com> * fix: restore stop-service after install preparation failure Signed-off-by: fufesou <linlong1266@gmail.com> * fix(windows): preserve special characters in installer paths Handle carets and exclamation marks safely across cmd.exe parsing stages. Add coverage for special-character paths in the elevated installer handoff. Signed-off-by: fufesou <linlong1266@gmail.com> * fix: installer, validate app name Signed-off-by: fufesou <linlong1266@gmail.com> * update tests Signed-off-by: fufesou <linlong1266@gmail.com> * Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
Generated
-13
@@ -7212,18 +7212,6 @@ dependencies = [
|
||||
"realfft",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "runas"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b96d6b6c505282b007a9b009f2aa38b2fd0359b81a0430ceacc60f69ade4c6a0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
"which",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.18.0"
|
||||
@@ -7342,7 +7330,6 @@ dependencies = [
|
||||
"reqwest",
|
||||
"ringbuf",
|
||||
"rubato",
|
||||
"runas",
|
||||
"rust-pulsectl",
|
||||
"samplerate",
|
||||
"sciter-rs",
|
||||
|
||||
+4
-1
@@ -124,14 +124,18 @@ windows = { version = "0.61", features = [
|
||||
"Win32_Security_Authorization",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Diagnostics",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_Environment",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Pipes",
|
||||
"Win32_System_Registry",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
winreg = "0.11"
|
||||
windows-service = "0.6"
|
||||
@@ -140,7 +144,6 @@ remote_printer = { path = "libs/remote_printer" }
|
||||
impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" }
|
||||
shared_memory = "0.12"
|
||||
tauri-winrt-notification = "0.1"
|
||||
runas = "1.2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2"
|
||||
|
||||
+135
-159
@@ -98,12 +98,19 @@ use windows_service::{
|
||||
use winreg::{enums::*, RegKey};
|
||||
|
||||
mod acl;
|
||||
mod installer_handoff;
|
||||
mod installer_shell;
|
||||
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,
|
||||
};
|
||||
use installer_handoff::run_cmds;
|
||||
use installer_shell::{
|
||||
embedded_shortcut_commands, embedded_tray_shortcut_commands, escape_nested_cmd_ampersands,
|
||||
shortcut_bytes, validate_install_value,
|
||||
};
|
||||
|
||||
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";
|
||||
@@ -113,6 +120,17 @@ const REG_NAME_INSTALL_DESKTOPSHORTCUTS: &str = "DESKTOPSHORTCUTS";
|
||||
const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS";
|
||||
pub const REG_NAME_INSTALL_PRINTER: &str = "PRINTER";
|
||||
|
||||
fn validate_install_app_name(app_name: &str) -> ResultType<()> {
|
||||
if app_name.is_empty()
|
||||
|| !app_name
|
||||
.chars()
|
||||
.all(|character| character.is_ascii_alphanumeric() || character == '-')
|
||||
{
|
||||
bail!("Application name must match [a-zA-Z0-9-]+");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
||||
unsafe {
|
||||
let hwnd = GetForegroundWindow();
|
||||
@@ -1499,6 +1517,7 @@ fn get_after_install(
|
||||
) -> String {
|
||||
let app_name = crate::get_app_name();
|
||||
let ext = app_name.to_lowercase();
|
||||
let nested_exe = escape_nested_cmd_ampersands(exe);
|
||||
|
||||
// reg delete HKEY_CURRENT_USER\Software\Classes for
|
||||
// https://github.com/rustdesk/rustdesk/commit/f4bdfb6936ae4804fc8ab1cf560db192622ad01a
|
||||
@@ -1534,17 +1553,17 @@ fn get_after_install(
|
||||
{start_menu_shortcuts}
|
||||
{reg_printer}
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{exe}\\\",0\"
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{nested_exe}\\\",0\"
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open /f
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{exe}\\\" --play \\\"%%1\\\"\"
|
||||
reg add HKEY_CLASSES_ROOT\\.{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{nested_exe}\\\" --play \\\"%%1\\\"\"
|
||||
reg add HKEY_CLASSES_ROOT\\{ext} /f
|
||||
reg add HKEY_CLASSES_ROOT\\{ext} /f /v \"URL Protocol\" /t REG_SZ /d \"\"
|
||||
reg add HKEY_CLASSES_ROOT\\{ext}\\shell /f
|
||||
reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open /f
|
||||
reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open\\command /f
|
||||
reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{exe}\\\" \\\"%%1\\\"\"
|
||||
reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{nested_exe}\\\" \\\"%%1\\\"\"
|
||||
netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=out action=allow program=\"{exe}\" enable=yes
|
||||
netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=in action=allow program=\"{exe}\" enable=yes
|
||||
{create_service}
|
||||
@@ -1578,48 +1597,38 @@ pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> Res
|
||||
let app_name = crate::get_app_name();
|
||||
|
||||
let current_exe = std::env::current_exe()?;
|
||||
|
||||
let tmp_path = std::env::temp_dir().to_string_lossy().to_string();
|
||||
let cur_exe = current_exe.to_str().unwrap_or("").to_owned();
|
||||
let shortcut_icon_location = get_shortcut_icon_location(&path, &cur_exe);
|
||||
let mk_shortcut = write_cmds(
|
||||
format!(
|
||||
"
|
||||
Set oWS = WScript.CreateObject(\"WScript.Shell\")
|
||||
sLinkFile = \"{tmp_path}\\{app_name}.lnk\"
|
||||
|
||||
Set oLink = oWS.CreateShortcut(sLinkFile)
|
||||
oLink.TargetPath = \"{exe}\"
|
||||
{shortcut_icon_location}
|
||||
oLink.Save
|
||||
"
|
||||
),
|
||||
"vbs",
|
||||
let cur_exe = current_exe
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Current executable path is not valid Unicode"))?
|
||||
.to_owned();
|
||||
for value in [&path, &exe, &cur_exe] {
|
||||
validate_install_value(value)?;
|
||||
}
|
||||
let config_path = Config::file();
|
||||
validate_install_value(
|
||||
config_path
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Configuration path is not valid Unicode"))?,
|
||||
)?;
|
||||
let shortcut_icon_location = get_custom_icon(&path, &cur_exe);
|
||||
if let Some(icon) = shortcut_icon_location.as_deref() {
|
||||
validate_install_value(icon)?;
|
||||
}
|
||||
// The elevated runner expands this to `%~f0.dir`, beside its protected copy.
|
||||
// Do not stage privileged shortcut artifacts in the user-writable `%TEMP%`.
|
||||
let tmp_path = "%RUSTDESK_OUTPUT_DIR%".to_owned();
|
||||
let mk_shortcut_commands = embedded_shortcut_commands(
|
||||
shortcut_bytes(&exe, None, shortcut_icon_location.as_deref())?,
|
||||
&format!("{app_name}.lnk"),
|
||||
"mk_shortcut",
|
||||
)?
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
// https://superuser.com/questions/392061/how-to-make-a-shortcut-from-cmd
|
||||
let uninstall_shortcut = write_cmds(
|
||||
format!(
|
||||
"
|
||||
Set oWS = WScript.CreateObject(\"WScript.Shell\")
|
||||
sLinkFile = \"{tmp_path}\\Uninstall {app_name}.lnk\"
|
||||
Set oLink = oWS.CreateShortcut(sLinkFile)
|
||||
oLink.TargetPath = \"{exe}\"
|
||||
oLink.Arguments = \"--uninstall\"
|
||||
oLink.IconLocation = \"msiexec.exe\"
|
||||
oLink.Save
|
||||
"
|
||||
),
|
||||
"vbs",
|
||||
);
|
||||
let uninstall_shortcut_commands = embedded_shortcut_commands(
|
||||
shortcut_bytes(&exe, Some("--uninstall"), Some("msiexec.exe"))?,
|
||||
&format!("Uninstall {app_name}.lnk"),
|
||||
"uninstall_shortcut",
|
||||
)?
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_owned();
|
||||
let tray_shortcut = get_tray_shortcut(&path, &exe, &cur_exe, &tmp_path)?;
|
||||
);
|
||||
let tray_shortcut_commands =
|
||||
embedded_tray_shortcut_commands(&app_name, &exe, shortcut_icon_location.as_deref())?;
|
||||
let mut reg_value_desktop_shortcuts = "0".to_owned();
|
||||
let mut reg_value_start_menu_shortcuts = "0".to_owned();
|
||||
let mut reg_value_printer = "0".to_owned();
|
||||
@@ -1660,15 +1669,12 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\"
|
||||
// Note: without if exist, the bat may exit in advance on some Windows7 https://github.com/rustdesk/rustdesk/issues/895
|
||||
let dels = format!(
|
||||
"
|
||||
if exist \"{mk_shortcut}\" del /f /q \"{mk_shortcut}\"
|
||||
if exist \"{uninstall_shortcut}\" del /f /q \"{uninstall_shortcut}\"
|
||||
if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\"
|
||||
if exist \"{tmp_path}\\{app_name}.lnk\" del /f /q \"{tmp_path}\\{app_name}.lnk\"
|
||||
if exist \"{tmp_path}\\Uninstall {app_name}.lnk\" del /f /q \"{tmp_path}\\Uninstall {app_name}.lnk\"
|
||||
if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} Tray.lnk\"
|
||||
"
|
||||
);
|
||||
let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_string();
|
||||
let src_exe = cur_exe.clone();
|
||||
|
||||
// potential bug here: if run_cmd cancelled, but config file is changed.
|
||||
if let Some(lic) = get_license() {
|
||||
@@ -1681,7 +1687,7 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name}
|
||||
"".to_owned()
|
||||
} else {
|
||||
format!("
|
||||
cscript \"{tray_shortcut}\"
|
||||
{tray_shortcut_commands}
|
||||
copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\"
|
||||
")
|
||||
};
|
||||
@@ -1716,11 +1722,11 @@ reg add {subkey} /f /v Publisher /t REG_SZ /d \"{app_name}\"
|
||||
reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {version_major}
|
||||
reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {version_minor}
|
||||
reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build}
|
||||
reg add {subkey} /f /v UninstallString /t REG_SZ /d \"\\\"{exe}\\\" --uninstall\"
|
||||
reg add {subkey} /f /v UninstallString /t REG_SZ /d \"\\\"{nested_exe}\\\" --uninstall\"
|
||||
reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
|
||||
reg add {subkey} /f /v WindowsInstaller /t REG_DWORD /d 0
|
||||
cscript \"{mk_shortcut}\"
|
||||
cscript \"{uninstall_shortcut}\"
|
||||
{mk_shortcut_commands}
|
||||
{uninstall_shortcut_commands}
|
||||
{tray_shortcuts}
|
||||
{shortcuts}
|
||||
copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
|
||||
@@ -1730,7 +1736,8 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
|
||||
{install_remote_printer}
|
||||
{sleep}
|
||||
",
|
||||
display_icon = get_custom_icon(&path, &cur_exe).unwrap_or(exe.to_string()),
|
||||
display_icon = shortcut_icon_location.as_deref().unwrap_or(exe.as_str()),
|
||||
nested_exe = escape_nested_cmd_ampersands(&exe),
|
||||
version = crate::VERSION.replace("-", "."),
|
||||
build_date = crate::BUILD_DATE,
|
||||
after_install = get_after_install(
|
||||
@@ -1836,10 +1843,9 @@ pub fn uninstall_me(kill_self: bool) -> ResultType<()> {
|
||||
run_cmds(get_uninstall(kill_self, true), true, "uninstall")
|
||||
}
|
||||
|
||||
fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType<std::path::PathBuf> {
|
||||
let mut cmds = cmds;
|
||||
fn write_vbs(cmds: String, tip: &str) -> ResultType<PathBuf> {
|
||||
const UTF16LE_BOM: &[u8] = &[0xFF, 0xFE];
|
||||
let mut tmp = std::env::temp_dir();
|
||||
// When dir contains these characters, the bat file will not execute in elevated mode.
|
||||
if vec!["&", "@", "^"]
|
||||
.drain(..)
|
||||
.any(|s| tmp.to_string_lossy().to_string().contains(s))
|
||||
@@ -1848,31 +1854,14 @@ fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType<std::path::PathB
|
||||
tmp = dir;
|
||||
}
|
||||
}
|
||||
tmp.push(format!("{}_{}.{}", crate::get_app_name(), tip, ext));
|
||||
let mut file = std::fs::File::create(&tmp)?;
|
||||
if ext == "bat" {
|
||||
let tmp2 = get_undone_file(&tmp)?;
|
||||
std::fs::File::create(&tmp2).ok();
|
||||
cmds = format!(
|
||||
"
|
||||
{cmds}
|
||||
if exist \"{path}\" del /f /q \"{path}\"
|
||||
",
|
||||
path = tmp2.to_string_lossy()
|
||||
);
|
||||
}
|
||||
// in case cmds mixed with \r\n and \n, make sure all ending with \r\n
|
||||
// in some windows, \r\n required for cmd file to run
|
||||
cmds = cmds.replace("\r\n", "\n").replace("\n", "\r\n");
|
||||
if ext == "vbs" {
|
||||
let mut v: Vec<u16> = cmds.encode_utf16().collect();
|
||||
// utf8 -> utf16le which vbs support it only
|
||||
file.write_all(to_le(&mut v))?;
|
||||
} else {
|
||||
file.write_all(cmds.as_bytes())?;
|
||||
}
|
||||
tmp.push(format!("{}_{}.vbs", crate::get_app_name(), tip));
|
||||
let mut file = fs::File::create(&tmp)?;
|
||||
let cmds = cmds.replace("\r\n", "\n").replace('\n', "\r\n");
|
||||
let mut utf16: Vec<u16> = cmds.encode_utf16().collect();
|
||||
file.write_all(UTF16LE_BOM)?;
|
||||
file.write_all(to_le(&mut utf16))?;
|
||||
file.sync_all()?;
|
||||
return Ok(tmp);
|
||||
Ok(tmp)
|
||||
}
|
||||
|
||||
fn to_le(v: &mut [u16]) -> &[u8] {
|
||||
@@ -1882,37 +1871,6 @@ fn to_le(v: &mut [u16]) -> &[u8] {
|
||||
unsafe { v.align_to().1 }
|
||||
}
|
||||
|
||||
fn get_undone_file(tmp: &Path) -> ResultType<PathBuf> {
|
||||
Ok(tmp.with_file_name(format!(
|
||||
"{}.undone",
|
||||
tmp.file_name()
|
||||
.ok_or(anyhow!("Failed to get filename of {:?}", tmp))?
|
||||
.to_string_lossy()
|
||||
)))
|
||||
}
|
||||
|
||||
fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> {
|
||||
let tmp = write_cmds(cmds, "bat", tip)?;
|
||||
let tmp2 = get_undone_file(&tmp)?;
|
||||
let tmp_fn = tmp.to_str().unwrap_or("");
|
||||
// https://github.com/rustdesk/rustdesk/issues/6786#issuecomment-1879655410
|
||||
// Specify cmd.exe explicitly to avoid the replacement of cmd commands.
|
||||
let res = runas::Command::new("cmd.exe")
|
||||
.args(&["/C", &tmp_fn])
|
||||
.show(show)
|
||||
.force_prompt(true)
|
||||
.status();
|
||||
if !show {
|
||||
allow_err!(std::fs::remove_file(tmp));
|
||||
}
|
||||
let _ = res?;
|
||||
if tmp2.exists() {
|
||||
allow_err!(std::fs::remove_file(tmp2));
|
||||
bail!("{} failed", tip);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn toggle_blank_screen(v: bool) {
|
||||
let v = if v { TRUE } else { FALSE };
|
||||
unsafe {
|
||||
@@ -2288,7 +2246,7 @@ pub fn create_shortcut(id: &str) -> ResultType<()> {
|
||||
// https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384
|
||||
let filename = id.replace(':', "_");
|
||||
let shortcut_icon_location = get_shortcut_icon_location("", &exe);
|
||||
let shortcut = write_cmds(
|
||||
let shortcut = write_vbs(
|
||||
format!(
|
||||
"
|
||||
Set oWS = WScript.CreateObject(\"WScript.Shell\")
|
||||
@@ -2302,7 +2260,6 @@ Set oLink = oWS.CreateShortcut(sLinkFile)
|
||||
oLink.Save
|
||||
"
|
||||
),
|
||||
"vbs",
|
||||
"connect_shortcut",
|
||||
)?
|
||||
.to_str()
|
||||
@@ -3245,29 +3202,52 @@ pub fn uninstall_service(show_new_window: bool, _: bool) -> bool {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
fn get_install_service_commands(path: &str, exe: &str) -> ResultType<String> {
|
||||
let app_name = crate::get_app_name();
|
||||
for value in [path, exe] {
|
||||
validate_install_value(value)?;
|
||||
}
|
||||
let config_path = Config::file();
|
||||
validate_install_value(
|
||||
config_path
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Configuration path is not valid Unicode"))?,
|
||||
)?;
|
||||
let shortcut_icon_location = get_custom_icon(path, exe);
|
||||
if let Some(icon) = shortcut_icon_location.as_deref() {
|
||||
validate_install_value(icon)?;
|
||||
}
|
||||
let tray_shortcut_commands =
|
||||
embedded_tray_shortcut_commands(&app_name, exe, shortcut_icon_location.as_deref())?;
|
||||
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
|
||||
Ok(format!(
|
||||
"
|
||||
chcp 65001
|
||||
taskkill /F /IM {app_name}.exe{filter}
|
||||
{tray_shortcut_commands}
|
||||
copy /Y \"%RUSTDESK_OUTPUT_DIR%\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\"
|
||||
{import_config}
|
||||
{create_service}
|
||||
",
|
||||
import_config = get_import_config(exe),
|
||||
create_service = get_create_service(exe),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn install_service() -> bool {
|
||||
log::info!("Installing service...");
|
||||
let _installing = crate::platform::InstallingService::new();
|
||||
let (_, path, _, exe) = get_install_info();
|
||||
let tmp_path = std::env::temp_dir().to_string_lossy().to_string();
|
||||
let tray_shortcut = get_tray_shortcut(&path, &exe, &exe, &tmp_path).unwrap_or_default();
|
||||
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
|
||||
Config::set_option("stop-service".into(), "".into());
|
||||
let cmds = match get_install_service_commands(&path, &exe) {
|
||||
Ok(cmds) => cmds,
|
||||
Err(err) => {
|
||||
Config::set_option("stop-service".into(), "Y".into());
|
||||
log::error!("Failed to prepare service installation: {err}");
|
||||
return true;
|
||||
}
|
||||
};
|
||||
crate::ipc::EXIT_RECV_CLOSE.store(false, Ordering::Relaxed);
|
||||
let cmds = format!(
|
||||
"
|
||||
chcp 65001
|
||||
taskkill /F /IM {app_name}.exe{filter}
|
||||
cscript \"{tray_shortcut}\"
|
||||
copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\\"
|
||||
{import_config}
|
||||
{create_service}
|
||||
if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\"
|
||||
",
|
||||
app_name = crate::get_app_name(),
|
||||
import_config = get_import_config(&exe),
|
||||
create_service = get_create_service(&exe),
|
||||
);
|
||||
if let Err(err) = run_cmds(cmds, false, "install") {
|
||||
Config::set_option("stop-service".into(), "Y".into());
|
||||
crate::ipc::EXIT_RECV_CLOSE.store(true, Ordering::Relaxed);
|
||||
@@ -3728,39 +3708,13 @@ pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_tray_shortcut(
|
||||
install_dir: &str,
|
||||
exe: &str,
|
||||
icon_source_exe: &str,
|
||||
tmp_path: &str,
|
||||
) -> ResultType<String> {
|
||||
let shortcut_icon_location = get_shortcut_icon_location(install_dir, icon_source_exe);
|
||||
Ok(write_cmds(
|
||||
format!(
|
||||
"
|
||||
Set oWS = WScript.CreateObject(\"WScript.Shell\")
|
||||
sLinkFile = \"{tmp_path}\\{app_name} Tray.lnk\"
|
||||
|
||||
Set oLink = oWS.CreateShortcut(sLinkFile)
|
||||
oLink.TargetPath = \"{exe}\"
|
||||
oLink.Arguments = \"--tray\"
|
||||
{shortcut_icon_location}
|
||||
oLink.Save
|
||||
",
|
||||
app_name = crate::get_app_name(),
|
||||
),
|
||||
"vbs",
|
||||
"tray_shortcut",
|
||||
)?
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.to_owned())
|
||||
}
|
||||
|
||||
fn get_import_config(exe: &str) -> String {
|
||||
if config::is_outgoing_only() {
|
||||
return "".to_string();
|
||||
}
|
||||
let exe = escape_nested_cmd_ampersands(exe);
|
||||
let config_path = Config::file();
|
||||
let config_path = escape_nested_cmd_ampersands(config_path.to_str().unwrap_or(""));
|
||||
format!("
|
||||
sc stop {app_name}
|
||||
sc delete {app_name}
|
||||
@@ -3770,7 +3724,6 @@ sc stop {app_name}
|
||||
sc delete {app_name}
|
||||
",
|
||||
app_name = crate::get_app_name(),
|
||||
config_path=Config::file().to_str().unwrap_or(""),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3784,6 +3737,7 @@ fn get_create_service(exe: &str) -> String {
|
||||
if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\"
|
||||
", app_name = crate::get_app_name())
|
||||
} else {
|
||||
let exe = escape_nested_cmd_ampersands(exe);
|
||||
format!("
|
||||
sc create {app_name} binpath= \"\\\"{exe}\\\" --service\" start= auto DisplayName= \"{app_name} Service\"
|
||||
sc start {app_name}
|
||||
@@ -4681,6 +4635,28 @@ mod tests {
|
||||
assert_eq!(chr, None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_app_names_enforce_ascii_command_safety() {
|
||||
assert!(validate_install_app_name("RustDesk-Admin1").is_ok());
|
||||
for app_name in ["", "RustDesk_Admin", "RustDesk&whoami", "RustDesk应用"] {
|
||||
assert!(
|
||||
validate_install_app_name(app_name).is_err(),
|
||||
"unsafe application name was accepted: {app_name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vbs_files_use_utf16le_with_bom_and_crlf() {
|
||||
const EXPECTED: &[u8] = &[0xFF, 0xFE, b'a', 0, b'\r', 0, b'\n', 0, b'b', 0];
|
||||
let tip = format!("vbs_encoding_{}", uuid::Uuid::new_v4().simple());
|
||||
let path = write_vbs("a\nb".to_owned(), &tip).expect("VBS file should be written");
|
||||
let bytes = std::fs::read(&path).expect("VBS file should be readable");
|
||||
std::fs::remove_file(path).expect("VBS file should be removed");
|
||||
|
||||
assert_eq!(bytes, EXPECTED);
|
||||
}
|
||||
|
||||
#[cfg(not(target_pointer_width = "64"))]
|
||||
#[test]
|
||||
fn test_get_pids_with_args_from_wmic_output() {
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
use super::{
|
||||
installer_shell::{
|
||||
get_system_executable, path_for_cmd_assignment, path_for_cmd_environment,
|
||||
run_elevated_and_wait, trusted_install_environment,
|
||||
BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE, CMD_RELATIVE_PATH,
|
||||
},
|
||||
validate_install_app_name, ResultType,
|
||||
};
|
||||
use hbb_common::{
|
||||
bail, log,
|
||||
sha2::{Digest, Sha256},
|
||||
};
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
const CERTUTIL_RELATIVE_PATH: &str = "certutil.exe";
|
||||
const CHCP_RELATIVE_PATH: &str = "chcp.com";
|
||||
const FINDSTR_RELATIVE_PATH: &str = "findstr.exe";
|
||||
const UTF8_CODE_PAGE: u32 = 65001;
|
||||
const INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE: u32 = 0x5253_0001;
|
||||
const INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE: u32 = 0x5253_0002;
|
||||
const INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE: u32 = 0x5253_0003;
|
||||
const INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE: u32 = 0x5253_0004;
|
||||
const BATCH_CODE_PAGE_FAILURE_EXIT_CODE: u32 = 0x5253_0005;
|
||||
const BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE: u32 = 0x5253_0006;
|
||||
const BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE: u32 = 0x5253_0007;
|
||||
const SHA256_HASH_LENGTH: usize = 32;
|
||||
|
||||
type BatchHash = [u8; SHA256_HASH_LENGTH];
|
||||
|
||||
struct InstallCommandScript {
|
||||
path: PathBuf,
|
||||
expected_hash: BatchHash,
|
||||
}
|
||||
|
||||
impl Drop for InstallCommandScript {
|
||||
fn drop(&mut self) {
|
||||
if let Err(err) = fs::remove_file(&self.path) {
|
||||
if err.kind() != io::ErrorKind::NotFound {
|
||||
log::warn!(
|
||||
"Failed to remove temporary installer file {:?}: {err}",
|
||||
self.path
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_install_commands(commands: &str) -> ResultType<String> {
|
||||
let commands = commands.replace("\r\n", "\n").replace('\n', "\r\n");
|
||||
let chcp_path = get_system_executable(CHCP_RELATIVE_PATH)?;
|
||||
let chcp = path_for_cmd_environment(&chcp_path)?;
|
||||
Ok(format!(
|
||||
"@echo off\r\nsetlocal EnableExtensions DisableDelayedExpansion\r\n\
|
||||
\"{chcp}\" {UTF8_CODE_PAGE} > nul || exit /b \
|
||||
{BATCH_CODE_PAGE_FAILURE_EXIT_CODE}\r\n\
|
||||
{}\r\n\
|
||||
if exist \"%~f0.dir\" exit /b {BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE}\r\n\
|
||||
md \"%~f0.dir\" || exit /b {BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE}\r\n\
|
||||
set \"RUSTDESK_OUTPUT_DIR=%~f0.dir\"\r\n{commands}\r\nexit /b 0\r\n",
|
||||
trusted_install_environment()?
|
||||
))
|
||||
}
|
||||
|
||||
fn write_install_script(cmds: String) -> ResultType<InstallCommandScript> {
|
||||
let directory = std::env::temp_dir();
|
||||
path_for_cmd_environment(&directory)?;
|
||||
let commands = prepare_install_commands(&cmds)?;
|
||||
let expected_hash = Sha256::digest(commands.as_bytes()).into();
|
||||
let path = directory.join(format!(
|
||||
"rustdesk_install_{}.bat",
|
||||
uuid::Uuid::new_v4().simple()
|
||||
));
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&path)?;
|
||||
let script = InstallCommandScript {
|
||||
path,
|
||||
expected_hash,
|
||||
};
|
||||
file.write_all(commands.as_bytes())?;
|
||||
file.sync_all()?;
|
||||
Ok(script)
|
||||
}
|
||||
|
||||
fn install_hash_pattern(hash: &BatchHash) -> String {
|
||||
hash.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" *")
|
||||
}
|
||||
|
||||
fn verified_install_bootstrap(
|
||||
script: &InstallCommandScript,
|
||||
runner_directory: &Path,
|
||||
) -> ResultType<String> {
|
||||
let source = path_for_cmd_assignment(&script.path)?;
|
||||
let runner = runner_directory.join(format!(
|
||||
"rustdesk_install_{}.bat",
|
||||
uuid::Uuid::new_v4().simple()
|
||||
));
|
||||
let runner = path_for_cmd_assignment(&runner)?;
|
||||
let cmd_path = get_system_executable(CMD_RELATIVE_PATH)?;
|
||||
let certutil_path = get_system_executable(CERTUTIL_RELATIVE_PATH)?;
|
||||
let findstr_path = get_system_executable(FINDSTR_RELATIVE_PATH)?;
|
||||
let cmd = path_for_cmd_assignment(&cmd_path)?;
|
||||
let certutil = path_for_cmd_assignment(&certutil_path)?;
|
||||
let findstr = path_for_cmd_assignment(&findstr_path)?;
|
||||
// Short names preserve headroom under the Windows 7 ShellExecuteExW 2,048
|
||||
// UTF-16-character parameter limit. Paths are stored with delayed expansion
|
||||
// disabled, then expanded indirectly so literal `!` survives: S=source,
|
||||
// R=runner, Q=cmd.exe, H=certutil.exe, F=findstr.exe, C=created flag, E=exit code.
|
||||
Ok(format!(
|
||||
"setlocal DisableDelayedExpansion & set \"S={source}\" & set \"R={runner}\" & \
|
||||
set \"Q={cmd}\" & set \"H={certutil}\" & set \"F={findstr}\" & \
|
||||
set \"C=0\" & set \"E=0\" & setlocal EnableDelayedExpansion & \
|
||||
if exist \"!R!\" (set \"E={INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE}\") else (\
|
||||
set \"C=1\" & copy /Y \"!S!\" \"!R!\" > nul || \
|
||||
(set \"E={INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE}\") & \
|
||||
if \"!E!\"==\"0\" (\"!H!\" -hashfile \"!R!\" SHA256 > \"!R!.hash\" || \
|
||||
set \"E={INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE}\") & \
|
||||
if \"!E!\"==\"0\" (\"!F!\" /R /I /X /C:\"{}\" \"!R!.hash\" > nul || \
|
||||
set \"E={INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE}\") & \
|
||||
if \"!E!\"==\"0\" (\"!Q!\" /D /E:ON /V:OFF /C \"\"!R!\"\" & \
|
||||
set \"E=!errorlevel!\")) & \
|
||||
if \"!C!\"==\"1\" (rd /s /q \"!R!.dir\" > nul 2>&1 & \
|
||||
del /f /q \"!R!\" \"!R!.*\" > nul 2>&1) & exit /b !E!",
|
||||
install_hash_pattern(&script.expected_hash),
|
||||
))
|
||||
}
|
||||
|
||||
fn verified_install_parameters(script: &InstallCommandScript) -> ResultType<String> {
|
||||
let system_directory = get_system_executable("")?;
|
||||
Ok(format!(
|
||||
"/D /E:ON /V:ON /C {}",
|
||||
verified_install_bootstrap(script, &system_directory)?
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> {
|
||||
validate_install_app_name(&crate::get_app_name())?;
|
||||
let script = write_install_script(cmds)?;
|
||||
let cmd_path = get_system_executable(CMD_RELATIVE_PATH)?;
|
||||
let parameters = verified_install_parameters(&script)?;
|
||||
let exit_code = run_elevated_and_wait(&cmd_path, ¶meters, show)?;
|
||||
if exit_code != 0 {
|
||||
bail!(
|
||||
"{tip} failed with elevated exit code {exit_code}: {}",
|
||||
elevated_install_failure_reason(exit_code)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn elevated_install_failure_reason(exit_code: u32) -> &'static str {
|
||||
match exit_code {
|
||||
INSTALL_HANDOFF_RUNNER_EXISTS_EXIT_CODE => "protected runner already exists",
|
||||
INSTALL_HANDOFF_COPY_FAILURE_EXIT_CODE => "failed to copy protected runner",
|
||||
INSTALL_HANDOFF_HASH_FAILURE_EXIT_CODE => "failed to hash protected runner",
|
||||
INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE => "protected runner hash mismatch",
|
||||
BATCH_CODE_PAGE_FAILURE_EXIT_CODE => "failed to set the installer code page",
|
||||
BATCH_OUTPUT_DIRECTORY_EXISTS_EXIT_CODE => "installer output directory already exists",
|
||||
BATCH_OUTPUT_DIRECTORY_CREATE_FAILURE_EXIT_CODE => {
|
||||
"failed to create the installer output directory"
|
||||
}
|
||||
BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE => "failed to decode an embedded shortcut",
|
||||
_ => "installer command failed",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::installer_shell::{
|
||||
embedded_shortcut_commands, shortcut_bytes, WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS,
|
||||
};
|
||||
use super::*;
|
||||
use ::windows::Win32::System::Threading;
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
#[test]
|
||||
fn native_install_handoff_verifies_before_execution() {
|
||||
let marker = std::env::temp_dir().join(format!(
|
||||
"rustdesk_install_marker_{}",
|
||||
uuid::Uuid::new_v4().simple()
|
||||
));
|
||||
let runner_dir = std::env::temp_dir().join(format!(
|
||||
"rustdesk_install_!RUSTDESK_HANDOFF_EXPAND!&^@()runner_{}",
|
||||
uuid::Uuid::new_v4().simple()
|
||||
));
|
||||
std::fs::create_dir(&runner_dir).expect("runner directory should be created");
|
||||
let shortcut_commands = embedded_shortcut_commands(
|
||||
shortcut_bytes(r"C:\RustDesk.exe", None, None)
|
||||
.expect("native shortcut should be generated"),
|
||||
"test.lnk",
|
||||
"test",
|
||||
);
|
||||
let script = write_install_script(format!(
|
||||
"if \"%PROGRAMDATA%\"==\"rustdesk_untrusted\" exit /b 77\r\n\
|
||||
if \"%PUBLIC%\"==\"rustdesk_untrusted\" exit /b 77\r\n\
|
||||
{shortcut_commands}\r\n\
|
||||
> \"{}\" echo verified",
|
||||
marker.display()
|
||||
))
|
||||
.expect("install script should be created");
|
||||
let bootstrap = verified_install_bootstrap(&script, &runner_dir)
|
||||
.expect("native verifier bootstrap should be generated");
|
||||
assert_native_handoff_structure(&script, &shortcut_commands, &bootstrap);
|
||||
|
||||
let output = run_install_bootstrap_for_test(&bootstrap);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"unchanged script failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(marker.exists(), "verified install script must execute");
|
||||
std::fs::remove_file(&marker).expect("test marker should be removed");
|
||||
assert_replaced_install_script_is_rejected(&script, &runner_dir, &marker);
|
||||
std::fs::remove_dir(runner_dir).expect("runner directory should be empty");
|
||||
}
|
||||
|
||||
fn assert_native_handoff_structure(
|
||||
script: &InstallCommandScript,
|
||||
shortcut_commands: &str,
|
||||
bootstrap: &str,
|
||||
) {
|
||||
assert!(shortcut_commands.contains("certutil"));
|
||||
assert!(shortcut_commands.contains("-decode"));
|
||||
assert!(!shortcut_commands.to_ascii_lowercase().contains("cscript"));
|
||||
assert!(!shortcut_commands
|
||||
.to_ascii_lowercase()
|
||||
.contains("powershell"));
|
||||
let win7_hash_pattern = script
|
||||
.expected_hash
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" *");
|
||||
assert!(bootstrap.contains(&format!("/R /I /X /C:\"{win7_hash_pattern}\"")));
|
||||
let parameters =
|
||||
verified_install_parameters(script).expect("elevated parameters should be generated");
|
||||
assert!(bootstrap.contains("certutil.exe"));
|
||||
assert!(bootstrap.contains("findstr.exe"));
|
||||
assert!(!bootstrap.to_ascii_lowercase().contains("powershell"));
|
||||
assert!(parameters.encode_utf16().count() < WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS);
|
||||
}
|
||||
|
||||
fn assert_replaced_install_script_is_rejected(
|
||||
script: &InstallCommandScript,
|
||||
runner_dir: &Path,
|
||||
marker: &Path,
|
||||
) {
|
||||
std::fs::write(
|
||||
&script.path,
|
||||
format!("> \"{}\" echo hijacked\r\n", marker.display()),
|
||||
)
|
||||
.expect("install script should be replaceable");
|
||||
let replaced = verified_install_bootstrap(&script, &runner_dir)
|
||||
.expect("replacement verifier should be generated");
|
||||
let output = run_install_bootstrap_for_test(&replaced);
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"replaced script unexpectedly passed verification"
|
||||
);
|
||||
assert_eq!(
|
||||
output.status.code(),
|
||||
Some(INSTALL_HANDOFF_HASH_MISMATCH_EXIT_CODE as i32)
|
||||
);
|
||||
assert!(!marker.exists(), "replaced script must not execute");
|
||||
}
|
||||
|
||||
fn run_install_bootstrap_for_test(bootstrap: &str) -> std::process::Output {
|
||||
let cmd = get_system_executable(CMD_RELATIVE_PATH).expect("system cmd.exe should resolve");
|
||||
let mut command = std::process::Command::new(cmd);
|
||||
command
|
||||
.env("PROGRAMDATA", "rustdesk_untrusted")
|
||||
.env("PUBLIC", "rustdesk_untrusted")
|
||||
.env("RUSTDESK_HANDOFF_EXPAND", "expanded");
|
||||
command.raw_arg(format!("/D /E:ON /V:ON /C {bootstrap}"));
|
||||
command
|
||||
.creation_flags(Threading::CREATE_NO_WINDOW.0)
|
||||
.output()
|
||||
.expect("native verifier should run")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
use super::{wide_string, ResultType};
|
||||
use hbb_common::{
|
||||
anyhow::anyhow,
|
||||
bail,
|
||||
base64::{engine::general_purpose::STANDARD, Engine as _},
|
||||
log,
|
||||
};
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
io, mem,
|
||||
os::windows::ffi::OsStringExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use windows::{
|
||||
core::{Interface, PCWSTR},
|
||||
Win32::{
|
||||
Foundation::{self, CloseHandle, HANDLE},
|
||||
System::{Com, SystemInformation, Threading},
|
||||
UI::{
|
||||
Shell::{
|
||||
self, FOLDERID_ProgramData, FOLDERID_Public, SHGetKnownFolderPath, KF_FLAG_DEFAULT,
|
||||
},
|
||||
WindowsAndMessaging,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
pub(super) const CMD_RELATIVE_PATH: &str = "cmd.exe";
|
||||
pub(super) const BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE: u32 = 0x5253_0008;
|
||||
pub(super) const WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS: usize = 2048;
|
||||
const SHORTCUT_ICON_INDEX: i32 = 0;
|
||||
|
||||
pub(super) fn shortcut_bytes(
|
||||
target_path: &str,
|
||||
arguments: Option<&str>,
|
||||
icon_location: Option<&str>,
|
||||
) -> ResultType<Vec<u8>> {
|
||||
let _com = initialize_shell_com()?;
|
||||
let link: Shell::IShellLinkW =
|
||||
unsafe { Com::CoCreateInstance(&Shell::ShellLink, None, Com::CLSCTX_INPROC_SERVER) }?;
|
||||
let target_path = wide_string(target_path);
|
||||
unsafe { link.SetPath(PCWSTR(target_path.as_ptr())) }?;
|
||||
if let Some(arguments) = arguments {
|
||||
let arguments = wide_string(arguments);
|
||||
unsafe { link.SetArguments(PCWSTR(arguments.as_ptr())) }?;
|
||||
}
|
||||
if let Some(icon_location) = icon_location {
|
||||
let icon_location = wide_string(icon_location);
|
||||
unsafe { link.SetIconLocation(PCWSTR(icon_location.as_ptr()), SHORTCUT_ICON_INDEX) }?;
|
||||
}
|
||||
|
||||
let stream = unsafe { Shell::SHCreateMemStream(None) }
|
||||
.ok_or_else(|| anyhow!("Failed to create shortcut memory stream"))?;
|
||||
let persist: Com::IPersistStream = link.cast()?;
|
||||
unsafe { persist.Save(&stream, true) }?;
|
||||
let mut stat = Com::STATSTG::default();
|
||||
unsafe { stream.Stat(&mut stat, Com::STATFLAG_NONAME) }?;
|
||||
let size = usize::try_from(stat.cbSize).map_err(|_| anyhow!("Shortcut data is too large"))?;
|
||||
let read_size = u32::try_from(size).map_err(|_| anyhow!("Shortcut data is too large"))?;
|
||||
let mut bytes = vec![0; size];
|
||||
let mut bytes_read = 0;
|
||||
unsafe {
|
||||
stream.Seek(0, Com::STREAM_SEEK_SET, None)?;
|
||||
stream
|
||||
.Read(bytes.as_mut_ptr().cast(), read_size, Some(&mut bytes_read))
|
||||
.ok()?;
|
||||
}
|
||||
if bytes_read != read_size {
|
||||
bail!("Failed to read complete shortcut data");
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub(super) fn embedded_shortcut_commands(bytes: Vec<u8>, filename: &str, name: &str) -> String {
|
||||
let encoded = STANDARD.encode(bytes);
|
||||
let encoded_path = format!("%~f0.{name}.b64");
|
||||
format!(
|
||||
"> \"{encoded_path}\" echo {encoded}\r\n\
|
||||
certutil -f -decode \"{encoded_path}\" \"%RUSTDESK_OUTPUT_DIR%\\{filename}\" > nul || exit /b {BATCH_SHORTCUT_DECODE_FAILURE_EXIT_CODE}"
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn embedded_tray_shortcut_commands(
|
||||
app_name: &str,
|
||||
exe: &str,
|
||||
icon_location: Option<&str>,
|
||||
) -> ResultType<String> {
|
||||
let filename = format!("{app_name} Tray.lnk");
|
||||
Ok(embedded_shortcut_commands(
|
||||
shortcut_bytes(exe, Some("--tray"), icon_location)?,
|
||||
&filename,
|
||||
"tray_shortcut",
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn validate_install_value(value: &str) -> ResultType<()> {
|
||||
if value.contains(['\0', '"', '%', '\r', '\n', '|', '<', '>']) {
|
||||
bail!("Installer path or name contains characters unsafe for cmd.exe");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn get_system_executable(relative_path: &str) -> ResultType<PathBuf> {
|
||||
let mut buffer = vec![0u16; Foundation::MAX_PATH as usize];
|
||||
let len = unsafe { SystemInformation::GetSystemDirectoryW(Some(&mut buffer)) } as usize;
|
||||
if len == 0 {
|
||||
return Err(io::Error::last_os_error().into());
|
||||
}
|
||||
if len >= buffer.len() {
|
||||
bail!("Windows system directory path is too long");
|
||||
}
|
||||
buffer.truncate(len);
|
||||
let mut path = PathBuf::from(OsString::from_wide(&buffer));
|
||||
path.push(relative_path);
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn get_known_folder(id: &windows::core::GUID) -> ResultType<PathBuf> {
|
||||
let value = unsafe { SHGetKnownFolderPath(id, KF_FLAG_DEFAULT, None) }?;
|
||||
let path = unsafe { value.to_string() };
|
||||
unsafe { Com::CoTaskMemFree(Some(value.0.cast())) };
|
||||
Ok(PathBuf::from(path?))
|
||||
}
|
||||
|
||||
// `%VAR%` is expanded before cmd.exe executes even inside a quoted `set` assignment.
|
||||
// Reject all `%` so a handoff path cannot alter the elevated bootstrap before hash verification.
|
||||
// https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/set_1
|
||||
pub(super) fn path_for_cmd_environment(path: &Path) -> ResultType<&str> {
|
||||
let value = path
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Path is not valid Unicode: {:?}", path))?;
|
||||
if value.contains(['\0', '"', '%', '\r', '\n']) {
|
||||
bail!("Path is unsafe for an elevated cmd.exe handoff: {:?}", path);
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
// Bootstrap assignments are parsed before DisableDelayedExpansion takes effect.
|
||||
// Escape carets first so `^!` preserves each literal exclamation mark.
|
||||
pub(super) fn path_for_cmd_assignment(path: &Path) -> ResultType<String> {
|
||||
Ok(path_for_cmd_environment(path)?
|
||||
.replace('^', "^^")
|
||||
.replace('!', "^!"))
|
||||
}
|
||||
|
||||
pub(super) fn trusted_install_environment() -> ResultType<String> {
|
||||
let system = get_system_executable("")?;
|
||||
let program_data = get_known_folder(&FOLDERID_ProgramData)?;
|
||||
let public = get_known_folder(&FOLDERID_Public)?;
|
||||
trusted_install_environment_from_paths(&system, &program_data, &public)
|
||||
}
|
||||
|
||||
fn trusted_install_environment_from_paths(
|
||||
system: &Path,
|
||||
program_data: &Path,
|
||||
public: &Path,
|
||||
) -> ResultType<String> {
|
||||
let windows = system
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("System directory has no parent"))?;
|
||||
let cmd = system.join(CMD_RELATIVE_PATH);
|
||||
// These paths are parsed once from the protected BAT, with delayed expansion disabled.
|
||||
let system = path_for_cmd_environment(system)?;
|
||||
let windows = path_for_cmd_environment(windows)?;
|
||||
let cmd = path_for_cmd_environment(&cmd)?;
|
||||
let program_data = path_for_cmd_environment(program_data)?;
|
||||
let public = path_for_cmd_environment(public)?;
|
||||
Ok(format!(
|
||||
"set \"ComSpec={cmd}\" & set \"PATH={system}\" & \
|
||||
set \"SystemRoot={windows}\" & set \"WINDIR={windows}\" & \
|
||||
set \"ProgramData={program_data}\" & set \"PUBLIC={public}\" & \
|
||||
set \"PATHEXT=.COM;.EXE;.BAT;.CMD\" & \
|
||||
set \"NoDefaultCurrentDirectoryInExePath=1\""
|
||||
))
|
||||
}
|
||||
|
||||
struct ShellComGuard;
|
||||
|
||||
impl Drop for ShellComGuard {
|
||||
fn drop(&mut self) {
|
||||
unsafe { Com::CoUninitialize() };
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_shell_com() -> ResultType<Option<ShellComGuard>> {
|
||||
let result = unsafe {
|
||||
Com::CoInitializeEx(
|
||||
None,
|
||||
Com::COINIT_APARTMENTTHREADED | Com::COINIT_DISABLE_OLE1DDE,
|
||||
)
|
||||
};
|
||||
if result == Foundation::RPC_E_CHANGED_MODE {
|
||||
return Ok(None);
|
||||
}
|
||||
if result.is_err() {
|
||||
bail!(
|
||||
"Failed to initialize COM: HRESULT 0x{:08X}",
|
||||
result.0 as u32
|
||||
);
|
||||
}
|
||||
Ok(Some(ShellComGuard))
|
||||
}
|
||||
|
||||
struct ProcessHandle(HANDLE);
|
||||
|
||||
impl Drop for ProcessHandle {
|
||||
fn drop(&mut self) {
|
||||
if let Err(err) = unsafe { CloseHandle(self.0) } {
|
||||
log::warn!("Failed to close elevated process handle: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn elevated_working_directory(executable: &Path) -> ResultType<&Path> {
|
||||
executable
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("Elevated executable has no parent directory"))
|
||||
}
|
||||
|
||||
pub(super) fn run_elevated_and_wait(
|
||||
executable: &Path,
|
||||
parameters: &str,
|
||||
show: bool,
|
||||
) -> ResultType<u32> {
|
||||
let parameter_chars = parameters.encode_utf16().count();
|
||||
if parameter_chars >= WIN7_SHELL_EXECUTE_MAX_PARAMETER_CHARS {
|
||||
bail!("Elevated command is too long: {parameter_chars} UTF-16 characters");
|
||||
}
|
||||
let _com = initialize_shell_com()?;
|
||||
let verb = wide_string("runas");
|
||||
let working_directory = wide_string(path_for_cmd_environment(elevated_working_directory(
|
||||
executable,
|
||||
)?)?);
|
||||
let executable = wide_string(path_for_cmd_environment(executable)?);
|
||||
let parameters = wide_string(parameters);
|
||||
let mut info = Shell::SHELLEXECUTEINFOW::default();
|
||||
info.cbSize = mem::size_of::<Shell::SHELLEXECUTEINFOW>() as u32;
|
||||
info.fMask = Shell::SEE_MASK_NOCLOSEPROCESS | Shell::SEE_MASK_NOASYNC;
|
||||
info.lpVerb = PCWSTR(verb.as_ptr());
|
||||
info.lpFile = PCWSTR(executable.as_ptr());
|
||||
info.lpParameters = PCWSTR(parameters.as_ptr());
|
||||
info.lpDirectory = PCWSTR(working_directory.as_ptr());
|
||||
info.nShow = if show {
|
||||
WindowsAndMessaging::SW_SHOWNORMAL.0
|
||||
} else {
|
||||
WindowsAndMessaging::SW_HIDE.0
|
||||
};
|
||||
unsafe { Shell::ShellExecuteExW(&mut info) }?;
|
||||
if info.hProcess.0.is_null() {
|
||||
bail!("Windows did not return an elevated process handle");
|
||||
}
|
||||
let process = ProcessHandle(info.hProcess);
|
||||
let wait_result = unsafe { Threading::WaitForSingleObject(process.0, Threading::INFINITE) };
|
||||
if wait_result == Foundation::WAIT_FAILED {
|
||||
return Err(io::Error::last_os_error().into());
|
||||
}
|
||||
if wait_result != Foundation::WAIT_OBJECT_0 {
|
||||
bail!("Unexpected elevated process wait result: {}", wait_result.0);
|
||||
}
|
||||
let mut exit_code = 0;
|
||||
unsafe { Threading::GetExitCodeProcess(process.0, &mut exit_code) }?;
|
||||
Ok(exit_code)
|
||||
}
|
||||
|
||||
// Escape `^` before using it to escape `&` so both survive nested cmd.exe parsing.
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/cc754250(v=ws.11)
|
||||
pub(super) fn escape_nested_cmd_ampersands(value: &str) -> String {
|
||||
value.replace('^', "^^").replace('&', "^&")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn install_values_enforce_command_safety() {
|
||||
assert!(validate_install_value(r"C:\safe ! path").is_ok());
|
||||
assert!(validate_install_value(r"C:\Program Files (x86)\RustDesk").is_ok());
|
||||
assert!(validate_install_value(r"C:\Users\R&D\RustDesk.exe").is_ok());
|
||||
assert!(validate_install_value(r"C:\A&^ B\RustDesk.exe").is_ok());
|
||||
for character in ['\0', '"', '%', '\r', '\n', '|', '<', '>'] {
|
||||
let value = format!(r"C:\unsafe{character}path");
|
||||
assert!(
|
||||
validate_install_value(&value).is_err(),
|
||||
"cmd.exe control character was accepted: {character:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_commands_escape_while_protected_environment_preserves_carets() {
|
||||
assert_eq!(
|
||||
escape_nested_cmd_ampersands(r"C:\A&^ B\RustDesk.exe"),
|
||||
r"C:\A^&^^ B\RustDesk.exe"
|
||||
);
|
||||
let path = Path::new(r"C:\Win^Root\System32");
|
||||
let environment = trusted_install_environment_from_paths(path, path, path).unwrap();
|
||||
assert!(environment.contains(r#"set "PATH=C:\Win^Root\System32""#));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user