diff --git a/internal/etw/approvers/approver.go b/internal/etw/approvers/approver.go new file mode 100644 index 000000000..1e7dc63ab --- /dev/null +++ b/internal/etw/approvers/approver.go @@ -0,0 +1,161 @@ +/* + * Copyright 2020-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 approvers + +import ( + "path/filepath" + "strings" + + "github.com/rabbitstack/fibratus/internal/etw/processors" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/filter/ql" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/sys/etw" + "github.com/rabbitstack/fibratus/pkg/util/wildcard" +) + +// Approver represents the contract that every approver must satisfy. +type Approver interface { + // Approve receives a raw event recored. It may return + // a new event record, and always returns a flag to + // indicate if the event is approved or rejected. + Approve(r *etw.EventRecord) (*etw.EventRecord, bool) +} + +// Approvers is the registry for all known approvers. +type Approvers struct { + approvers []Approver + + fs *fs +} + +// New creates a new approvers set. +func New(psnap ps.Snapshotter, r *config.RulesCompileResult, processors *processors.Chain) Approvers { + p := Approvers{ + approvers: make([]Approver, 0), + } + + p.fs = newFSApprover(r, processors).(*fs) + + p.approvers = append(p.approvers, p.fs) + if r != nil { + p.approvers = append(p.approvers, newRegistryApprover(r)) + } + if r != nil { + p.approvers = append(p.approvers, newProcApprover(psnap, r)) + } + + return p +} + +// Approve renders a verdict about the event record. +// It evalutes available approvers against the event +// and if it satisifes the main condition for rule +// assertion, this method returns true. Otherwise, it +// returns false and instructs the consumer to drop the +// event record. +func (p *Approvers) Approve(r *etw.EventRecord) (*etw.EventRecord, bool) { + if len(p.approvers) == 0 { + return r, true + } + + rec := r + for _, approver := range p.approvers { + e, ok := approver.Approve(rec) + if !ok { + return rec, false + } + rec = e + } + + return rec, true +} + +// Cleanup housekeeps approvers state. +func (p *Approvers) Cleanup(r *etw.EventRecord) { + p.fs.cleanup(r) +} + +// approver contains the base logic any approver can consume. +type approver struct { + r *config.RulesCompileResult +} + +func (p *approver) approvePath(path string) bool { + return p.matchPredicate(p.r.Approvers.Paths, path) +} + +func (p *approver) approveBasename(path string) bool { + return p.matchPredicate(p.r.Approvers.Bases, filepath.Base(path)) +} + +func (p *approver) approveExtension(path string) bool { + return p.matchPredicate(p.r.Approvers.Extensions, filepath.Ext(path)) +} + +func (p *approver) approveKey(key string) bool { + return p.matchPredicate(p.r.Approvers.Keys, key) +} + +func (p *approver) approveExecutable(exe string) bool { + return p.matchPredicate(p.r.Approvers.Executables, exe) +} + +func (*approver) matchPredicate(m map[string][]string, v string) bool { + s := strings.ToLower(v) + for op, patterns := range m { + for _, pattern := range patterns { + switch op { + case ql.IMatches.String(): + if wildcard.Match(strings.ToLower(pattern), s) { + return true + } + case ql.Matches.String(): + if wildcard.Match(pattern, v) { + return true + } + case ql.IContains.String(): + if strings.Contains(s, strings.ToLower(pattern)) { + return true + } + case ql.Contains.String(): + if strings.Contains(v, pattern) { + return true + } + case ql.IEq.String(), ql.IIn.String(): + if s == strings.ToLower(pattern) { + return true + } + case ql.Eq.String(), ql.In.String(): + if v == pattern { + return true + } + case ql.IStartswith.String(): + return strings.HasPrefix(s, pattern) + case ql.Startswith.String(): + return strings.HasPrefix(v, pattern) + case ql.IEndswith.String(): + return strings.HasSuffix(s, pattern) + case ql.Endswith.String(): + return strings.HasSuffix(v, pattern) + } + } + } + return false +} diff --git a/internal/etw/approvers/approver_test.go b/internal/etw/approvers/approver_test.go new file mode 100644 index 000000000..8a61dbc47 --- /dev/null +++ b/internal/etw/approvers/approver_test.go @@ -0,0 +1,388 @@ +/* + * Copyright 2020-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 approvers + +import ( + "testing" + "unsafe" + + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/ps" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/rabbitstack/fibratus/pkg/sys/etw" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "golang.org/x/sys/windows" +) + +func makeRecord(providerID windows.GUID, opcode uint8, eventID uint16, buf []byte) *etw.EventRecord { + b := make([]byte, len(buf)) + copy(b, buf) + r := &etw.EventRecord{} + r.Header.ProviderID = providerID + r.Header.EventDescriptor.Opcode = opcode + r.Header.EventDescriptor.ID = eventID + r.BufferLen = uint16(len(b)) + if len(b) > 0 { + r.Buffer = uintptr(unsafe.Pointer(&b[0])) + } + return r +} + +func TestApproversNoApprovers(t *testing.T) { + p := Approvers{approvers: make([]Approver, 0)} + r := &etw.EventRecord{} + rec, approved := p.Approve(r) + assert.True(t, approved) + assert.Equal(t, r, rec) +} + +func TestApproversChainPassesEnrichedRecord(t *testing.T) { + psnap := &ps.SnapshotterMock{} + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Paths: map[string][]string{ + "IMATCHES": {`C:\Windows\*`}, + }, + }, + } + + p := New(psnap, rules, nil) + + r := &etw.EventRecord{} + r.Header.ProviderID = etw.WindowsKernelProcessGUID + rec, approved := p.Approve(r) + assert.True(t, approved) + assert.Equal(t, r, rec) +} + +func TestApproversFirstApproverRejectsShortCircuits(t *testing.T) { + callCount := 0 + first := &mockApprover{fn: func(r *etw.EventRecord) (*etw.EventRecord, bool) { + callCount++ + return r, false + }} + second := &mockApprover{fn: func(r *etw.EventRecord) (*etw.EventRecord, bool) { + callCount++ + return r, true + }} + + p := Approvers{approvers: []Approver{first, second}} + r := &etw.EventRecord{} + + _, approved := p.Approve(r) + assert.False(t, approved) + assert.Equal(t, 1, callCount, "second approver should not be called") +} + +func TestApproversCleanupDelegatesToFS(t *testing.T) { + psnap := &ps.SnapshotterMock{} + p := New(psnap, nil, nil) + + // enqueue a CreateFile + cr := createFileRecord(t, createBuf) + p.Approve(cr) + assert.Equal(t, 1, len(p.fs.irps)) + + // simulate consumer calling cleanup with the returned CreateFile record + irp := p.fs.irps[*(*uint64)(unsafe.Pointer(&createBuf[0]))] + p.Cleanup(irp.rec) + assert.Equal(t, 0, len(p.fs.irps)) +} + +func TestApproversFileEventFullFlow(t *testing.T) { + psnap := &ps.SnapshotterMock{} + p := New(psnap, nil, nil) + + // CreateFile is suppressed and stored + cr := createFileRecord(t, createBuf) + _, approved := p.Approve(cr) + assert.False(t, approved, "CreateFile should be put in queue") + assert.Equal(t, 1, len(p.fs.irps)) + + // FileOpEnd with matching IRP releases stored CreateFile + foe := buildMatchingFileOpEnd(t, createBuf, uint64(windows.FILE_CREATE)) + rec, approved := p.Approve(foe) + assert.True(t, approved, "Pending CreateFile should be approved") + assert.Equal(t, event.CreateFileID, rec.Header.EventDescriptor.Opcode, + "should return stored CreateFile record") + assert.NotNil(t, rec.ExtendedData, "extended data should be attached") +} + +func TestApproversFileEventWithRulesApproved(t *testing.T) { + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Paths: map[string][]string{ + "ICONTAINS": {`Windows\AppCompat`}, + }, + }, + } + + psnap := &ps.SnapshotterMock{} + p := New(psnap, rules, nil) + + cr := createFileRecord(t, createBuf) + p.Approve(cr) + assert.Equal(t, 1, len(p.fs.irps)) + + foe := buildMatchingFileOpEnd(t, createBuf, uint64(windows.FILE_OPEN)) + rec, approved := p.Approve(foe) + assert.True(t, approved, "Pending CreateFile should be approved") + assert.Equal(t, event.CreateFileID, rec.Header.EventDescriptor.Opcode, + "should return stored CreateFile record") + assert.NotNil(t, rec.ExtendedData, "extended data should be attached") +} + +func TestApproversFileEventWithRulesRejected(t *testing.T) { + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Extensions: map[string][]string{ + "IN": {".exe", ".cpl"}, + }, + }, + } + + psnap := &ps.SnapshotterMock{} + p := New(psnap, rules, nil) + + cr := createFileRecord(t, createBuf) + p.Approve(cr) + assert.Equal(t, 1, len(p.fs.irps)) + + foe := buildMatchingFileOpEnd(t, createBuf, uint64(windows.FILE_OPEN)) + _, approved := p.Approve(foe) + assert.False(t, approved, "Pending CreateFile shouldn't be approved") +} + +func TestApproversRegistryEventNoRulesApprovesAll(t *testing.T) { + psnap := &ps.SnapshotterMock{} + + p := New(psnap, nil, nil) + r := makeRecord(event.RegistryEventGUID, event.RegOpenKeyID, 0, regOpenKeyBuf) + + _, approved := p.Approve(r) + assert.True(t, approved) +} + +func TestApproversRegistryEventWithRulesApproved(t *testing.T) { + psnap := &ps.SnapshotterMock{} + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Keys: map[string][]string{ + "IMATCHES": {`HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\*`}, + }, + }, + } + + p := New(psnap, rules, nil) + + k := makeRecord(event.RegistryEventGUID, event.RegCreateKCBID, 0, regCreateKCBBuf) + r := makeRecord(event.RegistryEventGUID, event.RegOpenKeyID, 0, regOpenKeyBuf) + + // first send the RegCreateKCB event to store the KCB mapping + _, approved := p.Approve(k) + assert.True(t, approved) + + _, approved = p.Approve(r) + assert.True(t, approved) +} + +func TestApproversRegistryEventWithRulesRejected(t *testing.T) { + psnap := &ps.SnapshotterMock{} + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Keys: map[string][]string{ + "IMATCHES": {`HKEY_LOCAL_MACHINE\SYSTEM\*`}, + }, + }, + } + + p := New(psnap, rules, nil) + + k := makeRecord(event.RegistryEventGUID, event.RegCreateKCBID, 0, regCreateKCBBuf) + r := makeRecord(event.RegistryEventGUID, event.RegOpenKeyID, 0, regOpenKeyBuf) + + p.Approve(k) + + _, approved := p.Approve(r) + assert.False(t, approved) +} + +func TestApproversProcEventNoRulesApprovesAll(t *testing.T) { + psnap := &ps.SnapshotterMock{} + p := New(psnap, nil, nil) + + buf := make([]byte, 4) + *(*uint32)(unsafe.Pointer(&buf[0])) = uint32(1234) + r := makeRecord(event.AuditAPIEventGUID, 0, event.OpenProcessID, buf) + r.Header.ProcessID = 1234 + + _, approved := p.Approve(r) + assert.True(t, approved) + psnap.AssertNotCalled(t, "Find", mock.Anything) +} + +func TestApproversProcEventWithRulesApproved(t *testing.T) { + psnap := &ps.SnapshotterMock{} + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Executables: map[string][]string{ + "IMATCHES": {`?:\Windows\System32\*`}, + }, + }, + } + + p := New(psnap, rules, nil) + + const pid = uint32(1234) + psnap.On("Find", pid).Return(true, &pstypes.PS{ + Exe: `C:\Windows\System32\svchost.exe`, + }) + + buf := make([]byte, 4) + *(*uint32)(unsafe.Pointer(&buf[0])) = pid + r := makeRecord(event.AuditAPIEventGUID, 0, event.OpenProcessID, buf) + r.Header.ProcessID = pid + + _, approved := p.Approve(r) + assert.True(t, approved) +} + +func TestApproversProcEventWithRulesRejected(t *testing.T) { + psnap := &ps.SnapshotterMock{} + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Executables: map[string][]string{ + "IMATCHES": {`C:\Windows\System32\*`}, + }, + }, + } + p := New(psnap, rules, nil) + + const pid = uint32(5678) + psnap.On("Find", pid).Return(true, &pstypes.PS{ + Exe: `C:\Users\Administrator\cmd.exe`, + }) + + buf := make([]byte, 4) + *(*uint32)(unsafe.Pointer(&buf[0])) = pid + r := makeRecord(event.AuditAPIEventGUID, 0, event.OpenProcessID, buf) + r.Header.ProcessID = pid + + _, approved := p.Approve(r) + assert.False(t, approved) +} + +func TestApproversMatchPredicate(t *testing.T) { + a := &approver{} + tests := []struct { + name string + m map[string][]string + val string + want bool + }{ + { + name: "imatches wildcard hit", + m: map[string][]string{"IMATCHES": {`C:\Windows\*`}}, + val: `C:\Windows\System32\ntdll.dll`, + want: true, + }, + { + name: "imatches wildcard miss", + m: map[string][]string{"IMATCHES": {`C:\Windows\*`}}, + val: `C:\Users\Administrator\cmd.exe`, + want: false, + }, + { + name: "imatches case insensitive", + m: map[string][]string{"IMATCHES": {`c:\windows\*`}}, + val: `C:\WINDOWS\System32\ntdll.dll`, + want: true, + }, + { + name: "icontains hit", + m: map[string][]string{"ICONTAINS": {"svchost"}}, + val: `C:\Windows\System32\svchost.exe`, + want: true, + }, + { + name: "icontains case insensitive", + m: map[string][]string{"ICONTAINS": {"SVCHOST"}}, + val: `C:\Windows\System32\svchost.exe`, + want: true, + }, + { + name: "icontains miss", + m: map[string][]string{"ICONTAINS": {"evil"}}, + val: `C:\Windows\System32\svchost.exe`, + want: false, + }, + { + name: "eq hit", + m: map[string][]string{"=": {`C:\Windows\System32\svchost.exe`}}, + val: `C:\Windows\System32\svchost.exe`, + want: true, + }, + { + name: "eq miss", + m: map[string][]string{"=": {`C:\Windows\System32\svchost.exe`}}, + val: `C:\Windows\System32\lsass.exe`, + want: false, + }, + { + name: "ieq hit", + m: map[string][]string{"~=": {`C:\Windows\System32\svchost.exe`}}, + val: `C:\Windows\System32\svchost.exe`, + want: true, + }, + { + name: "multiple patterns first hits", + m: map[string][]string{ + "IMATCHES": {`C:\Windows\*`, `C:\System32\*`}, + }, + val: `C:\Windows\notepad.exe`, + want: true, + }, + { + name: "multiple operators one hits", + m: map[string][]string{ + "IMATCHES": {`C:\System32\*`}, + "ICONTAINS": {"notepad"}, + }, + val: `C:\Windows\notepad.exe`, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := a.matchPredicate(tt.m, tt.val) + assert.Equal(t, tt.want, got) + }) + } +} + +// mockApprover is a simple test double for the Approver interface +type mockApprover struct { + fn func(r *etw.EventRecord) (*etw.EventRecord, bool) +} + +func (m *mockApprover) Approve(r *etw.EventRecord) (*etw.EventRecord, bool) { + return m.fn(r) +} diff --git a/internal/etw/approvers/fs.go b/internal/etw/approvers/fs.go new file mode 100644 index 000000000..34f3681c5 --- /dev/null +++ b/internal/etw/approvers/fs.go @@ -0,0 +1,158 @@ +/* + * Copyright 2020-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 approvers + +import ( + "expvar" + + "github.com/rabbitstack/fibratus/internal/etw/processors" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + devmapper "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/sys/etw" + "golang.org/x/sys/windows" +) + +var ( + fsApproverApprovals = expvar.NewInt("approver.fs.approvals") + fsApproverRejections = expvar.NewInt("approver.fs.rejections") +) + +// irp acts as a scratch area for the pending IRP request +// that is used as a signal to promote the file operation. +// The memory buffer backing the event record must outlive +// event processors scope. +type irp struct { + rec *etw.EventRecord + buf []byte // keeps the data buffer alive + items *etw.FileExtendedDataItems // keeps the extended data items alive +} + +// fs is the file system approver that accepts or discards +// file events as soon as they are pulled from the session +// buffers. +type fs struct { + approver + + irps map[uint64]irp + + processors *processors.Chain + approvers []func(string) bool +} + +func newFSApprover(r *config.RulesCompileResult, processors *processors.Chain) Approver { + fs := &fs{ + approver: approver{ + r: r, + }, + approvers: make([]func(string) bool, 0), + processors: processors, + irps: make(map[uint64]irp), + } + + if r != nil && len(r.Approvers.Paths) > 0 { + fs.approvers = append(fs.approvers, fs.approvePath) + } + if r != nil && len(r.Approvers.Extensions) > 0 { + fs.approvers = append(fs.approvers, fs.approveExtension) + } + if r != nil && len(r.Approvers.Bases) > 0 { + fs.approvers = append(fs.approvers, fs.approveBasename) + } + + return fs +} + +func (f *fs) Approve(r *etw.EventRecord) (*etw.EventRecord, bool) { + if r.Header.ProviderID != event.FileEventGUID { + return r, true + } + + // enqueue in flight CreateFile event + if r.Header.EventDescriptor.Opcode == event.CreateFileID { + rec, buf := r.Copy() + f.irps[r.ReadUint64(0)] = irp{rec: rec, buf: buf} + return r, false + } + + if r.Header.EventDescriptor.Opcode == event.FileOpEndID { + disposition := r.ReadUint64(8) + + // the file operation finalization event arrived but not + // for our previously queued CreateFile event as the extra + // file information doesn't match any of the known file + // dispositions flags. We can safely drop the event + if disposition > windows.FILE_MAXIMUM_DISPOSITION { + return r, false + } + + i := r.ReadUint64(0) + irp, ok := f.irps[i] + if !ok { + return r, false + } + + rec := irp.rec + status := r.ReadUint32(16) + // if the I/O status is different than file open + // or the rules compilation result is not present + // we'll allow events flow downstream processors + if f.r == nil || disposition != windows.FILE_OPEN { + irp.items = etw.AppendEventHeaderFileExtendedDataItems(rec, disposition, status) + f.irps[i] = irp + return rec, true + } + + // evaluate against available approvers + var approved bool + path := devmapper.GetDevMapper().Convert(rec.ConsumeUTF16String(32)) + for _, approver := range f.approvers { + if approver(path) { + approved = true + break + } + } + if !approved { + // the event is rejected by approvers. Make sure to + // evict the enqueded StackWalk event produced by the + // CreateFile operation + delete(f.irps, i) + fsApproverRejections.Add(1) + stackID := uint64(rec.Header.ProcessID + rec.Header.ThreadID) + if f.processors != nil { + f.processors.DequeueStackwalk(stackID) + } + return rec, false + } + + fsApproverApprovals.Add(1) + irp.items = etw.AppendEventHeaderFileExtendedDataItems(rec, disposition, status) + f.irps[i] = irp + return rec, true + } + + return r, true +} + +func (f *fs) cleanup(r *etw.EventRecord) { + if r.Header.ProviderID == event.FileEventGUID && r.Header.EventDescriptor.Opcode == event.CreateFileID { + i := r.ReadUint64(0) + delete(f.irps, i) + } +} diff --git a/internal/etw/approvers/fs_test.go b/internal/etw/approvers/fs_test.go new file mode 100644 index 000000000..78b471266 --- /dev/null +++ b/internal/etw/approvers/fs_test.go @@ -0,0 +1,328 @@ +/* + * Copyright 2020-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 approvers + +import ( + "testing" + "unsafe" + + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/sys/etw" + "golang.org/x/sys/windows" +) + +// createFileRecord builds an EventRecord for a CreateFile event +// using the provided raw buffer. +func createFileRecord(t *testing.T, buf []byte) *etw.EventRecord { + t.Helper() + b := make([]byte, len(buf)) + copy(b, buf) + r := &etw.EventRecord{} + r.Header.ProviderID = event.FileEventGUID + r.Header.EventDescriptor.Opcode = event.CreateFileID + r.BufferLen = uint16(len(b)) + r.Buffer = uintptr(unsafe.Pointer(&b[0])) + // store b to prevent GC + t.Cleanup(func() { _ = b }) + return r +} + +// fileOpEndRecord builds an EventRecord for a FileOpEnd event. +func fileOpEndRecord(t *testing.T, buf []byte) *etw.EventRecord { + t.Helper() + b := make([]byte, len(buf)) + copy(b, buf) + r := &etw.EventRecord{} + r.Header.ProviderID = event.FileEventGUID + r.Header.EventDescriptor.Opcode = event.FileOpEndID + r.BufferLen = uint16(len(b)) + r.Buffer = uintptr(unsafe.Pointer(&b[0])) + t.Cleanup(func() { _ = b }) + return r +} + +var ( + createBuf = []byte{ + 200, 7, 94, 150, 141, 215, 255, 255, // Irp + 80, 102, 11, 146, 141, 215, 255, 255, // FileObject + 136, 25, 0, 0, // ThreadId + 36, 128, 0, 3, // Options + 128, 0, 0, 0, // Attributes + 0, 0, 0, 0, // ShareAccess + // \Device\HarddiskVolume3\WINDOWS\AppCompat\Programs\Amcache.hve + 92, 0, + 68, 0, 101, 0, 118, 0, 105, 0, 99, 0, 101, 0, + 92, 0, + 72, 0, 97, 0, 114, 0, 100, 0, 100, 0, 105, 0, + 115, 0, 107, 0, + 86, 0, 111, 0, 108, 0, 117, 0, 109, 0, 101, 0, + 51, 0, + 92, 0, + 87, 0, 73, 0, 78, 0, 68, 0, 79, 0, 87, 0, + 83, 0, + 92, 0, + 65, 0, 112, 0, 112, 0, + 67, 0, 111, 0, 109, 0, 112, 0, 97, 0, 116, 0, + 92, 0, + 80, 0, 114, 0, 111, 0, 103, 0, 114, 0, 97, 0, + 109, 0, 115, 0, + 92, 0, + 65, 0, 109, 0, 99, 0, 97, 0, 99, 0, 104, 0, + 101, 0, 46, 0, 104, 0, 118, 0, 101, 0, + 0, 0, + } + opendBuf = []byte{ + 248, 240, 61, 151, 141, 215, 255, 255, // Irp + 0, 0, 0, 0, 40, 0, 0, 0, // ExtraInformation + 0, 0, 0, 0, // Status + } +) + +// buildMatchingFileOpEnd builds a FileOpEnd whose IRP matches the given CreateFile buffer. +func buildMatchingFileOpEnd(t *testing.T, createBuf []byte, disposition uint64) *etw.EventRecord { + t.Helper() + // read IRP from createFile buffer (first 8 bytes) + irp := *(*uint64)(unsafe.Pointer(&createBuf[0])) + + buf := make([]byte, 20) + *(*uint64)(unsafe.Pointer(&buf[0])) = irp + *(*uint64)(unsafe.Pointer(&buf[8])) = disposition + *(*uint32)(unsafe.Pointer(&buf[16])) = 0 + + r := &etw.EventRecord{} + r.Header.ProviderID = event.FileEventGUID + r.Header.EventDescriptor.Opcode = event.FileOpEndID + r.BufferLen = uint16(len(buf)) + r.Buffer = uintptr(unsafe.Pointer(&buf[0])) + t.Cleanup(func() { _ = buf }) + return r +} + +func newTestFSApprover(r *config.RulesCompileResult) *fs { + return newFSApprover(r, nil).(*fs) +} + +func TestFSApproverNonFileEvent(t *testing.T) { + f := newTestFSApprover(nil) + r := &etw.EventRecord{} + r.Header.ProviderID = event.ProcessEventGUID + rec, approved := f.Approve(r) + if !approved { + t.Error("non-file event should be approved") + } + if rec != r { + t.Error("non-file event should return original record") + } +} + +func TestFSApproverCreateFileIsEnqueuedAndSuppressed(t *testing.T) { + f := newTestFSApprover(nil) + r := createFileRecord(t, createBuf) + + rec, approved := f.Approve(r) + if approved { + t.Error("CreateFile should be suppressed") + } + if rec != r { + t.Error("CreateFile should return original record unchanged") + } + if len(f.irps) != 1 { + t.Errorf("expected 1 IRP in map, got %d", len(f.irps)) + } +} + +func TestFSApproverFileOpEndNoMatchingIRP(t *testing.T) { + f := newTestFSApprover(nil) + // FileOpEnd arrives with no prior CreateFile + r := fileOpEndRecord(t, opendBuf) + rec, approved := f.Approve(r) + if approved { + t.Error("FileOpEnd with no matching IRP should be suppressed") + } + if rec != r { + t.Error("should return original FileOpEnd record") + } +} + +func TestFSApproverFileOpEndDispositionOutOfRange(t *testing.T) { + f := newTestFSApprover(nil) + // first enqueue CreateFile + f.Approve(createFileRecord(t, createBuf)) + + // FileOpEnd with disposition > FILE_MAXIMUM_DISPOSITION + r := buildMatchingFileOpEnd(t, createBuf, windows.FILE_MAXIMUM_DISPOSITION+1) + _, approved := f.Approve(r) + if approved { + t.Error("FileOpEnd with out-of-range disposition should be suppressed") + } +} + +func TestFSApproverFileOpEndNoRules(t *testing.T) { + // r == nil means no rules compiled, all events flow through + f := newTestFSApprover(nil) + f.Approve(createFileRecord(t, createBuf)) + + r := buildMatchingFileOpEnd(t, createBuf, windows.FILE_OPEN) + rec, approved := f.Approve(r) + if !approved { + t.Error("with no rules, all file events should be approved") + } + if rec.Header.EventDescriptor.Opcode != event.CreateFileID { + t.Error("should return stored CreateFile record, not FileOpEnd") + } + if rec.ExtendedData == nil { + t.Error("extended data should be attached to the returned record") + } +} + +func TestFSApproverFileOpEndNonOpenDispositionApprovesWithoutPathCheck(t *testing.T) { + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Paths: map[string][]string{ + "IMATCHES": {`C:\Windows\*`}, + }, + }, + } + f := newTestFSApprover(rules) + f.Approve(createFileRecord(t, createBuf)) + + // FILE_CREATE disposition should bypass path approvers + r := buildMatchingFileOpEnd(t, createBuf, windows.FILE_CREATE) + rec, approved := f.Approve(r) + if !approved { + t.Error("non-OPEN disposition should be approved without path check") + } + if rec.ExtendedData == nil { + t.Error("extended data should be attached") + } +} + +func TestFSApproverFileOpEndPathApproverApproved(t *testing.T) { + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Paths: map[string][]string{ + "ICONTAINS": {`Windows\AppCompat`}, + }, + }, + } + f := newTestFSApprover(rules) + f.Approve(createFileRecord(t, createBuf)) + + r := buildMatchingFileOpEnd(t, createBuf, windows.FILE_OPEN) + rec, approved := f.Approve(r) + if !approved { + t.Error("path matching approver should approve the event") + } + if rec.ExtendedData == nil { + t.Error("extended data should be attached") + } + if len(f.irps) != 1 { + t.Error("IRP should still be in map until cleanup is called") + } +} + +func TestFSApproverFileOpEndPathApproverRejected(t *testing.T) { + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Paths: map[string][]string{ + "ICONTAINS": {`Windows\System32`}, + }, + }, + } + f := newTestFSApprover(rules) + + f.Approve(createFileRecord(t, createBuf)) + + r := buildMatchingFileOpEnd(t, createBuf, windows.FILE_OPEN) + _, approved := f.Approve(r) + if approved { + t.Error("non-matching path should reject the event") + } + if len(f.irps) != 0 { + t.Error("rejected IRP should be deleted from map immediately") + } +} + +func TestFSApproverCleanup(t *testing.T) { + f := newTestFSApprover(nil) + cr := createFileRecord(t, createBuf) + f.Approve(cr) + + if len(f.irps) != 1 { + t.Fatalf("expected 1 IRP before cleanup") + } + + // simulate what consumer does: cleanup with the stored CreateFile record + irp := f.irps[*(*uint64)(unsafe.Pointer(&createBuf[0]))] + f.cleanup(irp.rec) + + if len(f.irps) != 0 { + t.Error("IRP should be removed after cleanup") + } +} + +func TestFSApproverCleanupNonFileEventIsNoop(t *testing.T) { + f := newTestFSApprover(nil) + f.Approve(createFileRecord(t, createBuf)) + + r := &etw.EventRecord{} + r.Header.ProviderID = event.ProcessEventGUID + f.cleanup(r) // should not panic or delete anything + + if len(f.irps) != 1 { + t.Error("cleanup of non-file event should be a no-op") + } +} + +func TestFSApproverMultipleIRPs(t *testing.T) { + f := newTestFSApprover(nil) + + // build two different CreateFile buffers with different IRPs + buf1 := make([]byte, len(createBuf)) + copy(buf1, createBuf) + buf2 := make([]byte, len(createBuf)) + copy(buf2, createBuf) + // modify IRP in buf2 + *(*uint64)(unsafe.Pointer(&buf2[0])) = 0xDEADBEEF + + f.Approve(createFileRecord(t, buf1)) + f.Approve(createFileRecord(t, buf2)) + + if len(f.irps) != 2 { + t.Fatalf("expected 2 IRPs in map, got %d", len(f.irps)) + } + + // complete first IRP + r1 := buildMatchingFileOpEnd(t, buf1, windows.FILE_OPEN) + _, approved := f.Approve(r1) + if !approved { + t.Error("first FileOpEnd should be approved") + } + if len(f.irps) != 2 { + t.Error("second IRP should still be in map") + } + + // complete second IRP + r2 := buildMatchingFileOpEnd(t, buf2, windows.FILE_OPEN) + _, approved = f.Approve(r2) + if !approved { + t.Error("second FileOpEnd should be approved") + } +} diff --git a/internal/etw/approvers/process.go b/internal/etw/approvers/process.go new file mode 100644 index 000000000..7154645d1 --- /dev/null +++ b/internal/etw/approvers/process.go @@ -0,0 +1,85 @@ +/* + * Copyright 2020-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 approvers + +import ( + "expvar" + + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/sys/etw" +) + +var ( + procApproverApprovals = expvar.NewInt("approver.proc.approvals") + procApproverRejections = expvar.NewInt("approver.proc.rejections") +) + +// proc approver is responsible for filtering out process +// and thread access events. +type proc struct { + approver + psnap ps.Snapshotter +} + +func newProcApprover(psnap ps.Snapshotter, r *config.RulesCompileResult) Approver { + return &proc{ + approver: approver{ + r: r, + }, + psnap: psnap, + } +} + +func (p *proc) Approve(r *etw.EventRecord) (*etw.EventRecord, bool) { + if r.Header.ProviderID != event.AuditAPIEventGUID { + return r, true + } + + id := r.Header.EventDescriptor.ID + if id != event.OpenProcessID && id != event.OpenThreadID { + return r, true + } + + // allow remote thread opens + pid := r.ReadUint32(0) + if id == event.OpenThreadID && r.Header.ProcessID != pid { + return r, true + } + + // attempt to find the target process in + // the snapshotter state and passs the + // executable path to the approver. If + // we can find the process in snapshot + // or the executable path is not resolve, + // the event is approved + ok, ps := p.psnap.Find(pid) + if !ok || ps == nil || ps.Exe == "" { + return r, true + } + if p.approveExecutable(ps.Exe) { + procApproverApprovals.Add(1) + return r, true + } + + procApproverRejections.Add(1) + + return r, false +} diff --git a/internal/etw/approvers/process_test.go b/internal/etw/approvers/process_test.go new file mode 100644 index 000000000..fc33155ee --- /dev/null +++ b/internal/etw/approvers/process_test.go @@ -0,0 +1,248 @@ +/* + * Copyright 2020-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 approvers + +import ( + "testing" + "unsafe" + + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/ps" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/rabbitstack/fibratus/pkg/sys/etw" + "github.com/stretchr/testify/mock" + "golang.org/x/sys/windows" +) + +func procRecord(opcode uint8, eventID uint16, pid uint32, providerID windows.GUID) *etw.EventRecord { + buf := make([]byte, 4) + *(*uint32)(unsafe.Pointer(&buf[0])) = pid + r := &etw.EventRecord{} + r.Header.ProviderID = providerID + r.Header.EventDescriptor.Opcode = opcode + r.Header.EventDescriptor.ID = eventID + r.Header.ProcessID = pid + r.BufferLen = uint16(len(buf)) + r.Buffer = uintptr(unsafe.Pointer(&buf[0])) + return r +} + +func openProcRecord(pid uint32) *etw.EventRecord { + r := procRecord(0, event.OpenProcessID, pid, event.AuditAPIEventGUID) + return r +} + +func openThreadRecord(callerPID, targetPID uint32) *etw.EventRecord { + buf := make([]byte, 4) + *(*uint32)(unsafe.Pointer(&buf[0])) = targetPID + r := &etw.EventRecord{} + r.Header.ProviderID = event.AuditAPIEventGUID + r.Header.EventDescriptor.ID = event.OpenThreadID + r.Header.ProcessID = callerPID + r.BufferLen = uint16(len(buf)) + r.Buffer = uintptr(unsafe.Pointer(&buf[0])) + return r +} + +func newTestProcApprover(psnap *ps.SnapshotterMock, r *config.RulesCompileResult) *proc { + return newProcApprover(psnap, r).(*proc) +} + +func TestProcApproverNonAuditAPIEvent(t *testing.T) { + psnap := &ps.SnapshotterMock{} + a := newTestProcApprover(psnap, nil) + + r := &etw.EventRecord{} + r.Header.ProviderID = event.FileEventGUID + rec, approved := a.Approve(r) + if !approved { + t.Error("non-AuditAPI event should be approved") + } + if rec != r { + t.Error("should return original record") + } + psnap.AssertNotCalled(t, "Find", mock.Anything) +} + +func TestProcApproverNonOpenProcessOrThreadID(t *testing.T) { + psnap := &ps.SnapshotterMock{} + a := newTestProcApprover(psnap, nil) + + r := &etw.EventRecord{} + r.Header.ProviderID = event.AuditAPIEventGUID + r.Header.EventDescriptor.ID = 9999 // some other event ID + rec, approved := a.Approve(r) + if !approved { + t.Error("non-OpenProcess/Thread event should be approved") + } + if rec != r { + t.Error("should return original record") + } + psnap.AssertNotCalled(t, "Find", mock.Anything) +} + +func TestProcApproverOpenRemoteThreadAlwaysApproved(t *testing.T) { + psnap := &ps.SnapshotterMock{} + a := newTestProcApprover(psnap, nil) + + // caller PID != target PID = remote thread open, always allow + r := openThreadRecord(1234, 5678) + _, approved := a.Approve(r) + if !approved { + t.Error("remote thread open should always be approved") + } + psnap.AssertNotCalled(t, "Find", mock.Anything) +} + +func TestProcApproverOpenProcessProcessNotInSnapshot(t *testing.T) { + psnap := &ps.SnapshotterMock{} + a := newTestProcApprover(psnap, nil) + + const pid = uint32(1234) + psnap.On("Find", pid).Return(false, (*pstypes.PS)(nil)) + + r := openProcRecord(pid) + _, approved := a.Approve(r) + if !approved { + t.Error("process not in snapshot should be approved") + } + psnap.AssertCalled(t, "Find", pid) +} + +func TestProcApproverOpenProcessNilPS(t *testing.T) { + psnap := &ps.SnapshotterMock{} + a := newTestProcApprover(psnap, nil) + + const pid = uint32(1234) + psnap.On("Find", pid).Return(true, (*pstypes.PS)(nil)) + + r := openProcRecord(pid) + _, approved := a.Approve(r) + if !approved { + t.Error("nil PS entry should be approved") + } +} + +func TestProcApproverOpenProcessExeApproved(t *testing.T) { + psnap := &ps.SnapshotterMock{} + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Executables: map[string][]string{ + "IMATCHES": {`C:\Windows\System32\*`}, + }, + }, + } + a := newTestProcApprover(psnap, rules) + + const pid = uint32(1234) + psnap.On("Find", pid).Return(true, &pstypes.PS{Exe: `C:\Windows\System32\svchost.exe`}) + + r := openProcRecord(pid) + _, approved := a.Approve(r) + if !approved { + t.Error("matching executable should be approved") + } +} + +func TestProcApproverOpenProcessExeRejected(t *testing.T) { + psnap := &ps.SnapshotterMock{} + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Executables: map[string][]string{ + "IMATCHES": {`C:\Windows\System32\*`}, + }, + }, + } + a := newTestProcApprover(psnap, rules) + + const pid = uint32(5678) + psnap.On("Find", pid).Return(true, &pstypes.PS{Exe: `C:\Users\Administrator\cmd.exe`}) + + r := openProcRecord(pid) + _, approved := a.Approve(r) + if approved { + t.Error("non-matching executable should be rejected") + } +} + +func TestProcApproverOpenProcessEmptyExeApproved(t *testing.T) { + psnap := &ps.SnapshotterMock{} + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Executables: map[string][]string{ + "IMATCHES": {`C:\Windows\*`}, + }, + }, + } + a := newTestProcApprover(psnap, rules) + + const pid = uint32(9999) + psnap.On("Find", pid).Return(true, &pstypes.PS{Exe: ""}) + + r := openProcRecord(pid) + _, approved := a.Approve(r) + if !approved { + t.Error("empty exe should be automatically approved") + } +} + +func TestProcApproverOpenThreadSameProcessExeApproved(t *testing.T) { + psnap := &ps.SnapshotterMock{} + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Executables: map[string][]string{ + "IMATCHES": {`C:\Windows\*`}, + }, + }, + } + + const pid = uint32(888) + a := newTestProcApprover(psnap, rules) + r := openThreadRecord(pid, pid) + + psnap.On("Find", pid).Return(true, &pstypes.PS{Exe: `C:\Windows\System32\lsass.exe`}) + + _, approved := a.Approve(r) + if !approved { + t.Error("self thread open with matching exe should be approved") + } +} + +func TestProcApproverOpenThreadSameProcessExeRejected(t *testing.T) { + psnap := &ps.SnapshotterMock{} + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Executables: map[string][]string{ + "IMATCHES": {`C:\Windows\*`}, + }, + }, + } + const pid = uint32(777) + + a := newTestProcApprover(psnap, rules) + r := openThreadRecord(pid, pid) + + psnap.On("Find", pid).Return(true, &pstypes.PS{Exe: `C:\suspicious\inject.exe`}) + + _, approved := a.Approve(r) + if approved { + t.Error("self thread open with non-matching exe should be rejected") + } +} diff --git a/internal/etw/approvers/registry.go b/internal/etw/approvers/registry.go new file mode 100644 index 000000000..ade474d52 --- /dev/null +++ b/internal/etw/approvers/registry.go @@ -0,0 +1,102 @@ +/* + * Copyright 2020-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 approvers + +import ( + "expvar" + + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/sys/etw" + "github.com/rabbitstack/fibratus/pkg/util/key" +) + +var ( + registryApproverApprovals = expvar.NewInt("approver.registry.approvals") + registryApproverRejections = expvar.NewInt("approver.registry.rejections") +) + +// registry approver accepts or discards key access events +// as soon as they are offloaded from the session buffer. +type registry struct { + approver + kcbs map[uint64]string +} + +func newRegistryApprover(r *config.RulesCompileResult) Approver { + return ®istry{ + approver: approver{ + r: r, + }, + kcbs: make(map[uint64]string), + } +} + +func (r *registry) Approve(rec *etw.EventRecord) (*etw.EventRecord, bool) { + if rec.Header.ProviderID != event.RegistryEventGUID { + return rec, true + } + + id := rec.Header.EventDescriptor.Opcode + + // keep the state of allocated key control blocks + // to be able to derive the full registry path + if id == event.RegKCBRundownID || id == event.RegCreateKCBID { + r.kcbs[rec.ReadUint64(16)] = rec.ConsumeUTF16String(24) + return rec, true + } + if id == event.RegDeleteKCBID { + delete(r.kcbs, rec.ReadUint64(16)) + return rec, true + } + + // accept all but key access events + if id != event.RegOpenKeyID { + return rec, true + } + + // lookup KCB map to check if the event + // KCB object address references a key + // we can use to reconstruct the full + // registry path + kcb := rec.ReadUint64(16) + path := rec.ConsumeUTF16String(24) + if kcb != 0 { + path = key.ConcatPaths(r.kcbs[kcb], path) + } + + rootkey, subkey := key.Format(path) + if rootkey != key.Invalid { + root := rootkey.String() + if subkey != "" { + path = key.ConcatPaths(root, subkey) + } else { + path = root + } + } + + if r.approveKey(path) { + registryApproverApprovals.Add(1) + return rec, true + } + + registryApproverRejections.Add(1) + + return rec, false +} diff --git a/internal/etw/approvers/registry_test.go b/internal/etw/approvers/registry_test.go new file mode 100644 index 000000000..3be9c3501 --- /dev/null +++ b/internal/etw/approvers/registry_test.go @@ -0,0 +1,288 @@ +/* + * Copyright 2020-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 approvers + +import ( + "testing" + "unsafe" + + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/sys/etw" +) + +var ( + regOpenKeyBuf = []byte{ + 248, 104, 16, 11, 5, 0, 0, 0, // Status + 0, 0, 0, 0, 0, 0, 0, 0, // Index + 144, 249, 47, 116, 139, 181, 255, 255, // KCB (offset 16) + 83, 0, 111, 0, 102, 0, 116, 0, // S o f t + 119, 0, 97, 0, 114, 0, 101, 0, // w a r e + 92, 0, + 77, 0, 105, 0, 99, 0, 114, 0, 111, 0, 115, 0, + 111, 0, 102, 0, 116, 0, + 92, 0, + 87, 0, 105, 0, 110, 0, 100, 0, 111, 0, 119, 0, + 115, 0, + 92, 0, + 67, 0, 117, 0, 114, 0, 114, 0, 101, 0, 110, 0, + 116, 0, 86, 0, 101, 0, 114, 0, 115, 0, 105, 0, + 111, 0, 110, 0, + 92, 0, + 67, 0, 97, 0, 112, 0, 97, 0, 98, 0, 105, 0, + 108, 0, 105, 0, 116, 0, 121, 0, + 65, 0, 99, 0, 99, 0, 101, 0, 115, 0, 115, 0, + 77, 0, 97, 0, 110, 0, 97, 0, 103, 0, 101, 0, + 114, 0, + 92, 0, + 67, 0, 97, 0, 112, 0, 97, 0, 98, 0, 105, 0, + 108, 0, 105, 0, 116, 0, 105, 0, 101, 0, 115, 0, + 0, 0, + } + regCreateKCBBuf = []byte{ + 248, 104, 16, 11, 5, 0, 0, 0, // Status + 0, 0, 0, 0, 0, 0, 0, 0, // Index + 144, 249, 47, 116, 139, 181, 255, 255, // KCB (offset 16) + // \REGISTRY\MACHINE (UTF-16LE) + 92, 0, // '\' + 82, 0, 69, 0, 71, 0, 73, 0, // R E G I + 83, 0, 84, 0, 82, 0, 89, 0, // S T R Y + 92, 0, // '\' + 77, 0, 65, 0, 67, 0, 72, 0, // M A C H + 73, 0, 78, 0, 69, 0, // I N E + 0, 0, // null terminator + } +) + +func registryRecord(t *testing.T, opcode uint8, buf []byte) *etw.EventRecord { + t.Helper() + b := make([]byte, len(buf)) + copy(b, buf) + r := &etw.EventRecord{} + r.Header.ProviderID = event.RegistryEventGUID + r.Header.EventDescriptor.Opcode = opcode + r.BufferLen = uint16(len(b)) + r.Buffer = uintptr(unsafe.Pointer(&b[0])) + t.Cleanup(func() { _ = b }) + return r +} + +// buildKCBRecord builds a RegCreateKCB/RegKCBRundown record with the given KCB key and path. +func buildKCBRecord(t *testing.T, opcode uint8, kcb uint64, path string) *etw.EventRecord { + t.Helper() + // layout: 8 bytes status + 8 bytes index + 8 bytes KCB + UTF-16 path + utf16Path := utf16Encode(path) + bufLen := 24 + len(utf16Path) + buf := make([]byte, bufLen) + *(*uint64)(unsafe.Pointer(&buf[16])) = kcb + copy(buf[24:], utf16Path) + r := &etw.EventRecord{} + r.Header.ProviderID = event.RegistryEventGUID + r.Header.EventDescriptor.Opcode = opcode + r.BufferLen = uint16(bufLen) + r.Buffer = uintptr(unsafe.Pointer(&buf[0])) + t.Cleanup(func() { _ = buf }) + return r +} + +// buildRegOpenKeyRecord builds a RegOpenKey record with given KCB and path. +func buildRegOpenKeyRecord(t *testing.T, kcb uint64, path string) *etw.EventRecord { + t.Helper() + utf16Path := utf16Encode(path) + bufLen := 24 + len(utf16Path) + buf := make([]byte, bufLen) + *(*uint64)(unsafe.Pointer(&buf[16])) = kcb + copy(buf[24:], utf16Path) + r := &etw.EventRecord{} + r.Header.ProviderID = event.RegistryEventGUID + r.Header.EventDescriptor.Opcode = event.RegOpenKeyID + r.BufferLen = uint16(bufLen) + r.Buffer = uintptr(unsafe.Pointer(&buf[0])) + t.Cleanup(func() { _ = buf }) + return r +} + +// utf16Encode encodes a string as UTF-16LE with null terminator +func utf16Encode(s string) []byte { + buf := make([]byte, (len(s)+1)*2) + for i, c := range s { + buf[i*2] = byte(c) + buf[i*2+1] = byte(c >> 8) + } + return buf +} + +func newTestRegistryApprover(r *config.RulesCompileResult) *registry { + return newRegistryApprover(r).(*registry) +} + +func TestRegistryApproverNonRegistryEvent(t *testing.T) { + a := newTestRegistryApprover(nil) + r := &etw.EventRecord{} + r.Header.ProviderID = event.FileEventGUID + rec, approved := a.Approve(r) + if !approved { + t.Error("non-registry event should be approved") + } + if rec != r { + t.Error("non-registry event should return original record") + } +} + +func TestRegistryApproverRegCreateKCBStoresPath(t *testing.T) { + a := newTestRegistryApprover(nil) + const kcb = uint64(0xffb58174f990) + r := buildKCBRecord(t, event.RegCreateKCBID, kcb, `HKEY_LOCAL_MACHINE\SYSTEM`) + rec, approved := a.Approve(r) + if !approved { + t.Error("RegCreateKCB should be approved") + } + if rec != r { + t.Error("should return original record") + } + if got := a.kcbs[kcb]; got != `HKEY_LOCAL_MACHINE\SYSTEM` { + t.Errorf("KCB path not stored correctly, got %q", got) + } +} + +func TestRegistryApproverRegKCBRundownStoresPath(t *testing.T) { + a := newTestRegistryApprover(nil) + const kcb = uint64(0xffb58174f990) + r := buildKCBRecord(t, event.RegKCBRundownID, kcb, `HKEY_CURRENT_USER\Software`) + rec, approved := a.Approve(r) + if !approved { + t.Error("RegKCBRundown should be approved") + } + if rec != r { + t.Error("should return original record") + } + if got := a.kcbs[kcb]; got != `HKEY_CURRENT_USER\Software` { + t.Errorf("KCB path not stored correctly, got %q", got) + } +} + +func TestRegistryApproverRegDeleteKCBRemovesEntry(t *testing.T) { + a := newTestRegistryApprover(nil) + const kcb = uint64(0xffb58174f990) + // first store it + a.kcbs[kcb] = `HKEY_LOCAL_MACHINE\SYSTEM` + r := buildKCBRecord(t, event.RegDeleteKCBID, kcb, "") + rec, approved := a.Approve(r) + if !approved { + t.Error("RegDeleteKCB should be approved") + } + if rec != r { + t.Error("should return original record") + } + if _, ok := a.kcbs[kcb]; ok { + t.Error("KCB entry should be removed after RegDeleteKCB") + } +} + +func TestRegistryApproverNonOpenKeyOpcodeApproved(t *testing.T) { + a := newTestRegistryApprover(nil) + r := registryRecord(t, event.RegSetValueID, regOpenKeyBuf) + _, approved := a.Approve(r) + if !approved { + t.Error("non-RegOpenKey registry event should be approved unconditionally") + } +} + +func TestRegistryApproverRegOpenKeyApproved(t *testing.T) { + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Keys: map[string][]string{ + "IMATCHES": {`HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\*`}, + }, + }, + } + + a := newTestRegistryApprover(rules) + r := registryRecord(t, event.RegOpenKeyID, regOpenKeyBuf) + + const kcb = uint64(18446662209287223696) + a.kcbs[kcb] = `HKEY_LOCAL_MACHINE` + + _, approved := a.Approve(r) + if !approved { + t.Error("key matching approver should approve the event") + } +} + +func TestRegistryApproverRegOpenKeyRejected(t *testing.T) { + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Keys: map[string][]string{ + "IMATCHES": {`HKEY_LOCAL_MACHINE\SYSTEM\*`}, + }, + }, + } + + a := newTestRegistryApprover(rules) + r := registryRecord(t, event.RegOpenKeyID, regOpenKeyBuf) + + const kcb = uint64(18446662209287223696) + a.kcbs[kcb] = `HKEY_LOCAL_MACHINE` + + _, approved := a.Approve(r) + if approved { + t.Error("non-matching key should reject the event") + } +} + +func TestRegistryApproverRegOpenKeyKCBPathPrepended(t *testing.T) { + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Keys: map[string][]string{ + "IMATCHES": {`HKEY_LOCAL_MACHINE\SYSTEM\*`}, + }, + }, + } + + a := newTestRegistryApprover(rules) + const kcb = uint64(0xffb58174f990) + // store KCB with a root path + a.kcbs[kcb] = `\REGISTRY\MACHINE` + + // build RegOpenKey with the same KCB and a relative subkey + r := buildRegOpenKeyRecord(t, kcb, `SYSTEM\CurrentControlSet`) + + _, approved := a.Approve(r) + if !approved { + t.Error("prepended KCB path should be approved") + } +} + +func TestRegistryApproverRegOpenKeyZeroKCBUsesPathDirectly(t *testing.T) { + rules := &config.RulesCompileResult{ + Approvers: config.Approvers{ + Keys: map[string][]string{ + "IMATCHES": {`HKEY_LOCAL_MACHINE\SOFTWARE\*`}, + }, + }, + } + + a := newTestRegistryApprover(rules) + r := buildRegOpenKeyRecord(t, 0, `HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.AAC`) + + _, approved := a.Approve(r) + if !approved { + t.Error("direct registry path from event should be directly passed to approver") + } +} diff --git a/internal/etw/consumer.go b/internal/etw/consumer.go index a54c5269d..f2f599d54 100644 --- a/internal/etw/consumer.go +++ b/internal/etw/consumer.go @@ -19,6 +19,7 @@ package etw import ( + "github.com/rabbitstack/fibratus/internal/etw/approvers" "github.com/rabbitstack/fibratus/internal/etw/processors" "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/event" @@ -36,7 +37,8 @@ import ( type Consumer struct { q *event.Queue sequencer *event.Sequencer - processors processors.Chain + processors *processors.Chain + approvers approvers.Approvers psnap ps.Snapshotter config *config.Config filter filter.Filter @@ -49,7 +51,8 @@ func NewConsumer( config *config.Config, sequencer *event.Sequencer, evts chan *event.Event, - processors processors.Chain, + processors *processors.Chain, + r *config.RulesCompileResult, ) *Consumer { return &Consumer{ q: event.NewQueueWithChannel(evts, config.EventSource.StackEnrichment, config.ForwardMode || config.IsCaptureSet()), @@ -57,6 +60,7 @@ func NewConsumer( processors: processors, psnap: psnap, config: config, + approvers: approvers.New(psnap, r, processors), } } @@ -69,25 +73,32 @@ func (c *Consumer) Close() error { return c.processors.Close() } -func (c *Consumer) ProcessEvent(ev *etw.EventRecord) error { +func (c *Consumer) ProcessEvent(r *etw.EventRecord) error { if c.isClosing { return nil } - if !c.config.EventSource.EventExists(ev.ID()) { + if !c.config.EventSource.EventExists(r.ID()) { eventsUnknown.Add(1) return nil } - if event.IsCurrentProcDropped(ev.Header.ProcessID) && ev.Header.ProviderID != etw.WindowsKernelProcessGUID { + if event.IsCurrentProcDropped(r.Header.ProcessID) && r.Header.ProviderID != etw.WindowsKernelProcessGUID { return nil } - if c.config.EventSource.ExcludeEvent(ev.ID()) { + + rec, approved := c.approvers.Approve(r) + if !approved { + return nil + } + defer c.approvers.Cleanup(rec) + + if c.config.EventSource.ExcludeEvent(rec.ID()) { eventsExcluded.Add(1) return nil } eventsProcessed.Add(1) - evt := event.New(c.sequencer.Get(), ev) + evt := event.New(c.sequencer.Get(), rec) // Dispatch each event to the processor chain. // Processors may further augment the event with @@ -100,9 +111,6 @@ func (c *Consumer) ProcessEvent(ev *etw.EventRecord) error { if err != nil { return err } - if evt.WaitEnqueue { - return nil - } ok, proc := c.psnap.Find(evt.PID) if !ok { c.psnap.Put(proc) diff --git a/internal/etw/processors/chain.go b/internal/etw/processors/chain.go index 81012a919..04c872e78 100644 --- a/internal/etw/processors/chain.go +++ b/internal/etw/processors/chain.go @@ -21,6 +21,7 @@ package processors import ( "expvar" "fmt" + "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/util/multierror" ) @@ -28,24 +29,14 @@ import ( // processorFailures counts the number of failures caused by event processors var processorFailures = expvar.NewInt("event.processor.failures") -// Chain defines the event process chain has to satisfy. -type Chain interface { - // ProcessEvent pushes the event into processor chain. Processors are applied sequentially, so we have to make - // sure that any processor providing additional context to the next processor is defined first in the chain. If - // one processor fails, the next processor in chain is invoked. - ProcessEvent(evt *event.Event) (*event.Event, error) - // Close closes the processor chain and frees all allocated resources. - Close() error -} - -func (c *chain) addProcessor(processor Processor) { +func (c *Chain) addProcessor(processor Processor) { if processor == nil { return } c.processors = append(c.processors, processor) } -func (c chain) ProcessEvent(e *event.Event) (*event.Event, error) { +func (c *Chain) ProcessEvent(e *event.Event) (*event.Event, error) { var errs = make([]error, 0) var evt *event.Event @@ -69,8 +60,14 @@ func (c chain) ProcessEvent(e *event.Event) (*event.Event, error) { return evt, nil } +func (c *Chain) DequeueStackwalk(stackID uint64) { + if c.fsProcessor != nil { + c.fsProcessor.(*fsProcessor).dequeueStackwalk(stackID) + } +} + // Close closes the processor chain and frees all allocated resources. -func (c chain) Close() error { +func (c *Chain) Close() error { for _, processor := range c.processors { processor.Close() } diff --git a/internal/etw/processors/chain_windows.go b/internal/etw/processors/chain_windows.go index f7db20e34..ea6514f95 100644 --- a/internal/etw/processors/chain_windows.go +++ b/internal/etw/processors/chain_windows.go @@ -20,37 +20,37 @@ package processors import ( "github.com/rabbitstack/fibratus/pkg/config" - "github.com/rabbitstack/fibratus/pkg/fs" "github.com/rabbitstack/fibratus/pkg/handle" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/va" ) -type chain struct { +type Chain struct { processors []Processor psnapshotter ps.Snapshotter + fsProcessor Processor } // NewChain constructs the processor chain. It arranges all the processors -// according to enabled kernel event categories. +// according to enabled event categories. func NewChain( psnap ps.Snapshotter, hsnap handle.Snapshotter, config *config.Config, -) Chain { +) *Chain { var ( - chain = &chain{ + chain = &Chain{ psnapshotter: psnap, processors: make([]Processor, 0), } - devMapper = fs.NewDevMapper() vaRegionProber = va.NewRegionProber() ) chain.addProcessor(newPsProcessor(psnap, vaRegionProber)) if config.EventSource.EnableFileIOEvents { - chain.addProcessor(newFsProcessor(hsnap, psnap, devMapper, config)) + chain.fsProcessor = newFsProcessor(hsnap, psnap, config) + chain.addProcessor(chain.fsProcessor) } if config.EventSource.EnableRegistryEvents { chain.addProcessor(newRegistryProcessor(hsnap)) @@ -62,7 +62,7 @@ func NewChain( chain.addProcessor(newNetProcessor()) } if config.EventSource.EnableHandleEvents { - chain.addProcessor(newHandleProcessor(hsnap, psnap, devMapper)) + chain.addProcessor(newHandleProcessor(hsnap, psnap)) } if config.EventSource.EnableMemEvents { chain.addProcessor(newMemProcessor(psnap, vaRegionProber)) diff --git a/internal/etw/processors/fs_windows.go b/internal/etw/processors/fs_windows.go index 5ad2f16db..20dbebbb2 100644 --- a/internal/etw/processors/fs_windows.go +++ b/internal/etw/processors/fs_windows.go @@ -52,11 +52,7 @@ type fsProcessor struct { hsnap handle.Snapshotter psnap ps.Snapshotter - // irps contains a mapping between the IRP (I/O request packet) and CreateFile events - irps map[uint64]*event.Event - - devMapper fs.DevMapper - config *config.Config + config *config.Config // buckets stores stack walk events per stack id buckets map[uint64][]*event.Event @@ -75,19 +71,16 @@ type FileInfo struct { func newFsProcessor( hsnap handle.Snapshotter, psnap ps.Snapshotter, - devMapper fs.DevMapper, config *config.Config, ) Processor { f := &fsProcessor{ - files: make(map[uint64]*FileInfo), - irps: make(map[uint64]*event.Event), - hsnap: hsnap, - psnap: psnap, - devMapper: devMapper, - config: config, - buckets: make(map[uint64][]*event.Event), - purger: time.NewTicker(time.Second * 5), - quit: make(chan struct{}, 1), + files: make(map[uint64]*FileInfo), + hsnap: hsnap, + psnap: psnap, + config: config, + buckets: make(map[uint64][]*event.Event), + purger: time.NewTicker(time.Second * 5), + quit: make(chan struct{}, 1), } go f.purge() @@ -145,13 +138,6 @@ func (f *fsProcessor) processEvent(e *event.Event) (*event.Event, error) { } return e, f.psnap.AddMmap(e) - case event.CreateFile: - // we defer the processing of the CreateFile event until we get - // the matching FileOpEnd event. This event contains the operation - // that was done on behalf of the file, e.g. create or open. - irp := e.Params.MustGetUint64(params.FileIrpPtr) - e.WaitEnqueue = true - f.irps[irp] = e case event.StackWalk: if !event.IsCurrentProcDropped(e.PID) { f.mu.Lock() @@ -166,45 +152,23 @@ func (f *fsProcessor) processEvent(e *event.Event) (*event.Event, error) { f.buckets[id] = append(q, e) } } - case event.FileOpEnd: - // get the CreateFile pending event by IRP identifier - // and fetch the file create disposition value - var ( - irp = e.Params.MustGetUint64(params.FileIrpPtr) - dispo = e.Params.MustGetUint64(params.FileExtraInfo) - status = e.Params.MustGetUint32(params.NTStatus) - ) - - if dispo > windows.FILE_MAXIMUM_DISPOSITION { - return e, nil - } - ev, ok := f.irps[irp] - if !ok { - return e, nil - } - delete(f.irps, irp) - - // reset the wait status to allow passage of this event to - // the aggregator queue. Additionally, append params to it - ev.WaitEnqueue = false - fileObject := ev.Params.MustGetUint64(params.FileObject) + case event.CreateFile: + fileObject := e.Params.MustGetUint64(params.FileObject) // try to get extended file info. If the file object is already // present in the map, we'll reuse the existing file information fileinfo, ok := f.files[fileObject] if !ok { - opts := ev.Params.MustGetUint32(params.FileCreateOptions) + opts := e.Params.MustGetUint32(params.FileCreateOptions) opts &= 0xFFFFFF - filepath := ev.GetParamAsString(params.FilePath) + filepath := e.GetParamAsString(params.FilePath) fileinfo = f.getFileInfo(filepath, opts) f.files[fileObject] = fileinfo } - ev.AppendParam(params.NTStatus, params.Status, status) if fileinfo.Type != fs.Unknown { - ev.AppendEnum(params.FileType, uint32(fileinfo.Type), fs.FileTypes) + e.AppendEnum(params.FileType, uint32(fileinfo.Type), fs.FileTypes) } - ev.AppendEnum(params.FileOperation, uint32(dispo), fs.FileCreateDispositions) // attach stack walk return addresses. CreateFile events // represent an edge case in callstack enrichment. Since @@ -220,17 +184,17 @@ func (f *fsProcessor) processEvent(e *event.Event) (*event.Event, error) { f.mu.Lock() defer f.mu.Unlock() - id := ev.StackID() + id := e.StackID() q, ok := f.buckets[id] if ok && len(q) > 0 { var s *event.Event s, f.buckets[id] = q[len(q)-1], q[:len(q)-1] callstack := s.Params.MustGetSlice(params.Callstack) - ev.AppendParam(params.Callstack, params.Slice, callstack) + e.AppendParam(params.Callstack, params.Slice, callstack) } } - return ev, nil + return e, nil case event.ReleaseFile: fileReleaseCount.Add(1) // delete file metadata by file object address @@ -303,6 +267,16 @@ func (f *fsProcessor) processEvent(e *event.Event) (*event.Event, error) { return e, nil } +func (f *fsProcessor) dequeueStackwalk(stackID uint64) { + f.mu.Lock() + defer f.mu.Unlock() + + q, ok := f.buckets[stackID] + if ok && len(q) > 0 { + f.buckets[stackID] = q[:len(q)-1] + } +} + func (f *fsProcessor) findFile(fileKey, fileObject uint64) *FileInfo { fileinfo, ok := f.files[fileKey] if ok { @@ -331,7 +305,7 @@ func (f *fsProcessor) getMappedFile(pid uint32, addr uint64) string { return "" } defer windows.Close(process) - return f.devMapper.Convert(sys.GetMappedFile(process, uintptr(addr))) + return fs.GetDevMapper().Convert(sys.GetMappedFile(process, uintptr(addr))) } func (f *fsProcessor) purge() { diff --git a/internal/etw/processors/fs_windows_test.go b/internal/etw/processors/fs_windows_test.go index 7efe7e548..70b841ccd 100644 --- a/internal/etw/processors/fs_windows_test.go +++ b/internal/etw/processors/fs_windows_test.go @@ -19,8 +19,6 @@ package processors import ( - "os" - "reflect" "testing" "github.com/rabbitstack/fibratus/pkg/config" @@ -38,9 +36,6 @@ import ( ) func TestFsProcessor(t *testing.T) { - exe, err := os.Executable() - require.NoError(t, err) - var tests = []struct { name string e *event.Event @@ -101,74 +96,6 @@ func TestFsProcessor(t *testing.T) { psnap.AssertNumberOfCalls(t, "AddMmap", 1) }, }, - { - "wait enqueue for create file events", - &event.Event{ - Type: event.CreateFile, - Category: event.File, - Params: event.Params{ - params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(18446738026482168384)}, - params.ThreadID: {Name: params.ThreadID, Type: params.Uint32, Value: uint32(1484)}, - params.FileCreateOptions: {Name: params.FileCreateOptions, Type: params.Uint32, Value: uint32(1223456)}, - params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\kernel32.dll"}, - params.FileShareMask: {Name: params.FileShareMask, Type: params.Uint32, Value: uint32(5)}, - params.FileIrpPtr: {Name: params.FileIrpPtr, Type: params.Uint64, Value: uint64(1234543123112321)}, - }, - }, - nil, - func() *handle.SnapshotterMock { - hsnap := new(handle.SnapshotterMock) - return hsnap - }, - func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { - fsProcessor := p.(*fsProcessor) - assert.True(t, e.WaitEnqueue) - assert.Contains(t, fsProcessor.irps, uint64(1234543123112321)) - assert.True(t, reflect.DeepEqual(e, fsProcessor.irps[1234543123112321])) - }, - }, - { - "get IRP completion for create file event", - &event.Event{ - Type: event.FileOpEnd, - Category: event.File, - Params: event.Params{ - params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(18446738026482168384)}, - params.FileExtraInfo: {Name: params.FileExtraInfo, Type: params.Uint64, Value: uint64(2)}, - params.FileIrpPtr: {Name: params.FileIrpPtr, Type: params.Uint64, Value: uint64(1334543123112321)}, - params.NTStatus: {Name: params.NTStatus, Type: params.Status, Value: uint32(0)}, - }, - }, - func(p Processor) { - fsProcessor := p.(*fsProcessor) - fsProcessor.irps[1334543123112321] = &event.Event{ - Type: event.CreateFile, - Category: event.File, - Params: event.Params{ - params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12446738026482168384)}, - params.FileCreateOptions: {Name: params.FileCreateOptions, Type: params.Uint32, Value: uint32(18874368)}, - params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: exe}, - params.FileShareMask: {Name: params.FileShareMask, Type: params.Uint32, Value: uint32(5)}, - params.FileIrpPtr: {Name: params.FileIrpPtr, Type: params.Uint64, Value: uint64(1334543123112321)}, - }, - } - }, - func() *handle.SnapshotterMock { - hsnap := new(handle.SnapshotterMock) - return hsnap - }, - func(e *event.Event, t *testing.T, hsnap *handle.SnapshotterMock, p Processor) { - fsProcessor := p.(*fsProcessor) - assert.Equal(t, event.CreateFile, e.Type) - assert.NotContains(t, fsProcessor.irps, uint64(1334543123112321)) - assert.False(t, e.WaitEnqueue) - assert.Contains(t, fsProcessor.files, uint64(12446738026482168384)) - assert.Equal(t, exe, fsProcessor.files[12446738026482168384].Name) - assert.Equal(t, "Success", e.GetParamAsString(params.NTStatus)) - assert.Equal(t, "File", e.GetParamAsString(params.FileType)) - assert.Equal(t, "CREATE", e.GetParamAsString(params.FileOperation)) - }, - }, { "release file and remove file info", &event.Event{ @@ -306,7 +233,7 @@ func TestFsProcessor(t *testing.T) { {File: "C:\\Windows\\System32\\kernel32.dll", BaseAddress: va.Address(0xffff23433), Size: 3098}, }, }) - p := newFsProcessor(hsnap, psnap, fs.NewDevMapper(), &config.Config{}) + p := newFsProcessor(hsnap, psnap, &config.Config{}) if tt.setupProcessor != nil { tt.setupProcessor(p) } diff --git a/internal/etw/processors/handle_windows.go b/internal/etw/processors/handle_windows.go index ea01de038..0ce844897 100644 --- a/internal/etw/processors/handle_windows.go +++ b/internal/etw/processors/handle_windows.go @@ -28,20 +28,17 @@ import ( ) type handleProcessor struct { - hsnap handle.Snapshotter - psnap ps.Snapshotter - devMapper fs.DevMapper + hsnap handle.Snapshotter + psnap ps.Snapshotter } func newHandleProcessor( hsnap handle.Snapshotter, psnap ps.Snapshotter, - devMapper fs.DevMapper, ) Processor { return &handleProcessor{ - hsnap: hsnap, - psnap: psnap, - devMapper: devMapper, + hsnap: hsnap, + psnap: psnap, } } @@ -80,7 +77,7 @@ func (h *handleProcessor) processEvent(e *event.Event) (*event.Event, error) { name += "\\" + keyName } case handle.File: - name = h.devMapper.Convert(name) + name = fs.GetDevMapper().Convert(name) } // assign the formatted handle name if err := e.Params.SetValue(params.HandleObjectName, name); err != nil { diff --git a/internal/etw/processors/handle_windows_test.go b/internal/etw/processors/handle_windows_test.go index d7f09138c..62fa5e50a 100644 --- a/internal/etw/processors/handle_windows_test.go +++ b/internal/etw/processors/handle_windows_test.go @@ -23,7 +23,6 @@ import ( "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/event/params" - "github.com/rabbitstack/fibratus/pkg/fs" "github.com/rabbitstack/fibratus/pkg/handle" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/stretchr/testify/assert" @@ -93,7 +92,7 @@ func TestHandleProcessor(t *testing.T) { t.Run(tt.name, func(t *testing.T) { hsnap := tt.hsnap() psnap := new(ps.SnapshotterMock) - p := newHandleProcessor(hsnap, psnap, fs.NewDevMapper()) + p := newHandleProcessor(hsnap, psnap) var err error tt.e, _, err = p.ProcessEvent(tt.e) require.NoError(t, err) diff --git a/internal/etw/processors/registry_windows.go b/internal/etw/processors/registry_windows.go index 80567675b..d3979493f 100644 --- a/internal/etw/processors/registry_windows.go +++ b/internal/etw/processors/registry_windows.go @@ -139,7 +139,7 @@ func (r *registryProcessor) processEvent(e *event.Event) (*event.Event, error) { path := e.Params.MustGetString(params.RegPath) if kcb != 0 { if baseKey, ok := r.keys[kcb]; ok { - path = baseKey + "\\" + path + path = key.ConcatPaths(baseKey, path) } else { kcbMissCount.Add(1) path = r.findMatchingKey(e.PID, path) diff --git a/internal/etw/source.go b/internal/etw/source.go index 8512899bb..bdb050290 100644 --- a/internal/etw/source.go +++ b/internal/etw/source.go @@ -75,7 +75,7 @@ type EventSource struct { r *config.RulesCompileResult traces []*Trace consumers []*Consumer - processors processors.Chain + processors *processors.Chain errs chan error evts chan *event.Event @@ -253,6 +253,7 @@ func (e *EventSource) Open(config *config.Config) error { e.sequencer, e.evts, e.processors, + e.r, ) consumer.SetFilter(e.filter) diff --git a/internal/evasion/scanner.go b/internal/evasion/scanner.go index e1eb08ad6..26951f0fd 100644 --- a/internal/evasion/scanner.go +++ b/internal/evasion/scanner.go @@ -56,13 +56,6 @@ func NewScanner(config Config) *Scanner { } func (s *Scanner) ProcessEvent(e *event.Event) (bool, error) { - // filter out CreateFile events with the open disposition - // as they tend to be noisy and could impact performance - // when hitting evasion detectors - if e.IsOpenDisposition() { - return true, nil - } - var enq bool // run registered evasion detectors diff --git a/pkg/config/filters.go b/pkg/config/filters.go index 87ab6bb96..c704f319d 100644 --- a/pkg/config/filters.go +++ b/pkg/config/filters.go @@ -202,11 +202,59 @@ type RulesCompileResult struct { HasThreadpoolEvents bool UsedEvents []event.Type NumberRules int + Approvers Approvers } -func (r RulesCompileResult) ContainsEvent(Type event.Type) bool { - for _, ktyp := range r.UsedEvents { - if ktyp == Type { +type Approvers struct { + Keys map[string][]string + Paths map[string][]string + Extensions map[string][]string + Bases map[string][]string + Executables map[string][]string +} + +func (p *Approvers) AppendKey(op, path string) { + if slices.Contains(p.Keys[op], path) { + return + } + p.Keys[op] = append(p.Keys[op], path) +} + +func (p *Approvers) AppendPath(op, path string) { + if slices.Contains(p.Paths[op], path) { + return + } + p.Paths[op] = append(p.Paths[op], path) +} + +func (p *Approvers) AppendExtension(op, ext string) { + if slices.Contains(p.Extensions[op], ext) { + return + } + p.Extensions[op] = append(p.Extensions[op], ext) +} + +func (p *Approvers) AppendBase(op, base string) { + if slices.Contains(p.Bases[op], base) { + return + } + p.Bases[op] = append(p.Bases[op], base) +} + +func (p *Approvers) AppendExecutable(op, exe string) { + if slices.Contains(p.Executables[op], exe) { + return + } + p.Executables[op] = append(p.Executables[op], exe) +} + +func (p Approvers) String() string { + return fmt.Sprintf("Keys: %v, Paths: %v, Extensions: %v, Bases: %v, Executables: %v", p.Keys, p.Paths, p.Extensions, p.Bases, p.Executables) +} + +func (r RulesCompileResult) ContainsEvent(e event.Type) bool { + for _, typ := range r.UsedEvents { + if typ == e { return true } } @@ -237,7 +285,8 @@ func (r RulesCompileResult) String() string { HasAuditAPIEvents: %t HasDNSEvents: %t HasThreadpoolEvents: %t - Events: %s`, + Events: %s + Approvers: %s`, r.HasProcEvents, r.HasThreadEvents, r.HasModuleEvents, @@ -251,6 +300,7 @@ func (r RulesCompileResult) String() string { r.HasDNSEvents, r.HasThreadpoolEvents, strings.Join(events, ", "), + r.Approvers, ) } diff --git a/pkg/event/event.go b/pkg/event/event.go index 65f21f719..bd1fc85d5 100644 --- a/pkg/event/event.go +++ b/pkg/event/event.go @@ -84,11 +84,7 @@ type Event struct { Type Type `json:"-"` // CPU designates the processor logical core where the event was originated. CPU uint8 `json:"cpu"` - // WaitEnqueue indicates if this event should temporarily defer pushing to - // the consumer output queue. This is usually required in event processors - // to propagate certain events stored in processor's state when the related - // event arrives. - WaitEnqueue bool `json:"waitenqueue"` + _ uint8 // padding // Name is the human friendly name of the event. Name string `json:"name"` diff --git a/pkg/event/param_decoder_windows.go b/pkg/event/param_decoder_windows.go index 271325088..3e8ad801c 100644 --- a/pkg/event/param_decoder_windows.go +++ b/pkg/event/param_decoder_windows.go @@ -168,6 +168,11 @@ func (d *ParamDecoder) DecodeFile(r *etw.EventRecord, e *Event) { e.AppendParam(params.FileAttributes, params.Flags, r.ReadUint32(24), WithFlags(FileAttributeFlags)) e.AppendParam(params.FileShareMask, params.Flags, r.ReadUint32(28), WithFlags(FileShareModeFlags)) e.AppendParam(params.FilePath, params.DOSPath, r.ConsumeUTF16String(32)) + + // read create disposition/status from extended data items + disposition, status := r.ReadEventHeaderFileExtendedDataItems() + e.AppendParam(params.NTStatus, params.Status, status) + e.AppendEnum(params.FileOperation, disposition, fs.FileCreateDispositions) case FileOpEndID: // typedef struct _PERFINFO_FILE_OPERATION_END { // ULONG_PTR Irp; diff --git a/pkg/event/param_decoder_windows_test.go b/pkg/event/param_decoder_windows_test.go index 85b6f493d..09363a3cf 100644 --- a/pkg/event/param_decoder_windows_test.go +++ b/pkg/event/param_decoder_windows_test.go @@ -147,13 +147,15 @@ func TestDecodeFile(t *testing.T) { { name: "CreateFile", opcode: CreateFileID, assertions: func(t *testing.T, e *Event) { - assert.Len(t, e.Params, 7) + assert.Len(t, e.Params, 9) assert.Equal(t, uint64(0xffffd78d965e07c8), e.Params.MustGetUint64(params.FileIrpPtr)) assert.Equal(t, uint64(0xffffd78d920b6650), e.Params.MustGetUint64(params.FileObject)) assert.Equal(t, `\Device\HarddiskVolume3\WINDOWS\AppCompat\Programs\Amcache.hve`, e.Params.MustGetString(params.FilePath)) assert.Equal(t, "NORMAL", e.GetParamAsString(params.FileAttributes)) assert.Equal(t, "SEQUENTIAL_ONLY|SYNCHRONOUS_IO_NONALERT|NO_COMPRESSION", e.GetParamAsString(params.FileCreateOptions)) assert.Equal(t, uint32(6536), e.Params.MustGetTid()) + assert.Equal(t, "SUPERSEDE", e.GetParamAsString(params.FileOperation)) + assert.Equal(t, "Success", e.GetParamAsString(params.NTStatus)) }, buf: []byte{ 200, 7, 94, 150, 141, 215, 255, 255, diff --git a/pkg/event/param_windows.go b/pkg/event/param_windows.go index b5f437f1e..2025461e0 100644 --- a/pkg/event/param_windows.go +++ b/pkg/event/param_windows.go @@ -63,8 +63,6 @@ func NewParam(name string, typ params.Type, value params.Value, options ...Param return &Param{Name: name, Type: typ, Value: v, Flags: opts.flags, Enum: opts.enum} } -var devMapper = fs.NewDevMapper() - // String returns the string representation of the parameter value. func (p Param) String() string { if p.Value == nil { @@ -83,7 +81,7 @@ func (p Param) String() string { } return sid.String() case params.DOSPath: - return devMapper.Convert(p.Value.(string)) + return fs.GetDevMapper().Convert(p.Value.(string)) case params.Key: rootKey, keyName := key.Format(p.Value.(string)) if keyName != "" && rootKey != key.Invalid { diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index 3a05dcec5..5ff528b7c 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -69,6 +69,8 @@ type Filter interface { GetSequence() *ql.Sequence // IsSequence determines if this filter is a sequence. IsSequence() bool + // Expr returns the raw AST expression. + Expr() ql.Expr } // Field contains field meta attributes all accessors need to extract the value. @@ -263,6 +265,10 @@ func (f *filter) EvalWithValuer(e *event.Event, cache *ValuerCache) bool { return ql.Eval(f.expr, f.mapValuer(e, cache), f.hasFunctions) } +func (f *filter) Expr() ql.Expr { + return f.expr +} + // evalBoundSequence evaluates the sequence with bound fields // and returns true if the sequence expression matches or false // otherwise. diff --git a/pkg/filter/ql/expr.go b/pkg/filter/ql/expr.go index cbf39499e..94f565ddb 100644 --- a/pkg/filter/ql/expr.go +++ b/pkg/filter/ql/expr.go @@ -49,7 +49,7 @@ func (e *ParenExpr) String() string { // BinaryExpr represents an operation between two expressions. type BinaryExpr struct { - Op token + Op Token LHS Expr RHS Expr } diff --git a/pkg/filter/ql/lexer.go b/pkg/filter/ql/lexer.go index 4733285e4..d9b5cc0d5 100644 --- a/pkg/filter/ql/lexer.go +++ b/pkg/filter/ql/lexer.go @@ -43,7 +43,7 @@ func newScanner(r io.Reader) *scanner { // scan returns the next token and position from the underlying reader. // Also returns the literal text read for strings, numbers, and duration tokens // since these token types can have different literal representations. -func (s *scanner) scan() (tok token, pos int, lit string) { +func (s *scanner) scan() (tok Token, pos int, lit string) { // Read next code point. ch0, pos := s.r.read() @@ -124,7 +124,7 @@ func (s *scanner) scan() (tok token, pos int, lit string) { } // scanWhitespace consumes the current rune and all contiguous whitespace. -func (s *scanner) scanWhitespace() (tok token, pos int, lit string) { +func (s *scanner) scanWhitespace() (tok Token, pos int, lit string) { // Create a buffer and read the current character into it. var buf bytes.Buffer ch, pos := s.r.curr() @@ -147,7 +147,7 @@ func (s *scanner) scanWhitespace() (tok token, pos int, lit string) { return WS, pos, buf.String() } -func (s *scanner) scanIdent() (tok token, pos int, lit string) { +func (s *scanner) scanIdent() (tok Token, pos int, lit string) { // Save the starting position of the identifier. _, pos = s.r.read() s.r.unread() @@ -180,7 +180,7 @@ func (s *scanner) scanIdent() (tok token, pos int, lit string) { } // scanNumber consumes anything that looks like the start of a number. -func (s *scanner) scanNumber() (tok token, pos int, lit string) { +func (s *scanner) scanNumber() (tok Token, pos int, lit string) { var buf bytes.Buffer // Check if the initial rune is a ".". @@ -322,7 +322,7 @@ func scanBareIdent(r io.RuneScanner) string { // scanString consumes a contiguous string of non-quote characters. // Quote characters can be consumed if they're first escaped with a backslash. -func (s *scanner) scanString() (tok token, pos int, lit string) { +func (s *scanner) scanString() (tok Token, pos int, lit string) { s.r.unread() _, pos = s.r.curr() @@ -384,7 +384,7 @@ type bufScanner struct { i int // buffer index n int // buffer size buf [3]struct { - tok token + tok Token pos int lit string } @@ -396,12 +396,12 @@ func newBufScanner(r io.Reader) *bufScanner { } // scan reads the next token from the scanner. -func (s *bufScanner) scan() (tok token, pos int, lit string) { +func (s *bufScanner) scan() (tok Token, pos int, lit string) { return s.scanFunc(s.s.scan) } // scanFunc uses the provided function to scan the next token. -func (s *bufScanner) scanFunc(scan func() (token, int, string)) (tok token, pos int, lit string) { +func (s *bufScanner) scanFunc(scan func() (Token, int, string)) (tok Token, pos int, lit string) { // If we have unread tokens then read them off the buffer first. if s.n > 0 { s.n-- @@ -420,7 +420,7 @@ func (s *bufScanner) scanFunc(scan func() (token, int, string)) (tok token, pos func (s *bufScanner) unscan() { s.n++ } // curr returns the last read token. -func (s *bufScanner) curr() (tok token, pos int, lit string) { +func (s *bufScanner) curr() (tok Token, pos int, lit string) { buf := &s.buf[(s.i-s.n+len(s.buf))%len(s.buf)] return buf.tok, buf.pos, buf.lit } diff --git a/pkg/filter/ql/lexer_test.go b/pkg/filter/ql/lexer_test.go index ee229eb21..b9f2929de 100644 --- a/pkg/filter/ql/lexer_test.go +++ b/pkg/filter/ql/lexer_test.go @@ -26,7 +26,7 @@ import ( func TestScanner(t *testing.T) { var tests = []struct { s string - tok token + tok Token lit string pos int }{ diff --git a/pkg/filter/ql/parser.go b/pkg/filter/ql/parser.go index d8edda461..a86ff98c4 100644 --- a/pkg/filter/ql/parser.go +++ b/pkg/filter/ql/parser.go @@ -650,10 +650,10 @@ func parseDuration(s string) (time.Duration, error) { } // scan returns the next token from the underlying scanner. -func (p *Parser) scan() (tok token, pos int, lit string) { return p.s.scan() } +func (p *Parser) scan() (tok Token, pos int, lit string) { return p.s.scan() } // scanIgnoreWhitespace scans the next non-whitespace. -func (p *Parser) scanIgnoreWhitespace() (tok token, pos int, lit string) { +func (p *Parser) scanIgnoreWhitespace() (tok Token, pos int, lit string) { for { tok, pos, lit = p.scan() if tok == WS { diff --git a/pkg/filter/ql/token.go b/pkg/filter/ql/token.go index 5cddc60b1..cad27783a 100644 --- a/pkg/filter/ql/token.go +++ b/pkg/filter/ql/token.go @@ -23,10 +23,10 @@ import ( ) // token represents the lexical token of the filter expression -type token int +type Token int const ( - Illegal token = iota + Illegal Token = iota WS EOF @@ -86,11 +86,11 @@ const ( As // AS ) -var keywords map[string]token +var keywords map[string]Token func init() { - keywords = make(map[string]token) - for _, tok := range []token{And, Or, Contains, IContains, In, + keywords = make(map[string]Token) + for _, tok := range []Token{And, Or, Contains, IContains, In, IIn, Not, Startswith, IStartswith, Endswith, IEndswith, Matches, IMatches, Fuzzy, IFuzzy, Fuzzynorm, IFuzzynorm, Intersects, IIntersects, Seq, MaxSpan, By, As} { @@ -161,18 +161,18 @@ var tokens = [...]string{ } // isOperator determines whether the current token is an operator. -func (tok token) isOperator() bool { return tok > opBeg && tok < opEnd } +func (tok Token) isOperator() bool { return tok > opBeg && tok < opEnd } // String returns the string representation of the token. -func (tok token) String() string { - if tok >= 0 && tok < token(len(tokens)) { +func (tok Token) String() string { + if tok >= 0 && tok < Token(len(tokens)) { return tokens[tok] } return "" } // precedence returns the operator precedence of the binary operator token. -func (tok token) precedence() int { +func (tok Token) precedence() int { switch tok { case Or: return 1 @@ -189,7 +189,7 @@ func (tok token) precedence() int { return 0 } -func tokstr(tok token, lit string) string { +func tokstr(tok Token, lit string) string { if lit != "" { return lit } @@ -197,7 +197,7 @@ func tokstr(tok token, lit string) string { } // lookup returns the token associated with a given string. -func lookup(id string) (token, string) { +func lookup(id string) (Token, string) { if tok, ok := keywords[strings.ToLower(id)]; ok { return tok, "" } diff --git a/pkg/fs/dev.go b/pkg/fs/dev.go index c55719cbf..68321fef4 100644 --- a/pkg/fs/dev.go +++ b/pkg/fs/dev.go @@ -22,28 +22,39 @@ package fs import ( - "github.com/rabbitstack/fibratus/pkg/sys" "os" "strings" + "sync" + + "github.com/rabbitstack/fibratus/pkg/sys" ) const deviceOffset = 8 const vmsmbDevice = `\Device\vmsmb` -// DevMapper is the minimal interface for the device converters. -type DevMapper interface { - // Convert receives the fully qualified file path and replaces the DOS device name with a drive letter. - Convert(filename string) string +var ( + devMapper *DevMapper + onceDevMapper sync.Once +) + +// GetDevMapper builds and returns the singleton dev mapper instance. +func GetDevMapper() *DevMapper { + onceDevMapper.Do(func() { + devMapper = newDevMapper() + }) + return devMapper } -type mapper struct { +// DevMapper converts the fully qualified file path and +// replaces the DOS device name with a drive letter. +type DevMapper struct { cache map[string]string sysroot string } -// NewDevMapper creates a new instance of the DOS device replacer. -func NewDevMapper() DevMapper { - m := &mapper{ +// newDevMapper creates a new instance of the DOS device replacer. +func newDevMapper() *DevMapper { + m := &DevMapper{ cache: make(map[string]string), } @@ -65,39 +76,39 @@ func NewDevMapper() DevMapper { return m } -func (m *mapper) Convert(filename string) string { - if filename == "" || len(filename) < deviceOffset { - return filename +func (m *DevMapper) Convert(path string) string { + if path == "" || len(path) < deviceOffset { + return path } // find the backslash index - n := strings.Index(filename[deviceOffset:], "\\") + n := strings.Index(path[deviceOffset:], "\\") if n < 0 { - if f, ok := m.cache[filename]; ok { + if f, ok := m.cache[path]; ok { return f } - return filename + return path } - dev := filename[:n+deviceOffset] + dev := path[:n+deviceOffset] if drive, ok := m.cache[dev]; ok { // the mapping for the DOS device exists - return strings.Replace(filename, dev, drive, 1) + return strings.Replace(path, dev, drive, 1) } switch { case dev == vmsmbDevice: // convert Windows Sandbox path to native path - if n := strings.Index(filename, "os"); n > 0 { - return "C:" + filename[n+2:] + if n := strings.Index(path, "os"); n > 0 { + return "C:" + path[n+2:] } - case strings.HasPrefix(filename, "\\SystemRoot"): + case strings.HasPrefix(path, "\\SystemRoot"): // normalize paths starting with SystemRoot - return strings.Replace(filename, "\\SystemRoot", m.sysroot, 1) - case strings.HasPrefix(filename, "\\SYSTEMROOT"): + return strings.Replace(path, "\\SystemRoot", m.sysroot, 1) + case strings.HasPrefix(path, "\\SYSTEMROOT"): // normalize paths starting with SYSTEMROOT - return strings.Replace(filename, "\\SYSTEMROOT", m.sysroot, 1) + return strings.Replace(path, "\\SYSTEMROOT", m.sysroot, 1) } - return filename + return path } diff --git a/pkg/fs/dev_test.go b/pkg/fs/dev_test.go index 972bdba1e..0624326ac 100644 --- a/pkg/fs/dev_test.go +++ b/pkg/fs/dev_test.go @@ -58,7 +58,7 @@ var drives = []string{ "Z"} func TestConvertDosDevice(t *testing.T) { - m := NewDevMapper() + m := GetDevMapper() files := make([]string, 0, len(drives)) for _, drive := range drives { @@ -74,9 +74,9 @@ func TestConvertDosDevice(t *testing.T) { } assert.Contains(t, files, filename) - m.(*mapper).cache["\\Device\\HarddiskVolume1"] = "C:" - m.(*mapper).cache["\\Device\\HarddiskVolume5"] = "\\Device\\HarddiskVolume5" - m.(*mapper).sysroot = "C:\\Windows" + m.cache["\\Device\\HarddiskVolume1"] = "C:" + m.cache["\\Device\\HarddiskVolume5"] = "\\Device\\HarddiskVolume5" + m.sysroot = "C:\\Windows" var tests = []struct { inputFilename string diff --git a/pkg/handle/object.go b/pkg/handle/object.go index 530fc1670..6a3584678 100644 --- a/pkg/handle/object.go +++ b/pkg/handle/object.go @@ -24,6 +24,7 @@ package handle import ( "errors" "fmt" + "github.com/rabbitstack/fibratus/pkg/fs" htypes "github.com/rabbitstack/fibratus/pkg/handle/types" "github.com/rabbitstack/fibratus/pkg/sys" @@ -31,8 +32,6 @@ import ( "golang.org/x/sys/windows" ) -var devMapper = fs.NewDevMapper() - // Duplicate duplicates the handle in the caller process's address space. func Duplicate(handle windows.Handle, pid uint32, access uint32) (windows.Handle, error) { // handle to the process with the handle to be duplicated. @@ -91,7 +90,7 @@ func QueryName(handle windows.Handle, typ string, withTimeout bool) (string, hty if err != nil { return "", nil, err } - name = devMapper.Convert(name) + name = fs.GetDevMapper().Convert(name) fileInfo := &htypes.FileInfo{IsDirectory: sys.PathIsDirectory(name)} return name, fileInfo, nil case ALPCPort: diff --git a/pkg/rules/compiler.go b/pkg/rules/compiler.go index 516e3668e..d0c107df3 100644 --- a/pkg/rules/compiler.go +++ b/pkg/rules/compiler.go @@ -21,12 +21,16 @@ package rules import ( "expvar" "fmt" + "slices" + "strings" semver "github.com/hashicorp/go-version" "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/filter" "github.com/rabbitstack/fibratus/pkg/filter/fields" + "github.com/rabbitstack/fibratus/pkg/filter/ql" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/version" log "github.com/sirupsen/logrus" @@ -54,12 +58,19 @@ var ( ) type compiler struct { - psnap ps.Snapshotter - config *config.Config + psnap ps.Snapshotter + config *config.Config + approvers config.Approvers } -func newCompiler(psnap ps.Snapshotter, config *config.Config) *compiler { - return &compiler{psnap: psnap, config: config} +func newCompiler(psnap ps.Snapshotter, cfg *config.Config) *compiler { + return &compiler{psnap: psnap, config: cfg, approvers: config.Approvers{ + Keys: make(map[string][]string), + Paths: make(map[string][]string), + Extensions: make(map[string][]string), + Bases: make(map[string][]string), + Executables: make(map[string][]string), + }} } func (c *compiler) compile() (map[*config.FilterConfig]filter.Filter, *config.RulesCompileResult, error) { @@ -125,6 +136,17 @@ func (c *compiler) compile() (map[*config.FilterConfig]filter.Filter, *config.Ru } } + // visit filter or sequence expressions + // to extract approver predicates + expr := fltr.Expr() + if expr != nil { + c.visitApproverPredicates(expr) + } else { + for _, expr := range fltr.GetSequence().Expressions { + c.visitApproverPredicates(expr.Expr) + } + } + filters[f] = fltr } @@ -132,7 +154,176 @@ func (c *compiler) compile() (map[*config.FilterConfig]filter.Filter, *config.Ru return filters, nil, nil } - return filters, c.buildCompileResult(filters), nil + r := c.buildCompileResult(filters) + if r != nil { + r.Approvers = c.approvers + } + + return filters, r, nil +} + +func (c *compiler) visitApproverPredicates(node ql.Node) { + walk := func(n ql.Node) { + expr, ok := n.(*ql.BinaryExpr) + if !ok { + return + } + + // skip expressions wrapped in NOT + if c.isNegated(node, n) { + return + } + + lhs, ok := expr.LHS.(*ql.FieldLiteral) + if !ok { + return + } + + // only extract if the rule targets interested event types + if !c.referencesApproverEvents(node) { + return + } + + // extract the string value(s) from RHS + values, ok := rhsToStrings(expr.RHS) + if !ok { + return + } + + op := expr.Op.String() + + switch lhs.Field { + case fields.RegistryPath: + for _, v := range values { + c.approvers.AppendKey(op, v) + } + case fields.FilePath: + for _, v := range values { + c.approvers.AppendPath(op, v) + } + case fields.FileExtension: + for _, v := range values { + c.approvers.AppendExtension(op, v) + } + case fields.FileName: + for _, v := range values { + c.approvers.AppendBase(op, v) + } + case fields.EvtArg: + if lhs.Arg == params.Exe { + for _, v := range values { + c.approvers.AppendExecutable(op, v) + } + } + } + } + ql.WalkFunc(node, walk) +} + +// referencesTargetEvents checks whether the rule AST contains +// an event type filter for high-volume events we want to approve. +func (c *compiler) referencesApproverEvents(root ql.Node) bool { + var found bool + ql.WalkFunc(root, func(n ql.Node) { + expr, ok := n.(*ql.BinaryExpr) + if !ok { + return + } + + // direct event match. We also include SetFileInformation + // to approve any paths referenced in the condition + if c.containsEventTypes(expr, event.RegOpenKey, event.OpenThread, event.OpenProcess, event.SetFileInformation) { + found = true + return + } + + // for file events require open file operation + if expr.Op == ql.And { + if c.containsEventTypes(expr, event.CreateFile) && c.containsFieldMatch(expr, fields.FileOperation, ql.Eq, "OPEN") { + found = true + } + } + }) + return found +} + +func (c *compiler) containsEventTypes(root ql.Node, types ...event.Type) bool { + var contains bool + ql.WalkFunc(root, func(n ql.Node) { + expr, ok := n.(*ql.BinaryExpr) + if !ok { + return + } + lhs, ok := expr.LHS.(*ql.FieldLiteral) + if !ok || lhs.Field != fields.EvtName { + return + } + + vals, ok := rhsToStrings(expr.RHS) + if !ok { + return + } + + evts := make([]event.Type, 0, len(vals)) + for _, v := range vals { + evts = append(evts, event.NameToType(v)) + } + + for _, typ := range types { + if slices.Contains(evts, typ) { + contains = true + return + } + } + }) + return contains +} + +func (c *compiler) containsFieldMatch(root ql.Node, field fields.Field, op ql.Token, val string) bool { + var contains bool + ql.WalkFunc(root, func(n ql.Node) { + expr, ok := n.(*ql.BinaryExpr) + if !ok { + return + } + + lhs, ok := expr.LHS.(*ql.FieldLiteral) + if !ok || lhs.Field != field { + return + } + + if expr.Op != op { + return + } + + values, ok := rhsToStrings(expr.RHS) + if !ok { + return + } + for _, v := range values { + if strings.EqualFold(v, val) { + contains = true + return + } + } + }) + return contains +} + +// isNegated walks up the AST to check if the given node +// is a direct child of a NOT unary expression. +func (c *compiler) isNegated(root ql.Node, node ql.Node) bool { + negated := false + ql.WalkFunc(root, func(n ql.Node) { + unary, ok := n.(*ql.NotExpr) + if !ok { + return + } + if unary.Expr == node { + negated = true + } + }) + return negated } func (c *compiler) buildCompileResult(filters map[*config.FilterConfig]filter.Filter) *config.RulesCompileResult { @@ -195,3 +386,13 @@ func (c *compiler) buildCompileResult(filters map[*config.FilterConfig]filter.Fi return rs } + +func rhsToStrings(n ql.Node) ([]string, bool) { + switch v := n.(type) { + case *ql.StringLiteral: + return []string{v.Value}, true + case *ql.ListLiteral: + return v.Values, true + } + return []string{}, false +} diff --git a/pkg/rules/compiler_test.go b/pkg/rules/compiler_test.go index 664ca01cd..46e54ac78 100644 --- a/pkg/rules/compiler_test.go +++ b/pkg/rules/compiler_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/filter/ql" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/util/version" "github.com/stretchr/testify/assert" @@ -97,3 +98,223 @@ func TestCompileEventCategoryFieldNames(t *testing.T) { }) } } + +func TestVisitApproverPredicatesRegistryEvents(t *testing.T) { + tests := []struct { + name string + expr string + want bool + }{ + { + name: "RegSetValue does not match", + expr: "evt.name = 'RegSetValue'", + want: false, + }, + { + name: "RegOpenKey matches", + expr: "evt.name = 'RegOpenKey'", + want: true, + }, + { + name: "CreateProcess does not match", + expr: "evt.name = 'CreateProcess'", + want: false, + }, + { + name: "RegOpenKey event nested in AND matches", + expr: `evt.name = 'RegOpenKey' and registry.path imatches ('HKEY_LOCAL_MACHINE\\SYSTEM\\*')`, + want: true, + }, + { + name: "Registry event nested in paren matches", + expr: "(evt.name = 'RegOpenKey')", + want: true, + }, + } + + c := newCompiler(new(ps.SnapshotterMock), newConfig("_fixtures/default/*.yml")) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := ql.NewParser(tt.expr) + n, err := p.ParseExpr() + require.NoError(t, err) + got := c.referencesApproverEvents(n) + if got != tt.want { + t.Errorf("registry approver predicates: %v, want %v", got, tt.want) + } + }) + } +} + +func TestVisitApproverPredicatesCreateFile(t *testing.T) { + tests := []struct { + name string + expr string + want bool + }{ + { + name: "CreateFile with OPEN operation matches", + expr: "evt.name = 'CreateFile' and file.operation = 'OPEN'", + want: true, + }, + { + name: "CreateFile without file.operation does not match", + expr: "evt.name = 'CreateFile'", + want: false, + }, + { + name: "CreateFile with non-OPEN operation does not match", + expr: "evt.name = 'CreateFile' and file.operation = 'CREATE'", + want: false, + }, + { + name: "file.operation OPEN without CreateFile does not match", + expr: "file.operation = 'OPEN'", + want: false, + }, + { + name: "CreateFile and OPEN in separate OR branches does not match", + expr: "evt.name = 'CreateFile' or file.operation = 'OPEN'", + want: false, + }, + { + name: "CreateFile with OPEN nested in paren matches", + expr: "evt.name = 'CreateFile' and (file.operation = 'OPEN')", + want: true, + }, + { + name: "CreateFile with OPEN and extra conditions matches", + expr: "evt.name = 'CreateFile' and file.operation = 'OPEN' and file.path imatches '?:\\\\Windows\\\\*'", + want: true, + }, + } + + c := newCompiler(new(ps.SnapshotterMock), newConfig("_fixtures/default/*.yml")) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := ql.NewParser(tt.expr) + n, err := p.ParseExpr() + require.NoError(t, err) + got := c.referencesApproverEvents(n) + if got != tt.want { + t.Errorf("file approver predicates: %v, want %v", got, tt.want) + } + }) + } +} + +func TestAccumulatedApproverPredicates(t *testing.T) { + tests := []struct { + name string + expr string + wantKeys map[string][]string + wantPaths map[string][]string + wantExtensions map[string][]string + wantExecutables map[string][]string + wantBases map[string][]string + }{ + { + name: "extracts registry key path with imatches", + expr: "evt.name = 'RegOpenKey' and registry.path imatches 'HKEY_LOCAL_MACHINE\\\\SYSTEM\\\\*'", + wantKeys: map[string][]string{ + "IMATCHES": {`HKEY_LOCAL_MACHINE\SYSTEM\*`}, + }, + }, + { + name: "extracts file path with imatches", + expr: "evt.name = 'CreateFile' and file.operation = 'OPEN' and file.path imatches 'C:\\\\Windows\\\\*'", + wantPaths: map[string][]string{ + "IMATCHES": {`C:\Windows\*`}, + }, + }, + { + name: "negated registry path is not extracted", + expr: "evt.name = 'RegSetValue' and registry.path not imatches 'HKEY_LOCAL_MACHINE\\\\SOFTWARE\\\\*'", + }, + { + name: "extracts file extension", + expr: "evt.name = 'CreateFile' and file.operation = 'OPEN' and file.extension = '.exe'", + wantExtensions: map[string][]string{ + "=": {".exe"}, + }, + }, + { + name: "extracts file base name", + expr: "evt.name = 'CreateFile' and file.operation = 'OPEN' and file.name icontains 'svchost'", + wantBases: map[string][]string{ + "ICONTAINS": {"svchost"}, + }, + }, + { + name: "extracts process executable", + expr: "evt.name = 'OpenProcess' and evt.arg[exe] icontains 'lsass'", + wantExecutables: map[string][]string{ + "ICONTAINS": {"lsass"}, + }, + }, + } + + c := newCompiler(new(ps.SnapshotterMock), newConfig("")) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := ql.NewParser(tt.expr) + n, err := p.ParseExpr() + require.NoError(t, err) + + c.visitApproverPredicates(n) + + if tt.wantKeys != nil { + assertMapEqual(t, "Keys", c.approvers.Keys, tt.wantKeys) + } + + if tt.wantPaths != nil { + assertMapEqual(t, "Paths", c.approvers.Paths, tt.wantPaths) + } + + if tt.wantExtensions != nil { + assertMapEqual(t, "Extensions", c.approvers.Extensions, tt.wantExtensions) + } + + if tt.wantBases != nil { + assertMapEqual(t, "Bases", c.approvers.Bases, tt.wantBases) + } + + if tt.wantExecutables != nil { + assertMapEqual(t, "Executables", c.approvers.Executables, tt.wantExecutables) + } + }) + } + + assert.Len(t, c.approvers.Paths, 1) + assert.Len(t, c.approvers.Keys, 1) + assert.Len(t, c.approvers.Extensions, 1) + assert.Len(t, c.approvers.Bases, 1) + assert.Len(t, c.approvers.Executables, 1) +} + +func assertMapEqual(t *testing.T, name string, got, want map[string][]string) { + t.Helper() + if len(got) != len(want) { + t.Errorf("%s: got %d keys, want %d keys. got=%v want=%v", name, len(got), len(want), got, want) + return + } + for k, wantVals := range want { + gotVals, ok := got[k] + if !ok { + t.Errorf("%s: missing key %q", name, k) + continue + } + if len(gotVals) != len(wantVals) { + t.Errorf("%s[%q]: got %v, want %v", name, k, gotVals, wantVals) + continue + } + for i, v := range wantVals { + if gotVals[i] != v { + t.Errorf("%s[%q][%d]: got %q, want %q", name, k, i, gotVals[i], v) + } + } + } +} diff --git a/pkg/sys/etw/types.go b/pkg/sys/etw/types.go index e540d1521..738b1ba5b 100644 --- a/pkg/sys/etw/types.go +++ b/pkg/sys/etw/types.go @@ -570,6 +570,72 @@ type EventFilterDescriptor struct { Type uint32 } +const ( + // ExtTypeDisposition represents a custom extended item type for the file disposition. + ExtTypeDisposition = 0x8000 + // ExtTypeStatus represents a custom extended item type for the file system status. + ExtTypeStatus = 0x8001 +) + +// FileExtendedDataItems stores file extended data items. +type FileExtendedDataItems struct { + status uint32 + disposition uint32 + items []EventHeaderExtendedDataItem +} + +// AppendEventHeaderFileExtendedDataItems appends custom file extendeed data items to the event record. +func AppendEventHeaderFileExtendedDataItems(r *EventRecord, disposition uint64, status uint32) *FileExtendedDataItems { + f := &FileExtendedDataItems{ + disposition: uint32(disposition), + status: status, + items: make([]EventHeaderExtendedDataItem, 2), + } + + f.items[0] = EventHeaderExtendedDataItem{ + ExtType: ExtTypeDisposition, + DataSize: 4, + DataPtr: uint64(uintptr(unsafe.Pointer(&f.disposition))), + } + f.items[1] = EventHeaderExtendedDataItem{ + ExtType: ExtTypeStatus, + DataSize: 4, + DataPtr: uint64(uintptr(unsafe.Pointer(&f.status))), + } + + r.ExtendedDataCount = uint16(len(f.items)) + r.ExtendedData = &f.items[0] + + return f +} + +// ReadEventHeaderFileExtendedDataItems reads the custom file extended data items from the event record. +func (r *EventRecord) ReadEventHeaderFileExtendedDataItems() (uint32, uint32) { + if r.ExtendedData == nil { + return 0, 0 + } + + items := unsafe.Slice(r.ExtendedData, r.ExtendedDataCount) + + var disposition uint32 + var status uint32 + + for _, item := range items { + switch item.ExtType { + case ExtTypeDisposition: + if item.DataSize == 4 && item.DataPtr != 0 { + disposition = *(*uint32)(unsafe.Pointer(uintptr(item.DataPtr))) + } + case ExtTypeStatus: + if item.DataSize == 4 && item.DataPtr != 0 { + status = *(*uint32)(unsafe.Pointer(uintptr(item.DataPtr))) + } + } + } + + return disposition, status +} + // NewClassicEventID creates a new instance of classic event identifier. func NewClassicEventID(guid windows.GUID, typ uint16) ClassicEventID { return ClassicEventID{GUID: guid, Type: uint8(typ)} @@ -606,6 +672,17 @@ func (e *EventRecord) ID() uint { return id } +// Copy makes a copy of this event record and returns the +// copy itself and the event buffer. The buffer must outlive +// the event record instance. +func (r *EventRecord) Copy() (*EventRecord, []byte) { + c := *r + buf := make([]byte, r.BufferLen) + copy(buf, unsafe.Slice((*byte)(unsafe.Pointer(r.Buffer)), r.BufferLen)) + c.Buffer = uintptr(unsafe.Pointer(&buf[0])) + return &c, buf +} + // ReadByte reads the byte from the buffer at the specified offset. func (e *EventRecord) ReadByte(offset uint16) byte { if offset > e.BufferLen { diff --git a/pkg/util/key/key.go b/pkg/util/key/key.go index cf59affc1..eed0cf71e 100644 --- a/pkg/util/key/key.go +++ b/pkg/util/key/key.go @@ -22,11 +22,12 @@ package key import ( - "github.com/rabbitstack/fibratus/pkg/sys" - "golang.org/x/sys/windows/registry" "path/filepath" "regexp" "strings" + + "github.com/rabbitstack/fibratus/pkg/sys" + "golang.org/x/sys/windows/registry" ) var ( @@ -202,6 +203,18 @@ func Format(key string) (Key, string) { return Invalid, key } +// ConcatPaths concatenates root and subkey registry paths. +func ConcatPaths(root, path string) string { + var b strings.Builder + b.Grow(len(root) + len(path) + 1) + if root != "" { + b.WriteString(root) + b.WriteByte('\\') + } + b.WriteString(path) + return b.String() +} + func subkey(key string, prefix string) string { if len(key) > len(prefix) { return key[len(prefix)+1:] diff --git a/pkg/util/key/key_test.go b/pkg/util/key/key_test.go index aed798cda..cb9f683b8 100644 --- a/pkg/util/key/key_test.go +++ b/pkg/util/key/key_test.go @@ -22,11 +22,12 @@ package key import ( + "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" - "testing" ) func init() { @@ -182,3 +183,33 @@ func TestReadValue(t *testing.T) { }) } } + +func TestConcatPaths(t *testing.T) { + var tests = []struct { + root string + subkey string + expected string + }{ + { + root: "HKEY_LOCAL_MACHINE", + subkey: `Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\Capabilities`, + expected: `HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\Capabilities`, + }, + { + root: "", + subkey: `Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\Capabilities`, + expected: `Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\Capabilities`, + }, + { + root: "HKEY_LOCAL_MACHINE", + subkey: "", + expected: "HKEY_LOCAL_MACHINE\\", + }, + } + + for _, tt := range tests { + t.Run(tt.root+tt.subkey, func(t *testing.T) { + assert.Equal(t, tt.expected, ConcatPaths(tt.root, tt.subkey)) + }) + } +}