diff --git a/pkg/filter/ql/function.go b/pkg/filter/ql/function.go index 46985dc5f..38ec4bcb8 100644 --- a/pkg/filter/ql/function.go +++ b/pkg/filter/ql/function.go @@ -69,6 +69,7 @@ var funcs = map[string]FunctionDef{ functions.IsAbsFn.String(): &functions.IsAbs{}, functions.VolumeFn.String(): &functions.Volume{}, functions.GetRegValueFn.String(): &functions.GetRegValue{}, + functions.YaraFn.String(): &functions.Yara{}, } // FunctionDef is the interface that all function definitions have to satisfy. diff --git a/pkg/filter/ql/functions/_fixtures/yara-test.dll b/pkg/filter/ql/functions/_fixtures/yara-test.dll new file mode 100644 index 000000000..937a81ad0 Binary files /dev/null and b/pkg/filter/ql/functions/_fixtures/yara-test.dll differ diff --git a/pkg/filter/ql/functions/glob.go b/pkg/filter/ql/functions/glob.go index 83d008432..aea0e9cf9 100644 --- a/pkg/filter/ql/functions/glob.go +++ b/pkg/filter/ql/functions/glob.go @@ -29,10 +29,7 @@ func (f Glob) Call(args []interface{}) (interface{}, bool) { if len(args) < 1 { return false, false } - pattern, ok := args[0].(string) - if !ok { - return false, false - } + pattern := parseString(0, args) matches, err := filepath.Glob(pattern) if err != nil { return nil, true diff --git a/pkg/filter/ql/functions/indexof.go b/pkg/filter/ql/functions/indexof.go index f59bfd871..cd0bf710c 100644 --- a/pkg/filter/ql/functions/indexof.go +++ b/pkg/filter/ql/functions/indexof.go @@ -23,25 +23,25 @@ import ( "strings" ) -// index is the type alias for the string position search order -type index uint8 +// IndexPosition is the type alias for the string position search order +type IndexPosition uint8 const ( - unknown index = iota - first // Index - any // IndexAny - last // LastIndex - lastany // LastIndexAny + UnknownIndex IndexPosition = iota + FirstIndex // Index + AnyIndex // IndexAny + LastIndex // LastIndex + LastAnyIndex // LastIndexAny ) -var indexMappings = map[string]index{ - "first": first, - "any": any, - "last": last, - "lastany": lastany, +var indexMappings = map[string]IndexPosition{ + "first": FirstIndex, + "any": AnyIndex, + "last": LastIndex, + "lastany": LastAnyIndex, } -func indexFromString(s string) index { return indexMappings[s] } +func indexFromString(s string) IndexPosition { return indexMappings[s] } // IndexOf returns the index of the instance of substring in a given string // depending on the provided search order. @@ -58,13 +58,13 @@ func (f IndexOf) Call(args []interface{}) (interface{}, bool) { } // index search order switch indexFromString(parseString(2, args)) { - case first: + case FirstIndex: return strings.Index(str, substr), true - case any: + case AnyIndex: return strings.IndexAny(str, substr), true - case last: + case LastIndex: return strings.LastIndex(str, substr), true - case lastany: + case LastAnyIndex: return strings.LastIndexAny(str, substr), true default: return false, false @@ -83,7 +83,7 @@ func (f IndexOf) Desc() FunctionDesc { if len(args) == 2 { return nil } - if len(args) == 3 && indexFromString(args[2]) == unknown { + if len(args) == 3 && indexFromString(args[2]) == UnknownIndex { return fmt.Errorf("%s is not a valid index search order. Available options are: first,any,last,lastany", args[2]) } return nil diff --git a/pkg/filter/ql/functions/length.go b/pkg/filter/ql/functions/length.go index 58dfc3972..aef474c76 100644 --- a/pkg/filter/ql/functions/length.go +++ b/pkg/filter/ql/functions/length.go @@ -39,7 +39,7 @@ func (f Length) Desc() FunctionDesc { desc := FunctionDesc{ Name: LengthFn, Args: []FunctionArgDesc{ - {Keyword: "string/slice", Types: []ArgType{Field, Slice, Func}, Required: true}, + {Keyword: "string|slice", Types: []ArgType{Field, Slice, Func}, Required: true}, }, } return desc diff --git a/pkg/filter/ql/functions/minidump.go b/pkg/filter/ql/functions/minidump.go index e15dbc162..406cb2885 100644 --- a/pkg/filter/ql/functions/minidump.go +++ b/pkg/filter/ql/functions/minidump.go @@ -34,7 +34,7 @@ func (f IsMinidump) Call(args []interface{}) (interface{}, bool) { if len(args) < 1 { return false, false } - path := args[0].(string) + path := parseString(0, args) file, err := os.Open(path) if err != nil { diff --git a/pkg/filter/ql/functions/types.go b/pkg/filter/ql/functions/types.go index fd3772ad2..e03f19faa 100644 --- a/pkg/filter/ql/functions/types.go +++ b/pkg/filter/ql/functions/types.go @@ -70,6 +70,8 @@ const ( VolumeFn // GetRegValueFn represents the GET_REG_VALUE function GetRegValueFn + // YaraFn represents the YARA function + YaraFn ) // ArgType is the type alias for the argument value type. @@ -204,6 +206,8 @@ func (f Fn) String() string { return "VOLUME" case GetRegValueFn: return "GET_REG_VALUE" + case YaraFn: + return "YARA" default: return "UNDEFINED" } @@ -211,7 +215,7 @@ func (f Fn) String() string { // parseString yields a string value from the specific position in the args slice. func parseString(index int, args []interface{}) string { - if index > len(args) { + if index > len(args)-1 { return "" } s, ok := args[index].(string) diff --git a/pkg/filter/ql/functions/yara.go b/pkg/filter/ql/functions/yara.go new file mode 100644 index 000000000..9eecf957a --- /dev/null +++ b/pkg/filter/ql/functions/yara.go @@ -0,0 +1,141 @@ +//go:build yara +// +build yara + +/* + * Copyright 2021-2022 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 functions + +import ( + "fmt" + "github.com/hillu/go-yara/v4" + "github.com/rabbitstack/fibratus/pkg/util/multierror" + log "github.com/sirupsen/logrus" + "strings" + "time" +) + +// scanTimeout specifies the timeout interval for the scan operation +const scanTimeout = time.Second * 10 + +// Yara provides signature-based detection in filters and rules. +// YARA is a tool aimed at (but not limited to) helping malware +// researchers to identify and classify malware samples. With YARA +// you can create descriptions of malware families based on textual +// or binary patterns. Depending on the parameter type supplied to this +// function, the scan can be performed on the process, filename or a +// memory block. +type Yara struct{} + +func (f Yara) Call(args []interface{}) (interface{}, bool) { + if len(args) < 2 { + return false, false + } + var rules string + var vars map[string]interface{} + switch r := args[1].(type) { + case string: + rules = r + case []string: + rules = strings.Join(r, " ") + } + if len(args) > 3 { + vars, _ = args[2].(map[string]interface{}) + } + scanner, err := f.newScanner(rules, vars) + if err != nil { + log.Warnf("erroneous scanner in YARA function: %v: %s", err, rules) + return false, true + } + defer scanner.Destroy() + + var cb yara.MatchRules + switch n := args[0].(type) { + case uint32: // pid + err = scanner.SetCallback(&cb).ScanProc(int(n)) + case string: // file + err = scanner.SetCallback(&cb).ScanFile(n) + case []byte: // mem block + err = scanner.SetCallback(&cb).ScanMem(n) + default: // invalid type + return false, false + } + if err != nil { + log.Warnf("YARA function scan failed: %v", err) + return false, true + } + if len(cb) > 0 { + log.Debugf("YARA function produced %d match(es)", len(cb)) + for _, match := range cb { + log.Debugf("Matched YARA rule: %s", match.Rule) + } + } + return len(cb) > 0, true +} + +func (f Yara) Desc() FunctionDesc { + desc := FunctionDesc{ + Name: YaraFn, + Args: []FunctionArgDesc{ + {Keyword: "pid|file|bytes", Types: []ArgType{Field, Func, String, Number}, Required: true}, + {Keyword: "rules", Types: []ArgType{Field, Func, String}, Required: true}, + {Keyword: "vars", Types: []ArgType{Field, Func, String}}, + }, + } + return desc +} + +func (f Yara) Name() Fn { return YaraFn } + +func (f Yara) newScanner(rules string, vars map[string]interface{}) (*yara.Scanner, error) { + c, err := yara.NewCompiler() + if err != nil { + return nil, err + } + defer c.Destroy() + if err := c.AddString(rules, ""); err != nil { + return nil, err + } + for k, v := range vars { + if err := c.DefineVariable(k, v); err != nil { + return nil, err + } + } + if len(c.Errors) > 0 { + return nil, parseCompilerErrors(c.Errors) + } + r, err := c.GetRules() + if err != nil { + return nil, err + } + scanner, err := yara.NewScanner(r) + if err != nil { + return nil, err + } + scanner.SetFlags(yara.ScanFlagsFastMode) + scanner.SetTimeout(scanTimeout) + return scanner, nil +} + +func parseCompilerErrors(errors []yara.CompilerMessage) error { + errs := make([]error, len(errors)) + for i, err := range errors { + errs[i] = fmt.Errorf("%s, line: %d", err.Text, err.Line) + } + return multierror.Wrap(errs...) +} diff --git a/pkg/filter/ql/functions/yara_test.go b/pkg/filter/ql/functions/yara_test.go new file mode 100644 index 000000000..030d0af3a --- /dev/null +++ b/pkg/filter/ql/functions/yara_test.go @@ -0,0 +1,137 @@ +//go:build yara +// +build yara + +/* + * Copyright 2021-2022 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 functions + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "syscall" + "testing" +) + +var pi syscall.ProcessInformation + +func TestYara(t *testing.T) { + var tests = []struct { + args []interface{} + expected bool + }{ + { + []interface{}{uint32(runNotepad()), ` +rule Notepad : notepad +{ + meta: + severity = "Normal" + date = "2016-07" + strings: + $c0 = "Notepad" fullword ascii + condition: + $c0 +} + `}, + true, + }, + { + []interface{}{"_fixtures/yara-test.dll", ` +rule DLL : dll +{ + meta: + severity = "Critical" + date = "2020-07" + strings: + $c0 = "Go" fullword ascii + condition: + $c0 +} + `}, + true, + }, + { + []interface{}{readNotepadBytes(), ` +rule Notepad : notepad +{ + meta: + severity = "Normal" + date = "2016-07" + strings: + $c0 = "Notepad" fullword ascii + condition: + $c0 +} + `}, + true, + }, + { + []interface{}{uint32(runNotepad()), ` +rule Notepad : notepad +{ + meta: + severity = "Normal" + date = "2016-07" + strings: + $c0 = "Notfound" fullword ascii + condition: + $c0 +} + `}, + false, + }, + } + defer syscall.TerminateProcess(pi.Process, uint32(257)) + + for i, tt := range tests { + f := Yara{} + res, _ := f.Call(tt.args) + assert.Equal(t, tt.expected, res, fmt.Sprintf("%d. result mismatch: exp=%v got=%v", i, tt.expected, res)) + } +} + +func runNotepad() uint32 { + var si syscall.StartupInfo + argv := syscall.StringToUTF16Ptr(filepath.Join(os.Getenv("windir"), "notepad.exe")) + err := syscall.CreateProcess( + nil, + argv, + nil, + nil, + true, + 0, + nil, + nil, + &si, + &pi) + if err != nil { + return 0 + } + return pi.ProcessId +} + +func readNotepadBytes() []byte { + p := filepath.Join(os.Getenv("windir"), "notepad.exe") + b, err := os.ReadFile(p) + if err != nil { + return nil + } + return b +} diff --git a/pkg/filter/ql/functions/yara_unsupported.go b/pkg/filter/ql/functions/yara_unsupported.go new file mode 100644 index 000000000..307bfcce3 --- /dev/null +++ b/pkg/filter/ql/functions/yara_unsupported.go @@ -0,0 +1,49 @@ +//go:build !yara +// +build !yara + +/* + * Copyright 2021-2022 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 functions + +import ( + "fmt" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" +) + +// Yara unsupported function +type Yara struct{} + +func (f Yara) Call(args []interface{}) (interface{}, bool) { return false, false } + +func (f Yara) Desc() FunctionDesc { + desc := FunctionDesc{ + Name: YaraFn, + Args: []FunctionArgDesc{ + {Keyword: "pid|file|bytes", Types: []ArgType{Field, Func, String, Number}, Required: true}, + {Keyword: "rules", Types: []ArgType{Field, Func, String}, Required: true}, + {Keyword: "vars", Types: []ArgType{Field, Func, String}}, + }, + ArgsValidationFunc: func(args []string) error { + return fmt.Errorf("yara function is not supported. %w", kerrors.ErrFeatureUnsupported("yara")) + }, + } + return desc +} + +func (f Yara) Name() Fn { return YaraFn }