diff --git a/configs/fibratus.yml b/configs/fibratus.yml index 573a7146b..01a21b405 100644 --- a/configs/fibratus.yml +++ b/configs/fibratus.yml @@ -171,6 +171,11 @@ evasion: # skipping the NTDLL stub that normally performs the transition to kernel mode. #enable-direct-syscall: true + # Indicates if indirect syscall evasion detection is enabled. Indirect syscall evasion refers + # to executing the syscall instruction by diverting the execution flow into a legitimate, clean + # ntdll stub that performs the syscall on process behalf. + #enable-indirect-syscall: true + # =============================== Event =============================================== # The following settings control the state of the event. diff --git a/internal/etw/_fixtures/Taskfile.yml b/internal/etw/_fixtures/Taskfile.yml index 58fb07264..308d8c85a 100644 --- a/internal/etw/_fixtures/Taskfile.yml +++ b/internal/etw/_fixtures/Taskfile.yml @@ -15,6 +15,16 @@ tasks: - 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 + indirect-syscall: + desc: Builds the binary to perform indirect syscalls via Syswhispers generated stubs + dir: indirect-syscall + cmds: + - git clone {{ .SYSWHISPERS3_REPO }} + - python SysWhispers3/syswhispers.py -a x64 -c msvc -p common -m jumper_randomized -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 + - indirect-syscall diff --git a/internal/etw/_fixtures/indirect-syscall/.gitignore b/internal/etw/_fixtures/indirect-syscall/.gitignore new file mode 100644 index 000000000..321a5cb03 --- /dev/null +++ b/internal/etw/_fixtures/indirect-syscall/.gitignore @@ -0,0 +1,6 @@ +SysWhishpers3 +*.obj +*.asm +*.exe +syscalls.c +syscalls.h diff --git a/internal/etw/_fixtures/indirect-syscall/Makefile.msvc b/internal/etw/_fixtures/indirect-syscall/Makefile.msvc new file mode 100644 index 000000000..9172f08b3 --- /dev/null +++ b/internal/etw/_fixtures/indirect-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:indirect-syscall.exe -nologo $(LIBS) /MACHINE:X64 -subsystem:console -nodefaultlib syscalls-asm.x64.obj syscalls.obj main.obj diff --git a/internal/etw/_fixtures/indirect-syscall/main.c b/internal/etw/_fixtures/indirect-syscall/main.c new file mode 100644 index 000000000..ba12537ff --- /dev/null +++ b/internal/etw/_fixtures/indirect-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 0447a01af..3890658a0 100644 --- a/internal/etw/source_test.go +++ b/internal/etw/source_test.go @@ -26,6 +26,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strings" "syscall" "testing" @@ -1281,12 +1282,7 @@ func containsEvasion(e *event.Event, evasion string) bool { if !ok { return false } - for _, eva := range evas { - if eva == evasion { - return true - } - } - return false + return slices.Contains(evas, evasion) } func TestEvasionScanner(t *testing.T) { @@ -1311,6 +1307,21 @@ func TestEvasionScanner(t *testing.T) { }, false, }, + { + "indirect syscall", + func() error { + cmd := exec.Command("_fixtures/indirect-syscall/indirect-syscall.exe") + return cmd.Run() + }, + func(e *event.Event) bool { + if strings.Contains(strings.ToLower(e.Callstack.String()), strings.ToLower("indirect-syscall.exe")) && e.Type == event.SetThreadContext { + log.Info(e, e.Callstack) + return containsEvasion(e, "indirect_syscall") + } + return false + }, + false, + }, } evsConfig := config.EventSourceConfig{ @@ -1347,7 +1358,12 @@ func TestEvasionScanner(t *testing.T) { defer symbolizer.Close() evs.RegisterEventListener(symbolizer) - scanner := evasion.NewScanner(evasion.Config{Enabled: true, EnableDirectSyscall: true}) + scanner := evasion.NewScanner( + evasion.Config{ + Enabled: true, + EnableDirectSyscall: true, + EnableIndirectSyscall: true, + }) evs.RegisterEventListener(scanner) require.NoError(t, evs.Open(cfg)) diff --git a/internal/evasion/config.go b/internal/evasion/config.go index 4e0d899a5..8832553f1 100644 --- a/internal/evasion/config.go +++ b/internal/evasion/config.go @@ -24,8 +24,9 @@ import ( ) const ( - enabled = "evasion.enabled" - enableDirectSyscall = "evasion.enable-direct-syscall" + enabled = "evasion.enabled" + enableDirectSyscall = "evasion.enable-direct-syscall" + enableIndirectSyscall = "evasion.enable-indirect-syscall" ) // Config contains the settings that influence the behaviour of the evasion scanner. @@ -34,16 +35,20 @@ type Config struct { 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"` + // EnableIndirectSyscall indicates if indirect syscall evasion detection is enabled. + EnableIndirectSyscall bool `json:"enable-indirect-syscall" yaml:"enable-indirect-syscall"` } // InitFromViper initializes evasion config from Viper. func (c *Config) InitFromViper(v *viper.Viper) { c.Enabled = v.GetBool(enabled) c.EnableDirectSyscall = v.GetBool(enableDirectSyscall) + c.EnableIndirectSyscall = v.GetBool(enableIndirectSyscall) } // 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") + flags.Bool(enableIndirectSyscall, true, "Indicates if indirect syscall evasion detection is enabled") } diff --git a/internal/evasion/direct_syscall.go b/internal/evasion/direct_syscall.go index 57b52c6ad..e7ef61a9c 100644 --- a/internal/evasion/direct_syscall.go +++ b/internal/evasion/direct_syscall.go @@ -19,9 +19,10 @@ package evasion import ( - "github.com/rabbitstack/fibratus/pkg/event" "path/filepath" "strings" + + "github.com/rabbitstack/fibratus/pkg/event" ) // directSyscall direct syscall evasion refers to a technique where @@ -54,11 +55,20 @@ func (d *directSyscall) Eval(e *event.Event) (bool, error) { mod := filepath.Base(strings.ToLower(frame.Module)) - // check if the last userspace frame is originated + // check if the last user space 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 + // likely invoking a direct syscall + switch mod { + case "ntdll.dll", "win32.dll", "win32u.dll", "wow64win.dll", "wow64cpu.dll": + return false, nil + default: + return true, nil + } } -func (d *directSyscall) Type() Type { return DirectSyscall } +func (*directSyscall) Type() Type { return DirectSyscall } + +func (*directSyscall) SetMask(e *event.Event) { + e.Evasions |= uint32(DirectSyscall) +} diff --git a/internal/evasion/indirect_syscall.go b/internal/evasion/indirect_syscall.go new file mode 100644 index 000000000..a339b7e45 --- /dev/null +++ b/internal/evasion/indirect_syscall.go @@ -0,0 +1,166 @@ +/* + * 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 ( + "path/filepath" + "strings" + "unsafe" + + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/sys" + "github.com/rabbitstack/fibratus/pkg/util/va" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows" +) + +var syscallStubs = map[event.Type]string{ + event.CreateProcess: "NtCreateUserProcess", + event.CreateThread: "NtCreateThreadEx", + event.TerminateThread: "NtTerminateThread", + event.RegCreateKey: "NtCreateKey", + event.RegDeleteKey: "NtDeleteKey", + event.RegSetValue: "NtSetValueKey", + event.RegDeleteValue: "NtDeleteValueKey", + event.SetThreadContext: "NtSetContextThread", + event.OpenProcess: "NtOpenProcess", + event.OpenThread: "NtOpenThread", + event.VirtualAlloc: "NtAllocateVirtualMemory", + event.CreateFile: "NtCreateFile", + event.DeleteFile: "NtDeleteFile", + event.RenameFile: "NtSetInformationFile", + event.CreateSymbolicLinkObject: "NtCreateSymbolicLinkObject", +} + +const syscallStubLength = 23 + +// indirectSyscall evasion refers to executing the syscall instruction by +// diverting the execution flow into a legitimate, clean ntdll stub that +// performs the syscall on process behalf. +// +// This achieves code origin legitimacy, since the execution lands in .text +// of a signed Microsoft module (ntdll.dll). Stack frames look identical to +// a normal API call, which achieves call stack normalization. +type indirectSyscall struct { + offsets map[event.Type]uintptr // stores expected syscall stub offsets +} + +func NewIndirectSyscall() Evasion { + return &indirectSyscall{} +} + +func (i *indirectSyscall) tryResolveSyscallStubOffsets(e *event.Event) error { + if i.offsets != nil { + return nil + } + + var ntdllBase va.Address + if e.PS != nil { + for _, mod := range e.PS.Modules { + if mod.IsNTDLL() { + ntdllBase = mod.BaseAddress + } + } + } + + if ntdllBase.IsZero() { + return nil + } + + var handle windows.Handle + if err := windows.GetModuleHandleEx(sys.ModuleHandleFromAddress, (*uint16)(unsafe.Pointer(ntdllBase.Uintptr())), &handle); err != nil { + return err + } + defer windows.Close(handle) + + i.offsets = make(map[event.Type]uintptr) + + for evt, stub := range syscallStubs { + addr, err := windows.GetProcAddress(handle, stub) + if err != nil { + log.Warnf("unable to get procedure address for %s: %v", evt, err) + continue + } + i.offsets[evt] = addr - ntdllBase.Uintptr() + log.Debugf("syscall stub %s resolved to address %x and offset %d", evt, addr, i.offsets[evt]) + } + + return nil +} + +func (i *indirectSyscall) Eval(e *event.Event) (bool, error) { + if err := i.tryResolveSyscallStubOffsets(e); err != nil { + return false, err + } + if e.Callstack.IsEmpty() { + return false, nil + } + + frame := e.Callstack.FinalUserspaceFrame() + if frame == nil { + return false, nil + } + + if frame.IsUnbacked() { + return false, nil + } + + sym := frame.Symbol + mod := filepath.Base(strings.ToLower(frame.Module)) + + if mod != "ntdll.dll" { + // only check ntdll syscall stubs + return false, nil + } + + // eliminate common false positives (there are + // many other false positives that can be directly + // tuned in the rules) + switch { + case e.IsCreateProcess() && sym == "ZwDeviceIoControlFile" && e.Callstack.ContainsSymbol("AttachConsole"): + return false, nil + case e.IsCreateThread() && (sym == "ZwSetInformationWorkerFactory" || sym == "ZwReleaseWorkerFactoryWorker"): + return false, nil + case e.IsOpenThread() && sym == "ZwAlpcOpenSenderThread": + return false, nil + case e.IsOpenProcess() && sym == "ZwAlpcOpenSenderProcess": + return false, nil + case e.IsCreateFile() && (sym == "ZwOpenFile" || sym == "NtOpenFile" || sym == "ZwQueryAttributesFile" || sym == "ZwQueryFullAttributesFile" || sym == "ZwQueryInformationByName" || sym == "ZwQuerySystemInformation"): + return false, nil + case e.IsDeleteFile() && (sym == "ZwSetInformationFile" && (e.Callstack.ContainsSymbol("DeleteFileA") || e.Callstack.ContainsSymbol("DeleteFileW"))): + return false, nil + case e.IsRegCreateKey() && sym == "ZwDeviceIoControlFile" && e.Callstack.ContainsSymbol("DllUnregisterServer"): + return false, nil + } + + exp, ok := i.offsets[e.Type] + if !ok { + return false, nil + } + curr := frame.Addr.Dec(uint64(frame.ModuleAddress)).Uintptr() + + //nolint:staticcheck + return !(curr > exp && curr <= exp+syscallStubLength), nil +} + +func (*indirectSyscall) Type() Type { return IndirectSyscall } + +func (*indirectSyscall) SetMask(e *event.Event) { + e.Evasions |= uint32(IndirectSyscall) +} diff --git a/internal/evasion/indirect_syscall_test.go b/internal/evasion/indirect_syscall_test.go new file mode 100644 index 000000000..7b07a655d --- /dev/null +++ b/internal/evasion/indirect_syscall_test.go @@ -0,0 +1,156 @@ +/* + * 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 ( + "testing" + "time" + "unsafe" + + "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" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/rabbitstack/fibratus/pkg/sys" + "github.com/rabbitstack/fibratus/pkg/util/va" + "github.com/stretchr/testify/require" + "golang.org/x/sys/windows" +) + +func TestIndirectSyscall(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}, + }, + PS: &pstypes.PS{ + Modules: []pstypes.Module{ + {Name: "C:\\Windows\\System32\\ntdll.dll", Size: 32358, Checksum: 23123343, BaseAddress: getNtdllAddress(), DefaultBaseAddress: getNtdllAddress()}, + {Name: "C:\\Windows\\System32\\kernel32.dll", Size: 12354, Checksum: 23123343, BaseAddress: va.Address(0x7ffb5c1d0126), DefaultBaseAddress: va.Address(0x7ffb5c1d0126)}, + {Name: "C:\\Windows\\System32\\user32.dll", Size: 212354, Checksum: 33123343, BaseAddress: va.Address(0x7ffb5d8e11c4), DefaultBaseAddress: va.Address(0x7ffb5d8e11c4)}, + }, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: getNtdllProcAddress("NtCreateUserProcess"), ModuleAddress: getNtdllAddress(), Module: "C:\\Windows\\System32\\ntdll.dll", Symbol: "ZwCreateFile"}, + 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.SetThreadContext, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "SetThreadContext", + Timestamp: time.Now(), + Category: event.Thread, + PS: &pstypes.PS{ + Modules: []pstypes.Module{ + {Name: "C:\\Windows\\System32\\ntdll.dll", Size: 32358, Checksum: 23123343, BaseAddress: getNtdllAddress(), DefaultBaseAddress: getNtdllAddress()}, + {Name: "C:\\Windows\\System32\\kernel32.dll", Size: 12354, Checksum: 23123343, BaseAddress: va.Address(0x7ffb5c1d0126), DefaultBaseAddress: va.Address(0x7ffb5c1d0126)}, + {Name: "C:\\Windows\\System32\\user32.dll", Size: 212354, Checksum: 33123343, BaseAddress: va.Address(0x7ffb5d8e11c4), DefaultBaseAddress: va.Address(0x7ffb5d8e11c4)}, + }, + }, + Callstack: callstackFromFrames( + callstack.Frame{Addr: 0xf259de, Module: "unbacked", Symbol: "?"}, + callstack.Frame{Addr: 0x7ffe4fda6e3b, Module: "C:\\Windows\\System32\\KernelBase.dll", Symbol: "SetThreadContext"}, + callstack.Frame{Addr: getNtdllProcAddress("ZwSetContextThread") + 20, ModuleAddress: getNtdllAddress(), 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: getNtdllProcAddress("ZwQueryAttributesFile"), ModuleAddress: getNtdllAddress(), Module: "C:\\Windows\\System32\\ntdll.dll", Symbol: "ZwQueryAttributesFile"}, + 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 := NewIndirectSyscall() + matches, err := eva.Eval(tt.evt) + require.NoError(t, err) + require.Equal(t, tt.matches, matches) + }) + } +} + +func getNtdllAddress() va.Address { + var moduleHandles [1024]windows.Handle + var cbNeeded uint32 + proc, err := windows.OpenProcess(windows.PROCESS_ALL_ACCESS, false, windows.GetCurrentProcessId()) + if err != nil { + return 0 + } + if err := windows.EnumProcessModules(proc, &moduleHandles[0], 1024, &cbNeeded); err != nil { + return 0 + } + moduleHandle := moduleHandles[1] + var moduleInfo windows.ModuleInfo + if err := windows.GetModuleInformation(proc, moduleHandle, &moduleInfo, uint32(unsafe.Sizeof(moduleInfo))); err != nil { + return 0 + } + return va.Address(moduleInfo.BaseOfDll) +} + +func getNtdllProcAddress(procname string) va.Address { + var handle windows.Handle + if err := windows.GetModuleHandleEx(sys.ModuleHandleFromAddress, (*uint16)(unsafe.Pointer(getNtdllAddress().Uintptr())), &handle); err != nil { + panic(err) + } + addr, err := windows.GetProcAddress(handle, procname) + if err != nil { + panic(err) + } + defer windows.CloseHandle(handle) + return va.Address(addr) +} diff --git a/internal/evasion/scanner.go b/internal/evasion/scanner.go index 00c28a901..e1eb08ad6 100644 --- a/internal/evasion/scanner.go +++ b/internal/evasion/scanner.go @@ -19,10 +19,14 @@ package evasion import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/event" log "github.com/sirupsen/logrus" ) +var evasionsCount expvar.Map + // Scanner is responsible for evaluating evasion detectors // and decorating the event with the reported behaviours. // Some behaviours represent strong IoCs, while other need @@ -31,16 +35,21 @@ import ( // that yields the evasion techniques, such as direct syscall. type Scanner struct { evasions []Evasion + config Config } // NewScanner instantiates the new evasion scanner. func NewScanner(config Config) *Scanner { s := &Scanner{ evasions: make([]Evasion, 0), + config: config, } if config.EnableDirectSyscall { - s.registerEvasion(NewDirectSyscall()) + s.addEvasion(NewDirectSyscall()) + } + if config.EnableIndirectSyscall { + s.addEvasion(NewIndirectSyscall()) } return s @@ -57,15 +66,17 @@ func (s *Scanner) ProcessEvent(e *event.Event) (bool, error) { var enq bool // run registered evasion detectors - for _, eva := range s.evasions { - matches, err := eva.Eval(e) + for _, evasion := range s.evasions { + matches, err := evasion.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) + evasion.SetMask(e) + evasionsCount.Add(evasion.Type().String(), 1) + e.AddOrAppendMetaSlice(event.EvasionsKey, evasion.Type().String()) + log.Debugf("detected evasion %q on event [%s] and callstack [%s]", evasion.Type(), e, e.Callstack) } } @@ -74,6 +85,8 @@ func (s *Scanner) ProcessEvent(e *event.Event) (bool, error) { func (s *Scanner) CanEnqueue() bool { return false } -func (s *Scanner) registerEvasion(eva Evasion) { - s.evasions = append(s.evasions, eva) +func (s *Scanner) addEvasion(evasion Evasion) { + if s.config.Enabled { + s.evasions = append(s.evasions, evasion) + } } diff --git a/internal/evasion/scanner_test.go b/internal/evasion/scanner_test.go index 36205511b..1cf73a887 100644 --- a/internal/evasion/scanner_test.go +++ b/internal/evasion/scanner_test.go @@ -19,14 +19,15 @@ package evasion import ( + "strings" + "testing" + "time" + "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) { @@ -66,6 +67,7 @@ func TestScannerProcessEvent(t *testing.T) { require.NoError(t, err) require.True(t, matches && len(tt.expectedEvasions) > 0) if len(tt.expectedEvasions) > 0 { + require.True(t, tt.evt.Evasions&uint32(DirectSyscall) != 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 index dc06a0e81..9988cb850 100644 --- a/internal/evasion/types.go +++ b/internal/evasion/types.go @@ -21,11 +21,13 @@ package evasion import "github.com/rabbitstack/fibratus/pkg/event" // Type is the alias for the evasion technique type. -type Type uint8 +type Type uint32 const ( // DirectSyscall represents the direct syscall evasion. - DirectSyscall Type = iota + DirectSyscall Type = 1 << iota + // IndirectSyscall represents the indirect syscall evasion. + IndirectSyscall ) // String returns the evasion human-friendly name. @@ -33,6 +35,8 @@ func (t Type) String() string { switch t { case DirectSyscall: return "direct_syscall" + case IndirectSyscall: + return "indirect_syscall" default: return "unknown" } @@ -42,9 +46,11 @@ func (t Type) String() string { 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. + // performed on behalf of the process. If the evasion is recognized, this + // method returns true, or false otherwise. Eval(*event.Event) (bool, error) // Type returns the type of the evasion technique. Type() Type + // SetMask sets the type in the event evasions bitmask. + SetMask(*event.Event) } diff --git a/pkg/callstack/callstack.go b/pkg/callstack/callstack.go index 934a2650b..b548180d6 100644 --- a/pkg/callstack/callstack.go +++ b/pkg/callstack/callstack.go @@ -19,13 +19,14 @@ package callstack import ( - "github.com/rabbitstack/fibratus/pkg/util/va" - "golang.org/x/arch/x86/x86asm" - "golang.org/x/sys/windows" "os" "path/filepath" "strconv" "strings" + + "github.com/rabbitstack/fibratus/pkg/util/va" + "golang.org/x/arch/x86/x86asm" + "golang.org/x/sys/windows" ) // unbacked represents the identifier for unbacked regions in stack frames @@ -60,6 +61,7 @@ func (f *Frame) AllocationSize(proc windows.Handle) uint64 { } r := va.VirtualQuery(proc, f.Addr.Uint64()) + if r == nil || (r.State != windows.MEM_COMMIT || r.Protect == windows.PAGE_NOACCESS || r.Type != va.MemImage) { return 0 } @@ -311,6 +313,16 @@ func (s Callstack) ContainsUnbacked() bool { return false } +// ContainsSymbol checks if the supplied symbol name is present in the callstack. +func (s Callstack) ContainsSymbol(sym string) bool { + for _, frame := range s { + if frame.Symbol == sym { + return true + } + } + return false +} + // Addresses returns stack retrun addresses. func (s Callstack) Addresses() []string { addrs := make([]string, len(s)) diff --git a/pkg/config/config.schema.json b/pkg/config/config.schema.json index ffe77ef80..1cdf2868e 100644 --- a/pkg/config/config.schema.json +++ b/pkg/config/config.schema.json @@ -300,6 +300,9 @@ }, "enable-direct-syscall": { "type": "boolean" + }, + "enable-indirect-syscall": { + "type": "boolean" } }, "additionalProperties": false diff --git a/pkg/event/event.go b/pkg/event/event.go index f376c6959..def2eb67b 100644 --- a/pkg/event/event.go +++ b/pkg/event/event.go @@ -20,13 +20,14 @@ package event import ( "fmt" + "strings" + "sync" + "time" + "github.com/rabbitstack/fibratus/pkg/callstack" capver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/rabbitstack/fibratus/pkg/event/params" pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" - "strings" - "sync" - "time" ) // TimestampFormat is the Go valid format for the event timestamp @@ -73,6 +74,8 @@ type Event struct { PID uint32 `json:"pid"` // Tid is the thread identifier of the thread that generated the event. Tid uint32 `json:"tid"` + // Evasions is the bitmask that stores detected evasion types on this event. + Evasions uint32 `json:"-"` // Type is the internal representation of the event. This field should be ignored by serializers. Type Type `json:"-"` // CPU designates the processor logical core where the event was originated. @@ -245,9 +248,9 @@ func (e *Event) AddMeta(k MetadataKey, v any) { e.Metadata[k] = v } -// AddSliceMetaOrAppend puts the provided string into the slice if the key +// AddOrAppendMetaSlice 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) { +func (e *Event) AddOrAppendMetaSlice(k MetadataKey, s string) { if e.ContainsMeta(k) { v := append(e.GetMeta(k).([]string), s) e.AddMeta(k, v) diff --git a/pkg/event/event_windows.go b/pkg/event/event_windows.go index 70ffb16ad..4308ce2f0 100644 --- a/pkg/event/event_windows.go +++ b/pkg/event/event_windows.go @@ -234,12 +234,15 @@ func (e *Event) IsImageRundown() bool { return e.Type == ImageRundown func (e *Event) IsFileOpEnd() bool { return e.Type == FileOpEnd } func (e *Event) IsRegSetValue() bool { return e.Type == RegSetValue } func (e *Event) IsRegSetValueInternal() bool { return e.Type == RegSetValueInternal } +func (e *Event) IsRegCreateKey() bool { return e.Type == RegCreateKey } func (e *Event) IsProcessRundown() bool { return e.Type == ProcessRundown } func (e *Event) IsProcessRundownInternal() bool { return e.Type == ProcessRundownInternal } func (e *Event) IsVirtualAlloc() bool { return e.Type == VirtualAlloc } func (e *Event) IsMapViewFile() bool { return e.Type == MapViewFile } func (e *Event) IsUnmapViewFile() bool { return e.Type == UnmapViewFile } func (e *Event) IsStackWalk() bool { return e.Type == StackWalk } +func (e *Event) IsOpenThread() bool { return e.Type == OpenThread } +func (e *Event) IsOpenProcess() bool { return e.Type == OpenProcess } // InvalidPid indicates if the process generating the event is invalid. func (e *Event) InvalidPid() bool { return e.PID == sys.InvalidProcessID } diff --git a/pkg/filter/accessor.go b/pkg/filter/accessor.go index bac4de3a8..f1a4c0063 100644 --- a/pkg/filter/accessor.go +++ b/pkg/filter/accessor.go @@ -20,12 +20,14 @@ package filter import ( "errors" - "github.com/rabbitstack/fibratus/pkg/event" - "github.com/rabbitstack/fibratus/pkg/event/params" - "github.com/rabbitstack/fibratus/pkg/filter/fields" "net" "reflect" "time" + + "github.com/rabbitstack/fibratus/internal/evasion" + "github.com/rabbitstack/fibratus/pkg/event" + "github.com/rabbitstack/fibratus/pkg/event/params" + "github.com/rabbitstack/fibratus/pkg/filter/fields" ) var ( @@ -136,6 +138,10 @@ func (*evtAccessor) Get(f Field, evt *event.Event) (params.Value, error) { default: return evt.GetParamAsString(name), nil } + case fields.EvtIsDirectSyscall: + return evt.Evasions&uint32(evasion.DirectSyscall) != 0, nil + case fields.EvtIsIndirectSyscall: + return evt.Evasions&uint32(evasion.IndirectSyscall) != 0, nil } return nil, nil diff --git a/pkg/filter/fields/fields_windows.go b/pkg/filter/fields/fields_windows.go index 435aa660a..d4e0e021f 100644 --- a/pkg/filter/fields/fields_windows.go +++ b/pkg/filter/fields/fields_windows.go @@ -362,6 +362,12 @@ const ( EvtNparams Field = "evt.nparams" // EvtArg represents the field sequence for generic argument access EvtArg Field = "evt.arg" + // EvtIsDirectSyscall represents the field that designates if this event is + // performing a direct syscall. + EvtIsDirectSyscall Field = "evt.is_direct_syscall" + // EvtIsIndirectSyscall represents the field that designates if this event is + // performing an indirect syscall. + EvtIsIndirectSyscall Field = "evt.is_indirect_syscall" // KevtSeq is the event sequence number KevtSeq Field = "kevt.seq" @@ -839,6 +845,9 @@ var fields = map[Field]FieldInfo{ } return true }}}, + EvtIsDirectSyscall: {EvtIsDirectSyscall, "indicates if the event is performing a direct syscall", params.Bool, []string{"evt.is_direct_syscall = true"}, nil, nil}, + EvtIsIndirectSyscall: {EvtIsIndirectSyscall, "indicates if the event is performing an indirect syscall", params.Bool, []string{"evt.is_indirect_syscall = true"}, nil, nil}, + KevtSeq: {KevtSeq, "event sequence number", params.Uint64, []string{"kevt.seq > 666"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtSeq}}, nil}, KevtPID: {KevtPID, "process identifier generating the event", params.Uint32, []string{"kevt.pid = 6"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtPID}}, nil}, KevtTID: {KevtTID, "thread identifier generating the event", params.Uint32, []string{"kevt.tid = 1024"}, &Deprecation{Since: "3.0.0", Fields: []Field{EvtTID}}, nil}, diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index f889b0d4f..8e6af3833 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -19,7 +19,15 @@ package filter import ( + "net" + "os" + "path/filepath" + "testing" + "time" + "unsafe" + "github.com/rabbitstack/fibratus/internal/etw/processors" + "github.com/rabbitstack/fibratus/internal/evasion" "github.com/rabbitstack/fibratus/pkg/callstack" "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/event" @@ -36,12 +44,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/sys/windows" - "net" - "os" - "path/filepath" - "testing" - "time" - "unsafe" ) var cfg = &config.Config{ @@ -747,6 +749,7 @@ func TestEventFilter(t *testing.T) { Category: event.File, Host: "archrabbit", Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Evasions: uint32(evasion.IndirectSyscall), Params: event.Params{ params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(3434)}, params.FileObject: {Name: params.FileObject, Type: params.Uint64, Value: uint64(12456738026482168384)}, @@ -775,6 +778,8 @@ func TestEventFilter(t *testing.T) { {`evt.arg[file_path] = '\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll'`, true}, {`evt.arg[type] = 'file'`, true}, {`evt.arg[pid] = 3434`, true}, + {`evt.is_direct_syscall = false`, true}, + {`evt.is_indirect_syscall`, true}, {`evt.desc contains 'Creates or opens a new file'`, true}, diff --git a/pkg/ps/types/types_windows.go b/pkg/ps/types/types_windows.go index 3d22d484f..fc1a78f68 100644 --- a/pkg/ps/types/types_windows.go +++ b/pkg/ps/types/types_windows.go @@ -346,6 +346,9 @@ func (m Module) String() string { // IsExecutable determines if the loaded module is an executable. func (m Module) IsExecutable() bool { return strings.ToLower(filepath.Ext(m.Name)) == ".exe" } +// IsNTDLL determines if the module is the native ntdll module. +func (m Module) IsNTDLL() bool { return strings.EqualFold(filepath.Base(m.Name), "ntdll.dll") } + // Mmap stores information related to the memory mapping. type Mmap struct { // BaseAddress represents the address where the view of section is mapped. diff --git a/pkg/symbolize/symbolizer.go b/pkg/symbolize/symbolizer.go index a62132254..f90bdfacd 100644 --- a/pkg/symbolize/symbolizer.go +++ b/pkg/symbolize/symbolizer.go @@ -21,6 +21,12 @@ package symbolize import ( "expvar" "fmt" + "path/filepath" + "slices" + "strings" + "sync" + "time" + "github.com/rabbitstack/fibratus/pkg/callstack" "github.com/rabbitstack/fibratus/pkg/config" "github.com/rabbitstack/fibratus/pkg/event" @@ -34,11 +40,6 @@ import ( "github.com/rabbitstack/fibratus/pkg/util/va" log "github.com/sirupsen/logrus" "golang.org/x/sys/windows" - "path/filepath" - "slices" - "strings" - "sync" - "time" ) // ErrSymInitialize is thrown if the process symbol handler fails to initialize @@ -108,8 +109,9 @@ type module struct { } type syminfo struct { - module string - symbol string + module string + symbol string + moduleAddress va.Address // base module address } func (m *module) keepalive() { @@ -453,7 +455,7 @@ func (s *Symbolizer) produceFrame(addr va.Address, e *event.Event) callstack.Fra if sym, ok := s.symbols[e.PID]; ok { if symbol, ok := sym[addr]; ok { symCacheHits.Add(1) - frame.Module, frame.Symbol = symbol.module, symbol.symbol + frame.Module, frame.Symbol, frame.ModuleAddress = symbol.module, symbol.symbol, symbol.moduleAddress return frame } } @@ -571,11 +573,11 @@ func (s *Symbolizer) cacheSymbol(pid uint32, addr va.Address, frame *callstack.F if sym, ok := s.symbols[pid]; ok { if _, ok := sym[addr]; !ok { symCachedSymbols.Add(1) - s.symbols[pid][addr] = syminfo{module: frame.Module, symbol: frame.Symbol} + s.symbols[pid][addr] = syminfo{module: frame.Module, symbol: frame.Symbol, moduleAddress: frame.ModuleAddress} } } else { symCachedSymbols.Add(1) - s.symbols[pid] = map[va.Address]syminfo{addr: {module: frame.Module, symbol: frame.Symbol}} + s.symbols[pid] = map[va.Address]syminfo{addr: {module: frame.Module, symbol: frame.Symbol, moduleAddress: frame.ModuleAddress}} } } diff --git a/pkg/sys/process.go b/pkg/sys/process.go index 04ad9b853..08af5ea9f 100644 --- a/pkg/sys/process.go +++ b/pkg/sys/process.go @@ -19,9 +19,10 @@ package sys import ( + "unsafe" + "golang.org/x/sys/windows" "golang.org/x/sys/windows/svc" - "unsafe" ) const ( @@ -31,6 +32,11 @@ const ( ProcessStatusStillActive uint32 = 259 ) +// ModuleHandleFromAddress is the flag of the GetModuleHandleEx +// function parameter that indicates the module handle is obtained +// from the address +const ModuleHandleFromAddress = 0x00000004 + // ProcessProtectionInformation is the information class that returns a // value indicating the type of protected process and the protected process // signer.