|
|
@@ -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))
|
|
|
+}
|