feat(agent): v2 secure-session-core Task 6 - full key fidelity
All checks were successful
Build and Test / Build Agent (Windows) (push) Successful in 7m1s
Build and Test / Build Server (Linux) (push) Successful in 11m32s
Build and Test / Security Audit (push) Successful in 4m31s
Build and Test / Build Summary (push) Successful in 11s

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:
2026-05-30 09:16:26 -07:00
parent d0de888dd1
commit bb73ba667f
8 changed files with 848 additions and 133 deletions

View File

@@ -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...");