mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-05 00:36:14 +00:00
* 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>
289 lines
12 KiB
Rust
289 lines
12 KiB
Rust
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")
|
|
}
|
|
}
|