From e217a1e93b21db5d883f251945024ad59cf7c199 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Wed, 20 Nov 2024 19:10:34 +0100 Subject: [PATCH 1/3] fix(systray): Read the entire connection Shift from conn.Read to io.ReadAll to consume the entire connection data. Also, add diag log messages. --- cmd/systray/main_windows.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/systray/main_windows.go b/cmd/systray/main_windows.go index 000e04815..9de8e2000 100644 --- a/cmd/systray/main_windows.go +++ b/cmd/systray/main_windows.go @@ -181,10 +181,9 @@ func (s *Systray) loadIconFromResource(mod windows.Handle) (windows.Handle, erro } func (s *Systray) handlePipeClient(conn net.Conn) { - buf := make([]byte, 1024) defer conn.Close() for { - n, err := conn.Read(buf) + buf, err := io.ReadAll(conn) if err != nil { if err != io.EOF { logrus.Errorf("pipe read: %v", err) @@ -192,7 +191,7 @@ func (s *Systray) handlePipeClient(conn net.Conn) { break } var m Msg - err = json.Unmarshal(buf[:n], &m) + err = json.Unmarshal(buf, &m) if err != nil { logrus.Error(err) break @@ -218,6 +217,7 @@ func (s *Systray) handleMessage(m Msg) error { var alert alertsender.Alert err := m.decode(&alert) if err != nil { + logrus.Errorf("unable to decode alert: %v", err) return err } text := alert.Text @@ -246,7 +246,7 @@ func main() { // Give generic read/write access to the // current user SID descriptor := "D:P(A;;GA;;;" + usr.Uid + ")" - // spin up named-pipe server + // spin up the named-pipe server l, err := winio.ListenPipe(systrayPipe, &winio.PipeConfig{SecurityDescriptor: descriptor}) if err != nil { logrus.Fatalf("unable to listen on named pipe: %s: %v", systrayPipe, err) From fe7a1f219f22df436a3dceed9bad0c791bf61def Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Wed, 20 Nov 2024 22:16:28 +0100 Subject: [PATCH 2/3] fix(systray): Use default named pipe security descriptor and small refactoring The default named pipe security descriptor grants full access to LocalSystem account, administrators, and the creator owner. --- cmd/systray/main_windows.go | 18 +++++------------- pkg/alertsender/systray/systray.go | 12 +++++++++--- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/cmd/systray/main_windows.go b/cmd/systray/main_windows.go index 9de8e2000..4721ee146 100644 --- a/cmd/systray/main_windows.go +++ b/cmd/systray/main_windows.go @@ -34,7 +34,6 @@ import ( "io" "net" "os" - "os/user" "path/filepath" "unsafe" ) @@ -238,23 +237,12 @@ func main() { os.Exit(1) } logrus.Info("starting systray server...") - usr, err := user.Current() - if err != nil { - logrus.Fatalf("failed to retrieve the current user: %v", err) - } - // Named pipe security and access rights. - // Give generic read/write access to the - // current user SID - descriptor := "D:P(A;;GA;;;" + usr.Uid + ")" // spin up the named-pipe server - l, err := winio.ListenPipe(systrayPipe, &winio.PipeConfig{SecurityDescriptor: descriptor}) + l, err := winio.ListenPipe(systrayPipe, nil) if err != nil { logrus.Fatalf("unable to listen on named pipe: %s: %v", systrayPipe, err) } - // detach console - sys.FreeConsole() - tray, err := newSystray() if err != nil { logrus.Fatalf("unable to create systray: %v", err) @@ -262,6 +250,7 @@ func main() { go func() { <-tray.quit + logrus.Info("shutting down...") l.Close() err := tray.shutdown() if err != nil { @@ -269,6 +258,9 @@ func main() { } }() + // detach console + sys.FreeConsole() + // server loop for { conn, err := l.Accept() diff --git a/pkg/alertsender/systray/systray.go b/pkg/alertsender/systray/systray.go index bdbf76589..dc1309fd2 100644 --- a/pkg/alertsender/systray/systray.go +++ b/pkg/alertsender/systray/systray.go @@ -25,6 +25,7 @@ import ( "github.com/Microsoft/go-winio" "github.com/cenkalti/backoff/v4" "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/kevent" log "github.com/sirupsen/logrus" "os" "time" @@ -111,11 +112,13 @@ func makeSender(config alertsender.Config) (alertsender.Sender, error) { break } - return s, s.writePipe(&Msg{Type: Conf, Data: c}) + return s, s.send(&Msg{Type: Conf, Data: c}) } func (s *systray) Send(alert alertsender.Alert) error { - return s.writePipe(&Msg{Type: Balloon, Data: alert}) + // remove all events to avoid decoding errors on systray server end + alert.Events = make([]*kevent.Kevent, 0) + return s.send(&Msg{Type: Balloon, Data: alert}) } func (*systray) Type() alertsender.Type { return alertsender.Systray } @@ -123,7 +126,7 @@ func (*systray) SupportsMarkdown() bool { return false } func (s *systray) Shutdown() error { return nil } -func (s *systray) writePipe(m *Msg) error { +func (s *systray) send(m *Msg) error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() conn, err := winio.DialPipeContext(ctx, systrayPipe) @@ -131,6 +134,7 @@ func (s *systray) writePipe(m *Msg) error { return fmt.Errorf("unable to dial %s pipe: %v", systrayPipe, err) } defer conn.Close() + b, err := m.encode() if err != nil { return err @@ -141,10 +145,12 @@ func (s *systray) writePipe(m *Msg) error { if _, err = conn.Write(b); err != nil { return fmt.Errorf("unable to write systray pipe: %v", err) } + return nil } func pipeExists() bool { _, err := os.Stat(systrayPipe) + log.Warnf("pipe not found: %v", err) return err == nil } From da096c5c3f296f528eb26df8f0314e1811687ade Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Thu, 28 Nov 2024 21:21:31 +0100 Subject: [PATCH 3/3] refactor(alertsenders): Launch systray server manually The systray server is automatically launched when the systray alert sender is enabled. The process is created either via the regular CreateProcess API or CreateProcessAsUser when Fibratus is running inside Windows Service. This requires querying the user token of the currently logged-in console session user. The TCB privilege must be present in the user token. --- build/msi/fibratus.wxs | 20 +------ cmd/systray/main_windows.go | 41 +++++++------- pkg/alertsender/systray/systray.go | 90 +++++++++++++++++++++++++++++- pkg/sys/privilege.go | 34 +++++++++-- pkg/sys/syscall.go | 2 + pkg/sys/util.go | 35 ++++++++++++ pkg/sys/zsyscall_windows.go | 14 +++++ 7 files changed, 188 insertions(+), 48 deletions(-) create mode 100644 pkg/sys/util.go diff --git a/build/msi/fibratus.wxs b/build/msi/fibratus.wxs index fb899d626..a00687178 100644 --- a/build/msi/fibratus.wxs +++ b/build/msi/fibratus.wxs @@ -18,11 +18,6 @@ - @@ -39,8 +34,6 @@ - - @@ -57,14 +50,6 @@ - - - - @@ -85,10 +70,7 @@ - - - - + diff --git a/cmd/systray/main_windows.go b/cmd/systray/main_windows.go index 4721ee146..c3e479b26 100644 --- a/cmd/systray/main_windows.go +++ b/cmd/systray/main_windows.go @@ -181,26 +181,25 @@ func (s *Systray) loadIconFromResource(mod windows.Handle) (windows.Handle, erro func (s *Systray) handlePipeClient(conn net.Conn) { defer conn.Close() - for { - buf, err := io.ReadAll(conn) - if err != nil { - if err != io.EOF { - logrus.Errorf("pipe read: %v", err) - } - break - } - var m Msg - err = json.Unmarshal(buf, &m) - if err != nil { - logrus.Error(err) - break - } - err = s.handleMessage(m) - if err != nil { - logrus.Error(err) - break + buf, err := io.ReadAll(conn) + if err != nil { + if err != io.EOF { + logrus.Errorf("pipe read: %v", err) } } + if len(buf) == 0 { + return + } + var m Msg + err = json.Unmarshal(buf, &m) + if err != nil { + logrus.Error(err) + return + } + err = s.handleMessage(m) + if err != nil { + logrus.Error(err) + } } func (s *Systray) handleMessage(m Msg) error { @@ -209,6 +208,7 @@ func (s *Systray) handleMessage(m Msg) error { var c systray.Config err := m.decode(&c) if err != nil { + logrus.Errorf("unable to decode systray server config: %v", err) return err } s.config = c @@ -231,7 +231,7 @@ func (s *Systray) handleMessage(m Msg) error { } func main() { - err := log.InitFromConfig(log.Config{Level: "info", LogStdout: true}, "fibratus-systray.log") + err := log.InitFromConfig(log.Config{Level: "info", LogStdout: true, Formatter: "text"}, "fibratus-systray.log") if err != nil { fmt.Printf("%v", err) os.Exit(1) @@ -258,9 +258,6 @@ func main() { } }() - // detach console - sys.FreeConsole() - // server loop for { conn, err := l.Accept() diff --git a/pkg/alertsender/systray/systray.go b/pkg/alertsender/systray/systray.go index dc1309fd2..cab1bfd7c 100644 --- a/pkg/alertsender/systray/systray.go +++ b/pkg/alertsender/systray/systray.go @@ -26,8 +26,12 @@ import ( "github.com/cenkalti/backoff/v4" "github.com/rabbitstack/fibratus/pkg/alertsender" "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/sys" log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" + "math" "os" + "path/filepath" "time" ) @@ -45,6 +49,7 @@ const systrayPipe = `\\.\pipe\fibratus-systray` // by the systray sender. type systray struct { config Config + proc windows.Handle // systray server process handle } // MsgType determines the type of the message sent @@ -85,6 +90,78 @@ func makeSender(config alertsender.Config) (alertsender.Sender, error) { return &systray{}, nil } + // spin up systray server process + var si windows.StartupInfo + var pi windows.ProcessInformation + + exe, err := os.Executable() + if err != nil { + return nil, err + } + cmdline := filepath.Join(filepath.Dir(exe), "fibratus-systray.exe") + argv, err := windows.UTF16PtrFromString(cmdline) + if err != nil { + return nil, err + } + + log.Infof("starting systray server process %q", cmdline) + + if sys.IsWindowsService() { + // if we're running inside Windows Service, the systray server + // process must be created in the console session and with the + // currently logged-in user access token + var token windows.Token + + // enable TCB privilege to obtain the console session user token + sys.SetTcbPrivilege() + + consoleSessionID := sys.WTSGetActiveConsoleSessionID() + if consoleSessionID == math.MaxUint32 && sys.IsInSandbox() { + // if we failed to obtain the console session ID and + // are running inside Windows Sandbox, use session 1 + consoleSessionID = 1 + } + + if sys.WTSQueryUserToken(consoleSessionID, &token) { + log.Infof("obtained user token for console session ID %d", consoleSessionID) + err = windows.CreateProcessAsUser( + token, + nil, + argv, + nil, + nil, + false, + windows.CREATE_NO_WINDOW, + nil, + nil, + &si, + &pi, + ) + } else { + err = fmt.Errorf("unable to obtain user token for console session ID %d", consoleSessionID) + } + + // drop TCB privilege and close the token handle + sys.RemoveTcbPrivilege() + _ = windows.CloseHandle(windows.Handle(token)) + } else { + err = windows.CreateProcess( + nil, + argv, + nil, + nil, + false, + windows.CREATE_NO_WINDOW, + nil, + nil, + &si, + &pi) + } + + if err != nil { + log.Warnf("unable to start systray server process: %v", err) + } + s := &systray{config: c} b := &backoff.ExponentialBackOff{ // first backoff timeout will be somewhere in the 100 - 300 ms range given the default multiplier @@ -112,6 +189,10 @@ func makeSender(config alertsender.Config) (alertsender.Sender, error) { break } + log.Info("established connection to systray server") + + s.proc = pi.Process + return s, s.send(&Msg{Type: Conf, Data: c}) } @@ -124,11 +205,17 @@ func (s *systray) Send(alert alertsender.Alert) error { func (*systray) Type() alertsender.Type { return alertsender.Systray } func (*systray) SupportsMarkdown() bool { return false } -func (s *systray) Shutdown() error { return nil } +func (s *systray) Shutdown() error { + if s.proc != 0 && sys.IsProcessRunning(s.proc) { + return windows.TerminateProcess(s.proc, 1) + } + return nil +} func (s *systray) send(m *Msg) error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() + conn, err := winio.DialPipeContext(ctx, systrayPipe) if err != nil { return fmt.Errorf("unable to dial %s pipe: %v", systrayPipe, err) @@ -151,6 +238,5 @@ func (s *systray) send(m *Msg) error { func pipeExists() bool { _, err := os.Stat(systrayPipe) - log.Warnf("pipe not found: %v", err) return err == nil } diff --git a/pkg/sys/privilege.go b/pkg/sys/privilege.go index 206d13b53..fc331ce20 100644 --- a/pkg/sys/privilege.go +++ b/pkg/sys/privilege.go @@ -29,6 +29,8 @@ import ( const ( // SeDebugPrivilege is the name of the privilege used to debug programs. SeDebugPrivilege = "SeDebugPrivilege" + // SeTcbPrivilege privilege identifies its holder as part of the trusted computer base. + SeTcbPrivilege = "SeTcbPrivilege" ) // Errors returned by AdjustTokenPrivileges. @@ -41,6 +43,8 @@ const ( const ( // PrivilegedEnabled enables the privilege. PrivilegedEnabled uint32 = 0x00000002 + // PrivilegeRemoved removes the privilege. + PrivilegeRemoved uint32 = 0x00000004 ) // mapPrivileges maps privilege names to LUID values. @@ -57,10 +61,10 @@ func mapPrivileges(names []string) ([]windows.LUID, error) { return privileges, nil } -// EnableTokenPrivileges enables the specified privileges in the given +// adjustTokenPrivileges enables/disables the specified privileges in the given // Token. The token must have TOKEN_ADJUST_PRIVILEGES access. If the token // does not already contain the privilege it cannot be enabled. -func EnableTokenPrivileges(token windows.Token, privileges ...string) error { +func adjustTokenPrivileges(token windows.Token, state uint32, privileges ...string) error { privValues, err := mapPrivileges(privileges) if err != nil { return err @@ -74,7 +78,7 @@ func EnableTokenPrivileges(token windows.Token, privileges ...string) error { if err := binary.Write(&b, binary.LittleEndian, p); err != nil { continue } - if err := binary.Write(&b, binary.LittleEndian, PrivilegedEnabled); err != nil { + if err := binary.Write(&b, binary.LittleEndian, state); err != nil { continue } } @@ -90,9 +94,29 @@ func EnableTokenPrivileges(token windows.Token, privileges ...string) error { return nil } -// SetDebugPrivilege sets the debug privilege in the current running process. +// SetDebugPrivilege sets the debug privilege in the current process token. func SetDebugPrivilege() { + enablePrivileges(SeDebugPrivilege) +} + +// SetTcbPrivilege sets the TCB privilege in the current process token. +func SetTcbPrivilege() { + enablePrivileges(SeTcbPrivilege) +} + +// RemoveTcbPrivilege removes the TCB privilege from the access token of the current process. +func RemoveTcbPrivilege() { + removePrivileges(SeTcbPrivilege) +} + +func enablePrivileges(privs ...string) { + var token windows.Token + _ = windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token) + _ = adjustTokenPrivileges(token, PrivilegedEnabled, privs...) +} + +func removePrivileges(privs ...string) { var token windows.Token _ = windows.OpenProcessToken(windows.CurrentProcess(), windows.TOKEN_ADJUST_PRIVILEGES|windows.TOKEN_QUERY, &token) - _ = EnableTokenPrivileges(token, SeDebugPrivilege) + _ = adjustTokenPrivileges(token, PrivilegeRemoved, privs...) } diff --git a/pkg/sys/syscall.go b/pkg/sys/syscall.go index f02f5709b..9e185b935 100644 --- a/pkg/sys/syscall.go +++ b/pkg/sys/syscall.go @@ -44,6 +44,8 @@ package sys // Windows Terminal Server Functions //sys WTSQuerySessionInformationA(handle windows.Handle, sessionID uint32, klass uint8, buf **uint16, size *uint32) (err error) = wtsapi32.WTSQuerySessionInformationW +//sys WTSGetActiveConsoleSessionID() (n uint32) = kernel32.WTSGetActiveConsoleSessionId +//sys WTSQueryUserToken(sessionID uint32, token *windows.Token) (ok bool) = wtsapi32.WTSQueryUserToken // Windows Trust Functions //sys WinVerifyTrust(handle windows.Handle, action *windows.GUID, data *WintrustData) (ret uint32, err error) [failretval!=0] = wintrust.WinVerifyTrust diff --git a/pkg/sys/util.go b/pkg/sys/util.go new file mode 100644 index 000000000..a159aee65 --- /dev/null +++ b/pkg/sys/util.go @@ -0,0 +1,35 @@ +/* + * Copyright 2021-2022 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sys + +import "golang.org/x/sys/windows/registry" + +// IsInSandbox indicates if the process is running inside an +// isolated environment such as Windows Containers or Windows +// Sandbox. +func IsInSandbox() bool { + key, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control`, registry.QUERY_VALUE) + if err != nil { + return false + } + defer key.Close() + v, _, err := key.GetIntegerValue("ContainerType") + + return err == nil && v > 0 +} diff --git a/pkg/sys/zsyscall_windows.go b/pkg/sys/zsyscall_windows.go index 04d957164..c22f380c4 100644 --- a/pkg/sys/zsyscall_windows.go +++ b/pkg/sys/zsyscall_windows.go @@ -61,6 +61,7 @@ var ( procGetPackageId = modkernel32.NewProc("GetPackageId") procGetProcessIdOfThread = modkernel32.NewProc("GetProcessIdOfThread") procTerminateThread = modkernel32.NewProc("TerminateThread") + procWTSGetActiveConsoleSessionId = modkernel32.NewProc("WTSGetActiveConsoleSessionId") procNtAlpcQueryInformation = modntdll.NewProc("NtAlpcQueryInformation") procNtCreateSection = modntdll.NewProc("NtCreateSection") procNtMapViewOfSection = modntdll.NewProc("NtMapViewOfSection") @@ -89,6 +90,7 @@ var ( procCryptCATCatalogInfoFromContext = modwintrust.NewProc("CryptCATCatalogInfoFromContext") procWinVerifyTrust = modwintrust.NewProc("WinVerifyTrust") procWTSQuerySessionInformationW = modwtsapi32.NewProc("WTSQuerySessionInformationW") + procWTSQueryUserToken = modwtsapi32.NewProc("WTSQueryUserToken") ) func SymEnumLoadedModules(handle windows.Handle, callback uintptr, ctx uintptr) (b bool) { @@ -175,6 +177,12 @@ func TerminateThread(handle windows.Handle, exitCode uint32) (err error) { return } +func WTSGetActiveConsoleSessionID() (n uint32) { + r0, _, _ := syscall.Syscall(procWTSGetActiveConsoleSessionId.Addr(), 0, 0, 0, 0) + n = uint32(r0) + return +} + func NtAlpcQueryInformation(handle windows.Handle, alpcInfoClass int32, alpcInfo unsafe.Pointer, alpcInfoLen uint32, retLen *uint32) (ntstatus error) { r0, _, _ := syscall.Syscall6(procNtAlpcQueryInformation.Addr(), 5, uintptr(handle), uintptr(alpcInfoClass), uintptr(alpcInfo), uintptr(alpcInfoLen), uintptr(unsafe.Pointer(retLen)), 0) if r0 != 0 { @@ -381,3 +389,9 @@ func WTSQuerySessionInformationA(handle windows.Handle, sessionID uint32, klass } return } + +func WTSQueryUserToken(sessionID uint32, token *windows.Token) (ok bool) { + r0, _, _ := syscall.Syscall(procWTSQueryUserToken.Addr(), 2, uintptr(sessionID), uintptr(unsafe.Pointer(token)), 0) + ok = r0 != 0 + return +}