diff --git a/internal/etw/consumer.go b/internal/etw/consumer.go index f2ce29b4a..6f5d2d979 100644 --- a/internal/etw/consumer.go +++ b/internal/etw/consumer.go @@ -79,7 +79,7 @@ func (c *Consumer) ProcessEvent(ev *etw.EventRecord) error { eventsUnknown.Add(1) return nil } - if event.IsCurrentProcDropped(ev.Header.ProcessID) { + if event.IsCurrentProcDropped(ev.Header.ProcessID) && ev.Header.ProviderID != etw.WindowsKernelProcessGUID { return nil } if c.config.EventSource.ExcludeEvent(ev.ID()) { diff --git a/internal/etw/processors/image_windows.go b/internal/etw/processors/image_windows.go index c0d262a87..9ab7d2b2f 100644 --- a/internal/etw/processors/image_windows.go +++ b/internal/etw/processors/image_windows.go @@ -55,6 +55,11 @@ func newImageProcessor(psnap ps.Snapshotter) Processor { func (*imageProcessor) Name() ProcessorType { return Image } func (m *imageProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { + if e.IsLoadImageInternal() { + // state management + return e, false, m.psnap.AddModule(e) + } + if e.IsLoadImage() { // is image characteristics data cached? path := e.GetParamAsString(params.ImagePath) diff --git a/internal/etw/processors/ps_windows.go b/internal/etw/processors/ps_windows.go index a52fe2e83..0e104e07c 100644 --- a/internal/etw/processors/ps_windows.go +++ b/internal/etw/processors/ps_windows.go @@ -41,7 +41,7 @@ func newPsProcessor(psnap ps.Snapshotter, regionProber *va.RegionProber) Process func (p psProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { switch e.Type { - case event.CreateProcess, event.TerminateProcess, event.ProcessRundown: + case event.CreateProcess, event.CreateProcessInternal, event.TerminateProcess, event.ProcessRundown, event.ProcessRundownInternal: evt, err := p.processEvent(e) if evt.IsTerminateProcess() { p.regionProber.Remove(evt.Params.MustGetPid()) @@ -86,6 +86,10 @@ func (p psProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { //nolint:unparam func (p psProcessor) processEvent(e *event.Event) (*event.Event, error) { + if e.IsCreateProcessInternal() || e.IsProcessRundownInternal() { + return e, nil + } + cmndline := cmdline.New(e.GetParamAsString(params.Cmdline)). // get rid of leading/trailing quotes in the executable path CleanExe(). diff --git a/internal/etw/source.go b/internal/etw/source.go index 7aa84f3cf..888d72ee6 100644 --- a/internal/etw/source.go +++ b/internal/etw/source.go @@ -161,12 +161,15 @@ func (e *EventSource) Open(config *config.Config) error { } } - // add the core NT Kernel Logger trace - e.addTrace(NewKernelTrace(config)) - - // security telemetry trace hosts remaining ETW providers + // security telemetry trace hosts all ETW providers but NT Kernel Logger trace := NewTrace(etw.SecurityTelemetrySession, config) + // Windows Kernel Process session permits enriching event state with + // additional attributes and guaranteeing that any event published by + // the security telemetry session doesn't miss its respective process + // from the snapshotter + trace.AddProvider(etw.WindowsKernelProcessGUID, false, WithKeywords(etw.ProcessKeyword|etw.ImageKeyword), WithCaptureState()) + if config.EventSource.EnableDNSEvents { trace.AddProvider(etw.DNSClientGUID, false) } @@ -186,21 +189,21 @@ func (e *EventSource) Open(config *config.Config) error { trace.AddProvider(etw.ThreadpoolGUID, config.EventSource.StackEnrichment, WithStackExts(stackexts)) } - if trace.HasProviders() { - // add security telemetry trace - e.addTrace(trace) - } + // add security telemetry trace + e.addTrace(trace) + // add the core NT Kernel Logger trace + e.addTrace(NewKernelTrace(config)) - for _, trace := range e.traces { - err := trace.Start() + for _, t := range e.traces { + err := t.Start() switch err { case errs.ErrTraceAlreadyRunning: - log.Debugf("%s trace is already running. Trying to restart...", trace.Name) - if err := trace.Stop(); err != nil { + log.Debugf("%s trace is already running. Trying to restart...", t.Name) + if err := t.Stop(); err != nil { return err } time.Sleep(time.Millisecond * 100) - if err := trace.Start(); err != nil { + if err := t.Start(); err != nil { return multierror.Wrap(errs.ErrRestartTrace, err) } case errs.ErrTraceNoSysResources: @@ -234,15 +237,22 @@ func (e *EventSource) Open(config *config.Config) error { } e.consumers = append(e.consumers, consumer) - err = trace.Open(consumer, e.errs) + // Open the trace and assign a consumer + err = t.Open(consumer, e.errs) + if err != nil { + return fmt.Errorf("unable to open %s trace: %v", t.Name, err) + } + log.Infof("starting [%s] trace processing", t.Name) + + // Instruct the provider to emit state information + err = t.CaptureState() if err != nil { - return fmt.Errorf("unable to open %s trace: %v", trace.Name, err) + log.Warn(err) } - log.Infof("starting [%s] trace processing", trace.Name) // Start event processing loop errch := make(chan error) - go trace.Process(errch) + go t.Process(errch) go func(trace *Trace) { select { @@ -254,7 +264,7 @@ func (e *EventSource) Open(config *config.Config) error { e.errs <- fmt.Errorf("unable to process %s trace: %v", trace.Name, err) } } - }(trace) + }(t) } return nil diff --git a/internal/etw/source_test.go b/internal/etw/source_test.go index 830dda6ad..adf7959d6 100644 --- a/internal/etw/source_test.go +++ b/internal/etw/source_test.go @@ -98,7 +98,7 @@ func TestEventSourceStartTraces(t *testing.T) { }, Filters: &config.Filters{}, }, - 1, + 2, []etw.EventTraceFlags{0x6018203, 0}, }, {"start kernel and security telemetry logger sessions", @@ -199,10 +199,10 @@ func TestEventSourceEnableFlagsDynamically(t *testing.T) { require.NoError(t, evs.Open(cfg)) defer evs.Close() - flags := evs.(*EventSource).traces[0].enableFlagsDynamically(cfg.EventSource) - require.Len(t, evs.(*EventSource).traces, 2) + flags := evs.(*EventSource).traces[1].enableFlagsDynamically(cfg.EventSource) + require.True(t, flags&etw.FileIO != 0) require.True(t, flags&etw.Process != 0) // rules compile result doesn't have the thread event @@ -284,7 +284,9 @@ func TestEventSourceEnableFlagsDynamicallyWithYaraEnabled(t *testing.T) { require.NoError(t, evs.Open(cfg)) defer evs.Close() - flags := evs.(*EventSource).traces[0].enableFlagsDynamically(cfg.EventSource) + require.Len(t, evs.(*EventSource).traces, 2) + + flags := evs.(*EventSource).traces[1].enableFlagsDynamically(cfg.EventSource) // rules compile result doesn't have file events // but Yara file scanning is enabled diff --git a/internal/etw/trace.go b/internal/etw/trace.go index c64a6c420..98d7eb31b 100644 --- a/internal/etw/trace.go +++ b/internal/etw/trace.go @@ -56,6 +56,7 @@ func initEventTraceProps(c config.EventSourceConfig) etw.EventTraceProperties { if flushTimer < time.Second { flushTimer = time.Second } + mode := uint32(etw.ProcessTraceModeRealtime) return etw.EventTraceProperties{ @@ -88,6 +89,9 @@ type ProviderInfo struct { // EnableStacks indicates if callstacks are enabled for // this provider. EnableStacks bool + // CaptureState requests that the provider log its state + // information, such as rundown events. + CaptureState bool // stackExtensions manager stack tracing enablement. // For each event present in the stack identifiers, // the StackWalk event is published by the provider. @@ -146,8 +150,9 @@ type Trace struct { } type opts struct { - stackexts *StackExtensions - keywords uint64 + stackexts *StackExtensions + keywords uint64 + captureState bool } // Option represents the option for the trace. @@ -168,6 +173,14 @@ func WithKeywords(keywords uint64) Option { } } +// WithCaptureState indicates that the provider should +// emit its state information. +func WithCaptureState() Option { + return func(o *opts) { + o.captureState = true + } +} + // NewKernelTrace creates a new NT Kernel Logger trace. func NewKernelTrace(config *config.Config) *Trace { t := &Trace{Name: etw.KernelLoggerSession, GUID: etw.KernelTraceControlGUID, stackExtensions: NewStackExtensions(config.EventSource), config: config} @@ -193,7 +206,10 @@ func (t *Trace) AddProvider(guid windows.GUID, enableStacks bool, options ...Opt opt(&opts) } - t.Providers = append(t.Providers, ProviderInfo{GUID: guid, Keywords: opts.keywords, EnableStacks: enableStacks, stackExtensions: opts.stackexts}) + t.Providers = append( + t.Providers, + ProviderInfo{GUID: guid, Keywords: opts.keywords, EnableStacks: enableStacks, CaptureState: opts.captureState, stackExtensions: opts.stackexts}, + ) } // HasProviders determines if this trace contains providers. @@ -235,6 +251,7 @@ func (t *Trace) Start() error { cfg := t.config.EventSource props := initEventTraceProps(cfg) flags := t.enableFlagsDynamically(cfg) + if t.IsKernelTrace() { props.EnableFlags = flags props.Wnode.GUID = t.GUID @@ -408,6 +425,20 @@ func (t *Trace) Close() error { return etw.CloseTrace(t.openHandle) } +// CaptureState forces the provider to publish state +// information such as rundown events. +func (t *Trace) CaptureState() error { + for _, provider := range t.Providers { + if !provider.CaptureState { + continue + } + if err := etw.CaptureProviderState(provider.GUID, t.startHandle); err != nil { + return fmt.Errorf("unable to capture %s provider state: %v", provider.GUID, err) + } + } + return nil +} + // IsKernelTrace determines if this is the system logger trace. func (t *Trace) IsKernelTrace() bool { return t.GUID == etw.KernelTraceControlGUID } diff --git a/pkg/alertsender/alert_test.go b/pkg/alertsender/alert_test.go index 697596269..e429d1f3e 100644 --- a/pkg/alertsender/alert_test.go +++ b/pkg/alertsender/alert_test.go @@ -52,16 +52,17 @@ func TestAlertString(t *testing.T) { Name: "CreateProcess", PID: 1023, PS: &pstypes.PS{ - Name: "svchost.exe", - Cmdline: "C:\\Windows\\System32\\svchost.exe", - Ppid: 345, - Username: "SYSTEM", - Domain: "NT AUTHORITY", - SID: "S-1-5-18", + Name: "svchost.exe", + Cmdline: "C:\\Windows\\System32\\svchost.exe", + Ppid: 345, + Username: "SYSTEM", + Domain: "NT AUTHORITY", + SID: "S-1-5-18", + TokenIntegrityLevel: "HIGH", }, }}), true, - "Credential discovery via VaultCmd.exe\n\nSuspicious vault enumeration via VaultCmd tool\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdline➜ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, name➜ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n", + "Credential discovery via VaultCmd.exe\n\nSuspicious vault enumeration via VaultCmd tool\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdline➜ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, name➜ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tIntegrity level: HIGH\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n", }, { NewAlertWithEvents("Credential discovery via VaultCmd.exe", "", nil, Normal, []*event.Event{{ @@ -73,16 +74,17 @@ func TestAlertString(t *testing.T) { Name: "CreateProcess", PID: 1023, PS: &pstypes.PS{ - Name: "svchost.exe", - Cmdline: "C:\\Windows\\System32\\svchost.exe", - Ppid: 345, - Username: "SYSTEM", - Domain: "NT AUTHORITY", - SID: "S-1-5-18", + Name: "svchost.exe", + Cmdline: "C:\\Windows\\System32\\svchost.exe", + Ppid: 345, + Username: "SYSTEM", + Domain: "NT AUTHORITY", + SID: "S-1-5-18", + TokenIntegrityLevel: "HIGH", }, }}), true, - "Credential discovery via VaultCmd.exe\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdline➜ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, name➜ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n", + "Credential discovery via VaultCmd.exe\n\nSeverity: low\n\nSystem event involved in this alert:\n\n\tEvent #1:\n\n\t\tSeq: 0\n\t\tPid: 1023\n\t\tTid: 0\n\t\tName: CreateProcess\n\t\tCategory: process\n\t\tHost: \n\t\tTimestamp: 0001-01-01 00:00:00 +0000 UTC\n\t\tParameters: cmdline➜ C:\\Windows\\system32\\svchost-fake.exe -k RPCSS, name➜ svchost-fake.exe\n \n\t\tPid: 0\n\t\tPpid: 345\n\t\tName: svchost.exe\n\t\tCmdline: C:\\Windows\\System32\\svchost.exe\n\t\tExe: \n\t\tCwd: \n\t\tSID: S-1-5-18\n\t\tIntegrity level: HIGH\n\t\tUsername: SYSTEM\n\t\tDomain: NT AUTHORITY\n\t\tArgs: []\n\t\tSession ID: 0\n\t\tAncestors: \n\t\n", }, } diff --git a/pkg/event/enum.go b/pkg/event/enum.go index 6d2f71c52..e4cd47c44 100644 --- a/pkg/event/enum.go +++ b/pkg/event/enum.go @@ -114,3 +114,16 @@ var DNSResponseCodes = ParamEnum{ uint32(windows.ERROR_INVALID_PARAMETER): "INVALID", uint32(windows.DNS_INFO_NO_RECORDS): "NXDOMAIN", } + +const ( + TokenElevationTypeDefault uint32 = iota + 1 + TokenElevationTypeFull + TokenElevationTypeLimited +) + +// PsTokenElevationTypes describes process token elevation types +var PsTokenElevationTypes = ParamEnum{ + TokenElevationTypeDefault: "DEFAULT", + TokenElevationTypeFull: "FULL", + TokenElevationTypeLimited: "LIMITED", +} diff --git a/pkg/event/event_windows.go b/pkg/event/event_windows.go index 32e103863..0782a9740 100644 --- a/pkg/event/event_windows.go +++ b/pkg/event/event_windows.go @@ -202,8 +202,8 @@ func (e *Event) IsSuccess() bool { // the tracing session to induce the arrival of rundown events // by calling into the `etw.SetTraceInformation` Windows API // function which causes duplicate rundown events. -// For more pointers check `kstream/controller_windows.go` -// and the `etw.SetTraceInformation` API function. +// For more pointers check `internal/etw/trace.go` and the +// `etw.SetTraceInformation` API function. func (e *Event) IsRundownProcessed() bool { mu.Lock() defer mu.Unlock() @@ -216,26 +216,29 @@ func (e *Event) IsRundownProcessed() bool { return false } -func (e *Event) IsCreateFile() bool { return e.Type == CreateFile } -func (e *Event) IsCreateProcess() bool { return e.Type == CreateProcess } -func (e *Event) IsCreateThread() bool { return e.Type == CreateThread } -func (e *Event) IsCloseFile() bool { return e.Type == CloseFile } -func (e *Event) IsCreateHandle() bool { return e.Type == CreateHandle } -func (e *Event) IsCloseHandle() bool { return e.Type == CloseHandle } -func (e *Event) IsDeleteFile() bool { return e.Type == DeleteFile } -func (e *Event) IsEnumDirectory() bool { return e.Type == EnumDirectory } -func (e *Event) IsTerminateProcess() bool { return e.Type == TerminateProcess } -func (e *Event) IsTerminateThread() bool { return e.Type == TerminateThread } -func (e *Event) IsUnloadImage() bool { return e.Type == UnloadImage } -func (e *Event) IsLoadImage() bool { return e.Type == LoadImage } -func (e *Event) IsImageRundown() bool { return e.Type == ImageRundown } -func (e *Event) IsFileOpEnd() bool { return e.Type == FileOpEnd } -func (e *Event) IsRegSetValue() bool { return e.Type == RegSetValue } -func (e *Event) IsProcessRundown() bool { return e.Type == ProcessRundown } -func (e *Event) IsVirtualAlloc() bool { return e.Type == VirtualAlloc } -func (e *Event) IsMapViewFile() bool { return e.Type == MapViewFile } -func (e *Event) IsUnmapViewFile() bool { return e.Type == UnmapViewFile } -func (e *Event) IsStackWalk() bool { return e.Type == StackWalk } +func (e *Event) IsCreateFile() bool { return e.Type == CreateFile } +func (e *Event) IsCreateProcess() bool { return e.Type == CreateProcess } +func (e *Event) IsCreateProcessInternal() bool { return e.Type == CreateProcessInternal } +func (e *Event) IsCreateThread() bool { return e.Type == CreateThread } +func (e *Event) IsCloseFile() bool { return e.Type == CloseFile } +func (e *Event) IsCreateHandle() bool { return e.Type == CreateHandle } +func (e *Event) IsCloseHandle() bool { return e.Type == CloseHandle } +func (e *Event) IsDeleteFile() bool { return e.Type == DeleteFile } +func (e *Event) IsEnumDirectory() bool { return e.Type == EnumDirectory } +func (e *Event) IsTerminateProcess() bool { return e.Type == TerminateProcess } +func (e *Event) IsTerminateThread() bool { return e.Type == TerminateThread } +func (e *Event) IsUnloadImage() bool { return e.Type == UnloadImage } +func (e *Event) IsLoadImage() bool { return e.Type == LoadImage } +func (e *Event) IsLoadImageInternal() bool { return e.Type == LoadImageInternal } +func (e *Event) IsImageRundown() bool { return e.Type == ImageRundown } +func (e *Event) IsFileOpEnd() bool { return e.Type == FileOpEnd } +func (e *Event) IsRegSetValue() bool { return e.Type == RegSetValue } +func (e *Event) IsProcessRundown() bool { return e.Type == ProcessRundown } +func (e *Event) IsProcessRundownInternal() bool { return e.Type == ProcessRundownInternal } +func (e *Event) IsVirtualAlloc() bool { return e.Type == VirtualAlloc } +func (e *Event) IsMapViewFile() bool { return e.Type == MapViewFile } +func (e *Event) IsUnmapViewFile() bool { return e.Type == UnmapViewFile } +func (e *Event) IsStackWalk() bool { return e.Type == StackWalk } // InvalidPid indicates if the process generating the event is invalid. func (e *Event) InvalidPid() bool { return e.PID == sys.InvalidProcessID } diff --git a/pkg/event/metainfo_windows.go b/pkg/event/metainfo_windows.go index d400563eb..7794315f0 100644 --- a/pkg/event/metainfo_windows.go +++ b/pkg/event/metainfo_windows.go @@ -238,6 +238,9 @@ func AllWithState() []Type { s = append(s, ReleaseFile) s = append(s, MapFileRundown) s = append(s, StackWalk) + s = append(s, CreateProcessInternal) + s = append(s, ProcessRundownInternal) + s = append(s, LoadImageInternal) return s } diff --git a/pkg/event/param_windows.go b/pkg/event/param_windows.go index 021c04bb2..870dd7ef9 100644 --- a/pkg/event/param_windows.go +++ b/pkg/event/param_windows.go @@ -24,7 +24,9 @@ import ( "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/fs" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/sys/etw" + "github.com/rabbitstack/fibratus/pkg/util/filetime" "github.com/rabbitstack/fibratus/pkg/util/ip" "github.com/rabbitstack/fibratus/pkg/util/key" "github.com/rabbitstack/fibratus/pkg/util/ntstatus" @@ -77,6 +79,9 @@ func (p Param) String() string { if err != nil { return "" } + if p.Name == params.ProcessIntegrityLevel { + return sys.RidToString(sid) + } return sid.String() case params.DOSPath: return devMapper.Convert(p.Value.(string)) @@ -180,7 +185,10 @@ func (pars Params) GetSID() (*windows.SID, error) { func getSID(param *Param) (*windows.SID, error) { sid, ok := param.Value.([]byte) if !ok { - return nil, fmt.Errorf("unable to type cast %q parameter to []byte value", params.UserSID) + return nil, fmt.Errorf("unable to type cast %q parameter to []byte value", param.Name) + } + if sid == nil { + return nil, fmt.Errorf("sid linked to parameter %s is empty", param.Name) } b := uintptr(unsafe.Pointer(&sid[0])) if param.Type == params.WbemSID { @@ -207,9 +215,7 @@ func (pars Params) MustGetSID() *windows.SID { // schema changes in order to parse new fields. func (e *Event) produceParams(evt *etw.EventRecord) { switch e.Type { - case ProcessRundown, - CreateProcess, - TerminateProcess: + case ProcessRundown, CreateProcess, TerminateProcess: var ( kproc uint64 pid, ppid uint32 @@ -247,7 +253,7 @@ func (e *Event) produceParams(evt *etw.EventRecord) { default: offset = 24 } - sid, soffset = evt.ReadSID(offset) + sid, soffset = evt.ReadSID(offset, true) name, noffset = evt.ReadAnsiString(soffset) cmdline, _ = evt.ReadUTF16String(soffset + noffset) e.AppendParam(params.ProcessObject, params.Address, kproc) @@ -261,6 +267,50 @@ func (e *Event) produceParams(evt *etw.EventRecord) { e.AppendParam(params.UserSID, params.WbemSID, sid) e.AppendParam(params.ProcessName, params.AnsiString, name) e.AppendParam(params.Cmdline, params.UnicodeString, cmdline) + case CreateProcessInternal, ProcessRundownInternal: + var ( + pid uint32 + createTime windows.Filetime + ppid uint32 + sessionID uint32 + flags uint32 + tokenElevationType uint32 + tokenIsElevated uint32 + tokenMandatoryLabel []byte + exe string + ) + + pid = evt.ReadUint32(0) + + if (e.IsCreateProcessInternal() && evt.Version() >= 3) || (e.IsProcessRundownInternal() && evt.Version() >= 1) { + createTime = windows.NsecToFiletime(int64(evt.ReadUint64(12))) // skip sequence number (8 bytes) + + ppid = evt.ReadUint32(20) + sessionID = evt.ReadUint32(32) // skip parent sequence number (8 bytes) + flags = evt.ReadUint32(36) + tokenElevationType = evt.ReadUint32(40) + tokenIsElevated = evt.ReadUint32(44) + + tokenMandatoryLabel, _ = evt.ReadSID(48, false) // integrity level SID size is 12 bytes + + exe, _ = evt.ReadNTUnicodeString(60) + } else { + createTime = windows.NsecToFiletime(int64(evt.ReadUint64(8))) + ppid = evt.ReadUint32(16) + sessionID = evt.ReadUint32(20) + flags = evt.ReadUint32(24) + exe, _ = evt.ReadNTUnicodeString(28) + } + + e.AppendParam(params.ProcessID, params.PID, pid) + e.AppendParam(params.StartTime, params.Time, filetime.ToEpoch(uint64(createTime.Nanoseconds()))) + e.AppendParam(params.ProcessParentID, params.PID, ppid) + e.AppendParam(params.SessionID, params.Uint32, sessionID) + e.AppendParam(params.ProcessFlags, params.Flags, flags, WithFlags(PsCreationFlags)) + e.AppendParam(params.ProcessTokenElevationType, params.Enum, tokenElevationType, WithEnum(PsTokenElevationTypes)) + e.AppendParam(params.ProcessTokenIsElevated, params.Bool, tokenIsElevated > 0) + e.AppendParam(params.ProcessIntegrityLevel, params.SID, tokenMandatoryLabel) + e.AppendParam(params.Exe, params.DOSPath, exe) case OpenProcess: processID := evt.ReadUint32(0) desiredAccess := evt.ReadUint32(4) @@ -276,9 +326,7 @@ func (e *Event) produceParams(evt *etw.EventRecord) { ((desiredAccess & windows.PROCESS_CREATE_THREAD) != 0) || ((desiredAccess & windows.PROCESS_SET_INFORMATION) != 0) { e.AppendParam(params.Callstack, params.Slice, evt.Callstack()) } - case CreateThread, - TerminateThread, - ThreadRundown: + case CreateThread, TerminateThread, ThreadRundown: var ( pid uint32 tid uint32 @@ -369,9 +417,7 @@ func (e *Event) produceParams(evt *etw.EventRecord) { e.AppendParam(params.HandleObjectTypeID, params.HandleType, typeID) e.AppendParam(params.ProcessID, params.PID, sourcePID) e.AppendParam(params.TargetProcessID, params.PID, targetPID) - case LoadImage, - UnloadImage, - ImageRundown: + case LoadImage, UnloadImage, ImageRundown: var ( pid uint32 checksum uint32 @@ -413,6 +459,29 @@ func (e *Event) produceParams(evt *etw.EventRecord) { e.AppendParam(params.ImagePath, params.DOSPath, filename) e.AppendParam(params.ImageSignatureLevel, params.Enum, uint32(sigLevel), WithEnum(signature.Levels)) e.AppendParam(params.ImageSignatureType, params.Enum, uint32(sigType), WithEnum(signature.Types)) + case LoadImageInternal: + var ( + pid uint32 + checksum uint32 + defaultBase uint64 + imageBase uint64 + imageSize uint64 + filename string + ) + + imageBase = evt.ReadUint64(0) + imageSize = evt.ReadUint64(8) + pid = evt.ReadUint32(16) + checksum = evt.ReadUint32(20) + defaultBase = evt.ReadUint64(28) // skip timestamp (4 bytes) + filename = evt.ConsumeUTF16String(36) + + e.AppendParam(params.ProcessID, params.PID, pid) + e.AppendParam(params.ImageCheckSum, params.Uint32, checksum) + e.AppendParam(params.ImageDefaultBase, params.Address, defaultBase) + e.AppendParam(params.ImageBase, params.Address, imageBase) + e.AppendParam(params.ImageSize, params.Uint64, imageSize) + e.AppendParam(params.ImagePath, params.DOSPath, filename) case RegOpenKey, RegCloseKey, RegCreateKCB, RegDeleteKCB, RegKCBRundown, RegCreateKey, @@ -512,9 +581,7 @@ func (e *Event) produceParams(evt *etw.EventRecord) { e.AppendParam(params.FileObject, params.Address, fileObject) e.AppendParam(params.FileKey, params.Address, fileKey) e.AppendParam(params.ThreadID, params.TID, tid) - case DeleteFile, - RenameFile, - SetFileInformation: + case DeleteFile, RenameFile, SetFileInformation: var ( irp uint64 fileObject uint64 diff --git a/pkg/event/params/params_windows.go b/pkg/event/params/params_windows.go index afed096c0..8be05a3bf 100644 --- a/pkg/event/params/params_windows.go +++ b/pkg/event/params/params_windows.go @@ -58,6 +58,12 @@ const ( ExitStatus = "exit_status" // StartTime field denotes the process start time. StartTime = "start_time" + // ProcessIntegrityLevel field denotes the process integrity level. + ProcessIntegrityLevel = "integrity_level" + // ProcessTokenElevationType field designates the process token elevation type. + ProcessTokenElevationType = "token_elevation_type" + // ProcessTokenIsElevated field designates if the process token is elevated. + ProcessTokenIsElevated = "token_is_elevated" // DesiredAccess field denotes the access rights for different kernel objects such as processes or threads. DesiredAccess = "desired_access" diff --git a/pkg/event/types_windows.go b/pkg/event/types_windows.go index 125f19d3f..52f7bb04b 100644 --- a/pkg/event/types_windows.go +++ b/pkg/event/types_windows.go @@ -65,6 +65,8 @@ var ( DNSEventGUID = windows.GUID{Data1: 0x1c95126e, Data2: 0x7eea, Data3: 0x49a9, Data4: [8]byte{0xa3, 0xfe, 0xa3, 0x78, 0xb0, 0x3d, 0xdb, 0x4d}} // ThreadpoolGUID represents the thread pool event GUID ThreadpoolGUID = windows.GUID{Data1: 0xc861d0e2, Data2: 0xa2c1, Data3: 0x4d36, Data4: [8]byte{0x9f, 0x9c, 0x97, 0x0b, 0xab, 0x94, 0x3a, 0x12}} + // ProcessKernelEventGUID represents the Process Kernel event GUID + ProcessKernelEventGUID = windows.GUID{Data1: 0x22fb2cd6, Data2: 0x0e7b, Data3: 0x422b, Data4: [8]byte{0xa0, 0xc7, 0x2f, 0xad, 0x1f, 0xd0, 0xe7, 0x16}} ) var ( @@ -76,6 +78,13 @@ var ( ProcessRundown = pack(ProcessEventGUID, 3) // OpenProcess identifies the kernel events that are triggered when the process handle is acquired OpenProcess = pack(AuditAPIEventGUID, 5) + // CreateProcessInternal identifies the process creation event emitted by the Microsoft Windows Kernel Process provider. + // The only purpose of this event is to enrich the process state with some extra attributes, and populates the snapshotter + // for events running in the Security Telemetry session that might miss process lookups because the core NT Kernel Provider + // hasn't still published the CreateProcess or ProcessRundown event + CreateProcessInternal = pack(ProcessKernelEventGUID, 1) + // ProcessRundownInternal same as above but for process rundown events originating from the Microsoft Windows Kernel Process provider. + ProcessRundownInternal = pack(ProcessKernelEventGUID, 15) // CreateThread identifies thread creation kernel events CreateThread = pack(ThreadEventGUID, 1) @@ -147,6 +156,8 @@ var ( ImageRundown = pack(ImageEventGUID, 3) // LoadImage represents load image kernel events that are triggered when a DLL or executable file is loaded LoadImage = pack(ImageEventGUID, 10) + // LoadImageInternal same as for process internal event originating from the Microsoft Windows Kernel Process provider. + LoadImageInternal = pack(ProcessKernelEventGUID, 5) // AcceptTCPv4 represents the TCPv4 kernel events for accepting connection requests from the socket queue. AcceptTCPv4 = pack(NetworkTCPEventGUID, 15) @@ -228,11 +239,11 @@ func NewTypeFromEventRecord(ev *etw.EventRecord) Type { // if the event type is not recognized. func (t Type) String() string { switch t { - case CreateProcess: + case CreateProcess, CreateProcessInternal: return "CreateProcess" case TerminateProcess: return "TerminateProcess" - case ProcessRundown: + case ProcessRundown, ProcessRundownInternal: return "ProcessRundown" case OpenProcess: return "OpenProcess" @@ -300,7 +311,7 @@ func (t Type) String() string { return "RegCreateKCB" case RegSetValue: return "RegSetValue" - case LoadImage: + case LoadImage, LoadImageInternal: return "LoadImage" case UnloadImage: return "UnloadImage" @@ -346,11 +357,11 @@ func (t Type) String() string { // Category determines the category to which the event type pertains. func (t Type) Category() Category { switch t { - case CreateProcess, TerminateProcess, OpenProcess, ProcessRundown: + case CreateProcess, CreateProcessInternal, TerminateProcess, OpenProcess, ProcessRundown, ProcessRundownInternal: return Process case CreateThread, TerminateThread, OpenThread, SetThreadContext, ThreadRundown, StackWalk: return Thread - case LoadImage, UnloadImage, ImageRundown: + case LoadImage, UnloadImage, ImageRundown, LoadImageInternal: return Image case CreateFile, ReadFile, WriteFile, EnumDirectory, DeleteFile, RenameFile, CloseFile, SetFileInformation, FileRundown, FileOpEnd, ReleaseFile, MapViewFile, UnmapViewFile, MapFileRundown: @@ -505,8 +516,11 @@ func (t Type) Exists() bool { func (t Type) OnlyState() bool { switch t { case ProcessRundown, + ProcessRundownInternal, + CreateProcessInternal, ThreadRundown, ImageRundown, + LoadImageInternal, FileRundown, RegKCBRundown, FileOpEnd, @@ -585,10 +599,8 @@ func (t Type) ID() uint { // Source designates the provenance of this event type. func (t Type) Source() Source { - switch t { - case OpenProcess, OpenThread, SetThreadContext, CreateSymbolicLinkObject, - QueryDNS, ReplyDNS, SubmitThreadpoolWork, SubmitThreadpoolCallback, - SetThreadpoolTimer: + switch t.GUID() { + case AuditAPIEventGUID, DNSEventGUID, ThreadpoolGUID, ProcessKernelEventGUID: return SecurityTelemetryLogger default: return SystemLogger diff --git a/pkg/ps/snapshotter_windows.go b/pkg/ps/snapshotter_windows.go index 0080d8b17..e2576efca 100644 --- a/pkg/ps/snapshotter_windows.go +++ b/pkg/ps/snapshotter_windows.go @@ -53,6 +53,7 @@ var ( moduleCount = expvar.NewInt("process.module.count") mmapCount = expvar.NewInt("process.mmap.count") pebReadErrors = expvar.NewInt("process.peb.read.errors") + processEnrichments = expvar.NewInt("process.enrichments") ) type snapshotter struct { @@ -146,6 +147,7 @@ func (s *snapshotter) Write(e *event.Event) error { s.mu.Lock() defer s.mu.Unlock() processCount.Add(1) + pid, err := e.Params.GetPid() if err != nil { return err @@ -154,8 +156,45 @@ func (s *snapshotter) Write(e *event.Event) error { if err != nil { return err } + proc, err := s.newProcState(pid, ppid, e) - s.procs[pid] = proc + if ps := s.procs[pid]; ps == nil && (e.IsCreateProcessInternal() || e.IsProcessRundownInternal()) { + // only modify the state if there is no process derived from the NT kernel logger process events + s.procs[pid] = proc + } else if ps, ok := s.procs[pid]; ok && (e.IsCreateProcessInternal() || e.IsProcessRundownInternal()) { + // process state derived from the core kernel events exists - enrich it + ps.TokenIntegrityLevel = proc.TokenIntegrityLevel + ps.TokenElevationType = proc.TokenElevationType + ps.IsTokenElevated = proc.IsTokenElevated + if len(proc.Exe) > len(ps.Exe) { + // prefer full executable path + ps.Exe = proc.Exe + } + s.procs[pid] = ps + } else if ps, ok := s.procs[pid]; ok && (e.IsCreateProcess() || e.IsProcessRundown()) && ps.TokenIntegrityLevel != "" { + // enrich the existing process state with the newly arrived NT kernel logger process events + // but obtain the integrity level and executable path from the previous proc state + processEnrichments.Add(1) + proc.TokenIntegrityLevel = ps.TokenIntegrityLevel + proc.TokenElevationType = ps.TokenElevationType + proc.IsTokenElevated = ps.IsTokenElevated + + if len(ps.Exe) > len(proc.Exe) { + // prefer full executable path + proc.Exe = ps.Exe + e.AppendParam(params.Exe, params.Path, ps.Exe) + } + + e.AppendParam(params.ProcessIntegrityLevel, params.AnsiString, ps.TokenIntegrityLevel) + e.AppendParam(params.ProcessTokenElevationType, params.AnsiString, ps.TokenElevationType) + e.AppendParam(params.ProcessTokenIsElevated, params.Bool, ps.IsTokenElevated) + + s.procs[pid] = proc + } else { + // in all other cases append the process state + s.procs[pid] = proc + } + // adjust the process which is generating // the event. For `CreateProcess` events // the process context is scoped to the @@ -165,9 +204,10 @@ func (s *snapshotter) Write(e *event.Event) error { // snapshot state if e.IsProcessRundown() { e.PS = proc - } else { + } else if !e.IsProcessRundownInternal() && !e.IsCreateProcessInternal() { e.PS = s.procs[e.PID] } + return err } @@ -206,7 +246,6 @@ func (s *snapshotter) AddModule(e *event.Event) error { if err != nil { return err } - moduleCount.Add(1) s.mu.Lock() defer s.mu.Unlock() @@ -225,6 +264,14 @@ func (s *snapshotter) AddModule(e *event.Event) error { module.Name = e.GetParamAsString(params.ImagePath) module.BaseAddress = e.Params.TryGetAddress(params.ImageBase) module.DefaultBaseAddress = e.Params.TryGetAddress(params.ImageDefaultBase) + + if e.IsLoadImageInternal() { + proc.AddModule(module) + return nil + } + + moduleCount.Add(1) + module.SignatureLevel, _ = e.Params.GetUint32(params.ImageSignatureLevel) module.SignatureType, _ = e.Params.GetUint32(params.ImageSignatureType) @@ -317,6 +364,23 @@ func (s *snapshotter) Close() error { } func (s *snapshotter) newProcState(pid, ppid uint32, e *event.Event) (*pstypes.PS, error) { + if e.IsCreateProcessInternal() || e.IsProcessRundownInternal() { + proc := &pstypes.PS{ + PID: pid, + Ppid: ppid, + Exe: e.GetParamAsString(params.Exe), + TokenIntegrityLevel: e.GetParamAsString(params.ProcessIntegrityLevel), + TokenElevationType: e.GetParamAsString(params.ProcessTokenElevationType), + IsTokenElevated: e.Params.TryGetBool(params.ProcessTokenIsElevated), + Threads: make(map[uint32]pstypes.Thread), + Modules: make([]pstypes.Module, 0), + Handles: make([]htypes.Handle, 0), + Mmaps: make([]pstypes.Mmap, 0), + } + + return proc, nil + } + proc := pstypes.New( pid, ppid, @@ -369,12 +433,38 @@ func (s *snapshotter) newProcState(pid, ppid uint32, e *event.Event) (*pstypes.P access := uint32(windows.PROCESS_QUERY_INFORMATION | windows.PROCESS_VM_READ) process, err := windows.OpenProcess(access, false, pid) if err != nil { - return proc, nil + process, err = windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, pid) + if err != nil { + return proc, nil + } } //nolint:errcheck defer windows.CloseHandle(process) - // read PEB + // query token attributes if not enriched by internal event + if s.procs[pid] == nil { + var token windows.Token + err = windows.OpenProcessToken(process, windows.TOKEN_QUERY, &token) + if err != nil { + goto readPEB + } + defer token.Close() + + // get process token integrity level + tokenMandatoryLabel, err := sys.GetProcessTokenInformation[windows.Tokenmandatorylabel](token, windows.TokenIntegrityLevel) + if err != nil { + goto readPEB + } + + proc.TokenIntegrityLevel = sys.RidToString(tokenMandatoryLabel.Label.Sid) + proc.IsTokenElevated = token.IsElevated() + + e.AppendParam(params.ProcessIntegrityLevel, params.AnsiString, proc.TokenIntegrityLevel) + e.AppendParam(params.ProcessTokenIsElevated, params.Bool, proc.IsTokenElevated) + } + +readPEB: + // read PEB (Process Environment Block) peb, err := ReadPEB(process) if err != nil { pebReadErrors.Add(1) @@ -525,27 +615,52 @@ func (s *snapshotter) Find(pid uint32) (bool, *pstypes.PS) { } proc.StartTime = time.Unix(0, ct.Nanoseconds()) + // get process creation attributes + var isWOW64 bool + if err := windows.IsWow64Process(process, &isWOW64); err == nil && isWOW64 { + proc.IsWOW64 = true + } + if isPackaged, err := sys.IsProcessPackaged(process); err == nil && isPackaged { + proc.IsPackaged = true + } + if prot, err := sys.QueryInformationProcess[sys.PsProtection](process, sys.ProcessProtectionInformation); err == nil && prot != nil { + proc.IsProtected = prot.IsProtected() + } + // get process token attributes var token windows.Token + var tokenUser *windows.Tokenuser + var tokenMandatoryLabel *windows.Tokenmandatorylabel + err = windows.OpenProcessToken(process, windows.TOKEN_QUERY, &token) if err != nil { - return false, proc + goto readPEB } defer token.Close() - usr, err := token.GetTokenUser() + tokenUser, err = token.GetTokenUser() if err != nil { - return false, proc + goto readPEB } - proc.SID = usr.User.Sid.String() - proc.Username, proc.Domain, _, _ = usr.User.Sid.LookupAccount("") + proc.SID = tokenUser.User.Sid.String() + proc.Username, proc.Domain, _, _ = tokenUser.User.Sid.LookupAccount("") + + // get process token integrity level + tokenMandatoryLabel, err = sys.GetProcessTokenInformation[windows.Tokenmandatorylabel](token, windows.TokenIntegrityLevel) + if err != nil { + goto readPEB + } + + proc.TokenIntegrityLevel = sys.RidToString(tokenMandatoryLabel.Label.Sid) + proc.IsTokenElevated = token.IsElevated() // retrieve process handles proc.Handles, err = s.hsnap.FindHandles(pid) if err != nil { - return false, proc + goto readPEB } - // read PEB +readPEB: + // read PEB (Process Environment Block) peb, err := ReadPEB(process) if err != nil { pebReadErrors.Add(1) @@ -556,18 +671,6 @@ func (s *snapshotter) Find(pid uint32) (bool, *pstypes.PS) { proc.SessionID = peb.GetSessionID() proc.Cwd = peb.GetCurrentWorkingDirectory() - // get process creation attributes - var isWOW64 bool - if err := windows.IsWow64Process(process, &isWOW64); err == nil && isWOW64 { - proc.IsWOW64 = true - } - if isPackaged, err := sys.IsProcessPackaged(process); err == nil && isPackaged { - proc.IsPackaged = true - } - if prot, err := sys.QueryInformationProcess[sys.PsProtection](process, sys.ProcessProtectionInformation); err == nil && prot != nil { - proc.IsProtected = prot.IsProtected() - } - return false, proc } diff --git a/pkg/ps/snapshotter_windows_test.go b/pkg/ps/snapshotter_windows_test.go index 3ec17f499..0a767fd7e 100644 --- a/pkg/ps/snapshotter_windows_test.go +++ b/pkg/ps/snapshotter_windows_test.go @@ -185,6 +185,131 @@ func TestWrite(t *testing.T) { } } +func TestWriteInternalEventsEnrichment(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + hsnap.On("FindHandles", mock.Anything).Return([]htypes.Handle{}, nil) + + var tests = []struct { + name string + evts []*event.Event + psnap Snapshotter + assertions func(t *testing.T, psnap Snapshotter) + }{ + {"write internal event without previous state", + []*event.Event{ + { + Type: event.CreateProcessInternal, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, + params.ProcessIntegrityLevel: {Name: params.ProcessIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, + params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, + params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, + }, + }, + }, + NewSnapshotter(hsnap, &config.Config{}), + func(t *testing.T, psnap Snapshotter) { + ok, proc := psnap.Find(1024) + assert.True(t, ok) + assert.Equal(t, "HIGH", proc.TokenIntegrityLevel) + assert.Equal(t, "FULL", proc.TokenElevationType) + assert.Equal(t, true, proc.IsTokenElevated) + assert.Equal(t, `C:\Windows\System32\svchost.exe`, proc.Exe) + }, + }, + {"enrich existing system provider proc state with internal event", + []*event.Event{ + { + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `svchost.exe`}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `svchost.exe -k LocalSystemNetworkRestricted -p -s NcbService`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, + }, + }, + { + Type: event.CreateProcessInternal, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, + params.ProcessIntegrityLevel: {Name: params.ProcessIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, + params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, + params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, + }, + }, + }, + NewSnapshotter(hsnap, &config.Config{}), + func(t *testing.T, psnap Snapshotter) { + ok, proc := psnap.Find(1024) + assert.True(t, ok) + assert.Equal(t, "HIGH", proc.TokenIntegrityLevel) + assert.Equal(t, "FULL", proc.TokenElevationType) + assert.Equal(t, true, proc.IsTokenElevated) + assert.Equal(t, `C:\Windows\System32\svchost.exe`, proc.Exe) + assert.Equal(t, "svchost.exe -k LocalSystemNetworkRestricted -p -s NcbService", proc.Cmdline) + assert.Equal(t, uint32(1), proc.SessionID) + }, + }, + {"enrich newly arrived system provider proc with previous internal event state", + []*event.Event{ + { + Type: event.CreateProcessInternal, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `C:\Windows\System32\svchost.exe`}, + params.ProcessIntegrityLevel: {Name: params.ProcessIntegrityLevel, Type: params.AnsiString, Value: "HIGH"}, + params.ProcessTokenIsElevated: {Name: params.ProcessTokenIsElevated, Type: params.Bool, Value: true}, + params.ProcessTokenElevationType: {Name: params.ProcessTokenElevationType, Type: params.AnsiString, Value: "FULL"}, + }, + }, + { + Type: event.CreateProcess, + Params: event.Params{ + params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1024)}, + params.ProcessParentID: {Name: params.ProcessParentID, Type: params.PID, Value: uint32(444)}, + params.Exe: {Name: params.Exe, Type: params.UnicodeString, Value: `svchost.exe`}, + params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: `svchost.exe -k LocalSystemNetworkRestricted -p -s NcbService`}, + params.UserSID: {Name: params.UserSID, Type: params.WbemSID, Value: []byte{224, 8, 226, 31, 15, 167, 255, 255, 0, 0, 0, 0, 15, 167, 255, 255, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}}, + params.SessionID: {Name: params.SessionID, Type: params.Uint32, Value: uint32(1)}, + params.ProcessFlags: {Name: params.ProcessFlags, Type: params.Flags, Value: uint32(0x00000010)}, + }, + }, + }, + NewSnapshotter(hsnap, &config.Config{}), + func(t *testing.T, psnap Snapshotter) { + ok, proc := psnap.Find(1024) + assert.True(t, ok) + assert.Equal(t, "HIGH", proc.TokenIntegrityLevel) + assert.Equal(t, "FULL", proc.TokenElevationType) + assert.Equal(t, true, proc.IsTokenElevated) + assert.Equal(t, `C:\Windows\System32\svchost.exe`, proc.Exe) + assert.Equal(t, "svchost.exe -k LocalSystemNetworkRestricted -p -s NcbService", proc.Cmdline) + assert.Equal(t, uint32(1), proc.SessionID) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, evt := range tt.evts { + require.NoError(t, tt.psnap.Write(evt)) + } + if tt.assertions != nil { + tt.assertions(t, tt.psnap) + } + defer tt.psnap.Close() + }) + } +} + func TestRemove(t *testing.T) { hsnap := new(handle.SnapshotterMock) hsnap.On("FindHandles", mock.Anything).Return([]htypes.Handle{}, nil) @@ -615,6 +740,7 @@ func TestFindQueryOS(t *testing.T) { assert.True(t, len(proc.Envs) > 0) assert.Contains(t, proc.Cwd, "fibratus\\pkg\\ps") assert.Equal(t, uint32(1), proc.SessionID) + assert.Equal(t, "HIGH", proc.TokenIntegrityLevel) wts, err := sys.LookupActiveWTS() require.NoError(t, err) diff --git a/pkg/ps/types/types_windows.go b/pkg/ps/types/types_windows.go index bc372b312..3d22d484f 100644 --- a/pkg/ps/types/types_windows.go +++ b/pkg/ps/types/types_windows.go @@ -91,6 +91,13 @@ type PS struct { // IsProtected denotes a protected process. The system restricts access to protected // processes and the threads of protected processes. IsProtected bool `json:"is_protected"` + // TokenIntegrityLevel designates the process token integrity level. (e.g. High) + // Integrity level defines the trust between the process and a securable object. + TokenIntegrityLevel string `json:"token_integrity_level"` + // TokenElevationType designates the process token elevation type. (e.g. Limited) + TokenElevationType string `json:"token_elevation_type"` + // IsTokenElevated indicates if the process token is elevated. + IsTokenElevated bool `json:"is_token_elevated"` } // UUID is meant to offer a more robust version of process ID that @@ -147,9 +154,10 @@ func (ps *PS) String() string { Parent name: %s Cmdline: %s Parent cmdline: %s - Exe: %s - Cwd: %s - SID: %s + Exe: %s + Cwd: %s + SID: %s + Integrity level: %s Username: %s Domain: %s Args: %s @@ -165,6 +173,7 @@ func (ps *PS) String() string { ps.Exe, ps.Cwd, ps.SID, + ps.TokenIntegrityLevel, ps.Username, ps.Domain, ps.Args, @@ -177,9 +186,10 @@ func (ps *PS) String() string { Ppid: %d Name: %s Cmdline: %s - Exe: %s - Cwd: %s - SID: %s + Exe: %s + Cwd: %s + SID: %s + Integrity level: %s Username: %s Domain: %s Args: %s @@ -193,6 +203,7 @@ func (ps *PS) String() string { ps.Exe, ps.Cwd, ps.SID, + ps.TokenIntegrityLevel, ps.Username, ps.Domain, ps.Args, @@ -213,9 +224,9 @@ func (ps *PS) StringShort() string { Parent name: %s Cmdline: %s Parent cmdline: %s - Exe: %s - Cwd: %s - SID: %s + Exe: %s + Cwd: %s + SID: %s Username: %s Domain: %s Args: %s @@ -243,9 +254,10 @@ func (ps *PS) StringShort() string { Ppid: %d Name: %s Cmdline: %s - Exe: %s - Cwd: %s - SID: %s + Exe: %s + Cwd: %s + SID: %s + Integrity level: %s Username: %s Domain: %s Args: %s @@ -259,6 +271,7 @@ func (ps *PS) StringShort() string { ps.Exe, ps.Cwd, ps.SID, + ps.TokenIntegrityLevel, ps.Username, ps.Domain, ps.Args, diff --git a/pkg/sys/etw/etw.go b/pkg/sys/etw/etw.go index 3874d7e94..32fbc1996 100644 --- a/pkg/sys/etw/etw.go +++ b/pkg/sys/etw/etw.go @@ -209,11 +209,23 @@ const ( // ControlCodeEnableProvider updates the session configuration so // that the session receives the requested events from the provider. ControlCodeEnableProvider = 1 + // ControlCodeCaptureState requests that the provider log its state + // information, such as rundown events + ControlCodeCaptureState = 2 ) // EnableTrace influences the behaviour of the specified event trace provider. -func EnableTrace(guid windows.GUID, handle TraceHandle, keyword uint64) error { - err := enableTraceEx2(handle, &guid, ControlCodeEnableProvider, TraceLevelInformation, keyword, 0, 0, nil) +func EnableTrace(guid windows.GUID, handle TraceHandle, keywords uint64) error { + err := enableTraceEx2(handle, &guid, ControlCodeEnableProvider, TraceLevelInformation, keywords, 0, 0, nil) + if err != nil { + return os.NewSyscallError("EnableTraceEx2", err) + } + return nil +} + +// CaptureProviderState requests that the provider log its state information. +func CaptureProviderState(guid windows.GUID, handle TraceHandle) error { + err := enableTraceEx2(handle, &guid, ControlCodeCaptureState, 0, 0, 0, 0, nil) if err != nil { return os.NewSyscallError("EnableTraceEx2", err) } @@ -228,7 +240,7 @@ type EnableTraceOpts struct { // EnableTraceWithOpts influences the behaviour of the specified event trace provider // by providing extra options to configure how events are writing to the session buffer. -func EnableTraceWithOpts(guid windows.GUID, handle TraceHandle, keyword uint64, opts EnableTraceOpts) error { +func EnableTraceWithOpts(guid windows.GUID, handle TraceHandle, keywords uint64, opts EnableTraceOpts) error { params := &EnableTraceParameters{ Version: EnableTraceParametersVersion, SourceID: guid, @@ -236,7 +248,7 @@ func EnableTraceWithOpts(guid windows.GUID, handle TraceHandle, keyword uint64, if opts.WithStacktrace { params.EnableProperty = EventEnablePropertyStacktrace } - err := enableTraceEx2(handle, &guid, ControlCodeEnableProvider, TraceLevelInformation, keyword, 0, 0, params) + err := enableTraceEx2(handle, &guid, ControlCodeEnableProvider, TraceLevelInformation, keywords, 0, 0, params) if err != nil { return os.NewSyscallError("EnableTraceEx2", err) } diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index ffc6c881b..cd4c78ae7 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -44,6 +44,9 @@ var DNSClientGUID = windows.GUID{Data1: 0x1c95126e, Data2: 0x7eea, Data3: 0x49a9 // ThreadpoolGUID represents the GUID for the thread pool provider var ThreadpoolGUID = windows.GUID{Data1: 0xc861d0e2, Data2: 0xa2c1, Data3: 0x4d36, Data4: [8]byte{0x9f, 0x9c, 0x97, 0x0b, 0xab, 0x94, 0x3a, 0x12}} +// WindowsKernelProcessGUID represents the GUID for the Microsoft Windows Kernel Process provider +var WindowsKernelProcessGUID = windows.GUID{Data1: 0x22fb2cd6, Data2: 0x0e7b, Data3: 0x422b, Data4: [8]byte{0xa0, 0xc7, 0x2f, 0xad, 0x1f, 0xd0, 0xe7, 0x16}} + const ( // TraceStackTracingInfo controls call stack tracing for kernel events TraceStackTracingInfo = uint8(3) @@ -51,6 +54,12 @@ const ( TraceSystemTraceEnableFlagsInfo = uint8(4) ) +// ProcessKeyword enables process events for Microsoft Windows Kernel Process provider +const ProcessKeyword = 0x10 + +// ImageKeyword enables images events for Microsoft Windows Kernel Process provider +const ImageKeyword = 0x40 + const ( // EventHeaderExtTypeStackTrace64 indicates that the extended data contains the call stack if the event is captured on a 64-bit host EventHeaderExtTypeStackTrace64 uint16 = 0x0006 @@ -650,7 +659,9 @@ func (e *EventRecord) ReadUTF16String(offset uint16) (string, uint16) { if offset > e.BufferLen { return "", 0 } + var length uint16 + if offset > 0 { length = e.BufferLen - offset } else { @@ -666,13 +677,47 @@ func (e *EventRecord) ReadUTF16String(offset uint16) (string, uint16) { i += 2 } } + s := (*[1<<30 - 1]uint16)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:length:length] if offset > 0 { return utf16.Decode(s[:len(s)/2-1-2]), uint16(len(s) + 2) } + return utf16.Decode(s[:len(s)/2]), uint16(len(s) + 2) } +// ReadNTUnicodeString reads the native Unicode string at the given offset. +func (e *EventRecord) ReadNTUnicodeString(offset uint16) (string, uint16) { + if offset > e.BufferLen { + return "", offset + } + + i := offset + var length uint16 + for i < e.BufferLen { + c := *(*uint16)(unsafe.Pointer(e.Buffer + uintptr(i))) + if c == 0 { + break // null terminator + } + length += 2 + i += 2 + } + + if length == 0 { + return "", offset + } + + b := (*[1<<30 - 1]byte)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:length:length] + + s := windows.NTUnicodeString{ + Length: uint16(len(b)), + MaximumLength: uint16(len(b)), + Buffer: (*uint16)(unsafe.Pointer(&b[0])), + } + + return s.String(), offset + s.Length +} + // ConsumeUTF16String reads the byte slice with UTF16-encoded string // when the UTF16 string is located at the end of the buffer. func (e *EventRecord) ConsumeUTF16String(offset uint16) string { @@ -684,7 +729,7 @@ func (e *EventRecord) ConsumeUTF16String(offset uint16) string { } // ReadSID reads the security identifier from the event buffer. -func (e *EventRecord) ReadSID(offset uint16) ([]byte, uint16) { +func (e *EventRecord) ReadSID(offset uint16, isWbemSid bool) ([]byte, uint16) { // this is a Security Token which can be null and takes 4 bytes. // Otherwise, it is an 8 byte structure (TOKEN_USER) followed by SID, // which is variable size depending on the 2nd byte in the SID @@ -692,7 +737,11 @@ func (e *EventRecord) ReadSID(offset uint16) ([]byte, uint16) { if sid == 0 { return nil, offset + 4 } - const tokenSize uint16 = 16 + + var tokenSize uint16 + if isWbemSid { + tokenSize = 16 // TOKEN_USER size + } authorities := e.ReadByte(offset + (tokenSize + 1)) end := offset + tokenSize + 8 + 4*uint16(authorities) diff --git a/pkg/sys/etw/types_test.go b/pkg/sys/etw/types_test.go index da44ba3dd..f82c7c7ef 100644 --- a/pkg/sys/etw/types_test.go +++ b/pkg/sys/etw/types_test.go @@ -75,7 +75,7 @@ func TestReadBuffer(t *testing.T) { assert.Equal(t, uint32(12520), ev.ReadUint32(8)) assert.Equal(t, uint32(5240), ev.ReadUint32(12)) - rawSid, offset := ev.ReadSID(36) + rawSid, offset := ev.ReadSID(36, true) b := uintptr(unsafe.Pointer(&rawSid[0])) b += uintptr(8 * 2) diff --git a/pkg/sys/process.go b/pkg/sys/process.go index b4db01b09..04ad9b853 100644 --- a/pkg/sys/process.go +++ b/pkg/sys/process.go @@ -173,3 +173,23 @@ func getModuleFileName(proc, mod windows.Handle) (string, error) { } return windows.UTF16ToString(buf), nil } + +// GetProcessTokenInformation returns the specified process token information. +func GetProcessTokenInformation[C any](token windows.Token, class uint32) (*C, error) { + var n uint32 + // get the buffer size to accommodate the desired token class structure + if err := windows.GetTokenInformation(token, class, nil, 0, &n); err != nil { + if err != windows.ERROR_INSUFFICIENT_BUFFER { + return nil, err + } + } + + // allocate the buffer and obtain token information + b := make([]byte, n) + err := windows.GetTokenInformation(token, class, &b[0], n, &n) + if err != nil { + return nil, err + } + + return (*C)(unsafe.Pointer(&b[0])), nil +} diff --git a/pkg/sys/rid.go b/pkg/sys/rid.go new file mode 100644 index 000000000..1c60763de --- /dev/null +++ b/pkg/sys/rid.go @@ -0,0 +1,81 @@ +/* + * Copyright 2021-present 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" +) + +const ( + // UntrustedRid designates the integrity level of anonymous + // logged on processes. Write access is mostly blocked. + UntrustedRid = 0x00000000 + + // LowRid designates low process token integrity. Used for + // AppContainers, browsers that access the Internet and + // prevent most write access to objects on the system. + LowRid = 0x00001000 + + // MediumRid designates the token integrity default for most + // processes. For authenticated users. + MediumRid = 0x00002000 + + // MediumPlusRid is often observed in AppContainer or UWP processes, + // especially when they require elevated trust compared to a typical + // Medium-level process but still shouldn't run with full administrative + // privileges. + MediumPlusRid = MediumRid | 0x100 + + // HighRid is the integrity level for Administrator-level processes. + // (Elevated) process with UAC. + HighRid = 0x00003000 + + // SystemRid is the integrity level reserved for system services/processes. + SystemRid = 0x00004000 + + // ProtectedProcessRid is not seen to be used by default. Windows Internals + // book says it can be set by a kernel-mode caller. + ProtectedProcessRid = 0x00005000 +) + +// RidToString given the SID representing the token mandatory label +// returns the string representation of the integrity level. +func RidToString(sid *windows.SID) string { + if sid == nil { + return "UNKNOWN" + } + switch sid.SubAuthority(uint32(sid.SubAuthorityCount() - 1)) { + case UntrustedRid: + return "UNTRUSTED" + case LowRid: + return "LOW" + case MediumRid: + return "MEDIUM" + case MediumPlusRid: + return "MEDIUM+" + case HighRid: + return "HIGH" + case SystemRid: + return "SYSTEM" + case ProtectedProcessRid: + return "PROTECTED" + default: + return "UNKNOWN" + } +}