//! GuruConnect SAS Service //! //! Windows Service running as SYSTEM to handle Ctrl+Alt+Del (Secure Attention Sequence). //! The agent communicates with this service via named pipe IPC. use std::ffi::OsString; use std::io::{Read, Write as IoWrite}; use std::sync::mpsc; use std::time::Duration; use anyhow::{Context, Result}; use windows::core::{s, w}; use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW}; use windows_service::{ define_windows_service, service::{ ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode, ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType, }, service_control_handler::{self, ServiceControlHandlerResult}, service_dispatcher, service_manager::{ServiceManager, ServiceManagerAccess}, }; // Service configuration const SERVICE_NAME: &str = "GuruConnectSAS"; const SERVICE_DISPLAY_NAME: &str = "GuruConnect SAS Service"; const SERVICE_DESCRIPTION: &str = "Handles Secure Attention Sequence (Ctrl+Alt+Del) for GuruConnect remote sessions"; const PIPE_NAME: &str = r"\\.\pipe\guruconnect-sas"; const INSTALL_DIR: &str = r"C:\Program Files\GuruConnect"; // Windows named pipe constants const PIPE_ACCESS_DUPLEX: u32 = 0x00000003; const PIPE_TYPE_MESSAGE: u32 = 0x00000004; const PIPE_READMODE_MESSAGE: u32 = 0x00000002; const PIPE_WAIT: u32 = 0x00000000; const PIPE_UNLIMITED_INSTANCES: u32 = 255; const INVALID_HANDLE_VALUE: isize = -1; const SECURITY_DESCRIPTOR_REVISION: u32 = 1; // FFI declarations for named pipe operations #[link(name = "kernel32")] extern "system" { fn CreateNamedPipeW( lpName: *const u16, dwOpenMode: u32, dwPipeMode: u32, nMaxInstances: u32, nOutBufferSize: u32, nInBufferSize: u32, nDefaultTimeOut: u32, lpSecurityAttributes: *mut SECURITY_ATTRIBUTES, ) -> isize; fn ConnectNamedPipe(hNamedPipe: isize, lpOverlapped: *mut std::ffi::c_void) -> i32; fn DisconnectNamedPipe(hNamedPipe: isize) -> i32; fn CloseHandle(hObject: isize) -> i32; fn ReadFile( hFile: isize, lpBuffer: *mut u8, nNumberOfBytesToRead: u32, lpNumberOfBytesRead: *mut u32, lpOverlapped: *mut std::ffi::c_void, ) -> i32; fn WriteFile( hFile: isize, lpBuffer: *const u8, nNumberOfBytesToWrite: u32, lpNumberOfBytesWritten: *mut u32, lpOverlapped: *mut std::ffi::c_void, ) -> i32; fn FlushFileBuffers(hFile: isize) -> i32; } #[link(name = "advapi32")] extern "system" { fn InitializeSecurityDescriptor(pSecurityDescriptor: *mut u8, dwRevision: u32) -> i32; fn SetSecurityDescriptorDacl( pSecurityDescriptor: *mut u8, bDaclPresent: i32, pDacl: *mut std::ffi::c_void, bDaclDefaulted: i32, ) -> i32; } #[repr(C)] struct SECURITY_ATTRIBUTES { nLength: u32, lpSecurityDescriptor: *mut u8, bInheritHandle: i32, } fn main() { // Set up logging tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .with_target(false) .init(); match std::env::args().nth(1).as_deref() { Some("install") => { if let Err(e) = install_service() { eprintln!("Failed to install service: {}", e); std::process::exit(1); } } Some("uninstall") => { if let Err(e) = uninstall_service() { eprintln!("Failed to uninstall service: {}", e); std::process::exit(1); } } Some("start") => { if let Err(e) = start_service() { eprintln!("Failed to start service: {}", e); std::process::exit(1); } } Some("stop") => { if let Err(e) = stop_service() { eprintln!("Failed to stop service: {}", e); std::process::exit(1); } } Some("status") => { if let Err(e) = query_status() { eprintln!("Failed to query status: {}", e); std::process::exit(1); } } Some("service") => { // Called by SCM when service starts if let Err(e) = run_as_service() { eprintln!("Service error: {}", e); std::process::exit(1); } } Some("test") => { // Test mode: run pipe server directly (for debugging) println!("Running in test mode (not as service)..."); if let Err(e) = run_pipe_server() { eprintln!("Pipe server error: {}", e); std::process::exit(1); } } _ => { print_usage(); } } } fn print_usage() { println!("GuruConnect SAS Service"); println!(); println!("Usage: guruconnect-sas-service "); println!(); println!("Commands:"); println!(" install Install the service"); println!(" uninstall Remove the service"); println!(" start Start the service"); println!(" stop Stop the service"); println!(" status Query service status"); println!(" test Run in test mode (not as service)"); } // Generate the Windows service boilerplate define_windows_service!(ffi_service_main, service_main); /// Entry point called by the Windows Service Control Manager fn run_as_service() -> Result<()> { service_dispatcher::start(SERVICE_NAME, ffi_service_main) .context("Failed to start service dispatcher")?; Ok(()) } /// Main service function called by the SCM fn service_main(_arguments: Vec) { if let Err(e) = run_service() { tracing::error!("Service error: {}", e); } } /// The actual service implementation fn run_service() -> Result<()> { // Create a channel to receive stop events let (shutdown_tx, shutdown_rx) = mpsc::channel(); // Create the service control handler let event_handler = move |control_event| -> ServiceControlHandlerResult { match control_event { ServiceControl::Stop | ServiceControl::Shutdown => { tracing::info!("Received stop/shutdown command"); let _ = shutdown_tx.send(()); ServiceControlHandlerResult::NoError } ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, _ => ServiceControlHandlerResult::NotImplemented, } }; // Register the service control handler let status_handle = service_control_handler::register(SERVICE_NAME, event_handler) .context("Failed to register service control handler")?; // Report that we're starting status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::StartPending, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::from_secs(5), process_id: None, }) .ok(); // Report that we're running status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Running, controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::default(), process_id: None, }) .ok(); tracing::info!("GuruConnect SAS Service started"); // Run the pipe server in a separate thread let pipe_handle = std::thread::spawn(|| { if let Err(e) = run_pipe_server() { tracing::error!("Pipe server error: {}", e); } }); // Wait for shutdown signal let _ = shutdown_rx.recv(); tracing::info!("Shutting down..."); // Report that we're stopping status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::StopPending, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::from_secs(3), process_id: None, }) .ok(); // The pipe thread will exit when the service stops drop(pipe_handle); // Report stopped status_handle .set_service_status(ServiceStatus { service_type: ServiceType::OWN_PROCESS, current_state: ServiceState::Stopped, controls_accepted: ServiceControlAccept::empty(), exit_code: ServiceExitCode::Win32(0), checkpoint: 0, wait_hint: Duration::default(), process_id: None, }) .ok(); Ok(()) } /// Run the named pipe server fn run_pipe_server() -> Result<()> { tracing::info!("Starting pipe server on {}", PIPE_NAME); loop { // Create security descriptor that allows everyone let mut sd = [0u8; 256]; unsafe { if InitializeSecurityDescriptor(sd.as_mut_ptr(), SECURITY_DESCRIPTOR_REVISION) == 0 { tracing::error!("Failed to initialize security descriptor"); std::thread::sleep(Duration::from_secs(1)); continue; } // Set NULL DACL = allow everyone if SetSecurityDescriptorDacl(sd.as_mut_ptr(), 1, std::ptr::null_mut(), 0) == 0 { tracing::error!("Failed to set security descriptor DACL"); std::thread::sleep(Duration::from_secs(1)); continue; } } let mut sa = SECURITY_ATTRIBUTES { nLength: std::mem::size_of::() as u32, lpSecurityDescriptor: sd.as_mut_ptr(), bInheritHandle: 0, }; // Create the pipe name as wide string let pipe_name: Vec = PIPE_NAME.encode_utf16().chain(std::iter::once(0)).collect(); // Create the named pipe let pipe = unsafe { CreateNamedPipeW( pipe_name.as_ptr(), PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 512, 512, 0, &mut sa, ) }; if pipe == INVALID_HANDLE_VALUE { tracing::error!("Failed to create named pipe"); std::thread::sleep(Duration::from_secs(1)); continue; } tracing::info!("Waiting for client connection..."); // Wait for a client to connect let connected = unsafe { ConnectNamedPipe(pipe, std::ptr::null_mut()) }; if connected == 0 { let err = std::io::Error::last_os_error(); // ERROR_PIPE_CONNECTED (535) means client connected between Create and Connect if err.raw_os_error() != Some(535) { tracing::warn!("ConnectNamedPipe error: {}", err); } } tracing::info!("Client connected"); // Read command from pipe let mut buffer = [0u8; 512]; let mut bytes_read = 0u32; let read_result = unsafe { ReadFile( pipe, buffer.as_mut_ptr(), buffer.len() as u32, &mut bytes_read, std::ptr::null_mut(), ) }; if read_result != 0 && bytes_read > 0 { let command = String::from_utf8_lossy(&buffer[..bytes_read as usize]); let command = command.trim(); tracing::info!("Received command: {}", command); let response = match command { "sas" => { match send_sas() { Ok(()) => { tracing::info!("SendSAS executed successfully"); "ok\n" } Err(e) => { tracing::error!("SendSAS failed: {}", e); "error\n" } } } "ping" => { tracing::info!("Ping received"); "pong\n" } _ => { tracing::warn!("Unknown command: {}", command); "unknown\n" } }; // Write response let mut bytes_written = 0u32; unsafe { WriteFile( pipe, response.as_ptr(), response.len() as u32, &mut bytes_written, std::ptr::null_mut(), ); FlushFileBuffers(pipe); } } // Disconnect and close the pipe unsafe { DisconnectNamedPipe(pipe); CloseHandle(pipe); } } } /// Call SendSAS via sas.dll fn send_sas() -> Result<()> { unsafe { let lib = LoadLibraryW(w!("sas.dll")).context("Failed to load sas.dll")?; let proc = GetProcAddress(lib, s!("SendSAS")); if proc.is_none() { anyhow::bail!("SendSAS function not found in sas.dll"); } // SendSAS takes a BOOL parameter: FALSE (0) = Ctrl+Alt+Del type SendSASFn = unsafe extern "system" fn(i32); let send_sas_fn: SendSASFn = std::mem::transmute(proc.unwrap()); tracing::info!("Calling SendSAS(0)..."); send_sas_fn(0); Ok(()) } } /// Install the service fn install_service() -> Result<()> { println!("Installing GuruConnect SAS Service..."); // Get current executable path let current_exe = std::env::current_exe().context("Failed to get current executable")?; let binary_dest = std::path::PathBuf::from(format!(r"{}\\guruconnect-sas-service.exe", INSTALL_DIR)); // Create install directory std::fs::create_dir_all(INSTALL_DIR).context("Failed to create install directory")?; // Copy binary println!("Copying binary to: {:?}", binary_dest); std::fs::copy(¤t_exe, &binary_dest).context("Failed to copy binary")?; // Open service manager let manager = ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE, ) .context("Failed to connect to Service Control Manager. Run as Administrator.")?; // Check if service exists and remove it if let Ok(service) = manager.open_service( SERVICE_NAME, ServiceAccess::QUERY_STATUS | ServiceAccess::DELETE | ServiceAccess::STOP, ) { println!("Removing existing service..."); if let Ok(status) = service.query_status() { if status.current_state != ServiceState::Stopped { let _ = service.stop(); std::thread::sleep(Duration::from_secs(2)); } } service.delete().context("Failed to delete existing service")?; drop(service); std::thread::sleep(Duration::from_secs(2)); } // Create the service let service_info = ServiceInfo { name: OsString::from(SERVICE_NAME), display_name: OsString::from(SERVICE_DISPLAY_NAME), service_type: ServiceType::OWN_PROCESS, start_type: ServiceStartType::AutoStart, error_control: ServiceErrorControl::Normal, executable_path: binary_dest.clone(), launch_arguments: vec![OsString::from("service")], dependencies: vec![], account_name: None, // LocalSystem account_password: None, }; let service = manager .create_service(&service_info, ServiceAccess::CHANGE_CONFIG | ServiceAccess::START) .context("Failed to create service")?; // Set description service .set_description(SERVICE_DESCRIPTION) .context("Failed to set service description")?; // Configure recovery let _ = std::process::Command::new("sc") .args([ "failure", SERVICE_NAME, "reset=86400", "actions=restart/5000/restart/5000/restart/5000", ]) .output(); println!("\n** GuruConnect SAS Service installed successfully!"); println!("\nBinary: {:?}", binary_dest); println!("\nStarting service..."); // Start the service start_service()?; Ok(()) } /// Uninstall the service fn uninstall_service() -> Result<()> { println!("Uninstalling GuruConnect SAS Service..."); let binary_path = std::path::PathBuf::from(format!(r"{}\\guruconnect-sas-service.exe", INSTALL_DIR)); let manager = ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT, ) .context("Failed to connect to Service Control Manager. Run as Administrator.")?; match manager.open_service( SERVICE_NAME, ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE, ) { Ok(service) => { if let Ok(status) = service.query_status() { if status.current_state != ServiceState::Stopped { println!("Stopping service..."); let _ = service.stop(); std::thread::sleep(Duration::from_secs(3)); } } println!("Deleting service..."); service.delete().context("Failed to delete service")?; } Err(_) => { println!("Service was not installed"); } } // Remove binary if binary_path.exists() { std::thread::sleep(Duration::from_secs(1)); if let Err(e) = std::fs::remove_file(&binary_path) { println!("Warning: Failed to remove binary: {}", e); } } println!("\n** GuruConnect SAS Service uninstalled successfully!"); Ok(()) } /// Start the service fn start_service() -> Result<()> { let manager = ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT, ) .context("Failed to connect to Service Control Manager")?; let service = manager .open_service(SERVICE_NAME, ServiceAccess::START | ServiceAccess::QUERY_STATUS) .context("Failed to open service. Is it installed?")?; service.start::(&[]).context("Failed to start service")?; std::thread::sleep(Duration::from_secs(1)); let status = service.query_status()?; match status.current_state { ServiceState::Running => println!("** Service started successfully"), ServiceState::StartPending => println!("** Service is starting..."), other => println!("Service state: {:?}", other), } Ok(()) } /// Stop the service fn stop_service() -> Result<()> { let manager = ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT, ) .context("Failed to connect to Service Control Manager")?; let service = manager .open_service(SERVICE_NAME, ServiceAccess::STOP | ServiceAccess::QUERY_STATUS) .context("Failed to open service")?; service.stop().context("Failed to stop service")?; std::thread::sleep(Duration::from_secs(1)); let status = service.query_status()?; match status.current_state { ServiceState::Stopped => println!("** Service stopped"), ServiceState::StopPending => println!("** Service is stopping..."), other => println!("Service state: {:?}", other), } Ok(()) } /// Query service status fn query_status() -> Result<()> { let manager = ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT, ) .context("Failed to connect to Service Control Manager")?; match manager.open_service(SERVICE_NAME, ServiceAccess::QUERY_STATUS) { Ok(service) => { let status = service.query_status()?; println!("GuruConnect SAS Service"); println!("======================="); println!("Name: {}", SERVICE_NAME); println!("State: {:?}", status.current_state); println!("Binary: {}\\guruconnect-sas-service.exe", INSTALL_DIR); println!("Pipe: {}", PIPE_NAME); } Err(_) => { println!("GuruConnect SAS Service"); println!("======================="); println!("Status: NOT INSTALLED"); println!("\nTo install: guruconnect-sas-service install"); } } Ok(()) }