Skip to content

Commit 8c917ad

Browse files
committed
feat(evasion): Implement indirect syscall evasion scanner
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. 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.
1 parent cba2e74 commit 8c917ad

File tree

15 files changed

+428
-19
lines changed

15 files changed

+428
-19
lines changed

configs/fibratus.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ evasion:
171171
# skipping the NTDLL stub that normally performs the transition to kernel mode.
172172
#enable-direct-syscall: true
173173

174+
# Indicates if indirect syscall evasion detection is enabled. Indirect syscall evasion refers
175+
# to executing the syscall instruction by diverting the execution flow into a legitimate, clean
176+
# ntdll stub that performs the syscall on process behalf.
177+
#enable-indirect-syscall: true
178+
174179
# =============================== Event ===============================================
175180

176181
# The following settings control the state of the event.

internal/etw/_fixtures/Taskfile.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ tasks:
1515
- cmd.exe /c 'C:\"Program Files"\"Microsoft Visual Studio"\{{ .VISUAL_STUDIO_VERSION }}\{{ .VISUAL_STUDIO_EDITION }}\VC\Auxiliary\Build\vcvars64.bat && nmake -f Makefile.msvc'
1616
silent: true
1717

18+
indirect-syscall:
19+
desc: Builds the binary to perform indirect syscalls via Syswhispers generated stubs
20+
dir: indirect-syscall
21+
cmds:
22+
- git clone {{ .SYSWHISPERS3_REPO }}
23+
- python SysWhispers3/syswhispers.py -a x64 -c msvc -p common -m jumper_randomized -o syscalls
24+
- cmd.exe /c 'C:\"Program Files"\"Microsoft Visual Studio"\{{ .VISUAL_STUDIO_VERSION }}\{{ .VISUAL_STUDIO_EDITION }}\VC\Auxiliary\Build\vcvars64.bat && nmake -f Makefile.msvc'
25+
silent: true
26+
1827
all:
1928
deps:
2029
- direct-syscall
30+
- indirect-syscall
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
SysWhishpers3
2+
*.obj
3+
*.asm
4+
*.exe
5+
syscalls.c
6+
syscalls.h
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
OPTIONS = -Zp8 -c -nologo -Gy -Os -O1 -GR- -EHa -Oi -GS-
2+
LIBS = libvcruntime.lib libcmt.lib ucrt.lib kernel32.lib
3+
4+
main:
5+
ML64 /c syscalls-asm.x64.asm /link /NODEFAULTLIB /RELEASE /MACHINE:X64
6+
cl.exe $(OPTIONS) syscalls.c main.c
7+
link.exe /OUT:indirect-syscall.exe -nologo $(LIBS) /MACHINE:X64 -subsystem:console -nodefaultlib syscalls-asm.x64.obj syscalls.obj main.obj
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#include "syscalls.h"
2+
3+
#include <Windows.h>
4+
5+
int main(int argc, char* argv[])
6+
{
7+
Sw3NtSetContextThread(-1, NULL);
8+
return 0;
9+
}

internal/etw/source_test.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"os/exec"
2727
"path/filepath"
2828
"runtime"
29+
"slices"
2930
"strings"
3031
"syscall"
3132
"testing"
@@ -1281,12 +1282,7 @@ func containsEvasion(e *event.Event, evasion string) bool {
12811282
if !ok {
12821283
return false
12831284
}
1284-
for _, eva := range evas {
1285-
if eva == evasion {
1286-
return true
1287-
}
1288-
}
1289-
return false
1285+
return slices.Contains(evas, evasion)
12901286
}
12911287

12921288
func TestEvasionScanner(t *testing.T) {
@@ -1311,6 +1307,21 @@ func TestEvasionScanner(t *testing.T) {
13111307
},
13121308
false,
13131309
},
1310+
{
1311+
"indirect syscall",
1312+
func() error {
1313+
cmd := exec.Command("_fixtures/indirect-syscall/indirect-syscall.exe")
1314+
return cmd.Run()
1315+
},
1316+
func(e *event.Event) bool {
1317+
if strings.Contains(strings.ToLower(e.Callstack.String()), strings.ToLower("indirect-syscall.exe")) && e.Type == event.SetThreadContext {
1318+
log.Info(e, e.Callstack)
1319+
return containsEvasion(e, "indirect_syscall")
1320+
}
1321+
return false
1322+
},
1323+
false,
1324+
},
13141325
}
13151326

