Skip to content

Commit 9b34161

Browse files
authored
feat(filter): YARA filter function (#147)
* implement YARA filter function * register Yara function and start adding tests * yara function tests * populate variable definitions * include import and small refactoring * add log messages * fix argument placing * improve YARA function logging * log failed YARA scan
1 parent a6d30eb commit 9b34161

10 files changed

Lines changed: 354 additions & 25 deletions

File tree

pkg/filter/ql/function.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ var funcs = map[string]FunctionDef{
6969
functions.IsAbsFn.String(): &functions.IsAbs{},
7070
functions.VolumeFn.String(): &functions.Volume{},
7171
functions.GetRegValueFn.String(): &functions.GetRegValue{},
72+
functions.YaraFn.String(): &functions.Yara{},
7273
}
7374

7475
// FunctionDef is the interface that all function definitions have to satisfy.
1.56 MB
Binary file not shown.

pkg/filter/ql/functions/glob.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@ func (f Glob) Call(args []interface{}) (interface{}, bool) {
2929
if len(args) < 1 {
3030
return false, false
3131
}
32-
pattern, ok := args[0].(string)
33-
if !ok {
34-
return false, false
35-
}
32+
pattern := parseString(0, args)
3633
matches, err := filepath.Glob(pattern)
3734
if err != nil {
3835
return nil, true

pkg/filter/ql/functions/indexof.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,25 @@ import (
2323
"strings"
2424
)
2525

26-
// index is the type alias for the string position search order
27-
type index uint8
26+
// IndexPosition is the type alias for the string position search order
27+
type IndexPosition uint8
2828

2929
const (
30-
unknown index = iota
31-
first // Index
32-
any // IndexAny
33-
last // LastIndex
34-
lastany // LastIndexAny
30+
UnknownIndex IndexPosition = iota
31+
FirstIndex // Index
32+
AnyIndex // IndexAny
33+
LastIndex // LastIndex
34+
LastAnyIndex // LastIndexAny
3535
)
3636

37-
var indexMappings = map[string]index{
38-
"first": first,
39-
"any": any,
40-
"last": last,
41-
"lastany": lastany,
37+
var indexMappings = map[string]IndexPosition{
38+
"first": FirstIndex,
39+
"any": AnyIndex,
40+
"last": LastIndex,
41+
"lastany": LastAnyIndex,
4242
}
4343

44-
func indexFromString(s string) index { return indexMappings[s] }
44+
func indexFromString(s string) IndexPosition { return indexMappings[s] }
4545

4646
// IndexOf returns the index of the instance of substring in a given string
4747
// depending on the provided search order.
@@ -58,13 +58,13 @@ func (f IndexOf) Call(args []interface{}) (interface{}, bool) {
5858
}
5959
// index search order
6060
switch indexFromString(parseString(2, args)) {
61-
case first:
61+
case FirstIndex:
6262
return strings.Index(str, substr), true
63-
case any:
63+
case AnyIndex:
6464
return strings.IndexAny(str, substr), true
65-
case last:
65+
case LastIndex:
6666
return strings.LastIndex(str, substr), true
67-
case lastany:
67+
case LastAnyIndex:
6868
return strings.LastIndexAny(str, substr), true
6969
default:
7070
return false, false
@@ -83,7 +83,7 @@ func (f IndexOf) Desc() FunctionDesc {
8383
if len(args) == 2 {
8484
return nil
8585
}
86-
if len(args) == 3 && indexFromString(args[2]) == unknown {
86+
if len(args) == 3 && indexFromString(args[2]) == UnknownIndex {
8787
return fmt.Errorf("%s is not a valid index search order. Available options are: first,any,last,lastany", args[2])
8888
}
8989
return nil

pkg/filter/ql/functions/length.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func (f Length) Desc() FunctionDesc {
3939
desc := FunctionDesc{
4040
Name: LengthFn,
4141
Args: []FunctionArgDesc{
42-
{Keyword: "string/slice", Types: []ArgType{Field, Slice, Func}, Required: true},
42+
{Keyword: "string|slice", Types: []ArgType{Field, Slice, Func}, Required: true},
4343
},
4444
}
4545
return desc

pkg/filter/ql/functions/minidump.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (f IsMinidump) Call(args []interface{}) (interface{}, bool) {
3434
if len(args) < 1 {
3535
return false, false
3636
}
37-
path := args[0].(string)
37+
path := parseString(0, args)
3838

3939
file, err := os.Open(path)
4040
if err != nil {

pkg/filter/ql/functions/types.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ const (
7070
VolumeFn
7171
// GetRegValueFn represents the GET_REG_VALUE function
7272
GetRegValueFn
73+
// YaraFn represents the YARA function
74+
YaraFn
7375
)
7476

7577
// ArgType is the type alias for the argument value type.
@@ -204,14 +206,16 @@ func (f Fn) String() string {
204206
return "VOLUME"
205207
case GetRegValueFn:
206208
return "GET_REG_VALUE"
209+
case YaraFn:
210+
return "YARA"
207211
default:
208212
return "UNDEFINED"
209213
}
210214
}
211215

212216
// parseString yields a string value from the specific position in the args slice.
213217
func parseString(index int, args []interface{}) string {
214-
if index > len(args) {
218+
if index > len(args)-1 {
215219
return ""
216220
}
217221
s, ok := args[index].(string)

pkg/filter/ql/functions/yara.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
//go:build yara
2+
// +build yara
3+
4+
/*
5+
* Copyright 2021-2022 by Nedim Sabic Sabic
6+
* https://www.fibratus.io
7+
* All Rights Reserved.
8+
*
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
*/
21+
22+
package functions
23+
24+
import (
25+
"fmt"
26+
"github.com/hillu/go-yara/v4"
27+
"github.com/rabbitstack/fibratus/pkg/util/multierror"
28+
log "github.com/sirupsen/logrus"
29+
"strings"
30+
"time"
31+
)
32+
33+
// scanTimeout specifies the timeout interval for the scan operation
34+
const scanTimeout = time.Second * 10
35+
36+
// Yara provides signature-based detection in filters and rules.
37+
// YARA is a tool aimed at (but not limited to) helping malware
38+
// researchers to identify and classify malware samples. With YARA
39+
// you can create descriptions of malware families based on textual
40+
// or binary patterns. Depending on the parameter type supplied to this
41+
// function, the scan can be performed on the process, filename or a
42+
// memory block.
43+
type Yara struct{}
44+
45+
func (f Yara) Call(args []interface{}) (interface{}, bool) {
46+
if len(args) < 2 {
47+
return false, false
48+
}
49+
var rules string
50+
var vars map[string]interface{}
51+
switch r := args[1].(type) {
52+
case string:
53+
rules = r
54+
case []string:
55+
rules = strings.Join(r, " ")
56+
}
57+
if len(args) > 3 {
58+
vars, _ = args[2].(map[string]interface{})
59+
}
60+
scanner, err := f.newScanner(rules, vars)
61+
if err != nil {
62+
log.Warnf("erroneous scanner in YARA function: %v: %s", err, rules)
63+
return false, true
64+
}
65+
defer scanner.Destroy()
66+
67+
var cb yara.MatchRules
68+
switch n := args[0].(type) {
69+
case uint32: // pid
70+
err = scanner.SetCallback(&cb).ScanProc(int(n))
71+
case string: // file
72+
err = scanner.SetCallback(&cb).ScanFile(n)
73+
case []byte: // mem block
74+
err = scanner.SetCallback(&cb).ScanMem(n)
75+
default: // invalid type
76+
return false, false
77+
}
78+
if err != nil {
79+
log.Warnf("YARA function scan failed: %v", err)
80+
return false, true
81+
}
82+
if len(cb) > 0 {
83+
log.Debugf("YARA function produced %d match(es)", len(cb))
84+
for _, match := range cb {
85+
log.Debugf("Matched YARA rule: %s", match.Rule)
86+
}
87+
}
88+
return len(cb) > 0, true
89+
}
90+
91+
func (f Yara) Desc() FunctionDesc {
92+
desc := FunctionDesc{
93+
Name: YaraFn,
94+
Args: []FunctionArgDesc{
95+
{Keyword: "pid|file|bytes", Types: []ArgType{Field, Func, String, Number}, Required: true},
96+
{Keyword: "rules", Types: []ArgType{Field, Func, String}, Required: true},
97+
{Keyword: "vars", Types: []ArgType{Field, Func, String}},
98+
},
99+
}
100+
return desc
101+
}
102+
103+
func (f Yara) Name() Fn { return YaraFn }
104+
105+
func (f Yara) newScanner(rules string, vars map[string]interface{}) (*yara.Scanner, error) {
106+
c, err := yara.NewCompiler()
107+
if err != nil {
108+
return nil, err
109+
}
110+
defer c.Destroy()
111+
if err := c.AddString(rules, ""); err != nil {
112+
return nil, err
113+
}
114+
for k, v := range vars {
115+
if err := c.DefineVariable(k, v); err != nil {
116+
return nil, err
117+
}
118+
}
119+
if len(c.Errors) > 0 {
120+
return nil, parseCompilerErrors(c.Errors)
121+
}
122+
r, err := c.GetRules()
123+
if err != nil {
124+
return nil, err
125+
}
126+
scanner, err := yara.NewScanner(r)
127+
if err != nil {
128+
return nil, err
129+
}
130+
scanner.SetFlags(yara.ScanFlagsFastMode)
131+
scanner.SetTimeout(scanTimeout)
132+
return scanner, nil
133+
}
134+
135+
func parseCompilerErrors(errors []yara.CompilerMessage) error {
136+
errs := make([]error, len(errors))
137+
for i, err := range errors {
138+
errs[i] = fmt.Errorf("%s, line: %d", err.Text, err.Line)
139+
}
140+
return multierror.Wrap(errs...)
141+
}

0 commit comments

Comments
 (0)