| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package winutil
- import (
- "errors"
- "fmt"
- "log"
- "os/exec"
- "os/user"
- "runtime"
- "strings"
- "syscall"
- "time"
- "unsafe"
- "golang.org/x/sys/windows"
- "golang.org/x/sys/windows/registry"
- )
- const (
- regBase = `SOFTWARE\Tailscale IPN`
- 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. Returns ErrNoShell if the shell is not present.
- // Usually the PID will be for explorer.exe.
- func GetDesktopPID() (uint32, error) {
- hwnd := windows.GetShellWindow()
- if hwnd == 0 {
- return 0, ErrNoShell
- }
- var pid uint32
- windows.GetWindowThreadProcessId(hwnd, &pid)
- if pid == 0 {
- return 0, fmt.Errorf("invalid PID for HWND %v", hwnd)
- }
- return pid, nil
- }
- func getPolicyString(name, defval string) string {
- s, err := getRegStringInternal(regPolicyBase, name)
- if err != nil {
- // Fall back to the legacy path
- return getRegString(name, defval)
- }
- return s
- }
- func getPolicyInteger(name string, defval uint64) uint64 {
- i, err := getRegIntegerInternal(regPolicyBase, name)
- if err != nil {
- // Fall back to the legacy path
- return getRegInteger(name, defval)
- }
- return i
- }
- func getRegString(name, defval string) string {
- s, err := getRegStringInternal(regBase, name)
- if err != nil {
- return defval
- }
- return s
- }
- func getRegInteger(name string, defval uint64) uint64 {
- i, err := getRegIntegerInternal(regBase, name)
- if err != nil {
- return defval
- }
- return i
- }
- func getRegStringInternal(subKey, name string) (string, error) {
- key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.READ)
- if err != nil {
- if err != registry.ErrNotExist {
- log.Printf("registry.OpenKey(%v): %v", subKey, err)
- }
- return "", err
- }
- defer key.Close()
- val, _, err := key.GetStringValue(name)
- if err != nil {
- if err != registry.ErrNotExist {
- log.Printf("registry.GetStringValue(%v): %v", name, err)
- }
- return "", err
- }
- return val, nil
- }
- // GetRegStrings looks up a registry value in the local machine path, or returns
- // the given default if it can't.
- func GetRegStrings(name string, defval []string) []string {
- s, err := getRegStringsInternal(regBase, name)
- if err != nil {
- return defval
- }
- return s
- }
- func getRegStringsInternal(subKey, name string) ([]string, error) {
- key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.READ)
- if err != nil {
- if err != registry.ErrNotExist {
- log.Printf("registry.OpenKey(%v): %v", subKey, err)
- }
- return nil, err
- }
- defer key.Close()
- val, _, err := key.GetStringsValue(name)
- if err != nil {
- if err != registry.ErrNotExist {
- log.Printf("registry.GetStringValue(%v): %v", name, err)
- }
- return nil, err
- }
- return val, nil
- }
- // SetRegStrings sets a MULTI_SZ value in the in the local machine path
- // to the strings specified by values.
- func SetRegStrings(name string, values []string) error {
- return setRegStringsInternal(regBase, name, values)
- }
- func setRegStringsInternal(subKey, name string, values []string) error {
- key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, subKey, registry.SET_VALUE)
- if err != nil {
- log.Printf("registry.CreateKey(%v): %v", subKey, err)
- }
- defer key.Close()
- return key.SetStringsValue(name, values)
- }
- // DeleteRegValue removes a registry value in the local machine path.
- func DeleteRegValue(name string) error {
- return deleteRegValueInternal(regBase, name)
- }
- func deleteRegValueInternal(subKey, name string) error {
- key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.SET_VALUE)
- if err == registry.ErrNotExist {
- return nil
- }
- if err != nil {
- log.Printf("registry.OpenKey(%v): %v", subKey, err)
- return err
- }
- defer key.Close()
- err = key.DeleteValue(name)
- if err == registry.ErrNotExist {
- err = nil
- }
- return err
- }
- func getRegIntegerInternal(subKey, name string) (uint64, error) {
- key, err := registry.OpenKey(registry.LOCAL_MACHINE, subKey, registry.READ)
- if err != nil {
- if err != registry.ErrNotExist {
- log.Printf("registry.OpenKey(%v): %v", subKey, err)
- }
- return 0, err
- }
- defer key.Close()
- val, _, err := key.GetIntegerValue(name)
- if err != nil {
- if err != registry.ErrNotExist {
- log.Printf("registry.GetIntegerValue(%v): %v", name, err)
- }
- return 0, err
- }
- return val, nil
- }
- var (
- kernel32 = syscall.NewLazyDLL("kernel32.dll")
- procWTSGetActiveConsoleSessionId = kernel32.NewProc("WTSGetActiveConsoleSessionId")
- )
- // TODO(crawshaw): replace with x/sys/windows... one day.
- // https://go-review.googlesource.com/c/sys/+/331909
- func WTSGetActiveConsoleSessionId() uint32 {
- r1, _, _ := procWTSGetActiveConsoleSessionId.Call()
- return uint32(r1)
- }
- func isSIDValidPrincipal(uid string) bool {
- usid, err := syscall.StringToSid(uid)
- if err != nil {
- return false
- }
- _, _, accType, err := usid.LookupAccount("")
- if err != nil {
- return false
- }
- switch accType {
- case syscall.SidTypeUser, syscall.SidTypeGroup, syscall.SidTypeDomain, syscall.SidTypeAlias, syscall.SidTypeWellKnownGroup, syscall.SidTypeComputer:
- return true
- default:
- // Reject deleted users, invalid SIDs, unknown SIDs, mandatory label SIDs, etc.
- 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))
- }
- func getTokenInfo(token windows.Token, infoClass uint32) ([]byte, error) {
- var desiredLen uint32
- err := windows.GetTokenInformation(token, infoClass, nil, 0, &desiredLen)
- if err != nil && err != windows.ERROR_INSUFFICIENT_BUFFER {
- return nil, err
- }
- buf := make([]byte, desiredLen)
- actualLen := desiredLen
- err = windows.GetTokenInformation(token, infoClass, &buf[0], desiredLen, &actualLen)
- return buf, err
- }
- func getTokenUserInfo(token windows.Token) (*windows.Tokenuser, error) {
- buf, err := getTokenInfo(token, windows.TokenUser)
- if err != nil {
- return nil, err
- }
- return (*windows.Tokenuser)(unsafe.Pointer(&buf[0])), nil
- }
- func getTokenPrimaryGroupInfo(token windows.Token) (*windows.Tokenprimarygroup, error) {
- buf, err := getTokenInfo(token, windows.TokenPrimaryGroup)
- if err != nil {
- return nil, err
- }
- return (*windows.Tokenprimarygroup)(unsafe.Pointer(&buf[0])), nil
- }
- // UserSIDs contains the SIDs for a Windows NT token object's associated user
- // as well as its primary group.
- type UserSIDs struct {
- User *windows.SID
- PrimaryGroup *windows.SID
- }
- // GetCurrentUserSIDs returns a UserSIDs struct containing SIDs for the
- // current process' user and primary group.
- func GetCurrentUserSIDs() (*UserSIDs, error) {
- token, err := windows.OpenCurrentProcessToken()
- if err != nil {
- return nil, err
- }
- defer token.Close()
- userInfo, err := getTokenUserInfo(token)
- if err != nil {
- return nil, err
- }
- primaryGroup, err := getTokenPrimaryGroupInfo(token)
- if err != nil {
- return nil, err
- }
- return &UserSIDs{userInfo.User.Sid, primaryGroup.PrimaryGroup}, nil
- }
- // IsCurrentProcessElevated returns true when the current process is
- // running with an elevated token, implying Administrator access.
- func IsCurrentProcessElevated() bool {
- token, err := windows.OpenCurrentProcessToken()
- if err != nil {
- return false
- }
- defer token.Close()
- return token.IsElevated()
- }
- // keyOpenTimeout is how long we wait for a registry key to appear. For some
- // reason, registry keys tied to ephemeral interfaces can take a long while to
- // appear after interface creation, and we can end up racing with that.
- const keyOpenTimeout = 20 * time.Second
- // RegistryPath represents a path inside a root registry.Key.
- type RegistryPath string
- // RegistryPathPrefix specifies a RegistryPath prefix that must be suffixed with
- // another RegistryPath to make a valid RegistryPath.
- type RegistryPathPrefix string
- // WithSuffix returns a RegistryPath with the given suffix appended.
- func (p RegistryPathPrefix) WithSuffix(suf string) RegistryPath {
- return RegistryPath(string(p) + suf)
- }
- const (
- IPv4TCPIPBase RegistryPath = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters`
- IPv6TCPIPBase RegistryPath = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters`
- NetBTBase RegistryPath = `SYSTEM\CurrentControlSet\Services\NetBT\Parameters`
- IPv4TCPIPInterfacePrefix RegistryPathPrefix = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\`
- IPv6TCPIPInterfacePrefix RegistryPathPrefix = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\`
- NetBTInterfacePrefix RegistryPathPrefix = `SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces\Tcpip_`
- )
- // ErrKeyWaitTimeout is returned by OpenKeyWait when calls timeout.
- var ErrKeyWaitTimeout = errors.New("timeout waiting for registry key")
- // OpenKeyWait opens a registry key, waiting for it to appear if necessary. It
- // returns the opened key, or ErrKeyWaitTimeout if the key does not appear
- // within 20s. The caller must call Close on the returned key.
- func OpenKeyWait(k registry.Key, path RegistryPath, access uint32) (registry.Key, error) {
- runtime.LockOSThread()
- defer runtime.UnlockOSThread()
- deadline := time.Now().Add(keyOpenTimeout)
- pathSpl := strings.Split(string(path), "\\")
- for i := 0; ; i++ {
- keyName := pathSpl[i]
- isLast := i+1 == len(pathSpl)
- event, err := windows.CreateEvent(nil, 0, 0, nil)
- if err != nil {
- return 0, fmt.Errorf("windows.CreateEvent: %w", err)
- }
- defer windows.CloseHandle(event)
- var key registry.Key
- for {
- err = windows.RegNotifyChangeKeyValue(windows.Handle(k), false, windows.REG_NOTIFY_CHANGE_NAME, event, true)
- if err != nil {
- return 0, fmt.Errorf("windows.RegNotifyChangeKeyValue: %w", err)
- }
- var accessFlags uint32
- if isLast {
- accessFlags = access
- } else {
- accessFlags = registry.NOTIFY
- }
- key, err = registry.OpenKey(k, keyName, accessFlags)
- if err == windows.ERROR_FILE_NOT_FOUND || err == windows.ERROR_PATH_NOT_FOUND {
- timeout := time.Until(deadline) / time.Millisecond
- if timeout < 0 {
- timeout = 0
- }
- s, err := windows.WaitForSingleObject(event, uint32(timeout))
- if err != nil {
- return 0, fmt.Errorf("windows.WaitForSingleObject: %w", err)
- }
- if s == uint32(windows.WAIT_TIMEOUT) { // windows.WAIT_TIMEOUT status const is misclassified as error in golang.org/x/sys/windows
- return 0, ErrKeyWaitTimeout
- }
- } else if err != nil {
- return 0, fmt.Errorf("registry.OpenKey(%v): %w", path, err)
- } else {
- if isLast {
- return key, nil
- }
- defer key.Close()
- break
- }
- }
- k = key
- }
- }
- func lookupPseudoUser(uid string) (*user.User, error) {
- sid, err := windows.StringToSid(uid)
- if err != nil {
- return nil, err
- }
- // We're looking for SIDs "S-1-5-x" where 17 <= x <= 20.
- // This is checking for the the "5"
- if sid.IdentifierAuthority() != windows.SECURITY_NT_AUTHORITY {
- return nil, fmt.Errorf(`SID %q does not use "NT AUTHORITY"`, uid)
- }
- // This is ensuring that there is only one sub-authority.
- // In other words, only one value after the "5".
- if sid.SubAuthorityCount() != 1 {
- return nil, fmt.Errorf("SID %q should have only one subauthority", uid)
- }
- // Get that sub-authority value (this is "x" above) and check it.
- rid := sid.SubAuthority(0)
- if rid < 17 || rid > 20 {
- return nil, fmt.Errorf("SID %q does not represent a known pseudo-user", uid)
- }
- // We've got one of the known pseudo-users. Look up the localized name of the
- // account.
- username, domain, _, err := sid.LookupAccount("")
- if err != nil {
- return nil, err
- }
- // This call is best-effort. If it fails, homeDir will be empty.
- homeDir, _ := findHomeDirInRegistry(uid)
- result := &user.User{
- Uid: uid,
- Gid: uid, // Gid == Uid with these accounts.
- Username: fmt.Sprintf(`%s\%s`, domain, username),
- Name: username,
- HomeDir: homeDir,
- }
- return result, nil
- }
- // findHomeDirInRegistry finds the user home path based on the uid.
- // This is borrowed from Go's std lib.
- func findHomeDirInRegistry(uid string) (dir string, err error) {
- k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+uid, registry.QUERY_VALUE)
- if err != nil {
- return "", err
- }
- defer k.Close()
- dir, _, err = k.GetStringValue("ProfileImagePath")
- if err != nil {
- return "", err
- }
- return dir, nil
- }
|