From da078b4c09993c3df4f7838e534fa92a2acba2e2 Mon Sep 17 00:00:00 2001 From: Aaron Klotz Date: Wed, 5 Jun 2024 14:50:18 -0600 Subject: [PATCH] util/winutil: add package for logging into Windows via Service-for-User (S4U) This PR ties together pseudoconsoles, user profiles, s4u logons, and process creation into what is (hopefully) a simple API for various Tailscale services to obtain Windows access tokens without requiring knowledge of any Windows passwords. It works both for domain-joined machines (Kerberos) and non-domain-joined machines. The former case is fairly straightforward as it is fully documented. OTOH, the latter case is not documented, though it is fully defined in the C headers in the Windows SDK. The documentation blanks were filled in by reading the source code of Microsoft's Win32 port of OpenSSH. We need to do a bit of acrobatics to make conpty work correctly while creating a child process with an s4u token; see the doc comments above startProcessInternal for details. Updates #12383 Signed-off-by: Aaron Klotz --- util/winutil/restartmgr_windows.go | 11 +- util/winutil/s4u/lsa_windows.go | 399 ++++++++++++ util/winutil/s4u/mksyscall.go | 16 + util/winutil/s4u/s4u_windows.go | 941 +++++++++++++++++++++++++++ util/winutil/s4u/zsyscall_windows.go | 104 +++ 5 files changed, 1467 insertions(+), 4 deletions(-) create mode 100644 util/winutil/s4u/lsa_windows.go create mode 100644 util/winutil/s4u/mksyscall.go create mode 100644 util/winutil/s4u/s4u_windows.go create mode 100644 util/winutil/s4u/zsyscall_windows.go diff --git a/util/winutil/restartmgr_windows.go b/util/winutil/restartmgr_windows.go index ee10559b8..a52e2fee9 100644 --- a/util/winutil/restartmgr_windows.go +++ b/util/winutil/restartmgr_windows.go @@ -23,8 +23,7 @@ import ( ) var ( - // ErrDefunctProcess is returned by (*UniqueProcess).AsRestartableProcess - // when the process no longer exists. + // ErrDefunctProcess is returned when the process no longer exists. ErrDefunctProcess = errors.New("process is defunct") // ErrProcessNotRestartable is returned by (*UniqueProcess).AsRestartableProcess // when the process has previously indicated that it must not be restarted @@ -799,7 +798,7 @@ func startProcessInSessionInternal(sessID SessionID, cmdLineInfo CommandLineInfo if err != nil { return nil, fmt.Errorf("token environment: %w", err) } - env16 := newEnvBlock(env) + env16 := NewEnvBlock(env) // The privileges in privNames are required for CreateProcessAsUser to be // able to start processes as other users in other logon sessions. @@ -826,7 +825,11 @@ func startProcessInSessionInternal(sessID SessionID, cmdLineInfo CommandLineInfo return &pi, nil } -func newEnvBlock(env []string) *uint16 { +// NewEnvBlock processes a slice of strings containing "NAME=value" pairs +// representing a process envionment into the environment block format used by +// Windows APIs such as CreateProcess. env must be sorted case-insensitively +// by variable name. +func NewEnvBlock(env []string) *uint16 { // Intentionally using bytes.Buffer here because we're writing nul bytes (the standard library does this too). var buf bytes.Buffer for _, v := range env { diff --git a/util/winutil/s4u/lsa_windows.go b/util/winutil/s4u/lsa_windows.go new file mode 100644 index 000000000..3ff2171f9 --- /dev/null +++ b/util/winutil/s4u/lsa_windows.go @@ -0,0 +1,399 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package s4u + +import ( + "errors" + "fmt" + "os" + "os/user" + "path/filepath" + "strings" + "unicode" + "unsafe" + + "github.com/dblohm7/wingoes" + "golang.org/x/sys/windows" + "tailscale.com/types/lazy" + "tailscale.com/util/winutil" + "tailscale.com/util/winutil/winenv" +) + +const ( + _MICROSOFT_KERBEROS_NAME = "Kerberos" + _MSV1_0_PACKAGE_NAME = "MICROSOFT_AUTHENTICATION_PACKAGE_V1_0" +) + +type _LSAHANDLE windows.Handle +type _LSA_OPERATIONAL_MODE uint32 + +type _KERB_LOGON_SUBMIT_TYPE int32 + +const ( + _KerbInteractiveLogon _KERB_LOGON_SUBMIT_TYPE = 2 + _KerbSmartCardLogon _KERB_LOGON_SUBMIT_TYPE = 6 + _KerbWorkstationUnlockLogon _KERB_LOGON_SUBMIT_TYPE = 7 + _KerbSmartCardUnlockLogon _KERB_LOGON_SUBMIT_TYPE = 8 + _KerbProxyLogon _KERB_LOGON_SUBMIT_TYPE = 9 + _KerbTicketLogon _KERB_LOGON_SUBMIT_TYPE = 10 + _KerbTicketUnlockLogon _KERB_LOGON_SUBMIT_TYPE = 11 + _KerbS4ULogon _KERB_LOGON_SUBMIT_TYPE = 12 + _KerbCertificateLogon _KERB_LOGON_SUBMIT_TYPE = 13 + _KerbCertificateS4ULogon _KERB_LOGON_SUBMIT_TYPE = 14 + _KerbCertificateUnlockLogon _KERB_LOGON_SUBMIT_TYPE = 15 + _KerbNoElevationLogon _KERB_LOGON_SUBMIT_TYPE = 83 + _KerbLuidLogon _KERB_LOGON_SUBMIT_TYPE = 84 +) + +type _KERB_S4U_LOGON_FLAGS uint32 + +const ( + _KERB_S4U_LOGON_FLAG_CHECK_LOGONHOURS _KERB_S4U_LOGON_FLAGS = 0x2 + //lint:ignore U1000 maps to a win32 API + _KERB_S4U_LOGON_FLAG_IDENTIFY _KERB_S4U_LOGON_FLAGS = 0x8 +) + +type _KERB_S4U_LOGON struct { + MessageType _KERB_LOGON_SUBMIT_TYPE + Flags _KERB_S4U_LOGON_FLAGS + ClientUpn windows.NTUnicodeString + ClientRealm windows.NTUnicodeString +} + +type _MSV1_0_LOGON_SUBMIT_TYPE int32 + +const ( + _MsV1_0InteractiveLogon _MSV1_0_LOGON_SUBMIT_TYPE = 2 + _MsV1_0Lm20Logon _MSV1_0_LOGON_SUBMIT_TYPE = 3 + _MsV1_0NetworkLogon _MSV1_0_LOGON_SUBMIT_TYPE = 4 + _MsV1_0SubAuthLogon _MSV1_0_LOGON_SUBMIT_TYPE = 5 + _MsV1_0WorkstationUnlockLogon _MSV1_0_LOGON_SUBMIT_TYPE = 7 + _MsV1_0S4ULogon _MSV1_0_LOGON_SUBMIT_TYPE = 12 + _MsV1_0VirtualLogon _MSV1_0_LOGON_SUBMIT_TYPE = 82 + _MsV1_0NoElevationLogon _MSV1_0_LOGON_SUBMIT_TYPE = 83 + _MsV1_0LuidLogon _MSV1_0_LOGON_SUBMIT_TYPE = 84 +) + +type _MSV1_0_S4U_LOGON_FLAGS uint32 + +const ( + _MSV1_0_S4U_LOGON_FLAG_CHECK_LOGONHOURS _MSV1_0_S4U_LOGON_FLAGS = 0x2 +) + +type _MSV1_0_S4U_LOGON struct { + MessageType _MSV1_0_LOGON_SUBMIT_TYPE + Flags _MSV1_0_S4U_LOGON_FLAGS + UserPrincipalName windows.NTUnicodeString + DomainName windows.NTUnicodeString +} + +type _SECURITY_LOGON_TYPE int32 + +const ( + _UndefinedLogonType _SECURITY_LOGON_TYPE = 0 + _Interactive _SECURITY_LOGON_TYPE = 2 + _Network _SECURITY_LOGON_TYPE = 3 + _Batch _SECURITY_LOGON_TYPE = 4 + _Service _SECURITY_LOGON_TYPE = 5 + _Proxy _SECURITY_LOGON_TYPE = 6 + _Unlock _SECURITY_LOGON_TYPE = 7 + _NetworkCleartext _SECURITY_LOGON_TYPE = 8 + _NewCredentials _SECURITY_LOGON_TYPE = 9 + _RemoteInteractive _SECURITY_LOGON_TYPE = 10 + _CachedInteractive _SECURITY_LOGON_TYPE = 11 + _CachedRemoteInteractive _SECURITY_LOGON_TYPE = 12 + _CachedUnlock _SECURITY_LOGON_TYPE = 13 +) + +const _TOKEN_SOURCE_LENGTH = 8 + +type _TOKEN_SOURCE struct { + SourceName [_TOKEN_SOURCE_LENGTH]byte + SourceIdentifier windows.LUID +} + +type _QUOTA_LIMITS struct { + PagedPoolLimit uintptr + NonPagedPoolLimit uintptr + MinimumWorkingSetSize uintptr + MaximumWorkingSetSize uintptr + PagefileLimit uintptr + TimeLimit int64 +} + +var ( + // ErrBadSrcName is returned if srcName contains non-ASCII characters, is + // empty, or is too long. It may be wrapped with additional information; use + // errors.Is when checking for it. + ErrBadSrcName = errors.New("srcName must be ASCII with length > 0 and <= 8") +) + +// LSA packages (and their IDs) are always initialized during system startup, +// so we can retain their resolved IDs for the lifetime of our process. +var ( + authPkgIDKerberos lazy.SyncValue[uint32] + authPkgIDMSV1_0 lazy.SyncValue[uint32] +) + +type lsaSession struct { + handle _LSAHANDLE +} + +func newLSASessionForQuery() (lsa *lsaSession, err error) { + var h _LSAHANDLE + if e := wingoes.ErrorFromNTStatus(lsaConnectUntrusted(&h)); e.Failed() { + return nil, e + } + + return &lsaSession{handle: h}, nil +} + +func newLSASessionForLogon(processName string) (lsa *lsaSession, err error) { + // processName is used by LSA for audit logging purposes. + // If empty, the current process name is used. + if processName == "" { + exe, err := os.Executable() + if err != nil { + return nil, err + } + + processName = strings.TrimSuffix(filepath.Base(exe), filepath.Ext(exe)) + } + + if err := checkASCII(processName); err != nil { + return nil, err + } + + logonProcessName, err := windows.NewNTString(processName) + if err != nil { + return nil, err + } + + var h _LSAHANDLE + var mode _LSA_OPERATIONAL_MODE + if e := wingoes.ErrorFromNTStatus(lsaRegisterLogonProcess(logonProcessName, &h, &mode)); e.Failed() { + return nil, e + } + + return &lsaSession{handle: h}, nil +} + +func (ls *lsaSession) getAuthPkgID(pkgName string) (id uint32, err error) { + ntPkgName, err := windows.NewNTString(pkgName) + if err != nil { + return 0, err + } + + if e := wingoes.ErrorFromNTStatus(lsaLookupAuthenticationPackage(ls.handle, ntPkgName, &id)); e.Failed() { + return 0, e + } + + return id, nil +} + +func (ls *lsaSession) Close() error { + if e := wingoes.ErrorFromNTStatus(lsaDeregisterLogonProcess(ls.handle)); e.Failed() { + return e + } + ls.handle = 0 + return nil +} + +func checkASCII(s string) error { + for _, c := range []byte(s) { + if c > unicode.MaxASCII { + return fmt.Errorf("%q must be ASCII but contains value 0x%02X", s, c) + } + } + + return nil +} + +var ( + thisComputer = []uint16{'.', 0} + computerName lazy.SyncValue[string] +) + +func getComputerName() (string, error) { + var buf [windows.MAX_COMPUTERNAME_LENGTH + 1]uint16 + size := uint32(len(buf)) + if err := windows.GetComputerName(&buf[0], &size); err != nil { + return "", err + } + + return windows.UTF16ToString(buf[:size]), nil +} + +// checkDomainAccount strips out the computer name (if any) from +// username and returns the result in sanitizedUserName. isDomainAccount is set +// to true if username contains a domain component that does not refer to the +// local computer. +func checkDomainAccount(username string) (sanitizedUserName string, isDomainAccount bool, err error) { + before, after, hasBackslash := strings.Cut(username, `\`) + if !hasBackslash { + return username, false, nil + } + if before == "." { + return after, false, nil + } + + comp, err := computerName.GetErr(getComputerName) + if err != nil { + return username, false, err + } + + if strings.EqualFold(before, comp) { + return after, false, nil + } + return username, true, nil +} + +// logonAs performs a S4U logon for u on behalf of srcName, and returns an +// access token for the user if successful. srcName must be non-empty, ASCII, +// and no more than 8 characters long. If srcName does not meet this criteria, +// LogonAs will return ErrBadSrcName wrapped with additional information; use +// errors.Is to check for it. When capLevel == CapCreateProcess, the logon +// enforces the user's logon hours policy (when present). +func (ls *lsaSession) logonAs(srcName string, u *user.User, capLevel CapabilityLevel) (token windows.Token, err error) { + if l := len(srcName); l == 0 || l > _TOKEN_SOURCE_LENGTH { + return 0, fmt.Errorf("%w, actual length is %d", ErrBadSrcName, l) + } + if err := checkASCII(srcName); err != nil { + return 0, fmt.Errorf("%w: %v", ErrBadSrcName, err) + } + + sanitizedUserName, isDomainUser, err := checkDomainAccount(u.Username) + if err != nil { + return 0, err + } + if isDomainUser && !winenv.IsDomainJoined() { + return 0, fmt.Errorf("%w: cannot logon as domain user without being joined to a domain", os.ErrInvalid) + } + + var pkgID uint32 + var authInfo unsafe.Pointer + var authInfoLen uint32 + enforceLogonHours := capLevel == CapCreateProcess + if isDomainUser { + pkgID, err = authPkgIDKerberos.GetErr(func() (uint32, error) { + return ls.getAuthPkgID(_MICROSOFT_KERBEROS_NAME) + }) + if err != nil { + return 0, err + } + + upn16, err := samToUPN16(sanitizedUserName) + if err != nil { + return 0, fmt.Errorf("samToUPN16: %w", err) + } + + logonInfo, logonInfoLen, slcs := winutil.AllocateContiguousBuffer[_KERB_S4U_LOGON](upn16) + logonInfo.MessageType = _KerbS4ULogon + if enforceLogonHours { + logonInfo.Flags = _KERB_S4U_LOGON_FLAG_CHECK_LOGONHOURS + } + winutil.SetNTString(&logonInfo.ClientUpn, slcs[0]) + + authInfo = unsafe.Pointer(logonInfo) + authInfoLen = logonInfoLen + } else { + pkgID, err = authPkgIDMSV1_0.GetErr(func() (uint32, error) { + return ls.getAuthPkgID(_MSV1_0_PACKAGE_NAME) + }) + if err != nil { + return 0, err + } + + upn16, err := windows.UTF16FromString(sanitizedUserName) + if err != nil { + return 0, err + } + + logonInfo, logonInfoLen, slcs := winutil.AllocateContiguousBuffer[_MSV1_0_S4U_LOGON](upn16, thisComputer) + logonInfo.MessageType = _MsV1_0S4ULogon + if enforceLogonHours { + logonInfo.Flags = _MSV1_0_S4U_LOGON_FLAG_CHECK_LOGONHOURS + } + for i, nts := range []*windows.NTUnicodeString{&logonInfo.UserPrincipalName, &logonInfo.DomainName} { + winutil.SetNTString(nts, slcs[i]) + } + + authInfo = unsafe.Pointer(logonInfo) + authInfoLen = logonInfoLen + } + + var srcContext _TOKEN_SOURCE + copy(srcContext.SourceName[:], []byte(srcName)) + if err := allocateLocallyUniqueId(&srcContext.SourceIdentifier); err != nil { + return 0, err + } + + originName, err := windows.NewNTString(srcName) + if err != nil { + return 0, err + } + + var profileBuf uintptr + var profileBufLen uint32 + var logonID windows.LUID + var quotas _QUOTA_LIMITS + var subNTStatus windows.NTStatus + ntStatus := lsaLogonUser(ls.handle, originName, _Network, pkgID, authInfo, authInfoLen, nil, &srcContext, &profileBuf, &profileBufLen, &logonID, &token, "as, &subNTStatus) + if e := wingoes.ErrorFromNTStatus(ntStatus); e.Failed() { + return 0, fmt.Errorf("LsaLogonUser(%q): %w, SubStatus: %v", u.Username, e, subNTStatus) + } + if profileBuf != 0 { + lsaFreeReturnBuffer(profileBuf) + } + return token, nil +} + +// samToUPN16 converts SAM-style account name samName to a UPN account name, +// returned as a UTF-16 slice. +func samToUPN16(samName string) (upn16 []uint16, err error) { + _, samAccount, hasSep := strings.Cut(samName, `\`) + if !hasSep { + return nil, fmt.Errorf("%w: expected samName to contain a backslash", os.ErrInvalid) + } + + // This is essentially the same algorithm used by Win32-OpenSSH: + // First, try obtaining a UPN directly... + upn16, err = translateName(samName, windows.NameSamCompatible, windows.NameUserPrincipal) + if err == nil { + return upn16, err + } + + // Fallback: Try manually composing a UPN. First obtain the canonical name... + canonical16, err := translateName(samName, windows.NameSamCompatible, windows.NameCanonical) + if err != nil { + return nil, err + } + canonical := windows.UTF16ToString(canonical16) + + // Extract the domain name... + domain, _, _ := strings.Cut(canonical, "/") + + // ...and finally create the UPN by joining the samAccount and domain. + upn := strings.Join([]string{samAccount, domain}, "@") + return windows.UTF16FromString(upn) +} + +func translateName(from string, fromFmt uint32, toFmt uint32) (result []uint16, err error) { + from16, err := windows.UTF16PtrFromString(from) + if err != nil { + return nil, err + } + + var to16Len uint32 + if err := windows.TranslateName(from16, fromFmt, toFmt, nil, &to16Len); err != nil { + return nil, err + } + + to16Buf := make([]uint16, to16Len) + if err := windows.TranslateName(from16, fromFmt, toFmt, unsafe.SliceData(to16Buf), &to16Len); err != nil { + return nil, err + } + + return to16Buf, nil +} diff --git a/util/winutil/s4u/mksyscall.go b/util/winutil/s4u/mksyscall.go new file mode 100644 index 000000000..8925c0209 --- /dev/null +++ b/util/winutil/s4u/mksyscall.go @@ -0,0 +1,16 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package s4u + +//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go +//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go + +//sys allocateLocallyUniqueId(luid *windows.LUID) (err error) [int32(failretval)==0] = advapi32.AllocateLocallyUniqueId +//sys impersonateLoggedOnUser(token windows.Token) (err error) [int32(failretval)==0] = advapi32.ImpersonateLoggedOnUser +//sys lsaConnectUntrusted(lsaHandle *_LSAHANDLE) (ret windows.NTStatus) = secur32.LsaConnectUntrusted +//sys lsaDeregisterLogonProcess(lsaHandle _LSAHANDLE) (ret windows.NTStatus) = secur32.LsaDeregisterLogonProcess +//sys lsaFreeReturnBuffer(buffer uintptr) (ret windows.NTStatus) = secur32.LsaFreeReturnBuffer +//sys lsaLogonUser(lsaHandle _LSAHANDLE, originName *windows.NTString, logonType _SECURITY_LOGON_TYPE, authenticationPackage uint32, authenticationInformation unsafe.Pointer, authenticationInformationLength uint32, localGroups *windows.Tokengroups, sourceContext *_TOKEN_SOURCE, profileBuffer *uintptr, profileBufferLength *uint32, logonID *windows.LUID, token *windows.Token, quotas *_QUOTA_LIMITS, subStatus *windows.NTStatus) (ret windows.NTStatus) = secur32.LsaLogonUser +//sys lsaLookupAuthenticationPackage(lsaHandle _LSAHANDLE, packageName *windows.NTString, authenticationPackage *uint32) (ret windows.NTStatus) = secur32.LsaLookupAuthenticationPackage +//sys lsaRegisterLogonProcess(logonProcessName *windows.NTString, lsaHandle *_LSAHANDLE, securityMode *_LSA_OPERATIONAL_MODE) (ret windows.NTStatus) = secur32.LsaRegisterLogonProcess diff --git a/util/winutil/s4u/s4u_windows.go b/util/winutil/s4u/s4u_windows.go new file mode 100644 index 000000000..c217f5fba --- /dev/null +++ b/util/winutil/s4u/s4u_windows.go @@ -0,0 +1,941 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Package s4u is an API for accessing Service-For-User (S4U) functionality on Windows. +package s4u + +import ( + "encoding/binary" + "errors" + "flag" + "fmt" + "io" + "math" + "os" + "os/user" + "runtime" + "slices" + "strconv" + "strings" + "sync/atomic" + "unsafe" + + "golang.org/x/sys/windows" + "tailscale.com/cmd/tailscaled/childproc" + "tailscale.com/types/logger" + "tailscale.com/util/winutil" + "tailscale.com/util/winutil/conpty" +) + +func init() { + childproc.Add("s4u", beRelay) +} + +var errInsufficientCapabilityLevel = errors.New("insufficient capability level") + +// ListGroupIDsForSSHPreAuthOnly returns user u's group memberships as a slice +// containing group SIDs. srcName must contain the name of the service that is +// retrieving this information. srcName must be non-empty, ASCII-only, and no +// longer than 8 characters. +// +// NOTE: This should only be used by Tailscale SSH! It is not a generic +// mechanism for access checks! +func ListGroupIDsForSSHPreAuthOnly(srcName string, u *user.User) ([]string, error) { + tok, err := createToken(srcName, u, tokenTypeIdentification, CapImpersonateOnly) + if err != nil { + return nil, err + } + defer tok.Close() + + tokenGroups, err := tok.GetTokenGroups() + if err != nil { + return nil, err + } + + result := make([]string, 0, tokenGroups.GroupCount) + for _, group := range tokenGroups.AllGroups() { + if group.Attributes&windows.SE_GROUP_ENABLED != 0 { + result = append(result, group.Sid.String()) + } + } + + return result, nil +} + +type tokenType uint + +const ( + tokenTypeIdentification tokenType = iota + tokenTypeImpersonation +) + +// createToken creates a new S4U access token for user u for the purposes +// specified by s4uType, with capability capLevel. srcName must contain the name +// of the service that is intended to use the token. srcName must be non-empty, +// ASCII-only, and no longer than 8 characters. +// +// When s4uType is tokenTypeImpersonation, the current OS thread's access token must have SeTcbPrivilege. +func createToken(srcName string, u *user.User, s4uType tokenType, capLevel CapabilityLevel) (tok windows.Token, err error) { + if u == nil { + return 0, os.ErrInvalid + } + + var lsa *lsaSession + switch s4uType { + case tokenTypeIdentification: + lsa, err = newLSASessionForQuery() + case tokenTypeImpersonation: + lsa, err = newLSASessionForLogon("") + default: + return 0, os.ErrInvalid + } + if err != nil { + return 0, err + } + defer lsa.Close() + + return lsa.logonAs(srcName, u, capLevel) +} + +// Session encapsulates an S4U login session. +type Session struct { + refCnt atomic.Int32 + logf logger.Logf + token windows.Token + userProfile *winutil.UserProfile + capLevel CapabilityLevel +} + +// CapabilityLevel specifies the desired capabilities that will be supported by a Session. +type CapabilityLevel uint + +const ( + // The Session supports Do but none of the StartProcess* methods. + CapImpersonateOnly CapabilityLevel = iota + // The Session supports both Do and the StartProcess* methods. + CapCreateProcess +) + +// Login logs user u into Windows on behalf of service srcName, loads the user's +// profile, and returns a Session that may be used for impersonating that user, +// or optionally creating processes as that user. Logs will be written to logf, +// if provided. srcName must be non-empty, ASCII-only, and no longer than 8 +// characters. +// +// The current OS thread's access token must have SeTcbPrivilege. +func Login(logf logger.Logf, srcName string, u *user.User, capLevel CapabilityLevel) (sess *Session, err error) { + token, err := createToken(srcName, u, tokenTypeIdentification, capLevel) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + token.Close() + } + }() + + sessToken := token + if capLevel == CapCreateProcess { + // Obtain token's security descriptor so that it may be applied to + // a primary token. + sd, err := windows.GetSecurityInfo(windows.Handle(token), + windows.SE_KERNEL_OBJECT, windows.DACL_SECURITY_INFORMATION) + if err != nil { + return nil, err + } + + sa := windows.SecurityAttributes{ + Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})), + SecurityDescriptor: sd, + } + + // token is an impersonation token. Upgrade us to a primary token so that + // our StartProcess* methods will work correctly. + var dupToken windows.Token + if err := windows.DuplicateTokenEx(token, 0, &sa, windows.SecurityImpersonation, + windows.TokenPrimary, &dupToken); err != nil { + return nil, err + } + sessToken = dupToken + defer func() { + if err != nil { + sessToken.Close() + } + }() + } + + userProfile, err := winutil.LoadUserProfile(sessToken, u) + if err != nil { + return nil, err + } + + if logf == nil { + logf = logger.Discard + } else { + logf = logger.WithPrefix(logf, "(s4u) ") + } + + return &Session{logf: logf, token: sessToken, userProfile: userProfile, capLevel: capLevel}, nil +} + +// Close unloads the user profile and S4U access token associated with the +// session. The close operation is not guaranteed to have finished when Close +// returns; it may remain alive until all processes created by ss have +// themselves been closed, and no more Do requests are pending. +func (ss *Session) Close() error { + refs := ss.refCnt.Load() + if (refs & 1) != 0 { + // Close already called + return nil + } + + // Set the low bit to indicate that a close operation has been requested. + // We don't have atomic OR so we need to use CAS. Sigh. + for !ss.refCnt.CompareAndSwap(refs, refs|1) { + refs = ss.refCnt.Load() + } + + if refs > 1 { + // Still active processes, just return. + return nil + } + + return ss.closeInternal() +} + +func (ss *Session) closeInternal() error { + if ss.userProfile != nil { + if err := ss.userProfile.Close(); err != nil { + return err + } + ss.userProfile = nil + } + + if ss.token != 0 { + if err := ss.token.Close(); err != nil { + return err + } + ss.token = 0 + } + return nil +} + +// CapabilityLevel returns the CapabilityLevel that was specified when the +// session was created. +func (ss *Session) CapabilityLevel() CapabilityLevel { + return ss.capLevel +} + +// Do executes fn while impersonating ss's user. Impersonation only affects +// the current goroutine; any new goroutines spawned by fn will not be +// impersonated. Do may be called concurrently by multiple goroutines. +// +// Do returns an error if impersonation did not succeed and fn could not be run. +// If called after ss has already been closed, it will panic. +func (ss *Session) Do(fn func()) error { + if fn == nil { + return os.ErrInvalid + } + + ss.addRef() + defer ss.release() + + // Impersonation touches thread-local state. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if err := impersonateLoggedOnUser(ss.token); err != nil { + return err + } + defer func() { + if err := windows.RevertToSelf(); err != nil { + // This is not recoverable in any way, shape, or form! + panic(fmt.Sprintf("RevertToSelf failed: %v", err)) + } + }() + + fn() + return nil +} + +func (ss *Session) addRef() { + if (ss.refCnt.Add(2) & 1) != 0 { + panic("addRef after Close") + } +} + +func (ss *Session) release() { + rc := ss.refCnt.Add(-2) + if rc < 0 { + panic("negative refcount") + } + if rc == 1 { + ss.closeInternal() + } +} + +type startProcessOpts struct { + token windows.Token + extraEnv map[string]string + ptySize windows.Coord + pipes bool +} + +// StartProcess creates a new process running under ss via cmdLineInfo. +// The process will be started with its working directory set to the S4U user's +// profile directory and its environment set to the S4U user's environment. +// extraEnv, when specified, contains any additional environment variables to +// be added to the process's environment. +// +// If called after ss has already been closed, StartProcess will panic. +func (ss *Session) StartProcess(cmdLineInfo winutil.CommandLineInfo, extraEnv map[string]string) (psp *Process, err error) { + if ss.capLevel != CapCreateProcess { + return nil, errInsufficientCapabilityLevel + } + + opts := startProcessOpts{ + token: ss.token, + extraEnv: extraEnv, + } + return startProcessInternal(ss, ss.logf, cmdLineInfo, opts) +} + +// StartProcessWithPTY creates a new process running under ss via cmdLineInfo +// with a pseudoconsole initialized to initialPtySize. The resulting Process +// will return non-nil values from Stdin and Stdout, but Stderr will return nil. +// The process will be started with its working directory set to the S4U user's +// profile directory and its environment set to the S4U user's environment. +// extraEnv, when specified, contains any additional environment variables to +// be added to the process's environment. +// +// If called after ss has already been closed, StartProcessWithPTY will panic. +func (ss *Session) StartProcessWithPTY(cmdLineInfo winutil.CommandLineInfo, extraEnv map[string]string, initialPtySize windows.Coord) (psp *Process, err error) { + if ss.capLevel != CapCreateProcess { + return nil, errInsufficientCapabilityLevel + } + + opts := startProcessOpts{ + token: ss.token, + extraEnv: extraEnv, + ptySize: initialPtySize, + } + return startProcessInternal(ss, ss.logf, cmdLineInfo, opts) +} + +// StartProcessWithPipes creates a new process running under ss via cmdLineInfo +// with all standard handles set to pipes. The resulting Process will return +// non-nil values from Stdin, Stdout, and Stderr. +// The process will be started with its working directory set to the S4U user's +// profile directory and its environment set to the S4U user's environment. +// extraEnv, when specified, contains any additional environment variables to +// be added to the process's environment. +// +// If called after ss has already been closed, StartProcessWithPipes will panic. +func (ss *Session) StartProcessWithPipes(cmdLineInfo winutil.CommandLineInfo, extraEnv map[string]string) (psp *Process, err error) { + if ss.capLevel != CapCreateProcess { + return nil, errInsufficientCapabilityLevel + } + + opts := startProcessOpts{ + token: ss.token, + extraEnv: extraEnv, + pipes: true, + } + return startProcessInternal(ss, ss.logf, cmdLineInfo, opts) +} + +// startProcessInternal is the common implementation behind Session's exported +// StartProcess* methods. It uses opts to distinguish between the various +// requested modes of operation. +// +// A note on pseudoconsoles: +// The conpty API currently does not provide a way to create a pseudoconsole for +// a different user than the current process. The way we deal with this is +// to first create a "relay" process running with the desired user token, +// and then create the actual requested process as a child of the relay, +// at which time we create the pseudoconsole. The relay simply copies the +// PTY's I/O into/out of its own stdin and stdout, which are piped to the +// parent still running as LocalSystem. We also relay pseudoconsole resize requests. +func startProcessInternal(ss *Session, logf logger.Logf, cmdLineInfo winutil.CommandLineInfo, opts startProcessOpts) (psp *Process, err error) { + var sib winutil.StartupInfoBuilder + defer sib.Close() + + var sp Process + defer func() { + if err != nil { + sp.Close() + } + }() + + var zeroCoord windows.Coord + ptySizeValid := opts.ptySize != zeroCoord + useToken := opts.token != 0 + usePty := ptySizeValid && !useToken + useRelay := ptySizeValid && useToken + useSystem32WD := useToken && opts.token.IsElevated() + + if usePty { + sp.pty, err = conpty.NewPseudoConsole(opts.ptySize) + if err != nil { + return nil, err + } + + if err := sp.pty.ConfigureStartupInfo(&sib); err != nil { + return nil, err + } + + sp.wStdin = sp.pty.InputPipe() + sp.rStdout = sp.pty.OutputPipe() + } else if useRelay || opts.pipes { + if sp.wStdin, sp.rStdout, sp.rStderr, err = createStdPipes(&sib); err != nil { + return nil, err + } + } + + var relayStderr io.ReadCloser + if useRelay { + // Later on we're going to use stderr for logging instead of providing it to the caller. + relayStderr = sp.rStderr + sp.rStderr = nil + defer func() { + if err != nil { + relayStderr.Close() + } + }() + + // Set up a pipe to send PTY resize requests. + var resizeRead, resizeWrite windows.Handle + if err := windows.CreatePipe(&resizeRead, &resizeWrite, nil, 0); err != nil { + return nil, err + } + sp.wResize = os.NewFile(uintptr(resizeWrite), "wPTYResizePipe") + defer windows.CloseHandle(resizeRead) + if err := sib.InheritHandles(resizeRead); err != nil { + return nil, err + } + + // Revise the command line. First, get the existing one. + _, _, strCmdLine, err := cmdLineInfo.Resolve() + if err != nil { + return nil, err + } + + // Now rebuild it, passing the strCmdLine as the --cmd argument... + newArgs := []string{ + "be-child", "s4u", + "--resize", fmt.Sprintf("0x%x", uintptr(resizeRead)), + "--x", strconv.Itoa(int(opts.ptySize.X)), + "--y", strconv.Itoa(int(opts.ptySize.Y)), + "--cmd", strCmdLine, + } + + // ...to be passed in as arguments to our own executable. + cmdLineInfo.ExePath, err = os.Executable() + if err != nil { + return nil, err + } + cmdLineInfo.SetArgs(newArgs) + } + + exePath, cmdLine, cmdLineStr, err := cmdLineInfo.Resolve() + if err != nil { + return nil, err + } + logf("starting %s", cmdLineStr) + + var env []string + var wd16 *uint16 + if useToken { + env, err = opts.token.Environ(false) + if err != nil { + return nil, err + } + + folderID := windows.FOLDERID_Profile + if useSystem32WD { + folderID = windows.FOLDERID_System + } + wd, err := opts.token.KnownFolderPath(folderID, windows.KF_FLAG_DEFAULT) + if err != nil { + return nil, err + } + wd16, err = windows.UTF16PtrFromString(wd) + if err != nil { + return nil, err + } + } else { + env = os.Environ() + } + + env = mergeEnv(env, opts.extraEnv) + + var env16 *uint16 + if useToken || len(opts.extraEnv) > 0 { + env16 = winutil.NewEnvBlock(env) + } + + if useToken { + // We want the child process to be assigned to job such that when it exits, + // its descendents within the job will be terminated as well. + job, err := createJob() + if err != nil { + return nil, err + } + // We don't need to hang onto job beyond this func... + defer job.Close() + + if err := sib.AssignToJob(job.Handle()); err != nil { + return nil, err + } + + // ...because we're now gonna make a read-only copy... + qjob, err := job.QueryOnlyClone() + if err != nil { + return nil, err + } + defer qjob.Close() + + // ...which will be inherited by the child process. + // When the child process terminates, the job will too. + if err := sib.InheritHandles(qjob.Handle()); err != nil { + return nil, err + } + } + + si, inheritHandles, creationFlags, err := sib.Resolve() + if err != nil { + return nil, err + } + + var pi windows.ProcessInformation + if useToken { + // DETACHED_PROCESS so that the child does not receive a console. + // CREATE_NEW_PROCESS_GROUP so that the child's console group is isolated from ours. + creationFlags |= windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP + doCreate := func() { + err = windows.CreateProcessAsUser(opts.token, exePath, cmdLine, nil, nil, inheritHandles, creationFlags, env16, wd16, si, &pi) + } + switch { + case useRelay: + doCreate() + case ss != nil: + // We want to ensure that the executable is accessible via the token's + // security context, not ours. + if err := ss.Do(doCreate); err != nil { + return nil, err + } + default: + panic("should not have reached here") + } + } else { + err = windows.CreateProcess(exePath, cmdLine, nil, nil, inheritHandles, creationFlags, env16, wd16, si, &pi) + } + if err != nil { + return nil, err + } + windows.CloseHandle(pi.Thread) + + if relayStderr != nil { + logw := logger.FuncWriter(logger.WithPrefix(logf, fmt.Sprintf("(s4u relay process %d [0x%x]) ", pi.ProcessId, pi.ProcessId))) + go func() { + defer relayStderr.Close() + io.Copy(logw, relayStderr) + }() + } + + sp.hproc = pi.Process + sp.pid = pi.ProcessId + if ss != nil { + ss.addRef() + sp.sess = ss + } + return &sp, nil +} + +type jobObject windows.Handle + +func createJob() (job *jobObject, err error) { + hjob, err := windows.CreateJobObject(nil, nil) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + windows.CloseHandle(hjob) + } + }() + + limitInfo := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{ + BasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{ + // We want every process within the job to terminate when the job is closed. + // We also want to allow processes within the job to create child processes + // that are outside the job (otherwise you couldn't leave background + // processes running after exiting a session, for example). + // These flags also match those used by the Win32 port of OpenSSH. + LimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | windows.JOB_OBJECT_LIMIT_BREAKAWAY_OK, + }, + } + _, err = windows.SetInformationJobObject(hjob, + windows.JobObjectExtendedLimitInformation, uintptr(unsafe.Pointer(&limitInfo)), + uint32(unsafe.Sizeof(limitInfo))) + if err != nil { + return nil, err + } + + jo := jobObject(hjob) + return &jo, nil +} + +func (job *jobObject) Close() error { + if hjob := job.Handle(); hjob != 0 { + windows.CloseHandle(hjob) + *job = 0 + } + return nil +} + +func (job *jobObject) Handle() windows.Handle { + if job == nil { + return 0 + } + return windows.Handle(*job) +} + +const _JOB_OBJECT_QUERY = 0x0004 + +func (job *jobObject) QueryOnlyClone() (*jobObject, error) { + hjob := job.Handle() + cp := windows.CurrentProcess() + + var dupe windows.Handle + err := windows.DuplicateHandle(cp, hjob, cp, &dupe, _JOB_OBJECT_QUERY, true, 0) + if err != nil { + return nil, err + } + + result := jobObject(dupe) + return &result, nil +} + +func createStdPipes(sib *winutil.StartupInfoBuilder) (stdin io.WriteCloser, stdout, stderr io.ReadCloser, err error) { + var rStdin, wStdin windows.Handle + if err := windows.CreatePipe(&rStdin, &wStdin, nil, 0); err != nil { + return nil, nil, nil, err + } + defer func() { + if err != nil { + windows.CloseHandle(rStdin) + windows.CloseHandle(wStdin) + } + }() + + var rStdout, wStdout windows.Handle + if err := windows.CreatePipe(&rStdout, &wStdout, nil, 0); err != nil { + return nil, nil, nil, err + } + defer func() { + if err != nil { + windows.CloseHandle(rStdout) + windows.CloseHandle(wStdout) + } + }() + + var rStderr, wStderr windows.Handle + if err := windows.CreatePipe(&rStderr, &wStderr, nil, 0); err != nil { + return nil, nil, nil, err + } + defer func() { + if err != nil { + windows.CloseHandle(rStderr) + windows.CloseHandle(wStderr) + } + }() + + if err := sib.SetStdHandles(rStdin, wStdout, wStderr); err != nil { + return nil, nil, nil, err + } + + stdin = os.NewFile(uintptr(wStdin), "wStdin") + stdout = os.NewFile(uintptr(rStdout), "rStdout") + stderr = os.NewFile(uintptr(rStderr), "rStderr") + return stdin, stdout, stderr, nil +} + +// Process encapsulates a child process started with a Session. +type Process struct { + sess *Session + wStdin io.WriteCloser + rStdout io.ReadCloser + rStderr io.ReadCloser + wResize io.WriteCloser + pty *conpty.PseudoConsole + hproc windows.Handle + pid uint32 +} + +// Stdin returns the write side of a pipe connected to the child process's +// stdin, or nil if no I/O was requested. +func (sp *Process) Stdin() io.WriteCloser { + return sp.wStdin +} + +// Stdout returns the read side of a pipe connected to the child process's +// stdout, or nil if no I/O was requested. +func (sp *Process) Stdout() io.ReadCloser { + return sp.rStdout +} + +// Stderr returns the read side of a pipe connected to the child process's +// stderr, or nil if no I/O was requested. +func (sp *Process) Stderr() io.ReadCloser { + return sp.rStderr +} + +// Terminate kills the process. +func (sp *Process) Terminate() { + if sp.hproc != 0 { + windows.TerminateProcess(sp.hproc, 255) + } +} + +// Close waits for sp to complete and then cleans up any resources owned by it. +// Close must wait because the Session associated with sp should not be destroyed +// until all its processes have terminated. If necessary, call Terminate to +// forcibly end the process. +// +// If the process was created with a pseudoconsole then the caller must continue +// concurrently draining sp's stdout until either Close finishes executing, or EOF. +func (sp *Process) Close() error { + for _, pc := range []*io.WriteCloser{&sp.wStdin, &sp.wResize} { + if *pc == nil { + continue + } + (*pc).Close() + (*pc) = nil + } + + if sp.pty != nil { + if err := sp.pty.Close(); err != nil { + return err + } + sp.pty = nil + } + + if sp.hproc != 0 { + if _, err := sp.Wait(); err != nil { + return err + } + windows.CloseHandle(sp.hproc) + sp.hproc = 0 + sp.pid = 0 + if sp.sess != nil { + sp.sess.release() + sp.sess = nil + } + } + + // Order is important here. Do not close sp.rStdout until _after_ + // ss.pty (when present) has been closed! We're going to do one better by + // doing this after the process is done. + for _, pc := range []*io.ReadCloser{&sp.rStdout, &sp.rStderr} { + if *pc == nil { + continue + } + (*pc).Close() + (*pc) = nil + } + return nil +} + +// Wait blocks the caller until sp terminates. It returns the process exit code. +// exitCode will be set to 254 if the process terminated but the exit code could +// not be retrieved. +func (sp *Process) Wait() (exitCode uint32, err error) { + _, err = windows.WaitForSingleObject(sp.hproc, windows.INFINITE) + if err == nil { + if err := windows.GetExitCodeProcess(sp.hproc, &exitCode); err != nil { + exitCode = 254 + } + } + return exitCode, err +} + +// OSProcess returns an *os.Process associated with sp. This is useful for +// integration with external code that expects an os.Process. +func (sp *Process) OSProcess() (*os.Process, error) { + if sp.hproc == 0 { + return nil, winutil.ErrDefunctProcess + } + return os.FindProcess(int(sp.pid)) +} + +// PTYResizer returns a function to be called to resize the pseudoconsole. +// It returns nil if no pseudoconsole was requested when creating sp. +func (sp *Process) PTYResizer() func(windows.Coord) error { + if sp.wResize != nil { + wResize := sp.wResize + return func(c windows.Coord) error { + return binary.Write(wResize, binary.LittleEndian, c) + } + } + + if sp.pty != nil { + pty := sp.pty + return func(c windows.Coord) error { + return pty.Resize(c) + } + } + + return nil +} + +type relayArgs struct { + command string + resize string + ptyX int + ptyY int +} + +func parseRelayArgs(args []string) (a relayArgs) { + flags := flag.NewFlagSet("", flag.ExitOnError) + flags.StringVar(&a.command, "cmd", "", "the command to run") + flags.StringVar(&a.resize, "resize", "", "handle to resize pipe") + flags.IntVar(&a.ptyX, "x", 80, "initial width of pty") + flags.IntVar(&a.ptyY, "y", 25, "initial height of pty") + flags.Parse(args) + return a +} + +func flagSizeErr(flagName byte) error { + return fmt.Errorf("--%c must be greater than zero and less than %d", flagName, math.MaxInt16) +} + +const debugRelay = false + +func beRelay(args []string) error { + ra := parseRelayArgs(args) + if ra.command == "" { + return fmt.Errorf("--cmd must be specified") + } + + bitSize := int(unsafe.Sizeof(windows.Handle(0)) * 8) + resize64, err := strconv.ParseUint(ra.resize, 0, bitSize) + if err != nil { + return err + } + hResize := windows.Handle(resize64) + if ft, _ := windows.GetFileType(hResize); ft != windows.FILE_TYPE_PIPE { + return fmt.Errorf("--resize is an invalid handle type") + } + resize := os.NewFile(uintptr(hResize), "rPTYResizePipe") + defer resize.Close() + + switch { + case ra.ptyX <= 0 || ra.ptyX > math.MaxInt16: + return flagSizeErr('x') + case ra.ptyY <= 0 || ra.ptyY > math.MaxInt16: + return flagSizeErr('y') + default: + } + + logf := logger.Discard + if debugRelay { + // Our parent process will write our stderr to its log. + logf = func(format string, args ...any) { + fmt.Fprintf(os.Stderr, format, args...) + } + } + + logf("starting") + argv, err := windows.DecomposeCommandLine(ra.command) + if err != nil { + logf("DecomposeCommandLine failed: %v", err) + return err + } + + cli := winutil.CommandLineInfo{ + ExePath: argv[0], + } + cli.SetArgs(argv[1:]) + + opts := startProcessOpts{ + ptySize: windows.Coord{X: int16(ra.ptyX), Y: int16(ra.ptyY)}, + } + psp, err := startProcessInternal(nil, logf, cli, opts) + if err != nil { + logf("startProcessInternal failed: %v", err) + return err + } + defer psp.Close() + + go resizeLoop(logf, resize, psp.PTYResizer()) + if debugRelay { + go debugLogPTYInput(logf, psp.wStdin, os.Stdin) + go debugLogPTYOutput(logf, os.Stdout, psp.rStdout) + } else { + go io.Copy(psp.wStdin, os.Stdin) + go io.Copy(os.Stdout, psp.rStdout) + } + + exitCode, err := psp.Wait() + if err != nil { + logf("waiting on relayed process: %v", err) + return err + } + if exitCode > 0 { + logf("relayed process returned %v", exitCode) + } + + if err := psp.Close(); err != nil { + logf("s4u.Process.Close error: %v", err) + return err + } + return nil +} + +func resizeLoop(logf logger.Logf, resizePipe io.Reader, resizeFn func(windows.Coord) error) { + var coord windows.Coord + for binary.Read(resizePipe, binary.LittleEndian, &coord) == nil { + logf("resizing pty window to %#v", coord) + resizeFn(coord) + } +} + +func debugLogPTYInput(logf logger.Logf, w io.Writer, r io.Reader) { + logw := logger.FuncWriter(logger.WithPrefix(logf, "(pty input) ")) + io.Copy(io.MultiWriter(w, logw), r) +} + +func debugLogPTYOutput(logf logger.Logf, w io.Writer, r io.Reader) { + logw := logger.FuncWriter(logger.WithPrefix(logf, "(pty output) ")) + io.Copy(w, io.TeeReader(r, logw)) +} + +// mergeEnv returns the union of existingEnv and extraEnv, deduplicated and +// sorted. +func mergeEnv(existingEnv []string, extraEnv map[string]string) []string { + if len(extraEnv) == 0 { + return existingEnv + } + + mergedMap := make(map[string]string, len(existingEnv)+len(extraEnv)) + for _, line := range existingEnv { + k, v, _ := strings.Cut(line, "=") + mergedMap[strings.ToUpper(k)] = v + } + + for k, v := range extraEnv { + mergedMap[strings.ToUpper(k)] = v + } + + result := make([]string, 0, len(mergedMap)) + for k, v := range mergedMap { + result = append(result, strings.Join([]string{k, v}, "=")) + } + + slices.SortFunc(result, func(l, r string) int { + kl, _, _ := strings.Cut(l, "=") + kr, _, _ := strings.Cut(r, "=") + return strings.Compare(kl, kr) + }) + return result +} diff --git a/util/winutil/s4u/zsyscall_windows.go b/util/winutil/s4u/zsyscall_windows.go new file mode 100644 index 000000000..6a8c78427 --- /dev/null +++ b/util/winutil/s4u/zsyscall_windows.go @@ -0,0 +1,104 @@ +// Code generated by 'go generate'; DO NOT EDIT. + +package s4u + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + modsecur32 = windows.NewLazySystemDLL("secur32.dll") + + procAllocateLocallyUniqueId = modadvapi32.NewProc("AllocateLocallyUniqueId") + procImpersonateLoggedOnUser = modadvapi32.NewProc("ImpersonateLoggedOnUser") + procLsaConnectUntrusted = modsecur32.NewProc("LsaConnectUntrusted") + procLsaDeregisterLogonProcess = modsecur32.NewProc("LsaDeregisterLogonProcess") + procLsaFreeReturnBuffer = modsecur32.NewProc("LsaFreeReturnBuffer") + procLsaLogonUser = modsecur32.NewProc("LsaLogonUser") + procLsaLookupAuthenticationPackage = modsecur32.NewProc("LsaLookupAuthenticationPackage") + procLsaRegisterLogonProcess = modsecur32.NewProc("LsaRegisterLogonProcess") +) + +func allocateLocallyUniqueId(luid *windows.LUID) (err error) { + r1, _, e1 := syscall.Syscall(procAllocateLocallyUniqueId.Addr(), 1, uintptr(unsafe.Pointer(luid)), 0, 0) + if int32(r1) == 0 { + err = errnoErr(e1) + } + return +} + +func impersonateLoggedOnUser(token windows.Token) (err error) { + r1, _, e1 := syscall.Syscall(procImpersonateLoggedOnUser.Addr(), 1, uintptr(token), 0, 0) + if int32(r1) == 0 { + err = errnoErr(e1) + } + return +} + +func lsaConnectUntrusted(lsaHandle *_LSAHANDLE) (ret windows.NTStatus) { + r0, _, _ := syscall.Syscall(procLsaConnectUntrusted.Addr(), 1, uintptr(unsafe.Pointer(lsaHandle)), 0, 0) + ret = windows.NTStatus(r0) + return +} + +func lsaDeregisterLogonProcess(lsaHandle _LSAHANDLE) (ret windows.NTStatus) { + r0, _, _ := syscall.Syscall(procLsaDeregisterLogonProcess.Addr(), 1, uintptr(lsaHandle), 0, 0) + ret = windows.NTStatus(r0) + return +} + +func lsaFreeReturnBuffer(buffer uintptr) (ret windows.NTStatus) { + r0, _, _ := syscall.Syscall(procLsaFreeReturnBuffer.Addr(), 1, uintptr(buffer), 0, 0) + ret = windows.NTStatus(r0) + return +} + +func lsaLogonUser(lsaHandle _LSAHANDLE, originName *windows.NTString, logonType _SECURITY_LOGON_TYPE, authenticationPackage uint32, authenticationInformation unsafe.Pointer, authenticationInformationLength uint32, localGroups *windows.Tokengroups, sourceContext *_TOKEN_SOURCE, profileBuffer *uintptr, profileBufferLength *uint32, logonID *windows.LUID, token *windows.Token, quotas *_QUOTA_LIMITS, subStatus *windows.NTStatus) (ret windows.NTStatus) { + r0, _, _ := syscall.Syscall15(procLsaLogonUser.Addr(), 14, uintptr(lsaHandle), uintptr(unsafe.Pointer(originName)), uintptr(logonType), uintptr(authenticationPackage), uintptr(authenticationInformation), uintptr(authenticationInformationLength), uintptr(unsafe.Pointer(localGroups)), uintptr(unsafe.Pointer(sourceContext)), uintptr(unsafe.Pointer(profileBuffer)), uintptr(unsafe.Pointer(profileBufferLength)), uintptr(unsafe.Pointer(logonID)), uintptr(unsafe.Pointer(token)), uintptr(unsafe.Pointer(quotas)), uintptr(unsafe.Pointer(subStatus)), 0) + ret = windows.NTStatus(r0) + return +} + +func lsaLookupAuthenticationPackage(lsaHandle _LSAHANDLE, packageName *windows.NTString, authenticationPackage *uint32) (ret windows.NTStatus) { + r0, _, _ := syscall.Syscall(procLsaLookupAuthenticationPackage.Addr(), 3, uintptr(lsaHandle), uintptr(unsafe.Pointer(packageName)), uintptr(unsafe.Pointer(authenticationPackage))) + ret = windows.NTStatus(r0) + return +} + +func lsaRegisterLogonProcess(logonProcessName *windows.NTString, lsaHandle *_LSAHANDLE, securityMode *_LSA_OPERATIONAL_MODE) (ret windows.NTStatus) { + r0, _, _ := syscall.Syscall(procLsaRegisterLogonProcess.Addr(), 3, uintptr(unsafe.Pointer(logonProcessName)), uintptr(unsafe.Pointer(lsaHandle)), uintptr(unsafe.Pointer(securityMode))) + ret = windows.NTStatus(r0) + return +}