fleet/orbit/pkg/execuser/execuser_windows.go
Marcos Oviedo ec3f49881f
8009 fleet desktop icon duplication (#8017)
* Adding a new synchronization mechanism between fleet-desktop app and Orbit service. Improved windows service teardown to ensure that fleet-desktop does not get force killed without getting signaled. Improved windows process enumeration to avoid unnecessary delays during windows service start and windows service teardown. Updating windows service to reflect service teardown extra time due to synchronization.
2022-10-13 10:58:37 -03:00

297 lines
10 KiB
Go

package execuser
// NOTE: The following was copied from
// https://gist.github.com/LiamHaworth/1ac37f7fb6018293fc43f86993db24fc
//
// To view what was modified/added, you can use the execuser_windows_diff.sh script.
import (
"errors"
"fmt"
"os"
"unsafe"
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
"golang.org/x/sys/windows"
)
var (
modwtsapi32 *windows.LazyDLL = windows.NewLazySystemDLL("wtsapi32.dll")
modkernel32 *windows.LazyDLL = windows.NewLazySystemDLL("kernel32.dll")
modadvapi32 *windows.LazyDLL = windows.NewLazySystemDLL("advapi32.dll")
moduserenv *windows.LazyDLL = windows.NewLazySystemDLL("userenv.dll")
procWTSEnumerateSessionsW *windows.LazyProc = modwtsapi32.NewProc("WTSEnumerateSessionsW")
procWTSGetActiveConsoleSessionId *windows.LazyProc = modkernel32.NewProc("WTSGetActiveConsoleSessionId")
procWTSQueryUserToken *windows.LazyProc = modwtsapi32.NewProc("WTSQueryUserToken")
procDuplicateTokenEx *windows.LazyProc = modadvapi32.NewProc("DuplicateTokenEx")
procCreateEnvironmentBlock *windows.LazyProc = moduserenv.NewProc("CreateEnvironmentBlock")
procCreateProcessAsUser *windows.LazyProc = modadvapi32.NewProc("CreateProcessAsUserW")
)
const (
WTS_CURRENT_SERVER_HANDLE uintptr = 0
)
type WTS_CONNECTSTATE_CLASS int
const (
WTSActive WTS_CONNECTSTATE_CLASS = iota
WTSConnected
WTSConnectQuery
WTSShadow
WTSDisconnected
WTSIdle
WTSListen
WTSReset
WTSDown
WTSInit
)
type SECURITY_IMPERSONATION_LEVEL int
const (
SecurityAnonymous SECURITY_IMPERSONATION_LEVEL = iota
SecurityIdentification
SecurityImpersonation
SecurityDelegation
)
type TOKEN_TYPE int
const (
TokenPrimary TOKEN_TYPE = iota + 1
TokenImpersonazion
)
type SW int
const (
SW_HIDE SW = 0
SW_SHOWNORMAL = 1
SW_NORMAL = 1
SW_SHOWMINIMIZED = 2
SW_SHOWMAXIMIZED = 3
SW_MAXIMIZE = 3
SW_SHOWNOACTIVATE = 4
SW_SHOW = 5
SW_MINIMIZE = 6
SW_SHOWMINNOACTIVE = 7
SW_SHOWNA = 8
SW_RESTORE = 9
SW_SHOWDEFAULT = 10
SW_MAX = 1
)
type WTS_SESSION_INFO struct {
SessionID windows.Handle
WinStationName *uint16
State WTS_CONNECTSTATE_CLASS
}
const (
CREATE_UNICODE_ENVIRONMENT uint16 = 0x00000400
CREATE_NO_WINDOW = 0x08000000
CREATE_NEW_CONSOLE = 0x00000010
)
// run uses the Windows API to run a child process as the current login user.
// It assumes the caller is running as a SYSTEM Windows service.
//
// It sets the environment of the current process so that it gets inherited by
// the child process (see call to CreateEnvironmentBlock).
// From https://docs.microsoft.com/en-us/windows/win32/procthread/changing-environment-variables:
//
// "If you want the child process to inherit most of the parent's environment with
// only a few changes, retrieve the current values using GetEnvironmentVariable, save these values,
// create an updated block for the child process to inherit, create the child process, and then
// restore the saved values using SetEnvironmentVariable, as shown in the following example."
func run(path string, opts eopts) error {
if err := setupSyncChannel(constant.DesktopAppExecName); err != nil {
return fmt.Errorf("sync channel creation failed: %w", err)
}
for _, nv := range opts.env {
os.Setenv(nv[0], nv[1])
}
return startProcessAsCurrentUser(path, "", "")
}
// getCurrentUserSessionId will attempt to resolve
// the session ID of the user currently active on
// the system.
func getCurrentUserSessionId() (windows.Handle, error) {
sessionList, err := wtsEnumerateSessions()
if err != nil {
return 0xFFFFFFFF, fmt.Errorf("get current user session token: %s", err)
}
for i := range sessionList {
if sessionList[i].State == WTSActive {
return sessionList[i].SessionID, nil
}
}
// TODO(lucas): Check which sessions is assigned to current log in user.
if sessionId, _, err := procWTSGetActiveConsoleSessionId.Call(); sessionId == 0xFFFFFFFF {
return 0xFFFFFFFF, fmt.Errorf("get current user session token: call native WTSGetActiveConsoleSessionId: %s", err)
} else {
return windows.Handle(sessionId), nil
}
}
// wtsEnumerateSession will call the native
// version for Windows and parse the result
// to a Golang friendly version
func wtsEnumerateSessions() ([]*WTS_SESSION_INFO, error) {
var (
sessionInformation windows.Handle = windows.Handle(0)
sessionCount int = 0
sessionList []*WTS_SESSION_INFO = make([]*WTS_SESSION_INFO, 0)
)
if returnCode, _, err := procWTSEnumerateSessionsW.Call(WTS_CURRENT_SERVER_HANDLE, 0, 1, uintptr(unsafe.Pointer(&sessionInformation)), uintptr(unsafe.Pointer(&sessionCount))); returnCode == 0 {
return nil, fmt.Errorf("call native WTSEnumerateSessionsW: %s", err)
}
structSize := unsafe.Sizeof(WTS_SESSION_INFO{})
current := uintptr(sessionInformation)
for i := 0; i < sessionCount; i++ {
sessionList = append(sessionList, (*WTS_SESSION_INFO)(unsafe.Pointer(current)))
current += structSize
}
return sessionList, nil
}
// duplicateUserTokenFromSessionID will attempt
// to duplicate the user token for the user logged
// into the provided session ID
func duplicateUserTokenFromSessionID(sessionId windows.Handle) (windows.Token, error) {
var (
impersonationToken windows.Handle = 0
userToken windows.Token = 0
)
if returnCode, _, err := procWTSQueryUserToken.Call(uintptr(sessionId), uintptr(unsafe.Pointer(&impersonationToken))); returnCode == 0 {
return 0xFFFFFFFF, fmt.Errorf("call native WTSQueryUserToken: %s", err)
}
if returnCode, _, err := procDuplicateTokenEx.Call(uintptr(impersonationToken), 0, 0, uintptr(SecurityImpersonation), uintptr(TokenPrimary), uintptr(unsafe.Pointer(&userToken))); returnCode == 0 {
return 0xFFFFFFFF, fmt.Errorf("call native DuplicateTokenEx: %s", err)
}
if err := windows.CloseHandle(impersonationToken); err != nil {
return 0xFFFFFFFF, fmt.Errorf("close windows handle used for token duplication: %s", err)
}
return userToken, nil
}
func startProcessAsCurrentUser(appPath, cmdLine, workDir string) error {
var (
sessionId windows.Handle
userToken windows.Token
envInfo windows.Handle
startupInfo windows.StartupInfo
processInfo windows.ProcessInformation
commandLine uintptr = 0
workingDir uintptr = 0
err error
)
if sessionId, err = getCurrentUserSessionId(); err != nil {
return err
}
if userToken, err = duplicateUserTokenFromSessionID(sessionId); err != nil {
return fmt.Errorf("get duplicate user token for current user session: %s", err)
}
if returnCode, _, err := procCreateEnvironmentBlock.Call(uintptr(unsafe.Pointer(&envInfo)), uintptr(userToken), 1); returnCode == 0 {
return fmt.Errorf("create environment details for process: %s", err)
}
// TODO(lucas): Test out creation flags and startup info values.
creationFlags := CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE
startupInfo.ShowWindow = SW_SHOW
startupInfo.Desktop = windows.StringToUTF16Ptr("winsta0\\default")
if len(cmdLine) > 0 {
commandLine = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(cmdLine)))
}
if len(workDir) > 0 {
workingDir = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(workDir)))
}
if returnCode, _, err := procCreateProcessAsUser.Call(
uintptr(userToken), uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(appPath))), commandLine, 0, 0, 0,
uintptr(creationFlags), uintptr(envInfo), workingDir, uintptr(unsafe.Pointer(&startupInfo)), uintptr(unsafe.Pointer(&processInfo)),
); returnCode == 0 {
return fmt.Errorf("create process as user: %s", err)
}
return nil
}
// setupSyncChannel creates the synchronization channel through which child process will be
// signalled for termination. Code is using kernel named object as the sync primitive.
// https://learn.microsoft.com/en-us/windows/win32/sync/using-event-objects
func setupSyncChannel(channelId string) error {
if channelId == "" {
return errors.New("communication channel name should not be empty")
}
// converting go string to UTF16 windows compatible string
targetChannel := "Global\\comm-" + channelId
ev, err := windows.UTF16PtrFromString(targetChannel)
if (err != nil) && (err != windows.ERROR_SUCCESS) {
return fmt.Errorf("there was a problem generating UTF16 string: %w", err)
}
// checking first if channel is already present through the OpenEvent API
// OpenEvent API opens a named event object from the kernel object manager
// https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-openeventw
ha, err := windows.OpenEvent(windows.EVENT_ALL_ACCESS, false, ev)
if (ha != windows.InvalidHandle) && (err == nil) {
windows.CloseHandle(ha) // closing the handle to avoid handle leaks
return nil // channel is already present - nothing to do here
}
// Security descriptor to be used with event object
// Security descriptor is set to
// - Allow SYSTEM user to have full control
// (SY=SYSTEM, 0x001F0003=EVENT_ALL_ACCESS)
// - Allow Authenticated user to have only SYNCHRONIZE privilege
// (AU=Authenticated Users, 0x00100000=SYNCHRONIZE)
// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-security_descriptor
sd, err := windows.SecurityDescriptorFromString("D:(A;;0x001F0003;;;SY)(A;;0x00100000;;;AU)")
if err != nil {
return fmt.Errorf("SecurityDescriptorFromString failed: %v", err)
}
// Security attributes passed to the event object
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa379560(v=vs.85)
sa := windows.SecurityAttributes{
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
SecurityDescriptor: sd,
}
// CreateEvent Api creates the named event object on the kernel object manager
// The obtained handle is not closed on purpose, the lifetime of the event object
// is going to be bound to the lifetime of the service
// https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventw
h, err := windows.CreateEvent(&sa, 0, 0, ev)
if (err != nil) && (err != windows.ERROR_SUCCESS) {
return fmt.Errorf("there was a problem calling CreateEvent: %w", err)
}
if h == windows.InvalidHandle {
return errors.New("event handle is invalid")
}
return nil
}