feat(agent): v2 secure-session-core Task 6 - full key fidelity
All checks were successful
All checks were successful
SPEC-002 Phase 1 Task 6, code-reviewed APPROVED (2 rounds), locally verified (cargo fmt + clippy -D warnings exit 0 + cargo test --workspace 70 pass + build). - Viewer WH_KEYBOARD_LL hook diverts system combos (Win/Win+R, Alt+Tab, Alt+Esc, Ctrl+Esc) to the remote as a full KeyEvent (vk + scan + is_extended + modifiers) and suppresses local handling - GATED on the viewer window having focus AND a "send system keys" toggle (default on; Pause/Break host-key), so it never bricks the technician's local keyboard when unfocused. - Agent injection via SendInput KEYEVENTF_SCANCODE + correct KEYEVENTF_EXTENDEDKEY (right Ctrl/Alt, arrows, nav, Win, NumLock, numpad Divide) - layout-independent, extended-key-correct. - Ctrl+Alt+Del completes through the SAS helper (SYSTEM SendSAS); installer sets the SoftwareSASGeneration policy; 3-tier fail-loud (no false success). SAS named pipe DACL tightened from NULL/Everyone to Authenticated Users. - Modifier hygiene: viewer emits key-ups for held Ctrl/Alt/Shift/Win on focus loss / close so modifiers never stick on the remote. - proto: KeyEvent.is_extended = 7 (additive; older agents derive the flag). Closes Win+R / Ctrl+C-V / Ctrl+Alt+Del / arrows-vs-numpad fidelity. Live on-device testing is plan Task 8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,19 @@ 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;
|
||||
/// SDDL revision passed to `ConvertStringSecurityDescriptorToSecurityDescriptorW`
|
||||
/// (`SDDL_REVISION_1`).
|
||||
const SDDL_REVISION_1: u32 = 1;
|
||||
|
||||
/// Restrictive DACL for the SAS named pipe, in SDDL form.
|
||||
///
|
||||
/// `D:` introduces the DACL; `(A;;GA;;;AU)` is an ACE granting GENERIC_ALL (`GA`) to
|
||||
/// Authenticated Users (`AU`). Anonymous / null-session callers are NOT authenticated and
|
||||
/// are therefore denied — closing the original NULL-DACL hole where any local process
|
||||
/// (Everyone) could connect and make this SYSTEM service raise the secure-attention
|
||||
/// screen. The agent runs in the interactive logon session and IS an authenticated user,
|
||||
/// so it can still connect and request a SAS.
|
||||
const PIPE_SDDL: &str = "D:(A;;GA;;;AU)";
|
||||
|
||||
// FFI declarations for named pipe operations
|
||||
#[link(name = "kernel32")]
|
||||
@@ -70,16 +82,18 @@ extern "system" {
|
||||
lpOverlapped: *mut std::ffi::c_void,
|
||||
) -> i32;
|
||||
fn FlushFileBuffers(hFile: isize) -> i32;
|
||||
fn LocalFree(hMem: *mut std::ffi::c_void) -> *mut std::ffi::c_void;
|
||||
}
|
||||
|
||||
#[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,
|
||||
/// Build a self-relative security descriptor from an SDDL string. The descriptor is
|
||||
/// allocated with `LocalAlloc` and must be released with `LocalFree`.
|
||||
fn ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
StringSecurityDescriptor: *const u16,
|
||||
StringSDRevision: u32,
|
||||
SecurityDescriptor: *mut *mut std::ffi::c_void,
|
||||
SecurityDescriptorSize: *mut u32,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
@@ -281,26 +295,31 @@ 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;
|
||||
}
|
||||
// Build a restrictive security descriptor from SDDL: grant access only to
|
||||
// Authenticated Users (excludes anonymous / null-session callers). See PIPE_SDDL.
|
||||
let sddl: Vec<u16> = PIPE_SDDL.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let mut sd_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
|
||||
let converted = unsafe {
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
sddl.as_ptr(),
|
||||
SDDL_REVISION_1,
|
||||
&mut sd_ptr,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if converted == 0 || sd_ptr.is_null() {
|
||||
let err = std::io::Error::last_os_error();
|
||||
tracing::error!(
|
||||
"Failed to build pipe security descriptor from SDDL: {}",
|
||||
err
|
||||
);
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut sa = SECURITY_ATTRIBUTES {
|
||||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: sd.as_mut_ptr(),
|
||||
lpSecurityDescriptor: sd_ptr as *mut u8,
|
||||
bInheritHandle: 0,
|
||||
};
|
||||
|
||||
@@ -321,6 +340,12 @@ fn run_pipe_server() -> Result<()> {
|
||||
)
|
||||
};
|
||||
|
||||
// CreateNamedPipeW copies the descriptor into the kernel object, so the SDDL-built
|
||||
// copy can be freed now regardless of success.
|
||||
unsafe {
|
||||
LocalFree(sd_ptr);
|
||||
}
|
||||
|
||||
if pipe == INVALID_HANDLE_VALUE {
|
||||
tracing::error!("Failed to create named pipe");
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
@@ -404,6 +429,69 @@ fn run_pipe_server() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable the `SoftwareSASGeneration` Winlogon policy so `SendSAS` is permitted.
|
||||
///
|
||||
/// Without this policy, `sas.dll!SendSAS` is a silent no-op even when called from
|
||||
/// SYSTEM. The value lives at
|
||||
/// `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\SoftwareSASGeneration`
|
||||
/// and is a DWORD bitmask:
|
||||
/// 0 = none, 1 = services, 2 = ease-of-access apps, 3 = both.
|
||||
///
|
||||
/// We set `1` (services) because the GuruConnect SAS helper runs as a SYSTEM service.
|
||||
/// This is invoked from the SAS service installer; the broader agent installer should
|
||||
/// ensure this runs (see `// TODO(installer)` below).
|
||||
fn set_software_sas_policy() -> Result<()> {
|
||||
use windows::core::PCWSTR;
|
||||
use windows::Win32::System::Registry::{
|
||||
RegCloseKey, RegCreateKeyExW, RegSetValueExW, HKEY, HKEY_LOCAL_MACHINE, KEY_SET_VALUE,
|
||||
REG_DWORD, REG_OPTION_NON_VOLATILE,
|
||||
};
|
||||
|
||||
let subkey: Vec<u16> = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let value_name: Vec<u16> = "SoftwareSASGeneration"
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
// DWORD 1 = allow services to generate a software SAS.
|
||||
let data: u32 = 1;
|
||||
|
||||
unsafe {
|
||||
let mut hkey = HKEY::default();
|
||||
let status = RegCreateKeyExW(
|
||||
HKEY_LOCAL_MACHINE,
|
||||
PCWSTR(subkey.as_ptr()),
|
||||
0,
|
||||
PCWSTR::null(),
|
||||
REG_OPTION_NON_VOLATILE,
|
||||
KEY_SET_VALUE,
|
||||
None,
|
||||
&mut hkey,
|
||||
None,
|
||||
);
|
||||
if status.is_err() {
|
||||
anyhow::bail!("RegCreateKeyExW(Policies\\System) failed: {:?}", status);
|
||||
}
|
||||
|
||||
let set = RegSetValueExW(
|
||||
hkey,
|
||||
PCWSTR(value_name.as_ptr()),
|
||||
0,
|
||||
REG_DWORD,
|
||||
Some(&data.to_ne_bytes()),
|
||||
);
|
||||
let _ = RegCloseKey(hkey);
|
||||
|
||||
if set.is_err() {
|
||||
anyhow::bail!("RegSetValueExW(SoftwareSASGeneration) failed: {:?}", set);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Call SendSAS via sas.dll
|
||||
fn send_sas() -> Result<()> {
|
||||
unsafe {
|
||||
@@ -506,6 +594,19 @@ fn install_service() -> Result<()> {
|
||||
])
|
||||
.output();
|
||||
|
||||
// Enable the SoftwareSASGeneration policy so SendSAS actually works from the
|
||||
// SYSTEM service. TODO(installer): the top-level managed agent installer should
|
||||
// also ensure this policy is set (and that this SAS service is installed) as part
|
||||
// of unattended deployment, rather than relying on a manual SAS-service install.
|
||||
match set_software_sas_policy() {
|
||||
Ok(()) => println!("Enabled SoftwareSASGeneration policy (services)"),
|
||||
Err(e) => println!(
|
||||
"Warning: failed to set SoftwareSASGeneration policy: {}. \
|
||||
Ctrl+Alt+Del may not reach the secure desktop until this is set.",
|
||||
e
|
||||
),
|
||||
}
|
||||
|
||||
println!("\n** GuruConnect SAS Service installed successfully!");
|
||||
println!("\nBinary: {:?}", binary_dest);
|
||||
println!("\nStarting service...");
|
||||
|
||||
Reference in New Issue
Block a user