From 2c1a0890bc54064224f6147d0bffde675ca15062 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 18:51:03 +0200 Subject: [PATCH 1/6] feat(event): Parse RegSetValueInternal event parameters --- pkg/event/event_windows.go | 1 + pkg/event/metainfo_windows.go | 1 + pkg/event/param_windows.go | 33 +++++++++++++++++++++++++++++- pkg/event/params/params_windows.go | 2 ++ pkg/event/types_windows.go | 15 ++++++++++---- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/pkg/event/event_windows.go b/pkg/event/event_windows.go index 0782a9740..70ffb16ad 100644 --- a/pkg/event/event_windows.go +++ b/pkg/event/event_windows.go @@ -233,6 +233,7 @@ func (e *Event) IsLoadImageInternal() bool { return e.Type == LoadImageInte 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) IsRegSetValueInternal() bool { return e.Type == RegSetValueInternal } 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 } diff --git a/pkg/event/metainfo_windows.go b/pkg/event/metainfo_windows.go index 7794315f0..d7913cf22 100644 --- a/pkg/event/metainfo_windows.go +++ b/pkg/event/metainfo_windows.go @@ -241,6 +241,7 @@ func AllWithState() []Type { s = append(s, CreateProcessInternal) s = append(s, ProcessRundownInternal) s = append(s, LoadImageInternal) + s = append(s, RegSetValueInternal) return s } diff --git a/pkg/event/param_windows.go b/pkg/event/param_windows.go index 870dd7ef9..0130265b8 100644 --- a/pkg/event/param_windows.go +++ b/pkg/event/param_windows.go @@ -19,6 +19,7 @@ package event import ( + "encoding/binary" "expvar" "fmt" "github.com/rabbitstack/fibratus/pkg/event/params" @@ -33,7 +34,9 @@ import ( "github.com/rabbitstack/fibratus/pkg/util/signature" "github.com/rabbitstack/fibratus/pkg/util/va" "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" "net" + "path/filepath" "strconv" "strings" "time" @@ -255,7 +258,7 @@ func (e *Event) produceParams(evt *etw.EventRecord) { } sid, soffset = evt.ReadSID(offset, true) name, noffset = evt.ReadAnsiString(soffset) - cmdline, _ = evt.ReadUTF16String(soffset + noffset) + cmdline, _ = evt.ReadUTF16String(noffset) e.AppendParam(params.ProcessObject, params.Address, kproc) e.AppendParam(params.ProcessID, params.PID, pid) e.AppendParam(params.ProcessParentID, params.PID, ppid) @@ -508,6 +511,34 @@ func (e *Event) produceParams(evt *etw.EventRecord) { e.AppendParam(params.RegKeyHandle, params.Address, keyHandle) e.AppendParam(params.RegPath, params.Key, keyName) e.AppendParam(params.NTStatus, params.Status, status) + case RegSetValueInternal: + keyObject := evt.ReadUint64(0) + status := evt.ReadUint32(8) + valueType := evt.ReadUint32(12) + keyName, koffset := evt.ReadUTF16String(20) // skip data size param (4 bytes) + valueName, voffset := evt.ReadUTF16String(koffset) + capturedSize := evt.ReadUint16(voffset) + capturedData := evt.ReadBytes(2+voffset, capturedSize) + + e.AppendParam(params.RegKeyHandle, params.Address, keyObject) + e.AppendParam(params.NTStatus, params.Status, status) + e.AppendParam(params.RegPath, params.Key, filepath.Join(keyName, valueName)) + e.AppendEnum(params.RegValueType, valueType, key.RegistryValueTypes) + + if len(capturedData) > 0 { + switch valueType { + case registry.SZ, registry.MULTI_SZ, registry.EXPAND_SZ: + e.AppendParam(params.RegData, params.UnicodeString, string(capturedData)) + case registry.BINARY: + e.AppendParam(params.RegData, params.Binary, capturedData) + case registry.DWORD: + e.AppendParam(params.RegData, params.Uint32, binary.LittleEndian.Uint32(capturedData)) + case registry.DWORD_BIG_ENDIAN: + e.AppendParam(params.RegData, params.Uint32, binary.BigEndian.Uint32(capturedData)) + case registry.QWORD: + e.AppendParam(params.RegData, params.Uint64, binary.LittleEndian.Uint64(capturedData)) + } + } case CreateFile: var ( irp uint64 diff --git a/pkg/event/params/params_windows.go b/pkg/event/params/params_windows.go index 8be05a3bf..1a860094d 100644 --- a/pkg/event/params/params_windows.go +++ b/pkg/event/params/params_windows.go @@ -149,6 +149,8 @@ const ( RegValue = "value" // RegValueType identifies the parameter that represents registry value type e.g (DWORD, BINARY) RegValueType = "value_type" + // RegData identifies the parameter that stores the captured registry data + RegData = "data" // ImageBase identifies the parameter name for the base address of the process in which the image is loaded. ImageBase = "base_address" diff --git a/pkg/event/types_windows.go b/pkg/event/types_windows.go index 52f7bb04b..d727e9178 100644 --- a/pkg/event/types_windows.go +++ b/pkg/event/types_windows.go @@ -67,6 +67,8 @@ var ( 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}} + // RegistryKernelEventGUID represents the Registry Kernel event GUID + RegistryKernelEventGUID = windows.GUID{Data1: 0x70eb4f03, Data2: 0xc1de, Data3: 0x4f73, Data4: [8]byte{0xa0, 0x51, 0x33, 0xd1, 0x3d, 0x54, 0x13, 0xbd}} ) var ( @@ -149,6 +151,10 @@ var ( RegDeleteKCB = pack(RegistryEventGUID, 23) // RegKCBRundown enumerates the registry keys open at the start of the kernel session. RegKCBRundown = pack(RegistryEventGUID, 25) + // RegSetValueInternal is the internal event that is used to + // enrich the corresponding public RegSetValue event with + // extra attributes + RegSetValueInternal = pack(RegistryKernelEventGUID, 36) // UnloadImage represents unload image kernel events UnloadImage = pack(ImageEventGUID, 2) @@ -309,7 +315,7 @@ func (t Type) String() string { return "RegQueryValue" case RegCreateKCB: return "RegCreateKCB" - case RegSetValue: + case RegSetValue, RegSetValueInternal: return "RegSetValue" case LoadImage, LoadImageInternal: return "LoadImage" @@ -367,7 +373,7 @@ func (t Type) Category() Category { FileRundown, FileOpEnd, ReleaseFile, MapViewFile, UnmapViewFile, MapFileRundown: return File case RegCreateKey, RegDeleteKey, RegOpenKey, RegCloseKey, RegQueryKey, RegQueryValue, RegSetValue, RegDeleteValue, - RegKCBRundown, RegDeleteKCB, RegCreateKCB: + RegKCBRundown, RegDeleteKCB, RegCreateKCB, RegSetValueInternal: return Registry case AcceptTCPv4, AcceptTCPv6, ConnectTCPv4, ConnectTCPv6, @@ -527,7 +533,8 @@ func (t Type) OnlyState() bool { ReleaseFile, MapFileRundown, RegCreateKCB, - RegDeleteKCB: + RegDeleteKCB, + RegSetValueInternal: return true default: return false @@ -600,7 +607,7 @@ func (t Type) ID() uint { // Source designates the provenance of this event type. func (t Type) Source() Source { switch t.GUID() { - case AuditAPIEventGUID, DNSEventGUID, ThreadpoolGUID, ProcessKernelEventGUID: + case AuditAPIEventGUID, DNSEventGUID, ThreadpoolGUID, ProcessKernelEventGUID, RegistryKernelEventGUID: return SecurityTelemetryLogger default: return SystemLogger From 2233f5cf5c339e41bcc74444e82c4d78f4d302b9 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 18:53:26 +0200 Subject: [PATCH 2/6] feat(sys): Pass filter data to ETW provider session's callback --- pkg/sys/etw/etw.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/sys/etw/etw.go b/pkg/sys/etw/etw.go index 32fbc1996..16905b101 100644 --- a/pkg/sys/etw/etw.go +++ b/pkg/sys/etw/etw.go @@ -236,6 +236,9 @@ func CaptureProviderState(guid windows.GUID, handle TraceHandle) error { type EnableTraceOpts struct { // WithStacktrace indicates call stack trace is added to the extended data of events. WithStacktrace bool + // EventFilterDescriptors defines the filter data that a session passes to the provider's + // enable callback function. + EventFilterDescriptors []EventFilterDescriptor } // EnableTraceWithOpts influences the behaviour of the specified event trace provider @@ -245,9 +248,16 @@ func EnableTraceWithOpts(guid windows.GUID, handle TraceHandle, keywords uint64, Version: EnableTraceParametersVersion, SourceID: guid, } + if opts.WithStacktrace { params.EnableProperty = EventEnablePropertyStacktrace } + + if len(opts.EventFilterDescriptors) > 0 { + params.EnableFilterDesc = uintptr(unsafe.Pointer(&opts.EventFilterDescriptors[0])) + params.FilterDescCount = uint32(len(opts.EventFilterDescriptors)) + } + err := enableTraceEx2(handle, &guid, ControlCodeEnableProvider, TraceLevelInformation, keywords, 0, 0, params) if err != nil { return os.NewSyscallError("EnableTraceEx2", err) From f25cf3b6ea0416c89feac3299e0898c52da7017d Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 18:56:14 +0200 Subject: [PATCH 3/6] fix(sys,etw): Correct string parsing functions --- pkg/sys/etw/types.go | 62 +++++++++++++++++++++++---------------- pkg/sys/etw/types_test.go | 2 +- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index cd4c78ae7..5fdba491e 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -555,6 +555,14 @@ type ClassicEventID struct { _ [7]uint8 // reserved } +// EventFilterDescriptor defines the filter data that +// a session passes to the provider's enable callback. +type EventFilterDescriptor struct { + Ptr uintptr + Size uint32 + Type uint32 +} + // NewClassicEventID creates a new instance of classic event identifier. func NewClassicEventID(guid windows.GUID, typ uint16) ClassicEventID { return ClassicEventID{GUID: guid, Type: uint8(typ)} @@ -604,7 +612,7 @@ func (e *EventRecord) ReadBytes(offset uint16, count uint16) []byte { if offset > e.BufferLen { return nil } - return (*[1<<30 - 1]byte)(unsafe.Pointer(e.Buffer + uintptr(offset) + uintptr(count)))[:count:count] + return (*[1<<30 - 1]byte)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:count:count] } // ReadUint16 reads the uint16 value from the buffer at the specified offset. @@ -637,6 +645,7 @@ func (e *EventRecord) ReadAnsiString(offset uint16) (string, uint16) { if offset > e.BufferLen { return "", 0 } + b := make([]byte, e.BufferLen) var i uint16 for i < e.BufferLen { @@ -647,49 +656,52 @@ func (e *EventRecord) ReadAnsiString(offset uint16) (string, uint16) { b[i] = c i++ } + + if i == 0 { + return "", offset + 1 + } + if int(i) > len(b) { - return string(b[:len(b)-1]), uint16(len(b)) + return string(b[:len(b)-1]), uint16(len(b)) + offset } - return string(b[:i]), i + 1 + + return string(b[:i]), i + 1 + offset } // ReadUTF16String reads the UTF-16 string from the buffer at the specified offset. -// Returns the UTF-8 string and the number of bytes read from the string. +// Returns the UTF-8 string and the number of bytes read from the string + the offset. func (e *EventRecord) ReadUTF16String(offset uint16) (string, uint16) { if offset > e.BufferLen { return "", 0 } + // we're reading the leading string. First, calculate + // the length of the null-terminated UTF16 string + i := offset var length uint16 - - if offset > 0 { - length = e.BufferLen - offset - } else { - // we're reading the leading string. First, calculate - // the length of the null-terminated UTF16 string - var i uint16 - for i < e.BufferLen { - c := *(*uint16)(unsafe.Pointer(e.Buffer + uintptr(i))) - if c == 0 { - break // null terminator - } - length += 2 - i += 2 + for i < e.BufferLen { + c := *(*uint16)(unsafe.Pointer(e.Buffer + uintptr(i))) + if c == 0 { + break // null terminator } + length += 2 + 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) + if length == 0 { + return "", offset + 2 // null terminator size } - return utf16.Decode(s[:len(s)/2]), uint16(len(s) + 2) + b := (*[1<<30 - 1]uint16)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:length:length] + s := b[:len(b)/2] + + return utf16.Decode(s), uint16((len(s)+1)*2) + offset } // ReadNTUnicodeString reads the native Unicode string at the given offset. func (e *EventRecord) ReadNTUnicodeString(offset uint16) (string, uint16) { if offset > e.BufferLen { - return "", offset + return "", 0 } i := offset @@ -704,7 +716,7 @@ func (e *EventRecord) ReadNTUnicodeString(offset uint16) (string, uint16) { } if length == 0 { - return "", offset + return "", offset + 2 // null terminator size } b := (*[1<<30 - 1]byte)(unsafe.Pointer(e.Buffer + uintptr(offset)))[:length:length] @@ -715,7 +727,7 @@ func (e *EventRecord) ReadNTUnicodeString(offset uint16) (string, uint16) { Buffer: (*uint16)(unsafe.Pointer(&b[0])), } - return s.String(), offset + s.Length + return s.String(), s.Length + offset } // ConsumeUTF16String reads the byte slice with UTF16-encoded string diff --git a/pkg/sys/etw/types_test.go b/pkg/sys/etw/types_test.go index f82c7c7ef..f65318e81 100644 --- a/pkg/sys/etw/types_test.go +++ b/pkg/sys/etw/types_test.go @@ -84,7 +84,7 @@ func TestReadBuffer(t *testing.T) { name, noffset := ev.ReadAnsiString(offset) assert.Equal(t, "cmd.exe", name) - cmdline, _ := ev.ReadUTF16String(noffset + offset) + cmdline, _ := ev.ReadUTF16String(noffset) assert.Equal(t, "C:\\WINDOWS\\system32\\cmd.exe /c dir /-C /W \"\\\\?\\c:\\Users\\nedo\\AppData\\Roaming\\RabbitMQ\\db\\rabbit@archrabbit-mnesia\"", cmdline) }, }, From 4ee2050df9aeb89c9c8d361e8666441b1af9479a Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 18:58:26 +0200 Subject: [PATCH 4/6] feat(etw): Enable Windows Kernel Registry provider This provider is enabled with the hidden filter flag to make the ETW session write captured registry data. The registry event processor keeps the queue of received internal set value events and enriches subsequent RegSetValue events emitted by the NT Kernel Logger provider. --- internal/etw/consumer.go | 5 +- internal/etw/processors/registry_windows.go | 184 +++++++++++++++--- .../etw/processors/registry_windows_test.go | 55 +++++- internal/etw/source.go | 59 ++++-- internal/etw/trace.go | 31 ++- pkg/sys/etw/types.go | 6 + 6 files changed, 284 insertions(+), 56 deletions(-) diff --git a/internal/etw/consumer.go b/internal/etw/consumer.go index 6f5d2d979..03787376a 100644 --- a/internal/etw/consumer.go +++ b/internal/etw/consumer.go @@ -23,7 +23,6 @@ import ( "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/filter" - "github.com/rabbitstack/fibratus/pkg/handle" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/sys/etw" ) @@ -47,15 +46,15 @@ type Consumer struct { // NewConsumer builds a new event consumer. func NewConsumer( psnap ps.Snapshotter, - hsnap handle.Snapshotter, config *config.Config, sequencer *event.Sequencer, evts chan *event.Event, + processors processors.Chain, ) *Consumer { return &Consumer{ q: event.NewQueueWithChannel(evts, config.EventSource.StackEnrichment, config.ForwardMode || config.IsCaptureSet()), sequencer: sequencer, - processors: processors.NewChain(psnap, hsnap, config), + processors: processors, psnap: psnap, config: config, } diff --git a/internal/etw/processors/registry_windows.go b/internal/etw/processors/registry_windows.go index 02f132aa7..a3ca6c7ea 100644 --- a/internal/etw/processors/registry_windows.go +++ b/internal/etw/processors/registry_windows.go @@ -27,6 +27,7 @@ import ( "os" "path/filepath" "strings" + "sync" "sync/atomic" "time" @@ -41,11 +42,20 @@ var ( return fmt.Errorf("unable to read value %s : %v", filepath.Join(key, subkey), err) } + // valueTTL specifies the maximum allowed period for RegSetValueInternal events + // to remain in the queue + valueTTL = time.Minute * 2 + // valuePurgerInterval specifies the purge interval for stale values + valuePurgerInterval = time.Minute + // kcbCount counts the total KCBs found during the duration of the kernel session kcbCount = expvar.NewInt("registry.kcb.count") kcbMissCount = expvar.NewInt("registry.kcb.misses") keyHandleHits = expvar.NewInt("registry.key.handle.hits") + readValueOps = expvar.NewInt("registry.read.value.ops") + capturedDataHits = expvar.NewInt("registry.data.hits") + handleThrottleCount uint32 ) @@ -57,6 +67,13 @@ type registryProcessor struct { // keys stores the mapping between the KCB (Key Control Block) and the key name. keys map[uint64]string hsnap handle.Snapshotter + + values map[uint32][]*event.Event + mu sync.Mutex + + purger *time.Ticker + + quit chan struct{} } func newRegistryProcessor(hsnap handle.Snapshotter) Processor { @@ -68,10 +85,18 @@ func newRegistryProcessor(hsnap handle.Snapshotter) Processor { atomic.StoreUint32(&handleThrottleCount, 0) } }() - return ®istryProcessor{ - keys: make(map[uint64]string), - hsnap: hsnap, + + r := ®istryProcessor{ + keys: make(map[uint64]string), + hsnap: hsnap, + values: make(map[uint32][]*event.Event), + purger: time.NewTicker(valuePurgerInterval), + quit: make(chan struct{}, 1), } + + go r.housekeep() + + return r } func (r *registryProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { @@ -93,6 +118,12 @@ func (r *registryProcessor) processEvent(e *event.Event) (*event.Event, error) { delete(r.keys, khandle) kcbCount.Add(-1) default: + if e.IsRegSetValueInternal() { + // store the event in temporary queue + r.pushSetValue(e) + return e, nil + } + khandle := e.Params.MustGetUint64(params.RegKeyHandle) // we have to obey a straightforward algorithm to connect relative // key names to their root keys. If key handle is equal to zero we @@ -116,7 +147,32 @@ func (r *registryProcessor) processEvent(e *event.Event) (*event.Event, error) { } } - // get the type/value of the registry key and append to parameters + if e.IsRegSetValue() { + // previously stored RegSetValueInternal event + // is popped from the queue. RegSetValue can + // be enriched with registry value type/data + v := r.popSetValue(e) + if v == nil { + // try to read captured data from userspace + goto readValue + } + + capturedDataHits.Add(1) + + // enrich the event with value data/type parameters + typ, err := v.Params.GetUint32(params.RegValueType) + if err == nil { + e.AppendEnum(params.RegValueType, typ, key.RegistryValueTypes) + } + data, err := v.Params.Get(params.RegData) + if err == nil { + e.AppendParam(params.RegData, data.Type, data.Value) + } + + return e, nil + } + + readValue: if !e.IsRegSetValue() || !e.IsSuccess() { return e, nil } @@ -126,36 +182,42 @@ func (r *registryProcessor) processEvent(e *event.Event) (*event.Event, error) { return e, nil } + // get the type/value of the registry key and append to parameters rootkey, subkey := key.Format(keyName) - if rootkey != key.Invalid { - typ, val, err := rootkey.ReadValue(subkey) - if err != nil { - errno, ok := err.(windows.Errno) - if ok && (errno.Is(os.ErrNotExist) || err == windows.ERROR_ACCESS_DENIED) { - return e, nil - } - return e, ErrReadValue(rootkey.String(), keyName, err) - } - e.AppendEnum(params.RegValueType, typ, key.RegistryValueTypes) - switch typ { - case registry.SZ, registry.EXPAND_SZ: - e.AppendParam(params.RegValue, params.UnicodeString, val) - case registry.MULTI_SZ: - e.AppendParam(params.RegValue, params.Slice, val) - case registry.BINARY: - e.AppendParam(params.RegValue, params.Binary, val) - case registry.QWORD: - e.AppendParam(params.RegValue, params.Uint64, val) - case registry.DWORD: - e.AppendParam(params.RegValue, params.Uint32, uint32(val.(uint64))) + if rootkey == key.Invalid { + return e, nil + } + + readValueOps.Add(1) + typ, val, err := rootkey.ReadValue(subkey) + if err != nil { + errno, ok := err.(windows.Errno) + if ok && (errno.Is(os.ErrNotExist) || err == windows.ERROR_ACCESS_DENIED) { + return e, nil } + return e, ErrReadValue(rootkey.String(), keyName, err) + } + e.AppendEnum(params.RegValueType, typ, key.RegistryValueTypes) + + switch typ { + case registry.SZ, registry.EXPAND_SZ: + e.AppendParam(params.RegData, params.UnicodeString, val) + case registry.MULTI_SZ: + e.AppendParam(params.RegData, params.Slice, val) + case registry.BINARY: + e.AppendParam(params.RegData, params.Binary, val) + case registry.QWORD: + e.AppendParam(params.RegData, params.Uint64, val) + case registry.DWORD: + e.AppendParam(params.RegData, params.Uint32, uint32(val.(uint64))) } } + return e, nil } -func (registryProcessor) Name() ProcessorType { return Registry } -func (registryProcessor) Close() {} +func (*registryProcessor) Name() ProcessorType { return Registry } +func (r *registryProcessor) Close() { r.quit <- struct{}{} } func (r *registryProcessor) findMatchingKey(pid uint32, relativeKeyName string) string { // we want to prevent too frequent queries on the process' handles @@ -166,10 +228,12 @@ func (r *registryProcessor) findMatchingKey(pid uint32, relativeKeyName string) if atomic.LoadUint32(&handleThrottleCount) > maxHandleQueries { return relativeKeyName } + handles, err := r.hsnap.FindHandles(pid) if err != nil { return relativeKeyName } + for _, h := range handles { if h.Type != handle.Key { continue @@ -179,5 +243,71 @@ func (r *registryProcessor) findMatchingKey(pid uint32, relativeKeyName string) return h.Name } } + return relativeKeyName } + +// pushSetValue stores the internal RegSetValue event +// into per process identifier queue. +func (r *registryProcessor) pushSetValue(e *event.Event) { + r.mu.Lock() + defer r.mu.Unlock() + vals, ok := r.values[e.PID] + if !ok { + r.values[e.PID] = []*event.Event{e} + } else { + r.values[e.PID] = append(vals, e) + } +} + +// popSetValue traverses the internal RegSetValue queue +// and pops the event if the suffixes match. +func (r *registryProcessor) popSetValue(e *event.Event) *event.Event { + r.mu.Lock() + defer r.mu.Unlock() + vals, ok := r.values[e.PID] + if !ok { + return nil + } + + var v *event.Event + for i := len(vals) - 1; i >= 0; i-- { + val := vals[i] + if strings.HasSuffix(e.GetParamAsString(params.RegPath), val.GetParamAsString(params.RegPath)) { + v = val + r.values[e.PID] = append(vals[:i], vals[i+1:]...) + break + } + } + + return v +} + +func (r *registryProcessor) valuesSize(pid uint32) int { + r.mu.Lock() + defer r.mu.Unlock() + return len(r.values[pid]) +} + +func (r *registryProcessor) housekeep() { + for { + select { + case <-r.purger.C: + r.mu.Lock() + for pid, vals := range r.values { + for i, val := range vals { + if time.Since(val.Timestamp) < valueTTL { + continue + } + r.values[pid] = append(vals[:i], vals[i+1:]...) + } + if len(vals) == 0 { + delete(r.values, pid) + } + } + r.mu.Unlock() + case <-r.quit: + return + } + } +} diff --git a/internal/etw/processors/registry_windows_test.go b/internal/etw/processors/registry_windows_test.go index 16c0d88db..fb2b6f014 100644 --- a/internal/etw/processors/registry_windows_test.go +++ b/internal/etw/processors/registry_windows_test.go @@ -23,11 +23,18 @@ import ( "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/handle" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/util/key" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" + "time" ) +func init() { + valueTTL = time.Millisecond * 150 + valuePurgerInterval = time.Millisecond * 300 +} + func TestRegistryProcessor(t *testing.T) { var tests = []struct { name string @@ -161,7 +168,53 @@ func TestRegistryProcessor(t *testing.T) { func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Windows\Directory`, e.GetParamAsString(params.RegPath)) assert.Equal(t, `REG_EXPAND_SZ`, e.GetParamAsString(params.RegValueType)) - assert.Equal(t, `%SystemRoot%`, e.GetParamAsString(params.RegValue)) + assert.Equal(t, `%SystemRoot%`, e.GetParamAsString(params.RegData)) + }, + }, + { + "process registry set value from internal event", + &event.Event{ + Type: event.RegSetValue, + Category: event.Registry, + PID: 23234, + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.Key, Value: `\REGISTRY\MACHINE\SYSTEM\CurrentControlSet\Control\Windows\Directory`}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(0)}, + }, + }, + func(p Processor) { + p.(*registryProcessor).values[23234] = []*event.Event{ + { + Type: event.RegSetValueInternal, + Timestamp: time.Now(), + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.Key, Value: `\SessionId`}, + params.RegData: {Name: params.RegData, Type: params.UnicodeString, Value: "{ABD9EA10-87F6-11EB-9ED5-645D86501328}"}, + params.RegValueType: {Name: params.RegValueType, Type: params.Enum, Value: uint32(1), Enum: key.RegistryValueTypes}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(0)}}, + }, + { + Type: event.RegSetValueInternal, + Timestamp: time.Now(), + Params: event.Params{ + params.RegPath: {Name: params.RegPath, Type: params.Key, Value: `\Directory`}, + params.RegData: {Name: params.RegData, Type: params.UnicodeString, Value: "%SYSTEMROOT%"}, + params.RegValueType: {Name: params.RegValueType, Type: params.Enum, Value: uint32(2), Enum: key.RegistryValueTypes}, + params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Uint64, Value: uint64(0)}}, + }, + } + }, + func() *handle.SnapshotterMock { + hsnap := new(handle.SnapshotterMock) + return hsnap + }, + func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { + assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Windows\Directory`, e.GetParamAsString(params.RegPath)) + assert.Equal(t, `REG_EXPAND_SZ`, e.GetParamAsString(params.RegValueType)) + assert.Equal(t, `%SYSTEMROOT%`, e.GetParamAsString(params.RegData)) + assert.Equal(t, p.(*registryProcessor).valuesSize(23234), 1) + time.Sleep(time.Millisecond * 500) + assert.Equal(t, p.(*registryProcessor).valuesSize(23234), 0) }, }, } diff --git a/internal/etw/source.go b/internal/etw/source.go index 888d72ee6..347b99160 100644 --- a/internal/etw/source.go +++ b/internal/etw/source.go @@ -22,6 +22,7 @@ import ( "errors" "expvar" "fmt" + "github.com/rabbitstack/fibratus/internal/etw/processors" "github.com/rabbitstack/fibratus/pkg/config" errs "github.com/rabbitstack/fibratus/pkg/errors" "github.com/rabbitstack/fibratus/pkg/event" @@ -34,6 +35,7 @@ import ( log "github.com/sirupsen/logrus" "golang.org/x/sys/windows/registry" "time" + "unsafe" ) const ( @@ -69,9 +71,10 @@ var ( // starting ETW tracing sessions and setting up event // consumers. type EventSource struct { - r *config.RulesCompileResult - traces []*Trace - consumers []*Consumer + r *config.RulesCompileResult + traces []*Trace + consumers []*Consumer + processors processors.Chain errs chan error evts chan *event.Event @@ -96,17 +99,18 @@ func NewEventSource( compiler *config.RulesCompileResult, ) source.EventSource { evs := &EventSource{ - r: compiler, - traces: make([]*Trace, 0), - consumers: make([]*Consumer, 0), - errs: make(chan error, 1000), - evts: make(chan *event.Event, 500), - sequencer: event.NewSequencer(), - config: config, - stop: make(chan struct{}), - psnap: psnap, - hsnap: hsnap, - listeners: make([]event.Listener, 0), + r: compiler, + traces: make([]*Trace, 0), + consumers: make([]*Consumer, 0), + processors: processors.NewChain(psnap, hsnap, config), + errs: make(chan error, 1000), + evts: make(chan *event.Event, 500), + sequencer: event.NewSequencer(), + config: config, + stop: make(chan struct{}), + psnap: psnap, + hsnap: hsnap, + listeners: make([]event.Listener, 0), } return evs } @@ -170,6 +174,20 @@ func (e *EventSource) Open(config *config.Config) error { // from the snapshotter trace.AddProvider(etw.WindowsKernelProcessGUID, false, WithKeywords(etw.ProcessKeyword|etw.ImageKeyword), WithCaptureState()) + // in a similar vein, Windows Kernel Registry provider publishes + // the RegSetValue event with the full captured data for the + // modified value. This data is used to attach various parameters + // to the RegSetValue event published by the NT Kernel Logger + if config.EventSource.EnableRegistryEvents { + // undocumented ETW feature to enable captured data in RegSetValue events + val := 0x2 + eventFilterDescriptor := etw.EventFilterDescriptor{ + Ptr: uintptr(unsafe.Pointer(&val)), + Size: 4, + } + trace.AddProvider(etw.WindowsKernelRegistryGUID, false, WithKeywords(etw.SetValueKeyword), WithEventFilterDescriptors(eventFilterDescriptor)) + } + if config.EventSource.EnableDNSEvents { trace.AddProvider(etw.DNSClientGUID, false) } @@ -228,7 +246,13 @@ func (e *EventSource) Open(config *config.Config) error { } // Init consumer and open the trace for processing - consumer := NewConsumer(e.psnap, e.hsnap, config, e.sequencer, e.evts) + consumer := NewConsumer( + e.psnap, + config, + e.sequencer, + e.evts, + e.processors, + ) consumer.SetFilter(e.filter) // Attach event listeners @@ -245,9 +269,8 @@ func (e *EventSource) Open(config *config.Config) error { log.Infof("starting [%s] trace processing", t.Name) // Instruct the provider to emit state information - err = t.CaptureState() - if err != nil { - log.Warn(err) + if err := t.CaptureState(); err != nil { + log.Warnf("unable to capture trace %s state: %v", t.Name, err) } // Start event processing loop diff --git a/internal/etw/trace.go b/internal/etw/trace.go index 98d7eb31b..e12969b8d 100644 --- a/internal/etw/trace.go +++ b/internal/etw/trace.go @@ -95,13 +95,18 @@ type ProviderInfo struct { // stackExtensions manager stack tracing enablement. // For each event present in the stack identifiers, // the StackWalk event is published by the provider. - stackExtensions *StackExtensions + stackExtensions *StackExtensions + eventFilterDescriptors []etw.EventFilterDescriptor } func (p *ProviderInfo) HasStackExtensions() bool { return p.stackExtensions != nil && !p.stackExtensions.Empty() } +func (p *ProviderInfo) HasEventFilterDescriptors() bool { + return len(p.eventFilterDescriptors) > 0 +} + // Trace is the essential building block for controlling // trace sessions and configuring event consumers. Such // operations include starting, stopping, and flushing @@ -150,9 +155,10 @@ type Trace struct { } type opts struct { - stackexts *StackExtensions - keywords uint64 - captureState bool + stackexts *StackExtensions + keywords uint64 + captureState bool + eventFilterDescriptors []etw.EventFilterDescriptor } // Option represents the option for the trace. @@ -181,6 +187,14 @@ func WithCaptureState() Option { } } +// WithEventFilterDescriptors assigns filters to be passed +// to the provider's enable callback function. +func WithEventFilterDescriptors(descriptors ...etw.EventFilterDescriptor) Option { + return func(o *opts) { + o.eventFilterDescriptors = descriptors + } +} + // 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} @@ -208,7 +222,7 @@ func (t *Trace) AddProvider(guid windows.GUID, enableStacks bool, options ...Opt t.Providers = append( t.Providers, - ProviderInfo{GUID: guid, Keywords: opts.keywords, EnableStacks: enableStacks, CaptureState: opts.captureState, stackExtensions: opts.stackexts}, + ProviderInfo{GUID: guid, Keywords: opts.keywords, EnableStacks: enableStacks, CaptureState: opts.captureState, stackExtensions: opts.stackexts, eventFilterDescriptors: opts.eventFilterDescriptors}, ) } @@ -325,8 +339,11 @@ func (t *Trace) Start() error { if err := etw.EnableTrace(provider.GUID, t.startHandle, provider.Keywords); err != nil { return err } - case provider.EnableStacks: - opts := etw.EnableTraceOpts{WithStacktrace: true} + case provider.EnableStacks || provider.HasEventFilterDescriptors(): + opts := etw.EnableTraceOpts{ + WithStacktrace: provider.EnableStacks, + EventFilterDescriptors: provider.eventFilterDescriptors, + } if err := etw.EnableTraceWithOpts(provider.GUID, t.startHandle, provider.Keywords, opts); err != nil { return err } diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index 5fdba491e..34a9c6c6b 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -47,6 +47,9 @@ var ThreadpoolGUID = windows.GUID{Data1: 0xc861d0e2, Data2: 0xa2c1, Data3: 0x4d3 // 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}} +// WindowsKernelRegistryGUID represents the GUID for the Microsoft Windows Kernel Registry provider +var WindowsKernelRegistryGUID = windows.GUID{Data1: 0x70eb4f03, Data2: 0xc1de, Data3: 0x4f73, Data4: [8]byte{0xa0, 0x51, 0x33, 0xd1, 0x3d, 0x54, 0x13, 0xbd}} + const ( // TraceStackTracingInfo controls call stack tracing for kernel events TraceStackTracingInfo = uint8(3) @@ -60,6 +63,9 @@ const ProcessKeyword = 0x10 // ImageKeyword enables images events for Microsoft Windows Kernel Process provider const ImageKeyword = 0x40 +// SetValueKeyword enables registry key value set events for Microsoft Windows Kernel Registry provider +const SetValueKeyword = 0x100 + const ( // EventHeaderExtTypeStackTrace64 indicates that the extended data contains the call stack if the event is captured on a 64-bit host EventHeaderExtTypeStackTrace64 uint16 = 0x0006 From 05f493a550068aa16a1b53e8528af0ebc679e397 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 19:00:27 +0200 Subject: [PATCH 5/6] refactor(yara): Use the new registry data parameter name --- pkg/yara/scanner.go | 2 +- pkg/yara/scanner_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/yara/scanner.go b/pkg/yara/scanner.go index fb3bf21bd..513ea64ce 100644 --- a/pkg/yara/scanner.go +++ b/pkg/yara/scanner.go @@ -334,7 +334,7 @@ func (s scanner) Scan(e *event.Event) (bool, error) { if typ := e.Params.TryGetUint32(params.RegValueType); typ != registry.BINARY { return false, nil } - v, err := e.Params.Get(params.RegValue) + v, err := e.Params.Get(params.RegData) if err != nil { // value not attached to the event return false, nil diff --git a/pkg/yara/scanner_test.go b/pkg/yara/scanner_test.go index 80f0d3a36..500a9df1d 100644 --- a/pkg/yara/scanner_test.go +++ b/pkg/yara/scanner_test.go @@ -933,7 +933,7 @@ func TestScan(t *testing.T) { PID: 565, Params: event.Params{ params.RegValueType: {Name: params.RegValueType, Type: params.Uint32, Value: uint32(registry.BINARY)}, - params.RegValue: {Name: params.RegValue, Type: params.Binary, Value: data}, + params.RegData: {Name: params.RegValue, Type: params.Binary, Value: data}, params.RegPath: {Name: params.RegPath, Type: params.UnicodeString, Value: `HKEY_LOCAL_MACHINE\CurrentControlSet\Control\DeviceGuard\Mal`}, }, Metadata: make(map[event.MetadataKey]any), From 6872c0bda0606f261bc08ae0981df1d9b84f018a Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 22 Jul 2025 19:18:54 +0200 Subject: [PATCH 6/6] refactor(filter): Change registry.value filter field semantics The registry.value filter field yields the name of the created or modified registry value. The new registry.data field returns the captured value data. --- pkg/filter/accessor_windows.go | 7 ++++++- pkg/filter/fields/fields_windows.go | 9 ++++++--- pkg/filter/filter_test.go | 5 +++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/filter/accessor_windows.go b/pkg/filter/accessor_windows.go index e3069ee59..d2ea35fd6 100644 --- a/pkg/filter/accessor_windows.go +++ b/pkg/filter/accessor_windows.go @@ -940,9 +940,14 @@ func (r *registryAccessor) Get(f Field, e *event.Event) (params.Value, error) { case fields.RegistryKeyHandle: return e.GetParamAsString(params.RegKeyHandle), nil case fields.RegistryValue: - return e.Params.GetRaw(params.RegValue) + if e.IsRegSetValue() { + return filepath.Base(filepath.Base(e.GetParamAsString(params.RegPath))), nil + } + return nil, nil case fields.RegistryValueType: return e.Params.GetString(params.RegValueType) + case fields.RegistryData: + return e.GetParamAsString(params.RegData), nil case fields.RegistryStatus: return e.GetParamAsString(params.NTStatus), nil } diff --git a/pkg/filter/fields/fields_windows.go b/pkg/filter/fields/fields_windows.go index 4c50c826f..84904fe02 100644 --- a/pkg/filter/fields/fields_windows.go +++ b/pkg/filter/fields/fields_windows.go @@ -479,10 +479,12 @@ const ( RegistryKeyName Field = "registry.key.name" // RegistryKeyHandle represents the registry KCB address RegistryKeyHandle Field = "registry.key.handle" - // RegistryValue represents the registry value + // RegistryValue represents the registry value name field RegistryValue Field = "registry.value" - // RegistryValueType represents the registry value type + // RegistryValueType represents the registry value type field RegistryValueType Field = "registry.value.type" + // RegistryData represents the captured registry data field + RegistryData Field = "registry.data" // RegistryStatus represent the registry operation status RegistryStatus Field = "registry.status" @@ -1000,8 +1002,9 @@ var fields = map[Field]FieldInfo{ RegistryPath: {RegistryPath, "fully qualified registry path", params.UnicodeString, []string{"registry.path = 'HKEY_LOCAL_MACHINE\\SYSTEM'"}, nil, nil}, RegistryKeyName: {RegistryKeyName, "registry key name", params.UnicodeString, []string{"registry.key.name = 'CurrentControlSet'"}, nil, nil}, RegistryKeyHandle: {RegistryKeyHandle, "registry key object address", params.Address, []string{"registry.key.handle = 'FFFFB905D60C2268'"}, nil, nil}, - RegistryValue: {RegistryValue, "registry value content", params.UnicodeString, []string{"registry.value = '%SystemRoot%\\system32'"}, nil, nil}, + RegistryValue: {RegistryValue, "registry value name", params.UnicodeString, []string{"registry.value = 'Epoch'"}, nil, nil}, RegistryValueType: {RegistryValueType, "type of registry value", params.UnicodeString, []string{"registry.value.type = 'REG_SZ'"}, nil, nil}, + RegistryData: {RegistryData, "registry value captured data", params.Object, []string{"registry.data = '%SystemRoot%'"}, nil, nil}, RegistryStatus: {RegistryStatus, "status of registry operation", params.UnicodeString, []string{"registry.status != 'success'"}, nil, nil}, NetDIP: {NetDIP, "destination IP address", params.IP, []string{"net.dip = 172.17.0.3"}, nil, nil}, diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index a105f25d7..2b46669fc 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -863,7 +863,7 @@ func TestRegistryFilter(t *testing.T) { Category: event.Registry, Params: event.Params{ params.RegPath: {Name: params.RegPath, Type: params.UnicodeString, Value: `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`}, - params.RegValue: {Name: params.RegValue, Type: params.Uint32, Value: uint32(10234)}, + params.RegData: {Name: params.RegData, Type: params.Uint32, Value: uint32(10234)}, params.RegValueType: {Name: params.RegValueType, Type: params.AnsiString, Value: "DWORD"}, params.NTStatus: {Name: params.NTStatus, Type: params.AnsiString, Value: "success"}, params.RegKeyHandle: {Name: params.RegKeyHandle, Type: params.Address, Value: uint64(18446666033449935464)}, @@ -878,8 +878,9 @@ func TestRegistryFilter(t *testing.T) { {`registry.status startswith ('key not', 'succ')`, true}, {`registry.path = 'HKEY_LOCAL_MACHINE\\SYSTEM\\Setup\\Pid'`, true}, {`registry.key.name icontains ('Setup', 'setup')`, true}, - {`registry.value = 10234`, true}, + {`registry.value = 'Pid'`, true}, {`registry.value.type in ('DWORD', 'QWORD')`, true}, + {`registry.data = '10234'`, true}, {`MD5(registry.path) = 'eab870b2a516206575d2ffa2b98d8af5'`, true}, }