NetDesk 1.5.0 — source release

This commit is contained in:
NetDesk committed 2026-10-18 01:35:59 +02:00
commit ec4955682f
227 files changed
+80669

No files matched your search

+289
View File
@@ -0,0 +1,289 @@
//! Windows SYSTEM service for UNATTENDED access + lock/login-screen capture.
//!
//! The service runs as LocalSystem (session 0) and spawns the NetDesk host INTO
//! the active console session **as SYSTEM**, with its thread desktop following
//! the current input desktop. That lets the host capture and inject on the
//! secure desktop (the lock screen, the Windows sign-in screen, and UAC) — so a
//! remote viewer can sign in and unlock an unattended machine, TeamViewer-style.
//!
//! Opt-in and isolated: nothing here runs unless `--install-service` is executed
//! elevated. The ordinary user-session host (Desktop autostart / admin task) is
//! untouched, so installing or removing the service never risks the path that
//! already works.
//!
//! Entry points (wired in the viewer binary's `main`):
//! --install-service register + start the service (self-elevates via UAC)
//! --uninstall-service stop + delete the service
//! --service-run the SCM service entry (run by Windows, not by hand)
//! --session-agent the per-session host the service spawns (follows the
//! input desktop so the lock screen is captured)
#![cfg(windows)]
use anyhow::{anyhow, Result};
use std::ffi::c_void;
use std::sync::atomic::{AtomicBool, Ordering};
use windows::core::{PCWSTR, PWSTR};
use windows::Win32::Foundation::{CloseHandle, HANDLE, LUID};
use windows::Win32::Security::{
AdjustTokenPrivileges, DuplicateTokenEx, LookupPrivilegeValueW, SetTokenInformation,
SecurityIdentification, TokenPrimary, TokenSessionId, LUID_AND_ATTRIBUTES,
SE_PRIVILEGE_ENABLED, TOKEN_ACCESS_MASK, TOKEN_ADJUST_DEFAULT, TOKEN_ADJUST_PRIVILEGES,
TOKEN_ADJUST_SESSIONID, TOKEN_ASSIGN_PRIMARY, TOKEN_DUPLICATE, TOKEN_PRIVILEGES, TOKEN_QUERY,
};
use windows::Win32::System::RemoteDesktop::WTSGetActiveConsoleSessionId;
use windows::Win32::System::Services::{
RegisterServiceCtrlHandlerW, SetServiceStatus, StartServiceCtrlDispatcherW,
SERVICE_STATUS, SERVICE_STATUS_HANDLE, SERVICE_TABLE_ENTRYW, SERVICE_ACCEPT_STOP,
SERVICE_CONTROL_SHUTDOWN, SERVICE_CONTROL_STOP, SERVICE_RUNNING, SERVICE_START_PENDING,
SERVICE_STOP_PENDING, SERVICE_STOPPED, SERVICE_WIN32_OWN_PROCESS,
};
use windows::Win32::System::Threading::{
CreateProcessAsUserW, GetCurrentProcess, OpenProcessToken, TerminateProcess,
CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, PROCESS_INFORMATION, STARTUPINFOW,
};
const SERVICE_NAME: &str = "NetDeskHost";
static STOP: AtomicBool = AtomicBool::new(false);
static mut STATUS_HANDLE: isize = 0;
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
// ---------------------------------------------------------------------------
// install / uninstall (shell out to sc.exe; requires elevation)
// ---------------------------------------------------------------------------
/// Register the service (LocalSystem, auto-start, auto-restart) and start it.
pub fn install_service(exe: &str) -> Result<()> {
let bin = format!("\"{exe}\" --service-run");
// Create (quirk: sc wants "key=" and the value as separate tokens).
run_sc(&["create", SERVICE_NAME, "binPath=", &bin, "start=", "auto",
"obj=", "LocalSystem", "DisplayName=", "NetDesk Host (unattended)"])?;
let _ = run_sc(&["description", SERVICE_NAME,
"NetDesk unattended host — captures the lock/sign-in screen so you can access this PC remotely."]);
// Restart on crash so unattended access survives faults.
let _ = run_sc(&["failure", SERVICE_NAME, "reset=", "0", "actions=",
"restart/5000/restart/5000/restart/5000"]);
run_sc(&["start", SERVICE_NAME])?;
Ok(())
}
/// Stop and delete the service.
pub fn uninstall_service() -> Result<()> {
let _ = run_sc(&["stop", SERVICE_NAME]);
run_sc(&["delete", SERVICE_NAME])?;
Ok(())
}
fn run_sc(args: &[&str]) -> Result<()> {
let status = std::process::Command::new("sc")
.args(args)
.status()
.map_err(|e| anyhow!("sc {args:?}: {e}"))?;
if status.success() {
Ok(())
} else {
Err(anyhow!("sc {:?} exited with {:?}", args, status.code()))
}
}
// ---------------------------------------------------------------------------
// service entry (run by the SCM via `--service-run`)
// ---------------------------------------------------------------------------
/// Hand control to the Service Control Manager. Blocks until the service stops.
pub fn run_service() -> Result<()> {
let name = wide(SERVICE_NAME);
let table = [
SERVICE_TABLE_ENTRYW {
lpServiceName: PWSTR(name.as_ptr() as *mut u16),
lpServiceProc: Some(service_main),
},
SERVICE_TABLE_ENTRYW {
lpServiceName: PWSTR::null(),
lpServiceProc: None,
},
];
unsafe {
StartServiceCtrlDispatcherW(table.as_ptr())
.map_err(|e| anyhow!("StartServiceCtrlDispatcher: {e}"))?;
}
Ok(())
}
extern "system" fn service_main(_argc: u32, _argv: *mut PWSTR) {
unsafe {
let name = wide(SERVICE_NAME);
match RegisterServiceCtrlHandlerW(PCWSTR(name.as_ptr()), Some(handler)) {
Ok(h) => STATUS_HANDLE = h.0 as isize,
Err(_) => return,
}
set_status(SERVICE_START_PENDING, 0);
set_status(SERVICE_RUNNING, SERVICE_ACCEPT_STOP);
// Main loop: keep a host alive in the active console session.
let exe = std::env::current_exe()
.map(|p| p.display().to_string())
.unwrap_or_default();
let mut child: Option<(HANDLE, u32)> = None;
while !STOP.load(Ordering::Relaxed) {
let session = WTSGetActiveConsoleSessionId();
let need_spawn = match child {
// 0xFFFFFFFF => no one attached to the console right now.
_ if session == 0xFFFF_FFFF => false,
None => true,
Some((_, sess)) => sess != session || !process_alive(child),
};
if need_spawn && session != 0xFFFF_FFFF {
if let Some((h, _)) = child.take() {
let _ = TerminateProcess(h, 1);
let _ = CloseHandle(h);
}
match spawn_host_in_session(session, &exe) {
Ok(h) => child = Some((h, session)),
Err(_) => {}
}
}
std::thread::sleep(std::time::Duration::from_millis(1500));
}
if let Some((h, _)) = child.take() {
let _ = TerminateProcess(h, 0);
let _ = CloseHandle(h);
}
set_status(SERVICE_STOPPED, 0);
}
}
extern "system" fn handler(control: u32) {
if control == SERVICE_CONTROL_STOP || control == SERVICE_CONTROL_SHUTDOWN {
STOP.store(true, Ordering::Relaxed);
unsafe { set_status(SERVICE_STOP_PENDING, 0) };
}
}
unsafe fn set_status(state: windows::Win32::System::Services::SERVICE_STATUS_CURRENT_STATE, accept: u32) {
if STATUS_HANDLE == 0 {
return;
}
let mut st = SERVICE_STATUS {
dwServiceType: SERVICE_WIN32_OWN_PROCESS,
dwCurrentState: state,
dwControlsAccepted: accept,
dwWin32ExitCode: 0,
dwServiceSpecificExitCode: 0,
dwCheckPoint: 0,
dwWaitHint: 3000,
};
let _ = SetServiceStatus(SERVICE_STATUS_HANDLE(STATUS_HANDLE as *mut c_void), &mut st);
}
fn process_alive(child: Option<(HANDLE, u32)>) -> bool {
use windows::Win32::Foundation::{STILL_ACTIVE, WAIT_OBJECT_0};
use windows::Win32::System::Threading::{GetExitCodeProcess, WaitForSingleObject};
match child {
None => false,
Some((h, _)) => unsafe {
// Fast path: a 0ms wait signals only if the process has exited.
if WaitForSingleObject(h, 0) == WAIT_OBJECT_0 {
return false;
}
let mut code = 0u32;
if GetExitCodeProcess(h, &mut code).is_ok() {
code == STILL_ACTIVE.0 as u32
} else {
false
}
},
}
}
// ---------------------------------------------------------------------------
// spawn the host as SYSTEM in the active session (so it can reach the secure
// desktop). Duplicates the service's SYSTEM token, re-targets it at the active
// session, and CreateProcessAsUser's the host on winsta0\default.
// ---------------------------------------------------------------------------
fn spawn_host_in_session(session: u32, exe: &str) -> Result<HANDLE> {
unsafe {
let proc = GetCurrentProcess();
let access = TOKEN_ACCESS_MASK(
TOKEN_DUPLICATE.0
| TOKEN_QUERY.0
| TOKEN_ASSIGN_PRIMARY.0
| TOKEN_ADJUST_DEFAULT.0
| TOKEN_ADJUST_SESSIONID.0
| TOKEN_ADJUST_PRIVILEGES.0,
);
let mut tok = HANDLE::default();
OpenProcessToken(proc, access, &mut tok).map_err(|e| anyhow!("OpenProcessToken: {e}"))?;
enable_privilege(tok, "SeTcbPrivilege");
let mut dup = HANDLE::default();
DuplicateTokenEx(
tok,
TOKEN_ACCESS_MASK(0x02000000), // MAXIMUM_ALLOWED
None,
SecurityIdentification,
TokenPrimary,
&mut dup,
)
.map_err(|e| anyhow!("DuplicateTokenEx: {e}"))?;
// Re-target the primary token at the active console session.
let sess = session;
SetTokenInformation(
dup,
TokenSessionId,
&sess as *const u32 as *const c_void,
std::mem::size_of::<u32>() as u32,
)
.map_err(|e| anyhow!("SetTokenInformation(session): {e}"))?;
let mut cmd = wide(&format!("\"{exe}\" --session-agent"));
let mut desktop = wide("winsta0\\default");
let mut si = STARTUPINFOW::default();
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
si.lpDesktop = PWSTR(desktop.as_mut_ptr());
let mut pi = PROCESS_INFORMATION::default();
CreateProcessAsUserW(
dup,
PCWSTR::null(),
PWSTR(cmd.as_mut_ptr()),
None,
None,
false,
CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT,
None,
PCWSTR::null(),
&si,
&mut pi,
)
.map_err(|e| anyhow!("CreateProcessAsUser: {e}"))?;
let _ = CloseHandle(pi.hThread);
let _ = CloseHandle(dup);
let _ = CloseHandle(tok);
Ok(pi.hProcess)
}
}
fn enable_privilege(token: HANDLE, name: &str) {
unsafe {
let wname = wide(name);
let mut luid = LUID::default();
if LookupPrivilegeValueW(PCWSTR::null(), PCWSTR(wname.as_ptr()), &mut luid).is_err() {
return;
}
let tp = TOKEN_PRIVILEGES {
PrivilegeCount: 1,
Privileges: [LUID_AND_ATTRIBUTES {
Luid: luid,
Attributes: SE_PRIVILEGE_ENABLED,
}],
};
let _ = AdjustTokenPrivileges(token, false, Some(&tp), 0, None, None);
}
}