Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rules): Vulnerable/malicious driver detection #203

Merged
merged 7 commits into from
Oct 28, 2023
65 changes: 62 additions & 3 deletions pkg/filter/accessor_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/rabbitstack/fibratus/pkg/kevent/ktypes"
psnap "github.com/rabbitstack/fibratus/pkg/ps"
"github.com/rabbitstack/fibratus/pkg/util/cmdline"
"github.com/rabbitstack/fibratus/pkg/util/loldrivers"
"path/filepath"
"strconv"
"strings"
Expand Down Expand Up @@ -589,10 +590,44 @@ func (t *threadAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value
return nil, nil
}

// evalLOLDrivers interacts with the loldrivers client to determine
// whether the loaded/dropped driver is malicious or vulnerable.
func evalLOLDrivers(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) {
filename := kevt.GetParamAsString(kparams.FileName)
isDriver := filepath.Ext(filename) == ".sys" || kevt.Kparams.TryGetBool(kparams.FileIsDriver)
if !isDriver {
return nil, nil
}
ok, driver := loldrivers.GetClient().MatchHash(filename)
if !ok {
return nil, nil
}
if (f == fields.FileIsDriverVulnerable || f == fields.ImageIsDriverVulnerable) && driver.IsVulnerable {
return true, nil
}
if (f == fields.FileIsDriverMalicious || f == fields.ImageIsDriverMalicious) && driver.IsMalicious {
return true, nil
}
return false, nil
}

// initLOLDriversClient initializes the loldrivers client if the filter expression
// contains any of the relevant fields.
func initLOLDriversClient(flds []fields.Field) {
for _, f := range flds {
if f == fields.FileIsDriverVulnerable || f == fields.FileIsDriverMalicious ||
f == fields.ImageIsDriverVulnerable || f == fields.ImageIsDriverMalicious {
loldrivers.InitClient(loldrivers.WithAsyncDownload())
}
}
}

// fileAccessor extracts file specific values.
type fileAccessor struct{}

func (fileAccessor) setFields(fields []fields.Field) {}
func (fileAccessor) setFields(fields []fields.Field) {
initLOLDriversClient(fields)
}

func newFileAccessor() accessor {
return &fileAccessor{}
Expand Down Expand Up @@ -629,14 +664,27 @@ func (l *fileAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value,
return kevt.Kparams.GetUint64(kparams.FileViewSize)
case fields.FileViewType:
return kevt.GetParamAsString(kparams.FileViewSectionType), nil
case fields.FileIsDriverVulnerable, fields.FileIsDriverMalicious:
if kevt.IsCreateDisposition() && kevt.IsSuccess() {
return evalLOLDrivers(f, kevt)
}
return false, nil
case fields.FileIsDLL:
return kevt.Kparams.GetBool(kparams.FileIsDLL)
case fields.FileIsDriver:
return kevt.Kparams.GetBool(kparams.FileIsDriver)
case fields.FileIsExecutable:
return kevt.Kparams.GetBool(kparams.FileIsExecutable)
}
return nil, nil
}

// imageAccessor extracts image (DLL) event values.
type imageAccessor struct{}

func (imageAccessor) setFields(fields []fields.Field) {}
func (imageAccessor) setFields(fields []fields.Field) {
initLOLDriversClient(fields)
}

func newImageAccessor() accessor {
return &imageAccessor{}
Expand Down Expand Up @@ -670,6 +718,17 @@ func (i *imageAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value,
return kevt.Kparams.GetTime(kparams.ImageCertNotBefore)
case fields.ImageCertAfter:
return kevt.Kparams.GetTime(kparams.ImageCertNotAfter)
case fields.ImageIsDriverVulnerable, fields.ImageIsDriverMalicious:
if kevt.IsLoadImage() {
return evalLOLDrivers(f, kevt)
}
return false, nil
case fields.ImageIsDLL:
return kevt.Kparams.GetBool(kparams.FileIsDLL)
case fields.ImageIsDriver:
return kevt.Kparams.GetBool(kparams.FileIsDriver)
case fields.ImageIsExecutable:
return kevt.Kparams.GetBool(kparams.FileIsExecutable)
}
return nil, nil
}
Expand Down Expand Up @@ -777,7 +836,7 @@ func (pa *peAccessor) parserOpts() []pe.Option {
opts = append(opts, pe.WithSymbols())
}
if f.IsPeSectionEntropy() {
opts = append(opts, pe.WithSectionEntropy())
opts = append(opts, pe.WithSections(), pe.WithSectionEntropy())
}
if f.IsPeVersionResource() || f.IsPeResourcesMap() {
opts = append(opts, pe.WithVersionResources())
Expand Down
4 changes: 2 additions & 2 deletions pkg/filter/fields/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ func (f FieldInfo) IsDeprecated() bool { return f.Deprecation != nil }
type Deprecation struct {
// Since denotes from which version the field is flagged as deprecated
Since string
// Field represents the field by which the deprecated field is superseded
Field Field
// Fields represents the fields by which the deprecated field is superseded
Fields []Field
}

// Get returns a slice of field information.
Expand Down
112 changes: 71 additions & 41 deletions pkg/filter/fields/fields_windows.go

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pkg/filter/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,9 @@ func (r *Rules) Compile() error {
if deprecated {
log.Warnf("%s rule uses the [%s] field which "+
"was deprecated starting from version %s. "+
"Please consider migrating to [%s] field "+
"Please consider migrating to %s field(s) "+
"because [%s] will be removed in future versions.",
rule.Name, field, d.Since, d.Field, field)
rule.Name, field, d.Since, d.Fields, field)
}
}
filtersCount.Add(1)
Expand Down
47 changes: 47 additions & 0 deletions pkg/fs/ntfs/ntfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import (
libntfs "www.velocidex.com/golang/go-ntfs/parser"
)

