mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
950 lines
29 KiB
Go
950 lines
29 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package winutil
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"os/exec"
|
|
"os/user"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"golang.org/x/exp/constraints"
|
|
"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")
|
|
|
|
// ErrNoValue is returned when the value doesn't exist in the registry.
|
|
var ErrNoValue = registry.ErrNotExist
|
|
|
|
// 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 string) (string, error) {
|
|
s, err := getRegStringInternal(registry.LOCAL_MACHINE, regPolicyBase, name)
|
|
if err != nil {
|
|
// Fall back to the legacy path
|
|
return getRegString(name)
|
|
}
|
|
return s, err
|
|
}
|
|
|
|
func getPolicyStringArray(name string) ([]string, error) {
|
|
return getRegStringsInternal(regPolicyBase, name)
|
|
}
|
|
|
|
func getRegString(name string) (string, error) {
|
|
s, err := getRegStringInternal(registry.LOCAL_MACHINE, regBase, name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return s, err
|
|
}
|
|
|
|
func getPolicyInteger(name string) (uint64, error) {
|
|
i, err := getRegIntegerInternal(regPolicyBase, name)
|
|
if err != nil {
|
|
// Fall back to the legacy path
|
|
return getRegInteger(name)
|
|
}
|
|
return i, err
|
|
}
|
|
|
|
func getRegInteger(name string) (uint64, error) {
|
|
i, err := getRegIntegerInternal(regBase, name)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return i, err
|
|
}
|
|
|
|
func getRegStringInternal(key registry.Key, subKey, name string) (string, error) {
|
|
key, err := registry.OpenKey(key, subKey, registry.READ)
|
|
if err != nil {
|
|
if err != ErrNoValue {
|
|
log.Printf("registry.OpenKey(%v): %v", subKey, err)
|
|
}
|
|
return "", err
|
|
}
|
|
defer key.Close()
|
|
|
|
val, _, err := key.GetStringValue(name)
|
|
if err != nil {
|
|
if err != ErrNoValue {
|
|
log.Printf("registry.GetStringValue(%v): %v", name, err)
|
|
}
|
|
return "", err
|
|
}
|
|
return val, nil
|
|
}
|
|
|
|
// GetRegUserString looks up a registry path in the current user key, or returns
|
|
// an empty string and error.
|
|
func GetRegUserString(name string) (string, error) {
|
|
return getRegStringInternal(registry.CURRENT_USER, regBase, name)
|
|
}
|
|
|
|
// SetRegUserString sets a SZ value identified by name in the current user key
|
|
// to the string specified by value.
|
|
func SetRegUserString(name, value string) error {
|
|
key, _, err := registry.CreateKey(registry.CURRENT_USER, regBase, registry.SET_VALUE)
|
|
if err != nil {
|
|
log.Printf("registry.CreateKey(%v): %v", regBase, err)
|
|
}
|
|
defer key.Close()
|
|
|
|
return key.SetStringValue(name, value)
|
|
}
|
|
|
|
// 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 != ErrNoValue {
|
|
log.Printf("registry.OpenKey(%v): %v", subKey, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
defer key.Close()
|
|
|
|
val, _, err := key.GetStringsValue(name)
|
|
if err != nil {
|
|
if err != ErrNoValue {
|
|
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 == ErrNoValue {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
log.Printf("registry.OpenKey(%v): %v", subKey, err)
|
|
return err
|
|
}
|
|
defer key.Close()
|
|
|
|
err = key.DeleteValue(name)
|
|
if err == ErrNoValue {
|
|
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 != ErrNoValue {
|
|
log.Printf("registry.OpenKey(%v): %v", subKey, err)
|
|
}
|
|
return 0, err
|
|
}
|
|
defer key.Close()
|
|
|
|
val, _, err := key.GetIntegerValue(name)
|
|
if err != nil {
|
|
if err != ErrNoValue {
|
|
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's access token. The current goroutine is also locked to
|
|
// the OS thread (runtime.LockOSThread). Callers must call the returned disable
|
|
// function when done with the privileged task.
|
|
func EnableCurrentThreadPrivilege(name string) (disable func(), err error) {
|
|
return EnableCurrentThreadPrivileges([]string{name})
|
|
}
|
|
|
|
// EnableCurrentThreadPrivileges enables the named privileges
|
|
// in the current thread's access token. The current goroutine is also locked to
|
|
// the OS thread (runtime.LockOSThread). Callers must call the returned disable
|
|
// function when done with the privileged task.
|
|
func EnableCurrentThreadPrivileges(names []string) (disable func(), err error) {
|
|
runtime.LockOSThread()
|
|
if len(names) == 0 {
|
|
// Nothing to enable; no-op isn't really an error...
|
|
return runtime.UnlockOSThread, nil
|
|
}
|
|
|
|
if err := windows.ImpersonateSelf(windows.SecurityImpersonation); err != nil {
|
|
runtime.UnlockOSThread()
|
|
return nil, err
|
|
}
|
|
|
|
disable = func() {
|
|
defer runtime.UnlockOSThread()
|
|
// If RevertToSelf fails, it's not really recoverable and we should panic.
|
|
// Failure to do so would leak the privileges we're enabling, which is a
|
|
// security issue.
|
|
if err := windows.RevertToSelf(); err != nil {
|
|
panic(fmt.Sprintf("RevertToSelf failed: %v", err))
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
disable()
|
|
}
|
|
}()
|
|
|
|
var t windows.Token
|
|
err = windows.OpenThreadToken(windows.CurrentThread(),
|
|
windows.TOKEN_QUERY|windows.TOKEN_ADJUST_PRIVILEGES, false, &t)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer t.Close()
|
|
|
|
tp := newTokenPrivileges(len(names))
|
|
privs := tp.AllPrivileges()
|
|
for i := range privs {
|
|
var privStr *uint16
|
|
privStr, err = windows.UTF16PtrFromString(names[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = windows.LookupPrivilegeValue(nil, privStr, &privs[i].Luid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
privs[i].Attributes = windows.SE_PRIVILEGE_ENABLED
|
|
}
|
|
|
|
err = windows.AdjustTokenPrivileges(t, false, tp, 0, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return disable, nil
|
|
}
|
|
|
|
func newTokenPrivileges(numPrivs int) *windows.Tokenprivileges {
|
|
if numPrivs <= 0 {
|
|
panic("numPrivs must be > 0")
|
|
}
|
|
numBytes := unsafe.Sizeof(windows.Tokenprivileges{}) + (uintptr(numPrivs-1) * unsafe.Sizeof(windows.LUIDAndAttributes{}))
|
|
buf := make([]byte, numBytes)
|
|
result := (*windows.Tokenprivileges)(unsafe.Pointer(unsafe.SliceData(buf)))
|
|
result.PrivilegeCount = uint32(numPrivs)
|
|
return result
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// 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
|
|
|
|
disableSeDebug, err := EnableCurrentThreadPrivilege("SeDebugPrivilege")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer disableSeDebug()
|
|
|
|
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))
|
|
}
|
|
|
|
// getTokenInfoFixedLen obtains known fixed-length token information. Use this
|
|
// function for information classes that output enumerations, BOOLs, integers etc.
|
|
func getTokenInfoFixedLen[T any](token windows.Token, infoClass uint32) (result T, err error) {
|
|
var actualLen uint32
|
|
p := (*byte)(unsafe.Pointer(&result))
|
|
err = windows.GetTokenInformation(token, infoClass, p, uint32(unsafe.Sizeof(result)), &actualLen)
|
|
return result, err
|
|
}
|
|
|
|
type tokenElevationType int32
|
|
|
|
const (
|
|
tokenElevationTypeDefault tokenElevationType = 1
|
|
tokenElevationTypeFull tokenElevationType = 2
|
|
tokenElevationTypeLimited tokenElevationType = 3
|
|
)
|
|
|
|
// IsTokenLimited returns whether token is a limited UAC token.
|
|
func IsTokenLimited(token windows.Token) (bool, error) {
|
|
elevationType, err := getTokenInfoFixedLen[tokenElevationType](token, windows.TokenElevationType)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return elevationType == tokenElevationTypeLimited, 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 := token.GetTokenUser()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
primaryGroup, err := token.GetTokenPrimaryGroup()
|
|
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
|
|
}
|
|
|
|
// ProcessImageName returns the fully-qualified path to the executable image
|
|
// associated with process.
|
|
func ProcessImageName(process windows.Handle) (string, error) {
|
|
var pathBuf [windows.MAX_PATH]uint16
|
|
pathBufLen := uint32(len(pathBuf))
|
|
if err := windows.QueryFullProcessImageName(process, 0, &pathBuf[0], &pathBufLen); err != nil {
|
|
return "", err
|
|
}
|
|
return windows.UTF16ToString(pathBuf[:pathBufLen]), nil
|
|
}
|
|
|
|
// TSSessionIDToLogonSessionID retrieves the logon session ID associated with
|
|
// tsSessionId, which is a Terminal Services / RDP session ID. The calling
|
|
// process must be running as LocalSystem.
|
|
func TSSessionIDToLogonSessionID(tsSessionID uint32) (logonSessionID windows.LUID, err error) {
|
|
var token windows.Token
|
|
if err := windows.WTSQueryUserToken(tsSessionID, &token); err != nil {
|
|
return logonSessionID, fmt.Errorf("WTSQueryUserToken: %w", err)
|
|
}
|
|
defer token.Close()
|
|
return LogonSessionID(token)
|
|
}
|
|
|
|
// TSSessionID obtains the Terminal Services (RDP) session ID associated with token.
|
|
func TSSessionID(token windows.Token) (tsSessionID uint32, err error) {
|
|
return getTokenInfoFixedLen[uint32](token, windows.TokenSessionId)
|
|
}
|
|
|
|
type tokenOrigin struct {
|
|
originatingLogonSession windows.LUID
|
|
}
|
|
|
|
// LogonSessionID obtains the logon session ID associated with token.
|
|
func LogonSessionID(token windows.Token) (logonSessionID windows.LUID, err error) {
|
|
origin, err := getTokenInfoFixedLen[tokenOrigin](token, windows.TokenOrigin)
|
|
if err != nil {
|
|
return logonSessionID, err
|
|
}
|
|
|
|
return origin.originatingLogonSession, nil
|
|
}
|
|
|
|
// BufUnit is a type constraint for buffers passed into AllocateContiguousBuffer
|
|
// and SetNTString.
|
|
type BufUnit interface {
|
|
byte | uint16
|
|
}
|
|
|
|
// AllocateContiguousBuffer allocates memory to satisfy the Windows idiom where
|
|
// some structs contain pointers that are expected to refer to memory within the
|
|
// same buffer containing the struct itself. T is the type that contains
|
|
// the pointers. values must contain the actual data that is to be copied
|
|
// into the buffer after T. AllocateContiguousBuffer returns a pointer to the
|
|
// struct, the total length of the buffer in bytes, and a slice containing
|
|
// each value within the buffer. The caller may use slcs to populate any
|
|
// pointers in t as needed. Each element of slcs corresponds to the element of
|
|
// values in the same position.
|
|
//
|
|
// It is the responsibility of the caller to ensure that any values expected
|
|
// to contain null-terminated strings are in fact null-terminated!
|
|
//
|
|
// AllocateContiguousBuffer panics if no values are passed in, as there are
|
|
// better alternatives for allocating a struct in that case.
|
|
func AllocateContiguousBuffer[T any, BU BufUnit](values ...[]BU) (t *T, tLenBytes uint32, slcs [][]BU) {
|
|
if len(values) == 0 {
|
|
panic("len(values) must be > 0")
|
|
}
|
|
|
|
// Get the sizes of T and BU, then compute a preferred alignment for T.
|
|
tT := reflect.TypeFor[T]()
|
|
szT := tT.Size()
|
|
szBU := int(unsafe.Sizeof(BU(0)))
|
|
alignment := max(tT.Align(), szBU)
|
|
|
|
// Our buffers for values will start at the next szBU boundary.
|
|
tLenBytes = alignUp(uint32(szT), szBU)
|
|
firstValueOffset := tLenBytes
|
|
|
|
// Accumulate the length of each value into tLenBytes
|
|
for _, v := range values {
|
|
tLenBytes += uint32(len(v) * szBU)
|
|
}
|
|
|
|
// Now that we know the final length, align up to our preferred boundary.
|
|
tLenBytes = alignUp(tLenBytes, alignment)
|
|
|
|
// Allocate the buffer. We choose a type for the slice that is appropriate
|
|
// for the desired alignment. Note that we do not have a strict requirement
|
|
// that T contain pointer fields; we could just be appending more data
|
|
// within the same buffer.
|
|
bufLen := tLenBytes / uint32(alignment)
|
|
var pt unsafe.Pointer
|
|
switch alignment {
|
|
case 1:
|
|
pt = unsafe.Pointer(unsafe.SliceData(make([]byte, bufLen)))
|
|
case 2:
|
|
pt = unsafe.Pointer(unsafe.SliceData(make([]uint16, bufLen)))
|
|
case 4:
|
|
pt = unsafe.Pointer(unsafe.SliceData(make([]uint32, bufLen)))
|
|
case 8:
|
|
pt = unsafe.Pointer(unsafe.SliceData(make([]uint64, bufLen)))
|
|
default:
|
|
panic(fmt.Sprintf("bad alignment %d", alignment))
|
|
}
|
|
|
|
t = (*T)(pt)
|
|
slcs = make([][]BU, 0, len(values))
|
|
|
|
// Use the limits of the buffer area after t to construct a slice representing the remaining buffer.
|
|
firstValuePtr := unsafe.Pointer(uintptr(pt) + uintptr(firstValueOffset))
|
|
buf := unsafe.Slice((*BU)(firstValuePtr), (tLenBytes-firstValueOffset)/uint32(szBU))
|
|
|
|
// Copy each value into the buffer and record a slice describing each value's limits into slcs.
|
|
var index int
|
|
for _, v := range values {
|
|
if len(v) == 0 {
|
|
// We allow zero-length values; we simply append a nil slice.
|
|
slcs = append(slcs, nil)
|
|
continue
|
|
}
|
|
valueSlice := buf[index : index+len(v)]
|
|
copy(valueSlice, v)
|
|
slcs = append(slcs, valueSlice)
|
|
index += len(v)
|
|
}
|
|
|
|
return t, tLenBytes, slcs
|
|
}
|
|
|
|
// alignment must be a power of 2
|
|
func alignUp[V constraints.Integer](v V, alignment int) V {
|
|
return v + ((-v) & (V(alignment) - 1))
|
|
}
|
|
|
|
// NTStr is a type constraint requiring the type to be either a
|
|
// windows.NTString or a windows.NTUnicodeString.
|
|
type NTStr interface {
|
|
windows.NTString | windows.NTUnicodeString
|
|
}
|
|
|
|
// SetNTString sets the value of nts in-place to point to the string contained
|
|
// within buf. A nul terminator is optional in buf.
|
|
func SetNTString[NTS NTStr, BU BufUnit](nts *NTS, buf []BU) {
|
|
isEmpty := len(buf) == 0
|
|
codeUnitSize := uint16(unsafe.Sizeof(BU(0)))
|
|
lenBytes := len(buf) * int(codeUnitSize)
|
|
if lenBytes > math.MaxUint16 {
|
|
panic("buffer length must fit into uint16")
|
|
}
|
|
lenBytes16 := uint16(lenBytes)
|
|
|
|
switch p := any(nts).(type) {
|
|
case *windows.NTString:
|
|
if isEmpty {
|
|
*p = windows.NTString{}
|
|
break
|
|
}
|
|
p.Buffer = unsafe.SliceData(any(buf).([]byte))
|
|
p.MaximumLength = lenBytes16
|
|
p.Length = lenBytes16
|
|
// account for nul terminator when present
|
|
if buf[len(buf)-1] == 0 {
|
|
p.Length -= codeUnitSize
|
|
}
|
|
case *windows.NTUnicodeString:
|
|
if isEmpty {
|
|
*p = windows.NTUnicodeString{}
|
|
break
|
|
}
|
|
p.Buffer = unsafe.SliceData(any(buf).([]uint16))
|
|
p.MaximumLength = lenBytes16
|
|
p.Length = lenBytes16
|
|
// account for nul terminator when present
|
|
if buf[len(buf)-1] == 0 {
|
|
p.Length -= codeUnitSize
|
|
}
|
|
default:
|
|
panic("unknown type")
|
|
}
|
|
}
|
|
|
|
type domainControllerAddressType uint32
|
|
|
|
const (
|
|
//lint:ignore U1000 maps to a win32 API
|
|
_DS_INET_ADDRESS domainControllerAddressType = 1
|
|
_DS_NETBIOS_ADDRESS domainControllerAddressType = 2
|
|
)
|
|
|
|
type domainControllerFlag uint32
|
|
|
|
const (
|
|
//lint:ignore U1000 maps to a win32 API
|
|
_DS_PDC_FLAG domainControllerFlag = 0x00000001
|
|
_DS_GC_FLAG domainControllerFlag = 0x00000004
|
|
_DS_LDAP_FLAG domainControllerFlag = 0x00000008
|
|
_DS_DS_FLAG domainControllerFlag = 0x00000010
|
|
_DS_KDC_FLAG domainControllerFlag = 0x00000020
|
|
_DS_TIMESERV_FLAG domainControllerFlag = 0x00000040
|
|
_DS_CLOSEST_FLAG domainControllerFlag = 0x00000080
|
|
_DS_WRITABLE_FLAG domainControllerFlag = 0x00000100
|
|
_DS_GOOD_TIMESERV_FLAG domainControllerFlag = 0x00000200
|
|
_DS_NDNC_FLAG domainControllerFlag = 0x00000400
|
|
_DS_SELECT_SECRET_DOMAIN_6_FLAG domainControllerFlag = 0x00000800
|
|
_DS_FULL_SECRET_DOMAIN_6_FLAG domainControllerFlag = 0x00001000
|
|
_DS_WS_FLAG domainControllerFlag = 0x00002000
|
|
_DS_DS_8_FLAG domainControllerFlag = 0x00004000
|
|
_DS_DS_9_FLAG domainControllerFlag = 0x00008000
|
|
_DS_DS_10_FLAG domainControllerFlag = 0x00010000
|
|
_DS_KEY_LIST_FLAG domainControllerFlag = 0x00020000
|
|
_DS_PING_FLAGS domainControllerFlag = 0x000FFFFF
|
|
_DS_DNS_CONTROLLER_FLAG domainControllerFlag = 0x20000000
|
|
_DS_DNS_DOMAIN_FLAG domainControllerFlag = 0x40000000
|
|
_DS_DNS_FOREST_FLAG domainControllerFlag = 0x80000000
|
|
)
|
|
|
|
type _DOMAIN_CONTROLLER_INFO struct {
|
|
DomainControllerName *uint16
|
|
DomainControllerAddress *uint16
|
|
DomainControllerAddressType domainControllerAddressType
|
|
DomainGuid windows.GUID
|
|
DomainName *uint16
|
|
DnsForestName *uint16
|
|
Flags domainControllerFlag
|
|
DcSiteName *uint16
|
|
ClientSiteName *uint16
|
|
}
|
|
|
|
func (dci *_DOMAIN_CONTROLLER_INFO) Close() error {
|
|
if dci == nil {
|
|
return nil
|
|
}
|
|
return windows.NetApiBufferFree((*byte)(unsafe.Pointer(dci)))
|
|
}
|
|
|
|
type dsGetDcNameFlag uint32
|
|
|
|
const (
|
|
//lint:ignore U1000 maps to a win32 API
|
|
_DS_FORCE_REDISCOVERY dsGetDcNameFlag = 0x00000001
|
|
_DS_DIRECTORY_SERVICE_REQUIRED dsGetDcNameFlag = 0x00000010
|
|
_DS_DIRECTORY_SERVICE_PREFERRED dsGetDcNameFlag = 0x00000020
|
|
_DS_GC_SERVER_REQUIRED dsGetDcNameFlag = 0x00000040
|
|
_DS_PDC_REQUIRED dsGetDcNameFlag = 0x00000080
|
|
_DS_BACKGROUND_ONLY dsGetDcNameFlag = 0x00000100
|
|
_DS_IP_REQUIRED dsGetDcNameFlag = 0x00000200
|
|
_DS_KDC_REQUIRED dsGetDcNameFlag = 0x00000400
|
|
_DS_TIMESERV_REQUIRED dsGetDcNameFlag = 0x00000800
|
|
_DS_WRITABLE_REQUIRED dsGetDcNameFlag = 0x00001000
|
|
_DS_GOOD_TIMESERV_PREFERRED dsGetDcNameFlag = 0x00002000
|
|
_DS_AVOID_SELF dsGetDcNameFlag = 0x00004000
|
|
_DS_ONLY_LDAP_NEEDED dsGetDcNameFlag = 0x00008000
|
|
_DS_IS_FLAT_NAME dsGetDcNameFlag = 0x00010000
|
|
_DS_IS_DNS_NAME dsGetDcNameFlag = 0x00020000
|
|
_DS_TRY_NEXTCLOSEST_SITE dsGetDcNameFlag = 0x00040000
|
|
_DS_DIRECTORY_SERVICE_6_REQUIRED dsGetDcNameFlag = 0x00080000
|
|
_DS_WEB_SERVICE_REQUIRED dsGetDcNameFlag = 0x00100000
|
|
_DS_DIRECTORY_SERVICE_8_REQUIRED dsGetDcNameFlag = 0x00200000
|
|
_DS_DIRECTORY_SERVICE_9_REQUIRED dsGetDcNameFlag = 0x00400000
|
|
_DS_DIRECTORY_SERVICE_10_REQUIRED dsGetDcNameFlag = 0x00800000
|
|
_DS_KEY_LIST_SUPPORT_REQUIRED dsGetDcNameFlag = 0x01000000
|
|
_DS_RETURN_DNS_NAME dsGetDcNameFlag = 0x40000000
|
|
_DS_RETURN_FLAT_NAME dsGetDcNameFlag = 0x80000000
|
|
)
|
|
|
|
func resolveDomainController(domainName *uint16, domainGUID *windows.GUID) (*_DOMAIN_CONTROLLER_INFO, error) {
|
|
const flags = _DS_DIRECTORY_SERVICE_REQUIRED | _DS_IS_FLAT_NAME | _DS_RETURN_DNS_NAME
|
|
var dcInfo *_DOMAIN_CONTROLLER_INFO
|
|
if err := dsGetDcName(nil, domainName, domainGUID, nil, flags, &dcInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
return dcInfo, nil
|
|
}
|
|
|
|
// ResolveDomainController resolves the DNS name of the nearest available
|
|
// domain controller for the domain specified by domainName.
|
|
func ResolveDomainController(domainName string) (string, error) {
|
|
domainName16, err := windows.UTF16PtrFromString(domainName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
dcInfo, err := resolveDomainController(domainName16, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer dcInfo.Close()
|
|
|
|
return windows.UTF16PtrToString(dcInfo.DomainControllerName), nil
|
|
}
|
|
|
|
type _NETSETUP_NAME_TYPE int32
|
|
|
|
const (
|
|
_NetSetupUnknown _NETSETUP_NAME_TYPE = 0
|
|
_NetSetupMachine _NETSETUP_NAME_TYPE = 1
|
|
_NetSetupWorkgroup _NETSETUP_NAME_TYPE = 2
|
|
_NetSetupDomain _NETSETUP_NAME_TYPE = 3
|
|
_NetSetupNonExistentDomain _NETSETUP_NAME_TYPE = 4
|
|
_NetSetupDnsMachine _NETSETUP_NAME_TYPE = 5
|
|
)
|
|
|
|
func isDomainName(name *uint16) (bool, error) {
|
|
err := netValidateName(nil, name, nil, nil, _NetSetupDomain)
|
|
switch err {
|
|
case nil:
|
|
return true, nil
|
|
case windows.ERROR_NO_SUCH_DOMAIN:
|
|
return false, nil
|
|
default:
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
// IsDomainName checks whether name represents an existing domain reachable by
|
|
// the current machine.
|
|
func IsDomainName(name string) (bool, error) {
|
|
name16, err := windows.UTF16PtrFromString(name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return isDomainName(name16)
|
|
}
|