From 82cd98609f1b15c2d52fa9acb569864903121779 Mon Sep 17 00:00:00 2001 From: Aaron Klotz Date: Thu, 10 Feb 2022 14:17:17 -0700 Subject: [PATCH] util/winutil: migrate corp's winutil into OSS. It makes the most sense to have all our utility functions reside in one place. There was nothing in corp that could not reasonably live in OSS. I also updated `StartProcessAsChild` to no longer depend on `futureexec`, thus reducing the amount of code that needed migration. I tested this change with `tswin` and it is working correctly. I have a follow-up PR to remove the corresponding code from corp. The migrated code was mostly written by @alexbrainman. Sourced from corp revision 03e90cfcc4dd7b8bc9b25eb13a26ec3a24ae0ef9 Signed-off-by: Aaron Klotz --- util/winutil/winutil_windows.go | 132 +++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 4 deletions(-) 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 @@ const ( 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)) +}