diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index de714d4cd..afdede38c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -68,6 +68,8 @@ > /area deps +> /area evasion + > /area other diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 21970a42d..106647c9c 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -169,6 +169,10 @@ jobs: run: | cd yara make install + - name: Setup test fixtures + run: | + choco install -y go-task + task --taskfile internal/etw/_fixtures/Taskfile.yml all - name: Test shell: bash run: | diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f4698bd18..364111125 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -151,6 +151,10 @@ jobs: run: | cd yara make install + - name: Setup test fixtures + run: | + choco install -y go-task + task --taskfile internal/etw/_fixtures/Taskfile.yml all - name: Test shell: bash run: | diff --git a/configs/fibratus.yml b/configs/fibratus.yml index 28a2cb34e..573a7146b 100644 --- a/configs/fibratus.yml +++ b/configs/fibratus.yml @@ -156,6 +156,21 @@ handle: # Indicates if process handles are collected during startup or when a new process is spawn. enumerate-handles: false +# =============================== Evasion ==================================================== + +# Tweaks for controlling evasion scanner behaviours. Evasion behaviours can represent strong +# IoC (Indicators of Compromise) such as direct syscall or require a combination of fine-tune +# exceptions to reduce the alert fatigue. +evasion: + # Indicates if evasion detections are enabled global-wise. If disabled, evasion scanner will + # not try to classify ad-hoc evasion techniques. + enabled: true + + # Indicates if direct syscall evasion detection is enabled. A direct syscall bypasses Windows + # API functions and calls the underlying system call directly using the syscall instruction, + # skipping the NTDLL stub that normally performs the transition to kernel mode. + #enable-direct-syscall: true + # =============================== Event =============================================== # The following settings control the state of the event. diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 7cd552db1..5014b87d9 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -21,6 +21,7 @@ package bootstrap import ( "context" "errors" + "github.com/rabbitstack/fibratus/internal/evasion" "github.com/rabbitstack/fibratus/pkg/aggregator" "github.com/rabbitstack/fibratus/pkg/alertsender" "github.com/rabbitstack/fibratus/pkg/api" @@ -138,7 +139,7 @@ func NewApp(cfg *config.Config, options ...Option) (*App, error) { var engine *rules.Engine var rs *config.RulesCompileResult - if cfg.Filters.Rules.Enabled && !cfg.ForwardMode && !cfg.IsCaptureSet() { + if cfg.Filters.Rules.Enabled && !cfg.ForwardMode && !cfg.IsCaptureSet() && !cfg.IsFilamentSet() { engine = rules.NewEngine(psnap, cfg) var err error rs, err = engine.Compile() @@ -203,9 +204,8 @@ func (f *App) Run(args []string) error { // In case of a regular run, we additionally set up the aggregator. // The aggregator will grab the events from the queue, assemble them // into batches and hand over to output sinks. - filamentName := cfg.Filament.Name - if filamentName != "" { - f.filament, err = filament.New(filamentName, f.psnap, f.hsnap, cfg) + if cfg.IsFilamentSet() { + f.filament, err = filament.New(cfg.Filament.Name, f.psnap, f.hsnap, cfg) if err != nil { return err } @@ -234,6 +234,10 @@ func (f *App) Run(args []string) error { f.symbolizer = symbolize.NewSymbolizer(symbolize.NewDebugHelpResolver(cfg), f.psnap, cfg, false) f.evs.RegisterEventListener(f.symbolizer) } + // register evasion scanner + if cfg.Evasion.Enabled { + f.evs.RegisterEventListener(evasion.NewScanner(cfg.Evasion)) + } // register rule engine if f.engine != nil { f.evs.RegisterEventListener(f.engine) @@ -314,9 +318,9 @@ func (f *App) ReadCapture(ctx context.Context, args []string) error { if err != nil { return err } - filamentName := f.config.Filament.Name - if filamentName != "" { - f.filament, err = filament.New(filamentName, f.psnap, f.hsnap, f.config) + + if f.config.IsFilamentSet() { + f.filament, err = filament.New(f.config.Filament.Name, f.psnap, f.hsnap, f.config) if err != nil { return err } @@ -355,6 +359,7 @@ func (f *App) ReadCapture(ctx context.Context, args []string) error { return err } } + return api.StartServer(f.config) } diff --git a/internal/etw/_fixtures/Taskfile.yml b/internal/etw/_fixtures/Taskfile.yml new file mode 100644 index 000000000..58fb07264 --- /dev/null +++ b/internal/etw/_fixtures/Taskfile.yml @@ -0,0 +1,20 @@ +version: '3' + +vars: + SYSWHISPERS3_REPO: https://github.com/klezVirus/SysWhispers3.git + VISUAL_STUDIO_EDITION: Enterprise + VISUAL_STUDIO_VERSION: 2022 + +tasks: + direct-syscall: + desc: Builds the binary to perform direct syscalls via Syswhispers generated stubs + dir: direct-syscall + cmds: + - git clone {{ .SYSWHISPERS3_REPO }} + - python SysWhispers3/syswhispers.py -a x64 -c msvc -p common -o syscalls + - cmd.exe /c 'C:\"Program Files"\"Microsoft Visual Studio"\{{ .VISUAL_STUDIO_VERSION }}\{{ .VISUAL_STUDIO_EDITION }}\VC\Auxiliary\Build\vcvars64.bat && nmake -f Makefile.msvc' + silent: true + + all: + deps: + - direct-syscall diff --git a/internal/etw/_fixtures/direct-syscall/.gitignore b/internal/etw/_fixtures/direct-syscall/.gitignore new file mode 100644 index 000000000..321a5cb03 --- /dev/null +++ b/internal/etw/_fixtures/direct-syscall/.gitignore @@ -0,0 +1,6 @@ +SysWhishpers3 +*.obj +*.asm +*.exe +syscalls.c +syscalls.h diff --git a/internal/etw/_fixtures/direct-syscall/Makefile.msvc b/internal/etw/_fixtures/direct-syscall/Makefile.msvc new file mode 100644 index 000000000..9b93f9026 --- /dev/null +++ b/internal/etw/_fixtures/direct-syscall/Makefile.msvc @@ -0,0 +1,7 @@ +OPTIONS = -Zp8 -c -nologo -Gy -Os -O1 -GR- -EHa -Oi -GS- +LIBS = libvcruntime.lib libcmt.lib ucrt.lib kernel32.lib + +main: + ML64 /c syscalls-asm.x64.asm /link /NODEFAULTLIB /RELEASE /MACHINE:X64 + cl.exe $(OPTIONS) syscalls.c main.c + link.exe /OUT:direct-syscall.exe -nologo $(LIBS) /MACHINE:X64 -subsystem:console -nodefaultlib syscalls-asm.x64.obj syscalls.obj main.obj diff --git a/internal/etw/_fixtures/direct-syscall/main.c b/internal/etw/_fixtures/direct-syscall/main.c new file mode 100644 index 000000000..ba12537ff --- /dev/null +++ b/internal/etw/_fixtures/direct-syscall/main.c @@ -0,0 +1,9 @@ +#include "syscalls.h" + +#include + +int main(int argc, char* argv[]) +{ + Sw3NtSetContextThread(-1, NULL); + return 0; +} diff --git a/internal/etw/source_test.go b/internal/etw/source_test.go index adf7959d6..88b27c1ce 100644 --- a/internal/etw/source_test.go +++ b/internal/etw/source_test.go @@ -20,6 +20,7 @@ package etw import ( "context" "fmt" + "github.com/rabbitstack/fibratus/internal/evasion" "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/event/params" @@ -41,6 +42,7 @@ import ( "net" "net/http" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -1294,6 +1296,127 @@ func testCallstackEnrichment(t *testing.T, hsnap handle.Snapshotter, psnap ps.Sn } } +func containsEvasion(e *event.Event, evasion string) bool { + m := e.GetMeta(event.EvasionsKey) + evas, ok := m.([]string) + if !ok { + return false + } + for _, eva := range evas { + if eva == evasion { + return true + } + } + return false +} + +func TestEvasionScanner(t *testing.T) { + var tests = []*struct { + name string + gen func() error + want func(e *event.Event) bool + completed bool + }{ + { + "direct syscall", + func() error { + cmd := exec.Command("_fixtures/direct-syscall/direct-syscall.exe") + return cmd.Run() + }, + func(e *event.Event) bool { + if strings.Contains(strings.ToLower(e.Callstack.String()), strings.ToLower("direct-syscall.exe")) && e.Type == event.SetThreadContext { + log.Info(e, e.Callstack) + return containsEvasion(e, "direct_syscall") + } + return false + }, + false, + }, + } + + evsConfig := config.EventSourceConfig{ + EnableThreadEvents: true, + EnableImageEvents: true, + EnableFileIOEvents: false, + EnableVAMapEvents: true, + EnableNetEvents: true, + EnableRegistryEvents: false, + EnableMemEvents: false, + EnableHandleEvents: false, + EnableDNSEvents: false, + EnableAuditAPIEvents: true, + StackEnrichment: true, + } + evsConfig.Init() + + hsnap := new(handle.SnapshotterMock) + hsnap.On("FindByObject", mock.Anything).Return(htypes.Handle{}, false) + hsnap.On("FindHandles", mock.Anything).Return([]htypes.Handle{}, nil) + hsnap.On("Write", mock.Anything).Return(nil) + hsnap.On("Remove", mock.Anything).Return(nil) + + cfg := &config.Config{EventSource: evsConfig, Filters: &config.Filters{}} + + psnap := ps.NewSnapshotter(hsnap, cfg) + + evs := NewEventSource(psnap, hsnap, cfg, nil) + + l := &MockListener{} + evs.RegisterEventListener(l) + + symbolizer := symbolize.NewSymbolizer(symbolize.NewDebugHelpResolver(cfg), psnap, cfg, true) + defer symbolizer.Close() + evs.RegisterEventListener(symbolizer) + + scanner := evasion.NewScanner(evasion.Config{Enabled: true, EnableDirectSyscall: true}) + evs.RegisterEventListener(scanner) + + require.NoError(t, evs.Open(cfg)) + defer evs.Close() + + time.Sleep(time.Second * 2) + + for _, tt := range tests { + gen := tt.gen + if gen != nil { + log.Infof("executing [%s] evasion test generator", tt.name) + require.NoError(t, gen(), tt.name) + } + } + + ntests := len(tests) + timeout := time.After(time.Duration(ntests) * time.Minute) + + for { + select { + case e := <-evs.Events(): + for _, tt := range tests { + if tt.completed { + continue + } + pred := tt.want + if pred(e) { + t.Logf("PASS: %s", tt.name) + tt.completed = true + ntests-- + } + if ntests == 0 { + return + } + } + case err := <-evs.Errors(): + t.Fatalf("FAIL: %v", err) + case <-timeout: + for _, tt := range tests { + if !tt.completed { + t.Logf("FAIL: %s", tt.name) + } + } + t.Fatal("FAIL: TestEvasionScanner") + } + } +} + var ( modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") kernel32 = windows.NewLazySystemDLL("kernel32.dll") diff --git a/internal/evasion/config.go b/internal/evasion/config.go new file mode 100644 index 000000000..4e0d899a5 --- /dev/null +++ b/internal/evasion/config.go @@ -0,0 +1,49 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + enabled = "evasion.enabled" + enableDirectSyscall = "evasion.enable-direct-syscall" +) + +// Config contains the settings that influence the behaviour of the evasion scanner. +type Config struct { + // Enabled indicates if evasion detections are enabled global-wise. + Enabled bool `json:"enabled" yaml:"enabled"` + // EnableDirectSyscall indicates if direct syscall evasion detection is enabled. + EnableDirectSyscall bool `json:"enable-direct-syscall" yaml:"enable-direct-syscall"` +} + +// InitFromViper initializes evasion config from Viper. +func (c *Config) InitFromViper(v *viper.Viper) { + c.Enabled = v.GetBool(enabled) + c.EnableDirectSyscall = v.GetBool(enableDirectSyscall) +} + +// AddFlags adds evasion config flags to the set. +func AddFlags(flags *pflag.FlagSet) { + flags.Bool(enabled, true, "Indicates if evasion detections are enabled global-wise") + flags.Bool(enableDirectSyscall, true, "Indicates if direct syscall evasion detection is enabled") +} diff --git a/internal/evasion/direct_syscall.go b/internal/evasion/direct_syscall.go new file mode 100644 index 000000000..57b52c6ad --- /dev/null +++ b/internal/evasion/direct_syscall.go @@ -0,0 +1,64 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import ( + "github.com/rabbitstack/fibratus/pkg/event" + "path/filepath" + "strings" +) + +// directSyscall direct syscall evasion refers to a technique where +// adversaries bypass traditional user-mode API monitoring and security +// hooks by invoking system calls directly, but does so in a way that +// evades detection or analysis. +// +// A direct syscall bypasses Windows API functions and calls the underlying +// system call directly using the syscall instruction, skipping the NTDLL +// stub that normally performs the transition to kernel mode. +type directSyscall struct{} + +func NewDirectSyscall() Evasion { + return &directSyscall{} +} + +func (d *directSyscall) Eval(e *event.Event) (bool, error) { + if e.Callstack.IsEmpty() { + return false, nil + } + + frame := e.Callstack.FinalUserspaceFrame() + if frame == nil { + return false, nil + } + + if frame.IsUnbacked() { + return false, nil + } + + mod := filepath.Base(strings.ToLower(frame.Module)) + + // check if the last userspace frame is originated + // from the allowed modules such as the native NTDLL + // module. If that's not the case, the process is + // invoking a direct syscall + return mod != "ntdll.dll" && mod != "win32.dll" && mod != "win32u.dll" && mod != "wow64win.dll" && mod != "wow64cpu.dll", nil +} + +func (d *directSyscall) Type() Type { return DirectSyscall } diff --git a/internal/evasion/direct_syscall_test.go b/internal/evasion/direct_syscall_test.go new file mode 100644 index 000000000..80a394b73 --- /dev/null +++ b/internal/evasion/direct_syscall_test.go @@ -0,0 +1,141 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import ( + "github.com/rabbitstack/fibratus/pkg/callstack" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestDirectSyscall(t *testing.T) { + var tests = []struct { + evt *event.Event + matches bool + }{ + {&event.Event{ + Type: event.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0x7ffe4fda6e3b, Module: "C:\\Windows\\System32\\KernelBase.dll", Symbol: "SetThreadContext"}, + callstack.Frame{Addr: 0xfffff807e228c555, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "setjmpex"}, + callstack.Frame{Addr: 0xfffff807e264805c, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "ObOpenObjectByPointerWithTag"}), + }, true}, + {&event.Event{ + Type: event.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0x7ffe4fda6e3b, Module: "C:\\Windows\\System32\\KernelBase.dll", Symbol: "SetThreadContext"}, + callstack.Frame{Addr: 0x7ffe52942b24, Module: "C:\\Windows\\System32\\ntdll.dll", Symbol: "ZwSetContextThread"}, + callstack.Frame{Addr: 0xfffff807e228c555, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "setjmpex"}, + callstack.Frame{Addr: 0xfffff807e264805c, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "ObOpenObjectByPointerWithTag"}), + }, false}, + {&event.Event{ + Type: event.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0x7ffe4fda6e3b, Module: "C:\\Windows\\System32\\KernelBase.dll", Symbol: "SetThreadContext"}, + callstack.Frame{Addr: 0x7ffe52942b24, Module: "C:\\Windows\\System32\\wow64win.dll", Symbol: "SetContextThread"}, + callstack.Frame{Addr: 0xfffff807e228c555, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "setjmpex"}, + callstack.Frame{Addr: 0xfffff807e264805c, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "ObOpenObjectByPointerWithTag"}), + }, false}, + {&event.Event{ + Type: event.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: event.File, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0x7ffe4fda6e3b, Module: "C:\\Windows\\System32\\KernelBase.dll", Symbol: "SetThreadContext"}, + callstack.Frame{Addr: 0x7ffe52942b24, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0xfffff807e228c555, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "setjmpex"}, + callstack.Frame{Addr: 0xfffff807e264805c, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "ObOpenObjectByPointerWithTag"}), + }, false}, + } + + for _, tt := range tests { + t.Run(tt.evt.Name, func(t *testing.T) { + eva := NewDirectSyscall() + matches, err := eva.Eval(tt.evt) + require.NoError(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} + +func callstackFromFrames(frames ...callstack.Frame) callstack.Callstack { + var c callstack.Callstack + for _, frame := range frames { + c.PushFrame(frame) + } + return c +} diff --git a/internal/evasion/scanner.go b/internal/evasion/scanner.go new file mode 100644 index 000000000..00c28a901 --- /dev/null +++ b/internal/evasion/scanner.go @@ -0,0 +1,79 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import ( + "github.com/rabbitstack/fibratus/pkg/event" + log "github.com/sirupsen/logrus" +) + +// Scanner is responsible for evaluating evasion detectors +// and decorating the event with the reported behaviours. +// Some behaviours represent strong IoCs, while other need +// careful tuning to avoid alert fatigue. Evasion behaviours +// are consumed by the rule engine through the filter fields +// that yields the evasion techniques, such as direct syscall. +type Scanner struct { + evasions []Evasion +} + +// NewScanner instantiates the new evasion scanner. +func NewScanner(config Config) *Scanner { + s := &Scanner{ + evasions: make([]Evasion, 0), + } + + if config.EnableDirectSyscall { + s.registerEvasion(NewDirectSyscall()) + } + + return s +} + +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 + for _, eva := range s.evasions { + matches, err := eva.Eval(e) + if err != nil { + return false, err + } + if matches { + enq = true + e.AddSliceMetaOrAppend(event.EvasionsKey, eva.Type().String()) + log.Infof("detected evasion %q on event [%s] and callstack [%s]", eva.Type(), e, e.Callstack) + } + } + + return enq, nil +} + +func (s *Scanner) CanEnqueue() bool { return false } + +func (s *Scanner) registerEvasion(eva Evasion) { + s.evasions = append(s.evasions, eva) +} diff --git a/internal/evasion/scanner_test.go b/internal/evasion/scanner_test.go new file mode 100644 index 000000000..36205511b --- /dev/null +++ b/internal/evasion/scanner_test.go @@ -0,0 +1,74 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import ( + "github.com/rabbitstack/fibratus/pkg/callstack" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/stretchr/testify/require" + "strings" + "testing" + "time" +) + +func TestScannerProcessEvent(t *testing.T) { + var tests = []struct { + evt *event.Event + expectedEvasions []string + }{ + {&event.Event{ + Type: event.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: event.File, + Metadata: event.Metadata{}, + Params: event.Params{ + params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + params.FileType: {Name: params.FileType, Type: params.AnsiString, Value: "file"}, + params.FileOperation: {Name: params.FileOperation, Type: params.Enum, Value: uint32(2), Enum: fs.FileCreateDispositions}, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0x7ffe4fda6e3b, Module: "C:\\Windows\\System32\\KernelBase.dll", Symbol: "SetThreadContext"}, + callstack.Frame{Addr: 0xfffff807e228c555, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "setjmpex"}, + callstack.Frame{Addr: 0xfffff807e264805c, Module: "C:\\WINDOWS\\system32\\ntoskrnl.exe", Symbol: "ObOpenObjectByPointerWithTag"}), + }, []string{"direct_syscall"}}, + } + + s := NewScanner(Config{Enabled: true, EnableDirectSyscall: true}) + + for _, tt := range tests { + t.Run(strings.Join(tt.expectedEvasions, ","), func(t *testing.T) { + matches, err := s.ProcessEvent(tt.evt) + require.NoError(t, err) + require.True(t, matches && len(tt.expectedEvasions) > 0) + if len(tt.expectedEvasions) > 0 { + require.True(t, tt.evt.ContainsMeta(event.EvasionsKey)) + require.Equal(t, tt.expectedEvasions, tt.evt.GetMeta(event.EvasionsKey).([]string)) + } + }) + } +} diff --git a/internal/evasion/types.go b/internal/evasion/types.go new file mode 100644 index 000000000..dc06a0e81 --- /dev/null +++ b/internal/evasion/types.go @@ -0,0 +1,50 @@ +/* + * Copyright 2021-present by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package evasion + +import "github.com/rabbitstack/fibratus/pkg/event" + +// Type is the alias for the evasion technique type. +type Type uint8 + +const ( + // DirectSyscall represents the direct syscall evasion. + DirectSyscall Type = iota +) + +// String returns the evasion human-friendly name. +func (t Type) String() string { + switch t { + case DirectSyscall: + return "direct_syscall" + default: + return "unknown" + } +} + +// Evasion defines the contract that all evasion detectors need to satisfy. +type Evasion interface { + // Eval executes the evasion logic. The evasion detector usually accesses + // the callstack from the given event to determine if any evasions are + // performed on behalf of the process. If the evasion is classified, this + // method return true, or false otherwise. + Eval(*event.Event) (bool, error) + // Type returns the type of the evasion technique. + Type() Type +} diff --git a/pkg/callstack/callstack.go b/pkg/callstack/callstack.go index a60ae1b4d..934a2650b 100644 --- a/pkg/callstack/callstack.go +++ b/pkg/callstack/callstack.go @@ -196,6 +196,24 @@ func (s *Callstack) FinalUserFrame() *Frame { return nil } +// FinalUserspaceFrame returns the final userspace frame. This +// frame is typically backed by the ntdll module. +func (s *Callstack) FinalUserspaceFrame() *Frame { + if s.IsEmpty() { + return nil + } + + for n := s.Depth() - 1; n > 0; n-- { + f := (*s)[n] + if f.Addr.InSystemRange() { + continue + } + return &f + } + + return nil +} + // FinalKernelFrame returns the final kernel space frame. func (s *Callstack) FinalKernelFrame() *Frame { if s.IsEmpty() { diff --git a/pkg/callstack/callstack_test.go b/pkg/callstack/callstack_test.go index 5ac3e4b48..c9a629559 100644 --- a/pkg/callstack/callstack_test.go +++ b/pkg/callstack/callstack_test.go @@ -56,6 +56,12 @@ func TestCallstack(t *testing.T) { assert.Equal(t, "fffff8015690b644", kframe.Addr.String()) assert.Equal(t, "ObDeleteCapturedInsertInfo", kframe.Symbol) assert.Equal(t, "C:\\WINDOWS\\system32\\ntoskrnl.exe", kframe.Module) + + finalUserspaceFrame := callstack.FinalUserspaceFrame() + require.NotNil(t, finalUserspaceFrame) + assert.Equal(t, "7ffb5c1d0396", finalUserspaceFrame.Addr.String()) + assert.Equal(t, "CreateProcessW", finalUserspaceFrame.Symbol) + assert.Equal(t, "C:\\WINDOWS\\System32\\KERNELBASE.dll", finalUserspaceFrame.Module) } func TestCallstackFinalUserFrame(t *testing.T) { diff --git a/pkg/config/config.schema.json b/pkg/config/config.schema.json index 4fd9b9ae1..ffe77ef80 100644 --- a/pkg/config/config.schema.json +++ b/pkg/config/config.schema.json @@ -292,6 +292,18 @@ }, "additionalProperties": false }, + "evasion": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "enable-direct-syscall": { + "type": "boolean" + } + }, + "additionalProperties": false + }, "event": { "type": "object", "properties": { diff --git a/pkg/config/config_windows.go b/pkg/config/config_windows.go index 2ca5cca84..59391fc97 100644 --- a/pkg/config/config_windows.go +++ b/pkg/config/config_windows.go @@ -21,6 +21,7 @@ package config import ( "encoding/json" "fmt" + "github.com/rabbitstack/fibratus/internal/evasion" "golang.org/x/sys/windows" "time" @@ -125,6 +126,9 @@ type Config struct { // Filters contains filter/rule definitions Filters *Filters `json:"filters" yaml:"filters"` + // Evasion controls the detection of evasion behaviours. + Evasion evasion.Config `json:"evasion" yaml:"evasion"` + flags *pflag.FlagSet viper *viper.Viper opts *Options @@ -236,6 +240,10 @@ func NewWithOpts(options ...Option) *Config { pe.AddFlags(flagSet) } + if opts.run { + evasion.AddFlags(flagSet) + } + c.addFlags() return c @@ -303,6 +311,11 @@ func (c *Config) Init() error { return err } } + + if c.opts.run { + c.Evasion.InitFromViper(c.viper) + } + return nil } @@ -310,6 +323,9 @@ func (c *Config) Init() error { // in the capture file. func (c *Config) IsCaptureSet() bool { return c.CapFile != "" } +// IsFilamentSet indicates if the filament is supplied. +func (c *Config) IsFilamentSet() bool { return c.Filament.Name != "" } + // TryLoadFile attempts to load the configuration file from specified path on the file system. func (c *Config) TryLoadFile(file string) error { c.viper.SetConfigFile(file) diff --git a/pkg/event/event.go b/pkg/event/event.go index 0383f0c76..f376c6959 100644 --- a/pkg/event/event.go +++ b/pkg/event/event.go @@ -48,6 +48,8 @@ const ( // RuleSequenceOOOKey the presence of this metadata key indicates the // event in the partials list arrived out of order and requires reevaluation RuleSequenceOOOKey MetadataKey = "rule.seq.ooo" + // EvasionsKey represents the evasion behaviours detected on the event + EvasionsKey MetadataKey = "evasions" ) func (key MetadataKey) String() string { return string(key) } @@ -243,6 +245,17 @@ func (e *Event) AddMeta(k MetadataKey, v any) { e.Metadata[k] = v } +// AddSliceMetaOrAppend puts the provided string into the slice if the key +// doesn't exist or appends the string to the slice. +func (e *Event) AddSliceMetaOrAppend(k MetadataKey, s string) { + if e.ContainsMeta(k) { + v := append(e.GetMeta(k).([]string), s) + e.AddMeta(k, v) + } else { + e.AddMeta(k, []string{s}) + } +} + // RemoveMeta removes the event metadata index by given key. func (e *Event) RemoveMeta(k MetadataKey) { e.mmux.Lock() @@ -262,6 +275,13 @@ func (e *Event) GetMetaAsString(k MetadataKey) string { return "" } +// GetMeta returns the metadata for the given key. +func (e *Event) GetMeta(k MetadataKey) any { + e.mmux.RLock() + defer e.mmux.RUnlock() + return e.Metadata[k] +} + // ContainsMeta returns true if the metadata contains the specified key. func (e *Event) ContainsMeta(k MetadataKey) bool { e.mmux.RLock() diff --git a/pkg/ps/snapshotter_windows.go b/pkg/ps/snapshotter_windows.go index 0656076c6..d95b475ef 100644 --- a/pkg/ps/snapshotter_windows.go +++ b/pkg/ps/snapshotter_windows.go @@ -369,6 +369,7 @@ func (s *snapshotter) newProcState(pid, ppid uint32, e *event.Event) (*pstypes.P PID: pid, Ppid: ppid, Exe: e.GetParamAsString(params.Exe), + Name: filepath.Base(e.GetParamAsString(params.Exe)), TokenIntegrityLevel: e.GetParamAsString(params.ProcessIntegrityLevel), TokenElevationType: e.GetParamAsString(params.ProcessTokenElevationType), IsTokenElevated: e.Params.TryGetBool(params.ProcessTokenIsElevated),