mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-15 19:29:08 +02:00
A service reads LocalMachine\MY, where AD and Intune enrol device certificates. CurrentUser\MY lives in the signed-in user's registry hive with keys protected against their profile, and a service that opens it does not fail: "current user" resolves to HKU\S-1-5-18, so it silently reads the service account's own empty store. The service therefore reads the machine store itself and launches "netbird posture cert-proof" with the session token for the rest, mirroring the macOS console user helper. Windows lets a privileged service assume a user identity, so the token goes straight into the child process and no external tooling is involved. CREATE_NO_WINDOW keeps a console window from flashing on the desktop every sync. In-process impersonation would also work but is per OS thread while goroutines migrate, so the child process avoids that class of bug. Session selection prefers the physical console and falls back to any active session, so remote desktop and VDI hosts are covered. WTSQueryUserToken needs SE_TCB_NAME, so a user-run client skips the helper and reads the machine store alone. SystemStore takes a store location, gaining NewUserStore alongside NewSystemStore and the per candidate logging macOS already had. The request building and proof merging move to helper_spawn.go, shared by both platforms, and helperStore picks what the helper reads per platform.
127 lines
3.9 KiB
Go
127 lines
3.9 KiB
Go
package certproof
|
|
|
|
import (
|
|
"fmt"
|
|
"unsafe"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
const (
|
|
noActiveSession = 0xFFFFFFFF
|
|
|
|
// wtsCurrentServer is WTS_CURRENT_SERVER_HANDLE and wtsActive is WTSActive of
|
|
// WTS_CONNECTSTATE_CLASS. Neither is exported by x/sys/windows.
|
|
wtsCurrentServer = windows.Handle(0)
|
|
wtsActive = 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 interactive user whose certificate store
|
|
// should be asked. The physical console comes first, and an active remote desktop
|
|
// session is used when nobody is at the console, which is how servers and VDI hosts are
|
|
// normally reached. The second return is false at the sign-in screen, where no
|
|
// interactive session exists and only machine certificates can be proven.
|
|
//
|
|
// Obtaining the token needs SE_TCB_NAME, which the LocalSystem service has and an
|
|
// ordinary process does not.
|
|
func CurrentDesktopUser() (DesktopUser, bool) {
|
|
if session := windows.WTSGetActiveConsoleSessionId(); session != noActiveSession {
|
|
if user, ok := desktopUser(session); ok {
|
|
return user, true
|
|
}
|
|
log.Infof("console session %d has nobody signed in, looking for an active remote session", session)
|
|
}
|
|
|
|
sessions, err := activeSessions()
|
|
if err != nil {
|
|
log.Infof("cannot enumerate terminal sessions: %v", err)
|
|
return DesktopUser{}, false
|
|
}
|
|
for _, session := range sessions {
|
|
if user, ok := desktopUser(session); ok {
|
|
return user, true
|
|
}
|
|
}
|
|
|
|
log.Info("no interactive session is signed in, no user certificate store is reachable")
|
|
return DesktopUser{}, false
|
|
}
|
|
|
|
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.Infof("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
|
|
}
|
|
|
|
func activeSessions() ([]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 active []uint32
|
|
for _, session := range unsafe.Slice(info, count) {
|
|
if session.State == wtsActive {
|
|
active = append(active, session.SessionID)
|
|
}
|
|
}
|
|
return active, 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)
|
|
}
|