From 2bc616508365a22c0bf07c439ca8140e92dad156 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Sun, 10 Aug 2025 19:18:49 +0200 Subject: [PATCH 1/2] fix(ps): Initialize process name on internal event arrival --- pkg/ps/snapshotter_windows.go | 1 + 1 file changed, 1 insertion(+) 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), From 3d8befa2da32c5bb639616f9400118cd1dab4fbc Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Mon, 11 Aug 2025 18:33:53 +0200 Subject: [PATCH 2/2] feat(evasion): Evasion scanner This changeset establishes the architecture for the evasion scanner. Evasion scanner piggybacks on top of different evasion detectors such as direct syscall (being the first evasion behaviour implemented). When the evasion behaviour is detected, the event is decorated with such evasion in its metadata. --- .github/PULL_REQUEST_TEMPLATE.md | 2 + .github/workflows/master.yml | 4 + .github/workflows/pr.yml | 4 + configs/fibratus.yml | 15 ++ internal/bootstrap/bootstrap.go | 19 ++- internal/etw/_fixtures/Taskfile.yml | 20 +++ .../etw/_fixtures/direct-syscall/.gitignore | 6 + .../_fixtures/direct-syscall/Makefile.msvc | 7 + internal/etw/_fixtures/direct-syscall/main.c | 9 ++ internal/etw/source_test.go | 123 +++++++++++++++ internal/evasion/config.go | 49 ++++++ internal/evasion/direct_syscall.go | 64 ++++++++ internal/evasion/direct_syscall_test.go | 141 ++++++++++++++++++ internal/evasion/scanner.go | 79 ++++++++++ internal/evasion/scanner_test.go | 74 +++++++++ internal/evasion/types.go | 50 +++++++ pkg/callstack/callstack.go | 18 +++ pkg/callstack/callstack_test.go | 6 + pkg/config/config.schema.json | 12 ++ pkg/config/config_windows.go | 16 ++ pkg/event/event.go | 20 +++ 21 files changed, 731 insertions(+), 7 deletions(-) create mode 100644 internal/etw/_fixtures/Taskfile.yml create mode 100644 internal/etw/_fixtures/direct-syscall/.gitignore create mode 100644 internal/etw/_fixtures/direct-syscall/Makefile.msvc create mode 100644 internal/etw/_fixtures/direct-syscall/main.c create mode 100644 internal/evasion/config.go create mode 100644 internal/evasion/direct_syscall.go create mode 100644 internal/evasion/direct_syscall_test.go create mode 100644 internal/evasion/scanner.go create mode 100644 internal/evasion/scanner_test.go create mode 100644 internal/evasion/types.go 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()