Hardening RustDesk provisioning flow

This commit is contained in:
Esdras Renan 2025-11-12 14:10:56 -03:00
parent f3d622eedd
commit ddcff6768d
2 changed files with 184 additions and 46 deletions

View file

@ -6,6 +6,7 @@ use once_cell::sync::Lazy;
use parking_lot::Mutex; use parking_lot::Mutex;
use reqwest::blocking::Client; use reqwest::blocking::Client;
use serde::Deserialize; use serde::Deserialize;
use serde_json::{Map as JsonMap, Value as JsonValue};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
@ -27,6 +28,10 @@ const SERVICE_NAME: &str = "RustDesk";
const CACHE_DIR_NAME: &str = "Rever\\RustDeskCache"; const CACHE_DIR_NAME: &str = "Rever\\RustDeskCache";
const LOCAL_SERVICE_CONFIG: &str = r"C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\RustDesk\\config"; const LOCAL_SERVICE_CONFIG: &str = r"C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Roaming\\RustDesk\\config";
const LOCAL_SYSTEM_CONFIG: &str = r"C:\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\RustDesk\\config"; const LOCAL_SYSTEM_CONFIG: &str = r"C:\\Windows\\System32\\config\\systemprofile\\AppData\\Roaming\\RustDesk\\config";
const APP_IDENTIFIER: &str = "br.com.esdrasrenan.sistemadechamados";
const MACHINE_STORE_FILENAME: &str = "machine-agent.json";
const ACL_FLAG_FILENAME: &str = "rustdesk_acl_unlocked.flag";
const RUSTDESK_ACL_STORE_KEY: &str = "rustdeskAclUnlockedAt";
const SECURITY_VERIFICATION_VALUE: &str = "use-permanent-password"; const SECURITY_VERIFICATION_VALUE: &str = "use-permanent-password";
const SECURITY_APPROVE_MODE_VALUE: &str = "password"; const SECURITY_APPROVE_MODE_VALUE: &str = "password";
const CREATE_NO_WINDOW: u32 = 0x08000000; const CREATE_NO_WINDOW: u32 = 0x08000000;
@ -81,6 +86,13 @@ pub fn ensure_rustdesk(
"RustDesk já instalado, usando binário existente" "RustDesk já instalado, usando binário existente"
}); });
match stop_rustdesk_processes() {
Ok(_) => log_event("Instâncias existentes do RustDesk encerradas"),
Err(error) => log_event(&format!(
"Aviso: não foi possível parar completamente o RustDesk antes da reprovisionamento ({error})"
)),
}
if let Some(value) = config_string.and_then(|raw| { if let Some(value) = config_string.and_then(|raw| {
let trimmed = raw.trim(); let trimmed = raw.trim();
if trimmed.is_empty() { None } else { Some(trimmed) } if trimmed.is_empty() { None } else { Some(trimmed) }
@ -120,19 +132,15 @@ pub fn ensure_rustdesk(
log_event(&format!("Falha ao ajustar approve-mode via CLI: {error}")); log_event(&format!("Falha ao ajustar approve-mode via CLI: {error}"));
} }
match propagate_password_profile() { match propagate_password_profile() {
Ok(true) => log_event("Perfil de senha propagado para ProgramData/LocalService"), Ok(_) => log_event("Perfil base propagado para ProgramData e perfis de serviço"),
Ok(false) => match ensure_password_files(&password) { Err(error) => log_event(&format!("Falha ao copiar perfil de senha: {error}")),
Ok(_) => log_event("Senha persistida via fallback nos perfis do RustDesk"),
Err(error) => log_event(&format!("Falha ao persistir senha nos perfis: {error}")),
},
Err(error) => {
log_event(&format!("Falha ao copiar perfil de senha: {error}"));
match ensure_password_files(&password) {
Ok(_) => log_event("Senha persistida via fallback nos perfis do RustDesk"),
Err(inner) => log_event(&format!("Falha ao persistir senha nos perfis: {inner}")),
}
}
} }
match ensure_password_files(&password) {
Ok(_) => log_event("Senha e flags de segurança gravadas em todos os perfis do RustDesk"),
Err(error) => log_event(&format!("Falha ao persistir senha nos perfis: {error}")),
}
match replicate_password_artifacts() { match replicate_password_artifacts() {
Ok(_) => log_event("Artefatos de senha replicados para o serviço do RustDesk"), Ok(_) => log_event("Artefatos de senha replicados para o serviço do RustDesk"),
Err(error) => log_event(&format!("Falha ao replicar artefatos de senha: {error}")), Err(error) => log_event(&format!("Falha ao replicar artefatos de senha: {error}")),
@ -272,12 +280,15 @@ fn write_config_files() -> Result<PathBuf, RustdeskError> {
main_path.display() main_path.display()
)); ));
let service_profile = PathBuf::from(LOCAL_SERVICE_CONFIG).join("RustDesk2.toml");
let _ = ensure_service_profiles_writable_preflight(); let _ = ensure_service_profiles_writable_preflight();
if let Err(error) = write_file(&service_profile, &config_contents) { for service_dir in service_profile_dirs() {
log_event(&format!( let service_profile = service_dir.join("RustDesk2.toml");
"Falha ao gravar config no perfil do serviço: {error}" if let Err(error) = write_file(&service_profile, &config_contents) {
)); log_event(&format!(
"Falha ao gravar config no perfil do serviço ({}): {error}",
service_profile.display()
));
}
} }
if let Some(appdata_path) = user_appdata_config_path("RustDesk2.toml") { if let Some(appdata_path) = user_appdata_config_path("RustDesk2.toml") {
@ -402,6 +413,30 @@ fn ensure_service_running(exe_path: &Path) -> Result<(), RustdeskError> {
} }
} }
fn stop_rustdesk_processes() -> Result<(), RustdeskError> {
if let Err(error) = run_sc(&["stop", SERVICE_NAME]) {
match error {
RustdeskError::CommandFailed { status: Some(code), .. } if code == 1062 || code == 1060 => {}
_ => log_event(&format!("Não foi possível parar o serviço RustDesk antes da sincronização: {error}")),
}
}
let status = hidden_command("taskkill")
.args(["/F", "/T", "/IM", "rustdesk.exe"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
if status.success() || matches!(status.code(), Some(128)) {
Ok(())
} else {
Err(RustdeskError::CommandFailed {
command: "taskkill /F /T /IM rustdesk.exe".into(),
status: status.code(),
})
}
}
fn run_sc(args: &[&str]) -> Result<(), RustdeskError> { fn run_sc(args: &[&str]) -> Result<(), RustdeskError> {
let status = hidden_command("sc") let status = hidden_command("sc")
.args(args) .args(args)
@ -512,6 +547,13 @@ fn service_profile_dirs() -> Vec<PathBuf> {
] ]
} }
fn propagation_destinations() -> Vec<PathBuf> {
let mut dirs = Vec::new();
dirs.push(program_data_config_dir());
dirs.extend(service_profile_dirs());
dirs
}
fn ensure_password_files(secret: &str) -> Result<(), String> { fn ensure_password_files(secret: &str) -> Result<(), String> {
let mut errors = Vec::new(); let mut errors = Vec::new();
@ -598,29 +640,36 @@ fn propagate_password_profile() -> io::Result<bool> {
log_event("AppData do usuário não disponível para copiar RustDesk.toml (propagação ignorada)"); log_event("AppData do usuário não disponível para copiar RustDesk.toml (propagação ignorada)");
return Ok(false); return Ok(false);
}; };
let src_path = src_dir.join("RustDesk.toml");
if !src_path.exists() { let files = ["RustDesk.toml", "RustDesk_local.toml"];
let mut propagated = false;
for filename in files {
let src_path = src_dir.join(filename);
if !src_path.exists() {
continue;
}
log_event(&format!( log_event(&format!(
"Arquivo {} não encontrado; usando fallback de escrita", "Copiando {} para ProgramData/serviços",
src_path.display() src_path.display()
)); ));
return Ok(false);
}
log_event(&format!(
"Copiando {} para ProgramData/LocalService",
src_path.display()
));
let mut propagated = false; for dest_root in propagation_destinations() {
for dest_root in [program_data_config_dir(), PathBuf::from(LOCAL_SERVICE_CONFIG)] { let target_path = dest_root.join(filename);
let target_path = dest_root.join("RustDesk.toml"); copy_overwrite(&src_path, &target_path)?;
copy_overwrite(&src_path, &target_path)?; log_event(&format!(
log_event(&format!( "{} propagado para {}",
"RustDesk.toml propagado para {}", filename,
target_path.display() target_path.display()
)); ));
propagated = true; propagated = true;
}
} }
if !propagated {
log_event("Nenhum arquivo de perfil encontrado para propagação; aplicando fallback");
}
Ok(propagated) Ok(propagated)
} }
@ -628,7 +677,7 @@ fn replicate_password_artifacts() -> io::Result<()> {
let Some(src) = user_appdata_config_dir() else { let Some(src) = user_appdata_config_dir() else {
return Ok(()); return Ok(());
}; };
let destinations = [program_data_config_dir(), PathBuf::from(LOCAL_SERVICE_CONFIG)]; let destinations = propagation_destinations();
let candidates = ["password", "passwd", "passwd.txt"]; let candidates = ["password", "passwd", "passwd.txt"];
for dest in destinations { for dest in destinations {
@ -750,28 +799,38 @@ try {{
} }
fn ensure_service_profiles_writable_preflight() -> Result<(), String> { fn ensure_service_profiles_writable_preflight() -> Result<(), String> {
let mut success = false; let mut blocked_dirs = Vec::new();
let mut last_error: Option<String> = None;
for dir in service_profile_dirs() { for dir in service_profile_dirs() {
if can_write_dir(&dir) { if !can_write_dir(&dir) {
success = true; blocked_dirs.push(dir);
continue;
} }
}
if blocked_dirs.is_empty() {
return Ok(());
}
if has_acl_unlock_flag() {
log_event("Perfis do serviço voltaram a bloquear escrita; reaplicando correção de ACL");
} else {
log_event("Executando ajuste inicial de ACL dos perfis do serviço (requer UAC)");
}
let mut last_error: Option<String> = None;
for dir in blocked_dirs.iter() {
log_event(&format!( log_event(&format!(
"Tentando corrigir ACL via UAC (preflight) em {}...", "Tentando corrigir ACL via UAC (preflight) em {}...",
dir.display() dir.display()
)); ));
if let Err(error) = fix_profile_acl(&dir) { if let Err(error) = fix_profile_acl(dir) {
last_error = Some(error); last_error = Some(error);
continue; continue;
} }
if can_write_dir(&dir) { if can_write_dir(dir) {
log_event(&format!( log_event(&format!(
"ACL ajustada com sucesso em {}", "ACL ajustada com sucesso em {}",
dir.display() dir.display()
)); ));
success = true;
} else { } else {
last_error = Some(format!( last_error = Some(format!(
"continua sem permissão para {} mesmo após preflight", "continua sem permissão para {} mesmo após preflight",
@ -780,7 +839,8 @@ fn ensure_service_profiles_writable_preflight() -> Result<(), String> {
} }
} }
if success { if blocked_dirs.iter().all(|dir| can_write_dir(dir)) {
mark_acl_unlock_flag();
Ok(()) Ok(())
} else { } else {
Err(last_error.unwrap_or_else(|| "nenhum perfil de serviço acessível".into())) Err(last_error.unwrap_or_else(|| "nenhum perfil de serviço acessível".into()))
@ -978,3 +1038,77 @@ fn append_log(dir: PathBuf, message: &str) -> io::Result<()> {
writeln!(file, "[{timestamp}] {message}")?; writeln!(file, "[{timestamp}] {message}")?;
Ok(()) Ok(())
} }
fn raven_appdata_root() -> Option<PathBuf> {
env::var("LOCALAPPDATA")
.ok()
.map(|value| Path::new(&value).join(APP_IDENTIFIER))
}
fn machine_store_path() -> Option<PathBuf> {
raven_appdata_root().map(|dir| dir.join(MACHINE_STORE_FILENAME))
}
fn read_machine_store_object() -> Option<JsonMap<String, JsonValue>> {
let path = machine_store_path()?;
let contents = fs::read_to_string(path).ok()?;
let value: JsonValue = serde_json::from_str(&contents).ok()?;
value.as_object().cloned()
}
fn write_machine_store_object(map: JsonMap<String, JsonValue>) -> Result<(), String> {
let path = machine_store_path().ok_or_else(|| "LOCALAPPDATA não disponível".to_string())?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| format!("mkdir AppData: {error}"))?;
}
let serialized = serde_json::to_vec_pretty(&JsonValue::Object(map))
.map_err(|error| format!("serialize machine-agent: {error}"))?;
fs::write(&path, serialized).map_err(|error| format!("write machine-agent: {error}"))?;
Ok(())
}
fn upsert_machine_store_value(key: &str, value: JsonValue) -> Result<(), String> {
let mut map = read_machine_store_object().unwrap_or_else(JsonMap::new);
map.insert(key.to_string(), value);
write_machine_store_object(map)
}
fn machine_store_key_exists(key: &str) -> bool {
read_machine_store_object()
.map(|map| map.contains_key(key))
.unwrap_or(false)
}
fn acl_flag_file_path() -> Option<PathBuf> {
raven_appdata_root().map(|dir| dir.join(ACL_FLAG_FILENAME))
}
fn has_acl_unlock_flag() -> bool {
if let Some(flag) = acl_flag_file_path() {
if flag.exists() {
return true;
}
}
machine_store_key_exists(RUSTDESK_ACL_STORE_KEY)
}
fn mark_acl_unlock_flag() {
let timestamp = Utc::now().timestamp_millis();
if let Some(flag_path) = acl_flag_file_path() {
if let Some(parent) = flag_path.parent() {
let _ = fs::create_dir_all(parent);
}
if let Err(error) = fs::write(&flag_path, timestamp.to_string()) {
log_event(&format!(
"Falha ao gravar flag de ACL em {}: {error}",
flag_path.display()
));
}
}
if let Err(error) = upsert_machine_store_value(RUSTDESK_ACL_STORE_KEY, JsonValue::from(timestamp)) {
log_event(&format!(
"Falha ao registrar flag de ACL no machine-agent: {error}"
));
}
}

View file

@ -52,6 +52,10 @@ Fluxo ideal:
- Instala/atualiza binário, configura `RustDesk2.toml` com `relay-server`, `api-server`, etc. - Instala/atualiza binário, configura `RustDesk2.toml` com `relay-server`, `api-server`, etc.
- Define ID determinístico (hash do `machine_id`, mas depois compara com `--get-id` e usa o “reportado” caso o serviço tenha um ID próprio). - Define ID determinístico (hash do `machine_id`, mas depois compara com `--get-id` e usa o “reportado” caso o serviço tenha um ID próprio).
- Reinicia serviço `RustDesk` (`sc start RustDesk` — pode falhar com `status Some(5)` se não há privilégio admin). Mesmo com falha, a CLI continua e grava o ID nos arquivos. - Reinicia serviço `RustDesk` (`sc start RustDesk` — pode falhar com `status Some(5)` se não há privilégio admin). Mesmo com falha, a CLI continua e grava o ID nos arquivos.
- **Autoelevação única**: na primeira execução do botão “Preparar”, o Raven dispara um PowerShell elevado (`takeown + icacls`) para liberar ACL dos perfis `LocalService` e `LocalSystem`. O sucesso grava `rustdeskAclUnlockedAt` dentro de `%LOCALAPPDATA%\br.com.esdrasrenan.sistemadechamados\machine-agent.json` e cria o flag `rustdesk_acl_unlocked.flag`, evitando novos prompts de UAC nas execuções seguintes.
- **Kill/restart seguro**: antes de tocar nos TOML, o Raven roda `sc stop RustDesk` + `taskkill /F /T /IM rustdesk.exe`. Isso garante que nenhum cliente sobrescreva o `RustDesk_local.toml` enquanto aplicamos a senha.
- **Replicação completa de perfis**: após aplicar `--password`, copiamos `RustDesk.toml` e `RustDesk_local.toml` do `%APPDATA%` para `C:\ProgramData\RustDesk\config`, `LocalService` e `LocalSystem`. Em seguida, escrevemos (via `write_toml_kv`) `password`, `verification-method = "use-permanent-password"` e `approve-mode = "password"` em **todos** os perfis, além de replicar `password/passwd/passwd.txt`.
- **Reforço contínuo**: toda nova execução do botão “Preparar” repete o ciclo (kill → copiar → gravar flags → `sc start`). Se alguém voltar manualmente para “Use both”, o Raven mata o processo, reescreve os TOML e reinicia o serviço — o RustDesk volta travado na senha permanente com o PIN registrado nos logs (`rustdesk.log`).
- **Sincronização**: `syncRustdeskAccess(machineToken, info)` chama `/api/machines/remote-access`. Há retries automáticos: - **Sincronização**: `syncRustdeskAccess(machineToken, info)` chama `/api/machines/remote-access`. Há retries automáticos:
```ts ```ts
if (response.status === 401/500 contendo "token revogado") { if (response.status === 401/500 contendo "token revogado") {