// MaxFullSizeRead specifies the maximum size in bytes for the file data
const MaxFullSizeRead int64 = 1024 * 1024 * 1024 * 50

// FS provides raw access to Master File Table (MFT)
// and file data blobs mounted on the NTFS.
type FS struct {
Expand Down Expand Up @@ -62,6 +65,50 @@ func (fs *FS) Read(path string, offset, size int64) ([]byte, int, error) {
return data, n, err
}

// ReadFull reads the entire content of the file into the byte buffer.
func (fs *FS) ReadFull(path string) ([]byte, int, error) {
defer func() {
if err := recover(); err != nil {
log.Warnf("unable to read %s from raw device: %v", path, err)
}
}()
ntfs, err := fs.getNTFSContext(path)
if err != nil {
return nil, 0, err
}
defer ntfs.Close()

// get root MFT entry
root, err := ntfs.GetMFT(5)
if err != nil {
return nil, 0, err
}
// get file size
filename := strings.ReplaceAll(path[3:], "\\", "/")
f, err := root.Open(ntfs, filename)
if err != nil {
return nil, 0, err
}
var size int64
if stats := libntfs.Stat(ntfs, f); len(stats) > 0 {
size = stats[0].Size
}
if size == 0 {
return nil, 0, nil
}
if size > MaxFullSizeRead {
return nil, 0, nil
}
// read file
data := make([]byte, size)
reader, err := libntfs.GetDataForPath(ntfs, filename)
if err != nil {
return nil, 0, err
}
n, err := reader.ReadAt(data, 0)
return data, n, err
}

// Close disposes all underlying resources.
func (fs *FS) Close() error {
if fs.dev != nil {
Expand Down
17 changes: 17 additions & 0 deletions pkg/fs/ntfs/ntfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,20 @@ func TestRead(t *testing.T) {
require.True(t, n != 0)
assert.Equal(t, []byte("ntfs read with a bit more of read"), b[:33])
}

func TestReadFull(t *testing.T) {
file := filepath.Join(os.TempDir(), "ntfs-read-full.txt")
err := os.WriteFile(file, []byte("ntfs read from mars to sirius where the whales fly into oblivion"), os.ModePerm)
require.NoError(t, err)
defer os.Remove(file)

fs := NewFS()
defer fs.Close()

b, n, err := fs.ReadFull(file)
assert.NotNil(t, fs)
require.NoError(t, err)
require.True(t, n != 0)

assert.Equal(t, []byte("ntfs read from mars to sirius where the whales fly into oblivion"), b)
}
6 changes: 3 additions & 3 deletions pkg/kevent/kevent_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,9 @@ func (e Kevent) CurrentPid() bool { return e.PID == currentPid }
// IsState indicates if this event is only used for state management.
func (e Kevent) IsState() bool { return e.Type.OnlyState() }

// IsCreatingFile determines if the event is creating a new file.
func (e Kevent) IsCreatingFile() bool {
return e.IsCreateFile() && e.Kparams.MustGetUint32(kparams.FileOperation) != windows.FILE_OPEN
// IsCreateDisposition determines if the file disposition leads to creating a new file.
func (e Kevent) IsCreateDisposition() bool {
return e.IsCreateFile() && e.Kparams.MustGetUint32(kparams.FileOperation) == windows.FILE_CREATE
}

// RundownKey calculates the rundown event hash. The hash is
Expand Down
19 changes: 3 additions & 16 deletions pkg/kstream/processors/fs_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,14 @@ import (
"expvar"
"github.com/rabbitstack/fibratus/pkg/config"
"github.com/rabbitstack/fibratus/pkg/fs"
libntfs "github.com/rabbitstack/fibratus/pkg/fs/ntfs"
"github.com/rabbitstack/fibratus/pkg/handle"
htypes "github.com/rabbitstack/fibratus/pkg/handle/types"
"github.com/rabbitstack/fibratus/pkg/kevent"
"github.com/rabbitstack/fibratus/pkg/kevent/kparams"
"github.com/rabbitstack/fibratus/pkg/kevent/ktypes"
"github.com/rabbitstack/fibratus/pkg/pe"
"github.com/rabbitstack/fibratus/pkg/sys"
"github.com/rabbitstack/fibratus/pkg/util/va"
"golang.org/x/sys/windows"
"os"
)

var (
Expand Down Expand Up @@ -185,23 +182,13 @@ func (f *fsProcessor) processEvent(e *kevent.Kevent) (*kevent.Kevent, error) {
ev.AppendEnum(kparams.FileType, uint32(fileinfo.Type), fs.FileTypes)
}
ev.AppendEnum(kparams.FileOperation, uint32(dispo), fs.FileCreateDispositions)

// parse PE data for created files and append parameters
if ev.IsCreatingFile() && ev.IsSuccess() {
filename := ev.GetParamAsString(kparams.FileName)
// read file data blob from raw device
ntfs := libntfs.NewFS()
defer ntfs.Close()
data, _, err := ntfs.Read(filename, 0, int64(os.Getpagesize()))
if err != nil {
return ev, nil
}
pefile, err := pe.ParseBytes(data, pe.WithSymbols())
if ev.IsCreateDisposition() && ev.IsSuccess() {
err := parseImageFileCharacteristics(ev)
if err != nil {
return ev, nil
}
ev.AppendParam(kparams.FileIsDLL, kparams.Bool, pefile.IsDLL)
ev.AppendParam(kparams.FileIsDriver, kparams.Bool, pefile.IsDriver)
ev.AppendParam(kparams.FileIsExecutable, kparams.Bool, pefile.IsExecutable)
}
return ev, nil
case ktypes.ReleaseFile:
Expand Down
15 changes: 1 addition & 14 deletions pkg/kstream/processors/image_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,11 @@ package processors

import (
"expvar"
libntfs "github.com/rabbitstack/fibratus/pkg/fs/ntfs"
"github.com/rabbitstack/fibratus/pkg/kevent"
"github.com/rabbitstack/fibratus/pkg/kevent/kparams"
"github.com/rabbitstack/fibratus/pkg/pe"
"github.com/rabbitstack/fibratus/pkg/ps"
"github.com/rabbitstack/fibratus/pkg/util/hashers"
"github.com/rabbitstack/fibratus/pkg/util/signature"
"os"
)

// signatureErrors counts signature check/verification errors
Expand Down Expand Up @@ -58,20 +55,10 @@ func (m *imageProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, e
signatureErrors.Add(1)
}
// parse PE image data
filename := e.GetParamAsString(kparams.FileName)
ntfs := libntfs.NewFS()
defer ntfs.Close()
data, _, err := ntfs.Read(filename, 0, int64(os.Getpagesize()))
err = parseImageFileCharacteristics(e)
if err != nil {
return e, false, m.psnap.AddModule(e)
}
pefile, err := pe.ParseBytes(data, pe.WithSymbols())
if err != nil {
return e, false, m.psnap.AddModule(e)
}
e.AppendParam(kparams.FileIsDLL, kparams.Bool, pefile.IsDLL)
e.AppendParam(kparams.FileIsDriver, kparams.Bool, pefile.IsDriver)
e.AppendParam(kparams.FileIsExecutable, kparams.Bool, pefile.IsExecutable)
}
if e.IsUnloadImage() {
pid := e.Kparams.MustGetPid()
Expand Down
50 changes: 49 additions & 1 deletion pkg/kstream/processors/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@

package processors

import "github.com/rabbitstack/fibratus/pkg/kevent"
import (
libntfs "github.com/rabbitstack/fibratus/pkg/fs/ntfs"
"github.com/rabbitstack/fibratus/pkg/kevent"
"github.com/rabbitstack/fibratus/pkg/kevent/kparams"
"github.com/rabbitstack/fibratus/pkg/pe"
"os"
)

// ProcessorType is an alias for the event processor type
type ProcessorType uint8
Expand Down Expand Up @@ -76,3 +82,45 @@ func (typ ProcessorType) String() string {
return "unknown"
}
}

// parseImageFileCharacteristics parses the PE structure for the file path
// residing in the given event parameters. The preferred method for getting
// the file data is accessing the raw device and consuming the blob data.
// If this operation fails, we fallback to using the regular file access.
// The given event is decorated with various parameters extracted from PE
// data. Most notably, parameters that indicate whether the file is a DLL,
// executable image, or a Windows driver.
func parseImageFileCharacteristics(e *kevent.Kevent) error {
filename := e.GetParamAsString(kparams.FileName)
// read file data blob from raw device
ntfs := libntfs.NewFS()
var (
data []byte
err error
)
data, _, err = ntfs.Read(filename, 0, int64(os.Getpagesize()))
defer ntfs.Close()
if err != nil {
// if we can't read from the raw device, try regular file access
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
data = make([]byte, os.Getpagesize())
if _, err = f.Read(data); err != nil {
return err
}
}
// parse image file
pefile, err := pe.ParseBytes(data, pe.WithSections(), pe.WithSymbols())
if err != nil {
return err
}
// append parameters
e.AppendParam(kparams.FileIsDLL, kparams.Bool, pefile.IsDLL)
e.AppendParam(kparams.FileIsDriver, kparams.Bool, pefile.IsDriver)
e.AppendParam(kparams.FileIsExecutable, kparams.Bool, pefile.IsExecutable)

return nil
}
Binary file not shown.
Loading
Loading