//! 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 { 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 { 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::() 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::() 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); } }