mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #33250 Waived most new failures. Planning to come back and fix some of them in subsequent PRs.
321 lines
11 KiB
Go
321 lines
11 KiB
Go
// nolint:gosec,G103,govet,unsafeptr // Reason: unsafe required for Windows API calls.
|
|
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"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"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
|
|
)
|
|
|
|
// Some Windows API functions pass these in a 32 bit DWORD others a 16 bit WORD.
|
|
// Not clear what the optimal type is in go, so casting may be required
|
|
type SW int
|
|
|
|
const (
|
|
SW_HIDE SW = 0
|
|
SW_SHOWNORMAL SW = 1
|
|
SW_NORMAL SW = 1
|
|
SW_SHOWMINIMIZED SW = 2
|
|
SW_SHOWMAXIMIZED SW = 3
|
|
SW_MAXIMIZE SW = 3
|
|
SW_SHOWNOACTIVATE SW = 4
|
|
SW_SHOW SW = 5
|
|
SW_MINIMIZE SW = 6
|
|
SW_SHOWMINNOACTIVE SW = 7
|
|
SW_SHOWNA SW = 8
|
|
SW_RESTORE SW = 9
|
|
SW_SHOWDEFAULT SW = 10
|
|
SW_MAX SW = 1
|
|
)
|
|
|
|
type WTS_SESSION_INFO struct {
|
|
SessionID windows.Handle
|
|
WinStationName *uint16
|
|
State WTS_CONNECTSTATE_CLASS
|
|
}
|
|
|
|
const (
|
|
CREATE_UNICODE_ENVIRONMENT int = 0x00000400
|
|
CREATE_NO_WINDOW int = 0x08000000
|
|
CREATE_NEW_CONSOLE int = 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) (lastLogs string, err 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])
|
|
}
|
|
|
|
var args []string
|
|
for _, arg := range opts.args {
|
|
args = append(args, arg[0])
|
|
if arg[1] != "" {
|
|
args = append(args, arg[1])
|
|
}
|
|
}
|
|
cmdLine := strings.Join(args, " ")
|
|
|
|
return "", startProcessAsCurrentUser(path, cmdLine, "")
|
|
}
|
|
|
|
func runWithOutput(path string, opts eopts) (output []byte, exitCode int, err error) {
|
|
return nil, 0, errors.New("not implemented")
|
|
}
|
|
|
|
func runWithStdin(path string, opts eopts) (io.WriteCloser, error) {
|
|
return nil, errors.New("not implemented")
|
|
}
|
|
|
|
// 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.
|
|
sessionId, _, err := procWTSGetActiveConsoleSessionId.Call()
|
|
if sessionId == 0xFFFFFFFF {
|
|
return 0xFFFFFFFF, fmt.Errorf("get current user session token: call native WTSGetActiveConsoleSessionId: %s", err)
|
|
}
|
|
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
|
|
sessionList = 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
|
|
userToken windows.Token
|
|
)
|
|
|
|
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
|
|
workingDir uintptr
|
|
|
|
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 = uint16(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) {
|
|
// Closing the handle to avoid handle leaks.
|
|
windows.CloseHandle(ha) //nolint:errcheck
|
|
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
|
|
}
|