13161327
evsConfig := config.EventSourceConfig{
@@ -1347,7 +1358,12 @@ func TestEvasionScanner(t *testing.T) {
13471358
defer symbolizer.Close()
13481359
evs.RegisterEventListener(symbolizer)
13491360

1350-
scanner := evasion.NewScanner(evasion.Config{Enabled: true, EnableDirectSyscall: true})
1361+
scanner := evasion.NewScanner(
1362+
evasion.Config{
1363+
Enabled: true,
1364+
EnableDirectSyscall: true,
1365+
EnableIndirectSyscall: true,
1366+
})
13511367
evs.RegisterEventListener(scanner)
13521368

13531369
require.NoError(t, evs.Open(cfg))

internal/evasion/config.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ import (
2424
)
2525

2626
const (
27-
enabled = "evasion.enabled"
28-
enableDirectSyscall = "evasion.enable-direct-syscall"
27+
enabled = "evasion.enabled"
28+
enableDirectSyscall = "evasion.enable-direct-syscall"
29+
enableIndirectSyscall = "evasion.enable-indirect-syscall"
2930
)
3031

3132
// Config contains the settings that influence the behaviour of the evasion scanner.
@@ -34,16 +35,20 @@ type Config struct {
3435
Enabled bool `json:"enabled" yaml:"enabled"`
3536
// EnableDirectSyscall indicates if direct syscall evasion detection is enabled.
3637
EnableDirectSyscall bool `json:"enable-direct-syscall" yaml:"enable-direct-syscall"`
38+
// EnableIndirectSyscall indicates if indirect syscall evasion detection is enabled.
39+
EnableIndirectSyscall bool `json:"enable-indirect-syscall" yaml:"enable-indirect-syscall"`
3740
}
3841

3942
// InitFromViper initializes evasion config from Viper.
4043
func (c *Config) InitFromViper(v *viper.Viper) {
4144
c.Enabled = v.GetBool(enabled)
4245
c.EnableDirectSyscall = v.GetBool(enableDirectSyscall)
46+
c.EnableIndirectSyscall = v.GetBool(enableIndirectSyscall)
4347
}
4448

4549
// AddFlags adds evasion config flags to the set.
4650
func AddFlags(flags *pflag.FlagSet) {
4751
flags.Bool(enabled, true, "Indicates if evasion detections are enabled global-wise")
4852
flags.Bool(enableDirectSyscall, true, "Indicates if direct syscall evasion detection is enabled")
53+
flags.Bool(enableIndirectSyscall, true, "Indicates if indirect syscall evasion detection is enabled")
4954
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2021-present by Nedim Sabic Sabic
3+
* https://www.fibratus.io
4+
* All Rights Reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package evasion
20+
21+
import (
22+
"path/filepath"
23+
"strings"
24+
"unsafe"
25+
26+
"github.com/rabbitstack/fibratus/pkg/event"
27+
"github.com/rabbitstack/fibratus/pkg/sys"
28+
"github.com/rabbitstack/fibratus/pkg/util/va"
29+
log "github.com/sirupsen/logrus"
30+
"golang.org/x/sys/windows"
31+
)
32+
33+
var syscallStubs = map[event.Type]string{
34+
event.CreateProcess: "NtCreateUserProcess",
35+
event.CreateThread: "NtCreateThreadEx",
36+
event.TerminateThread: "NtTerminateThread",
37+
event.RegCreateKey: "NtCreateKey",
38+
event.RegDeleteKey: "NtDeleteKey",
39+
event.RegSetValue: "NtSetValueKey",
40+
event.RegDeleteValue: "NtDeleteValueKey",
41+
event.SetThreadContext: "NtSetContextThread",
42+
event.OpenProcess: "NtOpenProcess",
43+
event.OpenThread: "NtOpenThread",
44+
event.VirtualAlloc: "NtAllocateVirtualMemory",
45+
event.CreateFile: "NtCreateFile",
46+
event.DeleteFile: "NtDeleteFile",
47+
event.RenameFile: "NtSetInformationFile",
48+
event.CreateSymbolicLinkObject: "NtCreateSymbolicLinkObject",
49+
}
50+
51+
const syscallStubLength = 23
52+
53+
// indirectSyscall evasion refers to executing the syscall instruction by
54+
// diverting the execution flow into a legitimate, clean ntdll stub that
55+
// performs the syscall on process behalf.
56+
//
57+
// This achieves code origin legitimacy, since the execution lands in .text
58+
// of a signed Microsoft module (ntdll.dll). Stack frames look identical to
59+
// a normal API call, which achieves call stack normalization.
60+
type indirectSyscall struct {
61+
offsets map[event.Type]uintptr // stores expected syscall stub offsets
62+
}
63+
64+
func NewIndirectSyscall() Evasion {
65+
return &indirectSyscall{}
66+
}
67+
68+
func (i *indirectSyscall) tryResolveSyscallStubOffsets(e *event.Event) error {
69+
if i.offsets != nil {
70+
return nil
71+
}
72+
73+
var ntdllBase va.Address
74+
if e.PS != nil {
75+
for _, mod := range e.PS.Modules {
76+
if mod.IsNTDLL() {
77+
ntdllBase = mod.BaseAddress
78+
}
79+
}
80+
}
81+
82+
if ntdllBase.IsZero() {
83+
return nil
84+
}
85+
86+
var handle windows.Handle
87+
if err := windows.GetModuleHandleEx(sys.ModuleHandleFromAddress, (*uint16)(unsafe.Pointer(ntdllBase.Uintptr())), &handle); err != nil {
88+
return err
89+
}
90+
defer windows.Close(handle)
91+
92+
i.offsets = make(map[event.Type]uintptr)
93+
94+
for evt, stub := range syscallStubs {
95+
addr, err := windows.GetProcAddress(handle, stub)
96+
if err != nil {
97+
log.Warnf("unable to get procedure address for %s: %v", evt, err)
98+
continue
99+
}
100+
i.offsets[evt] = addr - ntdllBase.Uintptr()
101+
log.Debugf("syscall stub %s resolved to address %x and offset %d", evt, addr, i.offsets[evt])
102+
}
103+
104+
return nil
105+
}
106+
107+
func (i *indirectSyscall) Eval(e *event.Event) (bool, error) {
108+
if err := i.tryResolveSyscallStubOffsets(e); err != nil {
109+
return false, err
110+
}
111+
if e.Callstack.IsEmpty() {
112+
return false, nil
113+
}
114+
115+
frame := e.Callstack.FinalUserspaceFrame()
116+
if frame == nil {
117+
return false, nil
118+
}
119+
120+
if frame.IsUnbacked() {
121+
return false, nil
122+
}
123+
124+
sym := frame.Symbol
125+
mod := filepath.Base(strings.ToLower(frame.Module))
126+
127+
if mod != "ntdll.dll" {
128+
// only check ntdll syscall stubs
129+
return false, nil
130+
}
131+
132+
// eliminate common false positives (there are
133+
// many other false positives that can be directly
134+
// tuned in the rules)
135+
switch {
136+
case e.IsCreateProcess() && sym == "ZwDeviceIoControlFile" && e.Callstack.ContainsSymbol("AttachConsole"):
137+
return false, nil
138+
case e.IsCreateThread() && (sym == "ZwSetInformationWorkerFactory" || sym == "ZwReleaseWorkerFactoryWorker"):
139+
return false, nil
140+
case e.IsOpenThread() && sym == "ZwAlpcOpenSenderThread":
141+
return false, nil
142+
case e.IsOpenProcess() && sym == "ZwAlpcOpenSenderProcess":
143+
return false, nil
144+
case e.IsCreateFile() && (sym == "ZwOpenFile" || sym == "NtOpenFile" || sym == "ZwQueryAttributesFile" || sym == "ZwQueryFullAttributesFile" || sym == "ZwQueryInformationByName" || sym == "ZwQuerySystemInformation"):
145+
return false, nil
146+
case e.IsDeleteFile() && (sym == "ZwSetInformationFile" && (e.Callstack.ContainsSymbol("DeleteFileA") || e.Callstack.ContainsSymbol("DeleteFileW"))):
147+
return false, nil
148+
case e.IsRegCreateKey() && sym == "ZwDeviceIoControlFile" && e.Callstack.ContainsSymbol("DllUnregisterServer"):
149+
return false, nil
150+
}
151+
152+
exp, ok := i.offsets[e.Type]
153+
if !ok {
154+
return false, nil
155+
}
156+
curr := frame.Addr.Dec(uint64(frame.ModuleAddress)).Uintptr()
157+
158+
//nolint:staticcheck
159+
return !(curr > exp && curr <= exp+syscallStubLength), nil
160+
}
161+
162+
func (*indirectSyscall) Type() Type { return IndirectSyscall }

0 commit comments

Comments
 (0)