NetDesk 1.5.0 — source release
This commit is contained in:
commit
ec4955682f
227 files changed
+80669
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user