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 000e04815..c3e479b26 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" ) @@ -181,28 +180,26 @@ 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) - if err != nil { - if err != io.EOF { - logrus.Errorf("pipe read: %v", err) - } - break - } - var m Msg - err = json.Unmarshal(buf[:n], &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 { @@ -211,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 @@ -218,6 +216,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 @@ -232,29 +231,18 @@ 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) } 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 named-pipe server - l, err := winio.ListenPipe(systrayPipe, &winio.PipeConfig{SecurityDescriptor: descriptor}) + // spin up the named-pipe server + 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 { diff --git a/pkg/alertsender/systray/systray.go b/pkg/alertsender/systray/systray.go index bdbf76589..cab1bfd7c 100644 --- a/pkg/alertsender/systray/systray.go +++ b/pkg/alertsender/systray/systray.go @@ -25,8 +25,13 @@ import ( "github.com/Microsoft/go-winio" "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" ) @@ -44,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 @@ -84,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 @@ -111,26 +189,39 @@ func makeSender(config alertsender.Config) (alertsender.Sender, error) { break } - return s, s.writePipe(&Msg{Type: Conf, Data: c}) + log.Info("established connection to systray server") + + s.proc = pi.Process + + 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 } 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) 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) if err != nil { return fmt.Errorf("unable to dial %s pipe: %v", systrayPipe, err) } defer conn.Close() + b, err := m.encode() if err != nil { return err @@ -141,6 +232,7 @@ 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 } 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 +}