diff --git a/util/winutil/winutil_windows.go b/util/winutil/winutil_windows.go index cc0d065b1..d52694ec8 100644 --- a/util/winutil/winutil_windows.go +++ b/util/winutil/winutil_windows.go @@ -5,7 +5,11 @@ package winutil import ( + "errors" + "fmt" "log" + "os/exec" + "runtime" "syscall" "golang.org/x/sys/windows" @@ -17,16 +21,25 @@ regPolicyBase = `SOFTWARE\Policies\Tailscale` ) +// ErrNoShell is returned when the shell process is not found. +var ErrNoShell = errors.New("no Shell process is present") + // GetDesktopPID searches the PID of the process that's running the -// currently active desktop and whether it was found. +// currently active desktop. Returns ErrNoShell if the shell is not present. // Usually the PID will be for explorer.exe. -func GetDesktopPID() (pid uint32, ok bool) { +func GetDesktopPID() (uint32, error) { hwnd := windows.GetShellWindow() if hwnd == 0 { - return 0, false + return 0, ErrNoShell } + + var pid uint32 windows.GetWindowThreadProcessId(hwnd, &pid) - return pid, pid != 0 + if pid == 0 { + return 0, fmt.Errorf("invalid PID for HWND %v", hwnd) + } + + return pid, nil } func getPolicyString(name, defval string) string { @@ -130,3 +143,114 @@ func isSIDValidPrincipal(uid string) bool { return false } } + +// EnableCurrentThreadPrivilege enables the named privilege +// in the current thread access token. +func EnableCurrentThreadPrivilege(name string) error { + var t windows.Token + err := windows.OpenThreadToken(windows.CurrentThread(), + windows.TOKEN_QUERY|windows.TOKEN_ADJUST_PRIVILEGES, false, &t) + if err != nil { + return err + } + defer t.Close() + + var tp windows.Tokenprivileges + + privStr, err := syscall.UTF16PtrFromString(name) + if err != nil { + return err + } + err = windows.LookupPrivilegeValue(nil, privStr, &tp.Privileges[0].Luid) + if err != nil { + return err + } + tp.PrivilegeCount = 1 + tp.Privileges[0].Attributes = windows.SE_PRIVILEGE_ENABLED + return windows.AdjustTokenPrivileges(t, false, &tp, 0, nil, nil) +} + +// StartProcessAsChild starts exePath process as a child of parentPID. +// StartProcessAsChild copies parentPID's environment variables into +// the new process, along with any optional environment variables in extraEnv. +func StartProcessAsChild(parentPID uint32, exePath string, extraEnv []string) error { + // The rest of this function requires SeDebugPrivilege to be held. + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := windows.ImpersonateSelf(windows.SecurityImpersonation) + if err != nil { + return err + } + defer windows.RevertToSelf() + + // According to https://docs.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights + // + // ... To open a handle to another process and obtain full access rights, + // you must enable the SeDebugPrivilege privilege. ... + // + // But we only need PROCESS_CREATE_PROCESS. So perhaps SeDebugPrivilege is too much. + // + // https://devblogs.microsoft.com/oldnewthing/20080314-00/?p=23113 + // + // TODO: try look for something less than SeDebugPrivilege + + err = EnableCurrentThreadPrivilege("SeDebugPrivilege") + if err != nil { + return err + } + + ph, err := windows.OpenProcess( + windows.PROCESS_CREATE_PROCESS|windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_DUP_HANDLE, + false, parentPID) + if err != nil { + return err + } + defer windows.CloseHandle(ph) + + var pt windows.Token + err = windows.OpenProcessToken(ph, windows.TOKEN_QUERY, &pt) + if err != nil { + return err + } + defer pt.Close() + + env, err := pt.Environ(false) + if err != nil { + return err + + } + env = append(env, extraEnv...) + + sys := &syscall.SysProcAttr{ParentProcess: syscall.Handle(ph)} + + cmd := exec.Command(exePath) + cmd.Env = env + cmd.SysProcAttr = sys + + return cmd.Start() +} + +// StartProcessAsCurrentGUIUser is like StartProcessAsChild, but if finds +// current logged in user desktop process (normally explorer.exe), +// and passes found PID to StartProcessAsChild. +func StartProcessAsCurrentGUIUser(exePath string, extraEnv []string) error { + // as described in https://devblogs.microsoft.com/oldnewthing/20190425-00/?p=102443 + desktop, err := GetDesktopPID() + if err != nil { + return fmt.Errorf("failed to find desktop: %v", err) + } + err = StartProcessAsChild(desktop, exePath, extraEnv) + if err != nil { + return fmt.Errorf("failed to start executable: %v", err) + } + return nil +} + +// CreateAppMutex creates a named Windows mutex, returning nil if the mutex +// is created successfully or an error if the mutex already exists or could not +// be created for some other reason. +func CreateAppMutex(name string) (windows.Handle, error) { + return windows.CreateMutex(nil, false, windows.StringToUTF16Ptr(name)) +}