@ -5,7 +5,11 @@
package winutil
package winutil
import (
import (
"errors"
"fmt"
"log"
"log"
"os/exec"
"runtime"
"syscall"
"syscall"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows"
@ -17,16 +21,25 @@ const (
regPolicyBase = ` SOFTWARE\Policies\Tailscale `
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
// 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.
// Usually the PID will be for explorer.exe.
func GetDesktopPID ( ) ( pid uint32 , ok bool ) {
func GetDesktopPID ( ) ( uint32 , error ) {
hwnd := windows . GetShellWindow ( )
hwnd := windows . GetShellWindow ( )
if hwnd == 0 {
if hwnd == 0 {
return 0 , false
return 0 , ErrNoShell
}
}
var pid uint32
windows . GetWindowThreadProcessId ( hwnd , & pid )
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 {
func getPolicyString ( name , defval string ) string {
@ -130,3 +143,114 @@ func isSIDValidPrincipal(uid string) bool {
return false
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 ) )
}