Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions configs/fibratus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions internal/etw/_fixtures/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions internal/etw/_fixtures/indirect-syscall/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
SysWhishpers3
*.obj
*.asm
*.exe
syscalls.c
syscalls.h
7 changes: 7 additions & 0 deletions internal/etw/_fixtures/indirect-syscall/Makefile.msvc
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions internal/etw/_fixtures/indirect-syscall/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#include "syscalls.h"

#include <Windows.h>

int main(int argc, char* argv[])
{
Sw3NtSetContextThread(-1, NULL);
return 0;
}
30 changes: 23 additions & 7 deletions internal/etw/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
"syscall"
"testing"
Expand Down Expand Up @@ -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) {
Expand All @@ -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{
Expand Down Expand Up @@ -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))
Expand Down
9 changes: 7 additions & 2 deletions internal/evasion/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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")
}
20 changes: 15 additions & 5 deletions internal/evasion/direct_syscall.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
166 changes: 166 additions & 0 deletions internal/evasion/indirect_syscall.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading