mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-19 20:09:07 +02:00
183 lines
6.1 KiB
Go
183 lines
6.1 KiB
Go
package certproof
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unsafe"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
const (
|
|
noActiveSession = 0xFFFFFFFF
|
|
servicesSession = 0
|
|
|
|
// wtsCurrentServer is WTS_CURRENT_SERVER_HANDLE, which x/sys/windows does not export.
|
|
wtsCurrentServer = windows.Handle(0)
|
|
)
|
|
|
|
// DesktopUser is an interactive session and the account signed into it. The user's
|
|
// certificate store is readable only from a process running as that account, because
|
|
// its private keys are protected against the user profile rather than the machine.
|
|
type DesktopUser struct {
|
|
Session uint32
|
|
Name string
|
|
Token windows.Token
|
|
}
|
|
|
|
// Close releases the session token.
|
|
func (u DesktopUser) Close() {
|
|
if err := u.Token.Close(); err != nil {
|
|
log.Debugf("failed closing desktop session token: %v", err)
|
|
}
|
|
}
|
|
|
|
// currentDesktopUser returns a token for the session whose certificate store should be
|
|
// asked: a session of owner, the account the active profile belongs to, with the
|
|
// physical console preferred over remote sessions. With no owner only the console user
|
|
// counts. Picking any signed-in user instead would let whoever else is logged in to a
|
|
// terminal server or VDI host decide the result. The second return is false when no
|
|
// such session exists, and only machine certificates can then be proven.
|
|
//
|
|
// Obtaining the token needs SE_TCB_NAME, which the LocalSystem service has and an
|
|
// ordinary process does not.
|
|
func currentDesktopUser(owner string) (DesktopUser, bool) {
|
|
console := windows.WTSGetActiveConsoleSessionId()
|
|
if owner == "" {
|
|
return consoleUser(console)
|
|
}
|
|
|
|
sessions, err := userSessions(console)
|
|
if err != nil {
|
|
log.Debugf("cannot enumerate terminal sessions: %v", err)
|
|
return DesktopUser{}, false
|
|
}
|
|
var found DesktopUser
|
|
var ok bool
|
|
for _, session := range sessions {
|
|
user, signedIn := desktopUser(session)
|
|
if !signedIn {
|
|
continue
|
|
}
|
|
switch {
|
|
case !sameAccountName(user.Name, owner):
|
|
user.Close()
|
|
case !ok:
|
|
found, ok = user, true
|
|
case strings.EqualFold(user.Name, found.Name):
|
|
// Another session of the same account; the first one in preference order wins.
|
|
user.Close()
|
|
default:
|
|
// An owner recorded without a domain matches accounts of several domains here.
|
|
// Picking one would let another domain's user answer for the owner.
|
|
log.Debugf("certificate posture: profile owner %s matches both %s and %s, no user certificate store is used", owner, found.Name, user.Name)
|
|
user.Close()
|
|
found.Close()
|
|
return DesktopUser{}, false
|
|
}
|
|
}
|
|
if !ok {
|
|
log.Debugf("certificate posture: profile owner %s has no signed-in session, no user certificate store is reachable", owner)
|
|
}
|
|
return found, ok
|
|
}
|
|
|
|
func consoleUser(console uint32) (DesktopUser, bool) {
|
|
if console == noActiveSession || console == servicesSession {
|
|
log.Debug("no console session, no user certificate store is reachable")
|
|
return DesktopUser{}, false
|
|
}
|
|
user, ok := desktopUser(console)
|
|
if !ok {
|
|
log.Debugf("console session %d has nobody signed in, no user certificate store is reachable", console)
|
|
}
|
|
return user, ok
|
|
}
|
|
|
|
// sameAccountName compares DOMAIN\account names case-insensitively, as Windows does, and an
|
|
// owner given without a domain against the account part alone. The session side comes from
|
|
// the session token's own SID, which Windows resolves from its cache of signed-in users.
|
|
// Resolving the owner name to a SID instead would ask the domain controller, which on a
|
|
// laptop that cannot reach it yet blocks for tens of seconds, past the collection deadline.
|
|
func sameAccountName(sessionName, owner string) bool {
|
|
if strings.EqualFold(sessionName, owner) {
|
|
return true
|
|
}
|
|
if strings.Contains(owner, `\`) {
|
|
return false
|
|
}
|
|
_, account, found := strings.Cut(sessionName, `\`)
|
|
return found && strings.EqualFold(account, owner)
|
|
}
|
|
|
|
func desktopUser(session uint32) (DesktopUser, bool) {
|
|
var token windows.Token
|
|
if err := windows.WTSQueryUserToken(session, &token); err != nil {
|
|
log.Debugf("no user token for session %d: %v", session, err)
|
|
return DesktopUser{}, false
|
|
}
|
|
|
|
name, err := tokenAccount(token)
|
|
if err != nil {
|
|
log.Debugf("session %d token has no readable account: %v", session, err)
|
|
if closeErr := token.Close(); closeErr != nil {
|
|
log.Debugf("failed closing session token: %v", closeErr)
|
|
}
|
|
return DesktopUser{}, false
|
|
}
|
|
return DesktopUser{Session: session, Name: name, Token: token}, true
|
|
}
|
|
|
|
func tokenAccount(token windows.Token) (string, error) {
|
|
user, err := token.GetTokenUser()
|
|
if err != nil {
|
|
return "", fmt.Errorf("read token user: %w", err)
|
|
}
|
|
account, domain, _, err := user.User.Sid.LookupAccount("")
|
|
if err != nil {
|
|
return "", fmt.Errorf("look up account: %w", err)
|
|
}
|
|
if domain == "" {
|
|
return account, nil
|
|
}
|
|
return domain + `\` + account, nil
|
|
}
|
|
|
|
// userSessions lists the sessions a user can be signed in to, console first, then
|
|
// active remote sessions, then disconnected ones, whose user is still signed in. Session
|
|
// 0 is skipped: it hosts services and never belongs to an interactive user.
|
|
func userSessions(console uint32) ([]uint32, error) {
|
|
var info *windows.WTS_SESSION_INFO
|
|
var count uint32
|
|
if err := windows.WTSEnumerateSessions(wtsCurrentServer, 0, 1, &info, &count); err != nil {
|
|
return nil, fmt.Errorf("enumerate sessions: %w", err)
|
|
}
|
|
defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(info)))
|
|
|
|
var sessions []uint32
|
|
if console != noActiveSession && console != servicesSession {
|
|
sessions = append(sessions, console)
|
|
}
|
|
for _, state := range []uint32{windows.WTSActive, windows.WTSDisconnected} {
|
|
for _, session := range unsafe.Slice(info, count) {
|
|
if session.SessionID == servicesSession || session.SessionID == console || session.State != state {
|
|
continue
|
|
}
|
|
sessions = append(sessions, session.SessionID)
|
|
}
|
|
}
|
|
return sessions, nil
|
|
}
|
|
|
|
// runningAsLocalSystem reports whether this process is the service. The helper runs as
|
|
// the signed-in user and must read its own store rather than launching another helper.
|
|
func runningAsLocalSystem() bool {
|
|
user, err := windows.GetCurrentProcessToken().GetTokenUser()
|
|
if err != nil {
|
|
log.Debugf("failed reading own token user: %v", err)
|
|
return false
|
|
}
|
|
return user.User.Sid.IsWellKnown(windows.WinLocalSystemSid)
|
|
}
|