From ba8fc59ce2abe4a074c8d75b142d8fb4c0d089d7 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Fri, 20 Jun 2025 11:44:42 +0200 Subject: [PATCH 1/3] feat(telemetry): Introduce Windows Kernel Process Provider This provider is fundamentally employed to enrich the process state with additional attributes such as integrity level and token elevation info. --- internal/etw/consumer.go | 2 +- internal/etw/processors/ps_windows.go | 6 ++- internal/etw/source.go | 46 ++++++++++------- internal/etw/source_test.go | 10 ++-- internal/etw/trace.go | 37 ++++++++++++-- pkg/event/event_windows.go | 46 +++++++++-------- pkg/event/metainfo_windows.go | 2 + pkg/event/param_windows.go | 72 +++++++++++++++++++++------ pkg/event/types_windows.go | 23 ++++++--- pkg/sys/etw/etw.go | 20 ++++++-- pkg/sys/etw/types.go | 50 ++++++++++++++++++- pkg/sys/etw/types_test.go | 2 +- 12 files changed, 239 insertions(+), 77 deletions(-) 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/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..a8b9d299c 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), 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/event/event_windows.go b/pkg/event/event_windows.go index 32e103863..7bc832d46 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,28 @@ 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) 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..4560698e4 100644 --- a/pkg/event/metainfo_windows.go +++ b/pkg/event/metainfo_windows.go @@ -238,6 +238,8 @@ func AllWithState() []Type { s = append(s, ReleaseFile) s = append(s, MapFileRundown) s = append(s, StackWalk) + s = append(s, CreateProcessInternal) + s = append(s, ProcessRundownInternal) return s } diff --git a/pkg/event/param_windows.go b/pkg/event/param_windows.go index 021c04bb2..a8922d6a2 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 @@ -512,9 +558,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/types_windows.go b/pkg/event/types_windows.go index 125f19d3f..3c2ee743f 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) @@ -228,11 +237,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" @@ -346,7 +355,7 @@ 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 @@ -505,6 +514,8 @@ func (t Type) Exists() bool { func (t Type) OnlyState() bool { switch t { case ProcessRundown, + ProcessRundownInternal, + CreateProcessInternal, ThreadRundown, ImageRundown, FileRundown, @@ -585,10 +596,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/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..3a5bea1e8 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,9 @@ const ( TraceSystemTraceEnableFlagsInfo = uint8(4) ) +// ProcessKeyword enables process events for Microsoft Windows Kernel Process provider +const ProcessKeyword = 0x10 + 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 +656,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 +674,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 +726,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 +734,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) From acf4f15d8d398db88777ed515d3906aa513ff1f4 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Fri, 20 Jun 2025 18:05:15 +0200 Subject: [PATCH 2/3] feat(process): Enrich processes with integrity level and token elevation info --- pkg/alertsender/alert_test.go | 30 +++--- pkg/event/enum.go | 13 +++ pkg/event/params/params_windows.go | 6 ++ pkg/ps/snapshotter_windows.go | 142 ++++++++++++++++++++++++----- pkg/ps/snapshotter_windows_test.go | 126 +++++++++++++++++++++++++ pkg/ps/types/types_windows.go | 37 +++++--- pkg/sys/process.go | 20 ++++ pkg/sys/rid.go | 81 ++++++++++++++++ 8 files changed, 406 insertions(+), 49 deletions(-) create mode 100644 pkg/sys/rid.go 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/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/ps/snapshotter_windows.go b/pkg/ps/snapshotter_windows.go index 0080d8b17..4749cc649 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 } @@ -317,6 +357,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 +426,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 +608,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 = 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.SID = usr.User.Sid.String() - proc.Username, proc.Domain, _, _ = usr.User.Sid.LookupAccount("") + + 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 +664,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/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" + } +} From 64b41d9db857aa1e6e919ffd3a13bb83c49d31e4 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 23 Jun 2025 21:16:19 +0200 Subject: [PATCH 3/3] chore(process): Populate process modules from internal events If the event arrives from the provider and the process state hasn't been populated with the expected module, we leverage the internal load image event to append the module. --- internal/etw/processors/image_windows.go | 5 +++++ internal/etw/source.go | 2 +- pkg/event/event_windows.go | 1 + pkg/event/metainfo_windows.go | 1 + pkg/event/param_windows.go | 23 +++++++++++++++++++++++ pkg/event/types_windows.go | 7 +++++-- pkg/ps/snapshotter_windows.go | 9 ++++++++- pkg/sys/etw/types.go | 3 +++ 8 files changed, 47 insertions(+), 4 deletions(-) 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/source.go b/internal/etw/source.go index a8b9d299c..888d72ee6 100644 --- a/internal/etw/source.go +++ b/internal/etw/source.go @@ -168,7 +168,7 @@ func (e *EventSource) Open(config *config.Config) error { // 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), WithCaptureState()) + trace.AddProvider(etw.WindowsKernelProcessGUID, false, WithKeywords(etw.ProcessKeyword|etw.ImageKeyword), WithCaptureState()) if config.EventSource.EnableDNSEvents { trace.AddProvider(etw.DNSClientGUID, false) diff --git a/pkg/event/event_windows.go b/pkg/event/event_windows.go index 7bc832d46..0782a9740 100644 --- a/pkg/event/event_windows.go +++ b/pkg/event/event_windows.go @@ -229,6 +229,7 @@ func (e *Event) IsTerminateProcess() bool { return e.Type == TerminateProc 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 } diff --git a/pkg/event/metainfo_windows.go b/pkg/event/metainfo_windows.go index 4560698e4..7794315f0 100644 --- a/pkg/event/metainfo_windows.go +++ b/pkg/event/metainfo_windows.go @@ -240,6 +240,7 @@ func AllWithState() []Type { 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 a8922d6a2..870dd7ef9 100644 --- a/pkg/event/param_windows.go +++ b/pkg/event/param_windows.go @@ -459,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, diff --git a/pkg/event/types_windows.go b/pkg/event/types_windows.go index 3c2ee743f..52f7bb04b 100644 --- a/pkg/event/types_windows.go +++ b/pkg/event/types_windows.go @@ -156,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) @@ -309,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" @@ -359,7 +361,7 @@ func (t Type) Category() Category { 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: @@ -518,6 +520,7 @@ func (t Type) OnlyState() bool { CreateProcessInternal, ThreadRundown, ImageRundown, + LoadImageInternal, FileRundown, RegKCBRundown, FileOpEnd, diff --git a/pkg/ps/snapshotter_windows.go b/pkg/ps/snapshotter_windows.go index 4749cc649..e2576efca 100644 --- a/pkg/ps/snapshotter_windows.go +++ b/pkg/ps/snapshotter_windows.go @@ -246,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() @@ -265,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) diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index 3a5bea1e8..cd4c78ae7 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -57,6 +57,9 @@ const ( // 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