diff --git a/pkg/filter/accessor.go b/pkg/filter/accessor.go index e14b3a479..36b3a13e7 100644 --- a/pkg/filter/accessor.go +++ b/pkg/filter/accessor.go @@ -34,6 +34,8 @@ var ( // kevtAccessor extracts generic event values. type kevtAccessor struct{} +func (kevtAccessor) setFields(fields []fields.Field) {} + func newKevtAccessor() accessor { return &kevtAccessor{} } @@ -195,6 +197,10 @@ func (f *filter) narrowAccessors() { if removeDNSAccessor { f.removeAccessor(&dnsAccessor{}) } + + for _, accessor := range f.accessors { + accessor.setFields(allFields) + } } func (f *filter) removeAccessor(removed accessor) { diff --git a/pkg/filter/accessor_windows.go b/pkg/filter/accessor_windows.go index 0544375c1..7f0a37770 100644 --- a/pkg/filter/accessor_windows.go +++ b/pkg/filter/accessor_windows.go @@ -19,9 +19,12 @@ package filter import ( + "errors" "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" psnap "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/util/cmdline" + "github.com/rabbitstack/fibratus/pkg/util/signature" "path/filepath" "strconv" "strings" @@ -39,6 +42,8 @@ import ( type accessor interface { // get fetches the parameter value for the specified filter field. get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) + // setFields sets all fields declared in the expression + setFields(fields []fields.Field) } // getAccessors initializes and returns all available accessors. @@ -70,6 +75,8 @@ type psAccessor struct { psnap psnap.Snapshotter } +func (psAccessor) setFields(fields []fields.Field) {} + func newPSAccessor(psnap psnap.Snapshotter) accessor { return &psAccessor{psnap: psnap} } func (ps *psAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { @@ -514,6 +521,8 @@ func ancestorFields(field string, kevt *kevent.Kevent) (kparams.Value, error) { // threadAccessor fetches thread parameters from thread kernel events. type threadAccessor struct{} +func (threadAccessor) setFields(fields []fields.Field) {} + func newThreadAccessor() accessor { return &threadAccessor{} } @@ -580,6 +589,8 @@ func (t *threadAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value // fileAccessor extracts file specific values. type fileAccessor struct{} +func (fileAccessor) setFields(fields []fields.Field) {} + func newFileAccessor() accessor { return &fileAccessor{} } @@ -622,6 +633,8 @@ func (l *fileAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, // imageAccessor extracts image (DLL) event values. type imageAccessor struct{} +func (imageAccessor) setFields(fields []fields.Field) {} + func newImageAccessor() accessor { return &imageAccessor{} } @@ -661,6 +674,8 @@ func (i *imageAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, // registryAccessor extracts registry specific parameters. type registryAccessor struct{} +func (registryAccessor) setFields(fields []fields.Field) {} + func newRegistryAccessor() accessor { return ®istryAccessor{} } @@ -687,6 +702,8 @@ func (r *registryAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Val // networkAccessor deals with extracting the network specific kernel event parameters. type networkAccessor struct{} +func (networkAccessor) setFields(fields []fields.Field) {} + func newNetworkAccessor() accessor { return &networkAccessor{} } func (n *networkAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { @@ -718,6 +735,8 @@ func (n *networkAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Valu // handleAccessor extracts handle event values. type handleAccessor struct{} +func (handleAccessor) setFields(fields []fields.Field) {} + func newHandleAccessor() accessor { return &handleAccessor{} } func (h *handleAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { @@ -735,21 +754,100 @@ func (h *handleAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value } // peAccessor extracts PE specific values. -type peAccessor struct{} +type peAccessor struct { + fields []fields.Field +} + +func (pa *peAccessor) setFields(fields []fields.Field) { + pa.fields = fields +} + +func (pa *peAccessor) parserOpts() []pe.Option { + var opts []pe.Option + for _, f := range pa.fields { + if f.IsPeSection() || f.IsPeSectionsMap() { + opts = append(opts, pe.WithSections()) + } + if f.IsPeSymbol() { + opts = append(opts, pe.WithSymbols()) + } + if f.IsPeSectionEntropy() { + opts = append(opts, pe.WithSectionEntropy()) + } + if f.IsPeVersionResource() || f.IsPeResourcesMap() { + opts = append(opts, pe.WithVersionResources()) + } + if f.IsPeImphash() { + opts = append(opts, pe.WithImphash()) + } + if f.IsPeDotnet() { + opts = append(opts, pe.WithCLR()) + } + if f.IsPeAnomalies() { + opts = append(opts, pe.WithSections(), pe.WithSymbols()) + } + if f.IsPeSignature() { + opts = append(opts, pe.WithSecurity()) + } + } + return opts +} + +// ErrPENilCertificate indicates the PE certificate is not available +var ErrPENilCertificate = errors.New("pe certificate is nil") func newPEAccessor() accessor { return &peAccessor{} } -func (*peAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { +func (pa *peAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { var p *pe.PE if kevt.PS != nil && kevt.PS.PE != nil { p = kevt.PS.PE } + // PE enrichment is likely disabled. Load PE data lazily + // by only requesting parsing of the PE directories that + // are relevant to the fields present in the expression + if (kevt.PS != nil && kevt.PS.Exe != "") && p == nil { + var err error + exe := kevt.PS.Exe + p, err = pe.ParseFile(exe, pa.parserOpts()...) + if err != nil { + return nil, err + } + if p != nil && f.IsPeSignature() { + // verify embedded signature + if p.IsSigned && f.IsPeIsTrusted() { + p.VerifySignature() + } + if !p.IsSigned { + if !sys.IsWintrustFound() { + goto cmp + } + // maybe the PE is catalog signed? + catalog := signature.NewCatalog() + err := catalog.Open(exe) + if err != nil { + goto cmp + } + defer catalog.Close() + p.IsSigned = catalog.IsCatalogSigned() + if p.IsSigned && f.IsPeIsTrusted() { + p.IsTrusted = catalog.Verify(exe) + } + if p.IsSigned && f.IsPeCert() { + p.Cert, _ = catalog.ParseCertificate() + } + } + } + kevt.PS.PE = p + } + if p == nil { return nil, nil } +cmp: switch f { case fields.PeEntrypoint: return p.EntryPoint, nil @@ -763,6 +861,47 @@ func (*peAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, erro return p.Symbols, nil case fields.PeImports: return p.Imports, nil + case fields.PeImphash: + return p.Imphash, nil + case fields.PeIsDotnet: + return p.IsDotnet, nil + case fields.PeAnomalies: + return p.Anomalies, nil + case fields.PeIsSigned: + return p.IsSigned, nil + case fields.PeIsTrusted: + return p.IsTrusted, nil + case fields.PeCertIssuer: + if p.Cert == nil { + return nil, ErrPENilCertificate + } + return p.Cert.Issuer, nil + case fields.PeCertSubject: + if p.Cert == nil { + return nil, ErrPENilCertificate + } + return p.Cert.Subject, nil + case fields.PeCertSerial: + if p.Cert == nil { + return nil, ErrPENilCertificate + } + return p.Cert.SerialNumber, nil + case fields.PeCertAfter: + if p.Cert == nil { + return nil, ErrPENilCertificate + } + return p.Cert.NotAfter, nil + case fields.PeCertBefore: + if p.Cert == nil { + return nil, ErrPENilCertificate + } + return p.Cert.NotBefore, nil + case fields.PeIsDLL: + return kevt.Kparams.GetBool(kparams.FileIsDLL) + case fields.PeIsDriver: + return kevt.Kparams.GetBool(kparams.FileIsDriver) + case fields.PeIsExecutable: + return kevt.Kparams.GetBool(kparams.FileIsExecutable) case fields.PeCompany: return p.VersionResources[pe.Company], nil case fields.PeCopyright: @@ -816,6 +955,8 @@ func (*peAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, erro // memAccessor extracts parameters from memory alloc/free events. type memAccessor struct{} +func (memAccessor) setFields(fields []fields.Field) {} + func newMemAccessor() accessor { return &memAccessor{} } @@ -841,6 +982,8 @@ func (*memAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, err // dnsAccessor extracts values from DNS query/response event parameters. type dnsAccessor struct{} +func (dnsAccessor) setFields(fields []fields.Field) {} + func newDNSAccessor() accessor { return &dnsAccessor{} } diff --git a/pkg/filter/accessor_windows_test.go b/pkg/filter/accessor_windows_test.go index b8671b134..bc819fb5a 100644 --- a/pkg/filter/accessor_windows_test.go +++ b/pkg/filter/accessor_windows_test.go @@ -26,6 +26,7 @@ import ( ptypes "github.com/rabbitstack/fibratus/pkg/ps/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "reflect" "testing" "time" ) @@ -100,3 +101,49 @@ func TestCaptureInBrackets(t *testing.T) { assert.Equal(t, ".debug$S", v) assert.Equal(t, fields.SectionEntropy, subfield) } + +func TestNarrowAccessors(t *testing.T) { + var tests = []struct { + f Filter + expectedAccessors int + }{ + { + New(`ps.name = 'cmd.exe' and kevt.name = 'CreateProcess' or kevt.name in ('TerminateProcess', 'CreateFile')`, cfg), + 2, + }, + { + New(`ps.modules[kernel32.dll].location = 'C:\\Windows\\System32'`, cfg), + 1, + }, + { + New(`handle.type = 'Section' and pe.sections > 1 and kevt.name = 'CreateHandle'`, cfg), + 3, + }, + { + New(`sequence |kevt.name = 'CreateProcess'| as e1 |kevt.name = 'CreateFile' and file.name = $e1.ps.exe |`, cfg), + 3, + }, + { + New(`base(file.name) = 'kernel32.dll'`, cfg), + 1, + }, + } + + var pea *peAccessor + + for i, tt := range tests { + require.NoError(t, tt.f.Compile()) + naccessors := len(tt.f.(*filter).accessors) + if tt.expectedAccessors != naccessors { + t.Errorf("%d. accessors mismatch: exp=%d got=%d", i, tt.expectedAccessors, naccessors) + } + for _, a := range tt.f.(*filter).accessors { + if reflect.TypeOf(a) == reflect.TypeOf(&peAccessor{}) { + pea = a.(*peAccessor) + } + } + } + // check if fields are set in the accessor + require.NotNil(t, pea) + assert.Len(t, pea.fields, 3) +} diff --git a/pkg/filter/fields/fields.go b/pkg/filter/fields/fields.go index 66122ed2d..7194b1479 100644 --- a/pkg/filter/fields/fields.go +++ b/pkg/filter/fields/fields.go @@ -62,3 +62,8 @@ func IsDeprecated(f Field) (bool, *Deprecation) { } return false, nil } + +// IsBoolean determines if the given field has the bool type. +func IsBoolean(f Field) bool { + return fields[f].Type == kparams.Bool +} diff --git a/pkg/filter/fields/fields_windows.go b/pkg/filter/fields/fields_windows.go index f09180356..39de87bdd 100644 --- a/pkg/filter/fields/fields_windows.go +++ b/pkg/filter/fields/fields_windows.go @@ -210,6 +210,32 @@ const ( PeProduct Field = "pe.product" // PeProductVersion represents the internal product version provided at compile-time PeProductVersion Field = "pe.product.version" + // PeIsDLL indicates if the file is a DLL + PeIsDLL Field = "pe.is_dll" + // PeIsDriver indicates if the file is a driver + PeIsDriver Field = "pe.is_driver" + // PeIsExecutable indicates if the file is an executable + PeIsExecutable Field = "pe.is_exec" + // PeAnomalies represents the field that contains PE anomalies detected during parsing + PeAnomalies Field = "pe.anomalies" + // PeImphash is the field that yields the PE import hash + PeImphash Field = "pe.imphash" + // PeIsDotnet is the field which indicates if the binary contains the .NET assembly + PeIsDotnet Field = "pe.is_dotnet" + // PeIsSigned is the field which indicates if the binary is signed, either by embedded or catalog signature + PeIsSigned Field = "pe.is_signed" + // PeIsTrusted is the field which indicates if the binary signature is trusted + PeIsTrusted Field = "pe.is_trusted" + // PeCertIssuer is the field which indicates the certificate issuer + PeCertIssuer Field = "pe.cert.issuer" + // PeCertSubject is the field which indicates the certificate subject + PeCertSubject Field = "pe.cert.subject" + // PeCertSerial is the field which indicates the certificate serial + PeCertSerial Field = "pe.cert.serial" + // PeCertAfter is the field which indicates the timestamp after certificate is no longer valid + PeCertAfter Field = "pe.cert.after" + // PeCertBefore is the field which indicates the timestamp of the certificate enrollment date + PeCertBefore Field = "pe.cert.before" // KevtSeq is the event sequence number KevtSeq Field = "kevt.seq" @@ -397,6 +423,26 @@ func (f Field) IsPeField() bool { return strings.HasPrefix(string(f), "pe. func (f Field) IsMemField() bool { return strings.HasPrefix(string(f), "mem.") } func (f Field) IsDNSField() bool { return strings.HasPrefix(string(f), "dns.") } +func (f Field) IsPeSection() bool { return f == PeNumSections } +func (f Field) IsPeSectionEntropy() bool { + fld := string(f) + return strings.HasPrefix(fld, "pe.sections[") && strings.HasSuffix(fld, ".entropy") +} +func (f Field) IsPeSymbol() bool { return f == PeSymbols || f == PeNumSymbols || f == PeImports } +func (f Field) IsPeVersionResource() bool { + return f == PeCompany || f == PeCopyright || f == PeDescription || f == PeFileName || f == PeFileVersion || f == PeProduct || f == PeProductVersion +} +func (f Field) IsPeImphash() bool { return f == PeImphash } +func (f Field) IsPeDotnet() bool { return f == PeIsDotnet } +func (f Field) IsPeAnomalies() bool { return f == PeAnomalies } +func (f Field) IsPeSignature() bool { + return f == PeIsTrusted || f == PeIsSigned || f == PeCertIssuer || f == PeCertSerial || f == PeCertSubject || f == PeCertBefore || f == PeCertAfter +} +func (f Field) IsPeIsTrusted() bool { return f == PeIsTrusted } +func (f Field) IsPeIsSigned() bool { return f == PeIsSigned } + +func (f Field) IsPeCert() bool { return strings.HasPrefix(string(f), "pe.cert.") } + // Segment represents the type alias for the segment. Segment // denotes the location of the value within an indexed field. type Segment string @@ -603,6 +649,19 @@ var fields = map[Field]FieldInfo{ PeFileVersion: {PeFileVersion, "file version supplied at compile-time", kparams.UnicodeString, []string{"pe.file.version = '10.0.18362.693 (WinBuild.160101.0800)'"}, nil}, PeProduct: {PeProduct, "internal product name of the file provided at compile-time", kparams.UnicodeString, []string{"pe.product = 'Microsoft® Windows® Operating System'"}, nil}, PeProductVersion: {PeProductVersion, "internal product version of the file provided at compile-time", kparams.UnicodeString, []string{"pe.product.version = '10.0.18362.693'"}, nil}, + PeIsDLL: {PeIsDLL, "indicates if the loaded image or created file is a DLL", kparams.Bool, []string{"pe.is_dll'"}, nil}, + PeIsDriver: {PeIsDriver, "indicates if the loaded image or created file is a driver", kparams.Bool, []string{"pe.is_driver'"}, nil}, + PeIsExecutable: {PeIsExecutable, "indicates if the loaded image or created file is an executable", kparams.Bool, []string{"pe.is_exec'"}, nil}, + PeImphash: {PeImphash, "import hash", kparams.AnsiString, []string{"pe.impash = '5d3861c5c547f8a34e471ba273a732b2'"}, nil}, + PeIsDotnet: {PeIsDotnet, "indicates if PE contains CLR data", kparams.Bool, []string{"pe.is_dotnet"}, nil}, + PeAnomalies: {PeAnomalies, "contains PE anomalies detected during parsing", kparams.Slice, []string{"pe.anomalies in ('number of sections is 0')"}, nil}, + PeIsSigned: {PeIsSigned, "indicates if the PE has embedded or catalog signature", kparams.Bool, []string{"pe.is_signed"}, nil}, + PeIsTrusted: {PeIsTrusted, "indicates if the PE certificate chain is trusted", kparams.Bool, []string{"pe.is_trusted"}, nil}, + PeCertSerial: {PeCertSerial, "PE certificate serial number", kparams.UnicodeString, []string{"pe.cert.serial = '330000023241fb59996dcc4dff000000000232'"}, nil}, + PeCertSubject: {PeCertSubject, "PE certificate subject", kparams.UnicodeString, []string{"pe.cert.subject contains 'Washington, Redmond, Microsoft Corporation'"}, nil}, + PeCertIssuer: {PeCertIssuer, "PE certificate CA", kparams.UnicodeString, []string{"pe.cert.issuer contains 'Washington, Redmond, Microsoft Corporation'"}, nil}, + PeCertAfter: {PeCertAfter, "PE certificate expiration date", kparams.Time, []string{"pe.cert.after contains '2024-02-01 00:05:42 +0000 UTC'"}, nil}, + PeCertBefore: {PeCertBefore, "PE certificate enrollment date", kparams.Time, []string{"pe.cert.before contains '2024-02-01 00:05:42 +0000 UTC'"}, nil}, MemBaseAddress: {MemBaseAddress, "region base address", kparams.Address, []string{"mem.address = '211d13f2000'"}, nil}, MemRegionSize: {MemRegionSize, "region size", kparams.Uint64, []string{"mem.size > 438272"}, nil}, diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index 4c6a626d3..feeb847fd 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -68,10 +68,9 @@ type filter struct { accessors []accessor fields []fields.Field boundFields []*ql.BoundFieldLiteral - // useFuncValuer determines whether we should supply the function valuer - useFuncValuer bool // stringFields contains filter field names mapped to their string values stringFields map[fields.Field][]string + hasFunctions bool } // Compile parsers the filter expression and builds a binary expression tree @@ -114,7 +113,7 @@ func (f *filter) Compile() error { f.addBoundField(rhs) } case *ql.Function: - f.useFuncValuer = true + f.hasFunctions = true for _, arg := range expr.Args { if field, ok := arg.(*ql.FieldLiteral); ok { f.addField(fields.Field(field.Value)) @@ -123,6 +122,11 @@ func (f *filter) Compile() error { f.addBoundField(field) } } + case *ql.FieldLiteral: + field := fields.Field(expr.Value) + if fields.IsBoolean(field) { + f.addField(field) + } } } if f.expr != nil { @@ -138,7 +142,7 @@ func (f *filter) Compile() error { } } } - if len(f.fields) == 0 && !f.useFuncValuer { + if len(f.fields) == 0 && !f.hasFunctions { return ErrNoFields } // only retain accessors for declared filter fields @@ -150,7 +154,7 @@ func (f *filter) Run(kevt *kevent.Kevent) bool { if f.expr == nil { return false } - return ql.Eval(f.expr, f.mapValuer(kevt), f.useFuncValuer) + return ql.Eval(f.expr, f.mapValuer(kevt), f.hasFunctions) } func (f *filter) RunSequence(kevt *kevent.Kevent, seqID uint16, partials map[uint16][]*kevent.Kevent) bool { @@ -208,7 +212,7 @@ func (f *filter) RunSequence(kevt *kevent.Kevent, seqID uint16, partials map[uin } } n++ - match = ql.Eval(expr.Expr, valuer, f.useFuncValuer) + match = ql.Eval(expr.Expr, valuer, f.hasFunctions) if match { break } @@ -231,9 +235,9 @@ func (f *filter) RunSequence(kevt *kevent.Kevent, seqID uint16, partials map[uin } } } - match = joinsEqual(joins) && ql.Eval(expr.Expr, valuer, f.useFuncValuer) + match = joinsEqual(joins) && ql.Eval(expr.Expr, valuer, f.hasFunctions) } else { - match = ql.Eval(expr.Expr, valuer, f.useFuncValuer) + match = ql.Eval(expr.Expr, valuer, f.hasFunctions) } if match && !by.IsEmpty() { if v := valuer[by.String()]; v != nil { diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index 1bfe90bb4..767d3fd07 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -24,6 +24,8 @@ import ( "github.com/rabbitstack/fibratus/pkg/ps" "github.com/stretchr/testify/assert" "net" + "os" + "path/filepath" "testing" "time" @@ -58,6 +60,10 @@ func TestFilterCompile(t *testing.T) { require.EqualError(t, f.Compile(), "expected at least one field or operator but zero found") f = New(`ps.name`, cfg) require.EqualError(t, f.Compile(), "expected at least one field or operator but zero found") + f = New(`pe.is_exec`, cfg) + require.NoError(t, f.Compile()) + f = New(`length(pe.imphash) > 0`, cfg) + require.NoError(t, f.Compile()) f = New(`ps.name =`, cfg) require.EqualError(t, f.Compile(), "ps.name =\n╭─────────^\n|\n|\n╰─────────────────── expected field, bound field, string, number, bool, ip, function") } @@ -74,42 +80,6 @@ func TestSeqFilterCompile(t *testing.T) { assert.True(t, len(f.GetStringFields()) > 0) } -func TestNarrowAccessors(t *testing.T) { - var tests = []struct { - f Filter - expectedAccesors int - }{ - { - New(`ps.name = 'cmd.exe' and kevt.name = 'CreateProcess' or kevt.name in ('TerminateProcess', 'CreateFile')`, cfg), - 2, - }, - { - New(`ps.modules[kernel32.dll].location = 'C:\\Windows\\System32'`, cfg), - 1, - }, - { - New(`handle.type = 'Section' and pe.sections > 1 and kevt.name = 'CreateHandle'`, cfg), - 3, - }, - { - New(`sequence |kevt.name = 'CreateProcess'| as e1 |kevt.name = 'CreateFile' and file.name = $e1.ps.exe |`, cfg), - 3, - }, - { - New(`base(file.name) = 'kernel32.dll'`, cfg), - 1, - }, - } - - for i, tt := range tests { - require.NoError(t, tt.f.Compile()) - naccessors := len(tt.f.(*filter).accessors) - if tt.expectedAccesors != naccessors { - t.Errorf("%d. accessors mismatch: exp=%d got=%d", i, tt.expectedAccesors, naccessors) - } - } -} - func TestSeqFilterInvalidBoundRefs(t *testing.T) { f := New(`sequence |kevt.name = 'CreateProcess'| as e1 @@ -630,6 +600,57 @@ func TestPEFilter(t *testing.T) { } } +func TestLazyPEFilter(t *testing.T) { + kevt := &kevent.Kevent{ + Type: ktypes.LoadImage, + PS: &pstypes.PS{ + PID: 2312, + Exe: filepath.Join(os.Getenv("windir"), "notepad.exe"), + }, + Kparams: kevent.Kparams{ + kparams.FileIsDLL: {Name: kparams.FileIsDLL, Type: kparams.Bool, Value: true}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\user32.dll"}, + }, + } + + var tests = []struct { + filter string + matches bool + }{ + + {`pe.sections[.text].entropy > 1.23`, true}, + {`pe.symbols IN ('GetTextFaceW', 'GetProcessHeap')`, true}, + {`pe.is_dll`, true}, + {`length(pe.imphash) > 0`, true}, + {`pe.is_dotnet`, false}, + {`pe.resources[FileDesc] icontains 'Notepad'`, true}, + {`pe.file.name ~= 'NOTEPAD.EXE'`, true}, + {`pe.nsymbols > 10 AND pe.nsections > 2`, true}, + {`pe.nsections > 1`, true}, + {`length(pe.anomalies) = 0`, true}, + {`pe.is_signed`, true}, + {`pe.is_trusted`, true}, + {`pe.cert.subject icontains 'microsoft'`, true}, + {`pe.cert.issuer icontains 'microsoft'`, true}, + {`length(pe.cert.serial) > 0`, true}, + } + + for i, tt := range tests { + f := New(tt.filter, cfg) + err := f.Compile() + if err != nil { + t.Fatal(err) + } + require.Nil(t, kevt.PS.PE) + matches := f.Run(kevt) + if matches != tt.matches { + t.Errorf("%d. %q pe lazy filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) + } + require.NotNil(t, kevt.PS.PE) + kevt.PS.PE = nil + } +} + func TestMemFilter(t *testing.T) { kpars := kevent.Kparams{ kparams.MemRegionSize: {Name: kparams.MemRegionSize, Type: kparams.Uint64, Value: uint64(8192)}, diff --git a/pkg/filter/filter_windows.go b/pkg/filter/filter_windows.go index 8c6ee9a23..5121d7359 100644 --- a/pkg/filter/filter_windows.go +++ b/pkg/filter/filter_windows.go @@ -54,6 +54,8 @@ func New(expr string, config *config.Config, options ...Option) Filter { newKevtAccessor(), // process state and parameters newPSAccessor(opts.psnap), + // PE metadata + newPEAccessor(), } kconfig := config.Kstream fconfig := config.Filters @@ -82,9 +84,6 @@ func New(expr string, config *config.Config, options ...Option) Filter { if kconfig.EnableDNSEvents { accessors = append(accessors, newDNSAccessor()) } - if config.PE.Enabled { - accessors = append(accessors, newPEAccessor()) - } var parser *ql.Parser if fconfig.HasMacros() { diff --git a/pkg/filter/ql/lexer_test.go b/pkg/filter/ql/lexer_test.go index d7c62a21b..85238d152 100644 --- a/pkg/filter/ql/lexer_test.go +++ b/pkg/filter/ql/lexer_test.go @@ -61,6 +61,7 @@ func TestScanner(t *testing.T) { // fields {s: `ps.name`, tok: Field, lit: "ps.name"}, + {s: `pe.is_exec`, tok: Field, lit: "pe.is_exec"}, {s: `ps.pe.sections[.debug$S].entropy`, tok: Field, lit: "ps.pe.sections[.debug$S].entropy"}, {s: `ps.envs[CommonProgramFiles86]`, tok: Field, lit: "ps.envs[CommonProgramFiles86]"}, diff --git a/pkg/kcap/version/version_windows.go b/pkg/kcap/version/version_windows.go index ef3dec657..ea37b8683 100644 --- a/pkg/kcap/version/version_windows.go +++ b/pkg/kcap/version/version_windows.go @@ -45,4 +45,6 @@ const ( const ( // PESecV1 is the v1 of the PE section PESecV1 Version = iota + 1 + // PESecV2 is the v2 of the PE section + PESecV2 ) diff --git a/pkg/kevent/kevent_windows.go b/pkg/kevent/kevent_windows.go index c7c5eb54b..28162abae 100644 --- a/pkg/kevent/kevent_windows.go +++ b/pkg/kevent/kevent_windows.go @@ -222,6 +222,11 @@ 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 +} + // RundownKey calculates the rundown event hash. The hash is // used to determine if the rundown event was already processed. func (e Kevent) RundownKey() uint64 { diff --git a/pkg/kevent/kparam.go b/pkg/kevent/kparam.go index 664ed4fa3..655427ca1 100644 --- a/pkg/kevent/kparam.go +++ b/pkg/kevent/kparam.go @@ -322,6 +322,19 @@ func (kpars Kparams) GetUint8(name string) (uint8, error) { return v, nil } +// GetBool returns the underlying boolean value from the parameter. +func (kpars Kparams) GetBool(name string) (bool, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return false, err + } + v, ok := kpar.Value.(bool) + if !ok { + return false, fmt.Errorf("unable to type cast %q parameter to bool value", name) + } + return v, nil +} + // GetInt8 returns the underlying int8 value from the parameter. func (kpars Kparams) GetInt8(name string) (int8, error) { kpar, err := kpars.findParam(name) diff --git a/pkg/kevent/kparams/fields_windows.go b/pkg/kevent/kparams/fields_windows.go index 3db8af039..fccdf43f8 100644 --- a/pkg/kevent/kparams/fields_windows.go +++ b/pkg/kevent/kparams/fields_windows.go @@ -109,6 +109,12 @@ const ( FileIrpPtr = "irp" // FileExtraInfo is the parameter that represents extra information returned by the file system for the operation. For example for a read request, the actual number of bytes that were read. FileExtraInfo = "extra_info" + // FileIsDLL is the parameter that indicates if the file is a DLL + FileIsDLL = "is_dll" + // FileIsDriver is the parameter that indicates if the file is a driver + FileIsDriver = "is_driver" + // FileIsExecutable is the parameter that indicates if the file is an executable + FileIsExecutable = "is_exec" // FileViewBase is the parameter that represents the base address of the mapped section. FileViewBase = "view_base" diff --git a/pkg/kevent/marshaller_windows.go b/pkg/kevent/marshaller_windows.go index 74df7b58b..de7156bd3 100644 --- a/pkg/kevent/marshaller_windows.go +++ b/pkg/kevent/marshaller_windows.go @@ -21,6 +21,7 @@ package kevent import ( "expvar" "fmt" + "github.com/rabbitstack/fibratus/pkg/util/convert" "math" "net" "sort" @@ -137,12 +138,7 @@ func (e *Kevent) MarshalRaw() []byte { case kparams.PID, kparams.TID: b = append(b, bytes.WriteUint32(kpar.Value.(uint32))...) case kparams.Bool: - v := kpar.Value.(bool) - if v { - b = append(b, 1) - } else { - b = append(b, 0) - } + b = append(b, convert.Btoi(kpar.Value.(bool))) case kparams.Time: v := kpar.Value.(time.Time) ts := make([]byte, 0) diff --git a/pkg/kstream/processors/fs_windows.go b/pkg/kstream/processors/fs_windows.go index 2311797f1..c5a4c59db 100644 --- a/pkg/kstream/processors/fs_windows.go +++ b/pkg/kstream/processors/fs_windows.go @@ -26,6 +26,7 @@ import ( "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" @@ -140,6 +141,17 @@ 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) + pefile, err := pe.ParseFile(filename, pe.WithSymbols()) + 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, ktypes.UnmapViewFile: var fileObject uint64 diff --git a/pkg/kstream/processors/image_windows.go b/pkg/kstream/processors/image_windows.go index a8942e44c..425801d34 100644 --- a/pkg/kstream/processors/image_windows.go +++ b/pkg/kstream/processors/image_windows.go @@ -22,6 +22,7 @@ import ( "expvar" "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/signature" ) @@ -53,6 +54,15 @@ func (m *imageProcessor) ProcessEvent(e *kevent.Kevent) (*kevent.Kevent, bool, e if err != nil { signatureErrors.Add(1) } + // parse PE image data + filename := e.GetParamAsString(kparams.FileName) + pefile, err := pe.ParseFile(filename, 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() diff --git a/pkg/pe/_fixtures/mscorlib.dll b/pkg/pe/_fixtures/mscorlib.dll new file mode 100644 index 000000000..482261960 Binary files /dev/null and b/pkg/pe/_fixtures/mscorlib.dll differ diff --git a/pkg/pe/marshaller.go b/pkg/pe/marshaller.go index 5f6fbe3a4..948928e87 100644 --- a/pkg/pe/marshaller.go +++ b/pkg/pe/marshaller.go @@ -23,7 +23,9 @@ package pe import ( "fmt" + kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" "github.com/rabbitstack/fibratus/pkg/util/bytes" + "github.com/rabbitstack/fibratus/pkg/util/convert" "math" "time" "unsafe" @@ -89,11 +91,39 @@ func (pe *PE) Marshal() []byte { b = append(b, v...) } + // signature and cert data + b = append(b, convert.Btoi(pe.IsSigned)) + b = append(b, convert.Btoi(pe.IsTrusted)) + if pe.Cert != nil { + crt := pe.Cert.Marshal() + b = append(b, bytes.WriteUint32(uint32(len(crt)))...) + b = append(b, crt...) + } else { + b = append(b, bytes.WriteUint32(0)...) + } + + // PE binary format + b = append(b, convert.Btoi(pe.IsDriver)) + b = append(b, convert.Btoi(pe.IsDLL)) + b = append(b, convert.Btoi(pe.IsExecutable)) + b = append(b, convert.Btoi(pe.IsDotnet)) + + // imphash + b = append(b, bytes.WriteUint16(uint16(len(pe.Imphash)))...) + b = append(b, pe.Imphash...) + + // anomalies + b = append(b, bytes.WriteUint16(uint16(len(pe.Anomalies)))...) + for _, s := range pe.Anomalies { + b = append(b, bytes.WriteUint16(uint16(len(s)))...) + b = append(b, s...) + } + return b } // Unmarshal recovers the PE metadata from the byte stream. -func (pe *PE) Unmarshal(b []byte) error { +func (pe *PE) Unmarshal(b []byte, ver kcapver.Version) error { if len(b) < 6 { return fmt.Errorf("expected at least 6 bytes but got %d bytes", len(b)) } @@ -208,18 +238,125 @@ func (pe *PE) Unmarshal(b []byte) error { } } + offset += roffset + + if ver >= kcapver.PESecV2 { + pe.IsSigned = convert.Itob(b[20+offset]) + pe.IsTrusted = convert.Itob(b[21+offset]) + + certSize := bytes.ReadUint32(b[22+offset:]) + if certSize > 0 { + pe.Cert = &Cert{} + err := pe.Cert.Unmarshal(b, offset, certSize) + if err != nil { + return err + } + } + + offset += certSize + + pe.IsDriver = convert.Itob(b[26+offset]) + pe.IsDLL = convert.Itob(b[27+offset]) + pe.IsExecutable = convert.Itob(b[28+offset]) + pe.IsDotnet = convert.Itob(b[29+offset]) + + // read impash + l = bytes.ReadUint16(b[30+offset:]) + buf = b[32+offset:] + offset += uint32(l) + pe.Imphash = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // read anomalies + nanomalies := bytes.ReadUint16(b[32+offset:]) + pe.Anomalies = make([]string, nanomalies) + var off uint32 + for n := uint16(0); n < nanomalies; n++ { + l := bytes.ReadUint16(b[34+offset+off:]) + buf := b[36+offset+off:] + pe.Anomalies[n] = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + off += 2 + uint32(l) + } + } + + return nil +} + +// Marshal writes certificate info into a raw buffer. +func (c *Cert) Marshal() []byte { + b := make([]byte, 0) + + before := make([]byte, 0) + before = c.NotBefore.AppendFormat(before, time.RFC3339Nano) + b = append(b, bytes.WriteUint16(uint16(len(before)))...) + b = append(b, before...) + + after := make([]byte, 0) + after = c.NotAfter.AppendFormat(after, time.RFC3339Nano) + b = append(b, bytes.WriteUint16(uint16(len(after)))...) + b = append(b, after...) + + b = append(b, bytes.WriteUint16(uint16(len(c.SerialNumber)))...) + b = append(b, c.SerialNumber...) + b = append(b, bytes.WriteUint16(uint16(len(c.Subject)))...) + b = append(b, c.Subject...) + b = append(b, bytes.WriteUint16(uint16(len(c.Issuer)))...) + b = append(b, c.Issuer...) + + return b +} + +// Unmarshal decodes cert info from the raw buffer. This method +// assumes the certificate structure size was already read. +func (c *Cert) Unmarshal(b []byte, offset, certSize uint32) error { + if certSize > uint32(len(b)) { + return fmt.Errorf("invalid PE cert size. Got %d but max buffer size is %d", certSize, len(b)) + } + + // read not before + l := bytes.ReadUint16(b[26+offset:]) + buf := b[28+offset:] + offset += uint32(l) + if len(buf) > 0 { + c.NotBefore, _ = time.Parse(time.RFC3339Nano, string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + } + + // read not after + l = bytes.ReadUint16(b[28+offset:]) + buf = b[30+offset:] + offset += uint32(l) + if len(buf) > 0 { + c.NotAfter, _ = time.Parse(time.RFC3339Nano, string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + } + + // read serial + l = bytes.ReadUint16(b[30+offset:]) + buf = b[32+offset:] + offset += uint32(l) + c.SerialNumber = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // read subject + l = bytes.ReadUint16(b[32+offset:]) + buf = b[34+offset:] + offset += uint32(l) + c.Subject = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // read issuer + l = bytes.ReadUint16(b[34+offset:]) + buf = b[36+offset:] + c.Issuer = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + return nil } // NewFromKcap restores the PE metadata from the byte stream. -func NewFromKcap(b []byte) (*PE, error) { +func NewFromKcap(b []byte, ver kcapver.Version) (*PE, error) { pe := &PE{ Sections: make([]Sec, 0), Symbols: make([]string, 0), Imports: make([]string, 0), VersionResources: make(map[string]string), } - if err := pe.Unmarshal(b); err != nil { + if err := pe.Unmarshal(b, ver); err != nil { return nil, err } return pe, nil diff --git a/pkg/pe/marshaller_test.go b/pkg/pe/marshaller_test.go index f8adb66c6..586b7c381 100644 --- a/pkg/pe/marshaller_test.go +++ b/pkg/pe/marshaller_test.go @@ -22,13 +22,14 @@ package pe import ( + kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" "time" ) -func TestMetadataMarshal(t *testing.T) { +func TestPEMarshal(t *testing.T) { now := time.Now() pe := &PE{ @@ -44,12 +45,25 @@ func TestMetadataMarshal(t *testing.T) { Symbols: []string{"SelectObject", "GetTextFaceW", "EnumFontsW", "TextOutW", "GetProcessHeap"}, Imports: []string{"GDI32.dll", "USER32.dll", "msvcrt.dll", "api-ms-win-core-libraryloader-l1-2-0.dl"}, VersionResources: map[string]string{"CompanyName": "Microsoft Corporation", "FileDescription": "Notepad", "FileVersion": "10.0.18362.693"}, + IsSigned: true, + IsTrusted: true, + Cert: &Cert{ + Issuer: "Washington, Redmond, Microsoft Corporation", + Subject: "Washington, Redmond, Microsoft Corporation", + SerialNumber: "330000023241fb59996dcc4dff000000000232", + NotBefore: time.Now(), + NotAfter: time.Now(), + }, + IsExecutable: true, + IsDotnet: true, + Imphash: "5d3861c5c547f8a34e471ba273a732b2", + Anomalies: []string{"optional header checksum is invalid", "image base is 0"}, } b := pe.Marshal() newPE := &PE{VersionResources: make(map[string]string)} - err := newPE.Unmarshal(b) + err := newPE.Unmarshal(b, kcapver.PESecV2) require.NoError(t, err) assert.Equal(t, uint16(7), newPE.NumberOfSections) @@ -83,4 +97,23 @@ func TestMetadataMarshal(t *testing.T) { assert.Equal(t, "10.0.18362.693", newPE.VersionResources["FileVersion"]) assert.Equal(t, "Microsoft Corporation", newPE.VersionResources["CompanyName"]) assert.Equal(t, "Notepad", newPE.VersionResources["FileDescription"]) + + assert.True(t, newPE.IsSigned) + assert.True(t, newPE.IsTrusted) + + assert.NotNil(t, newPE.Cert) + assert.False(t, newPE.Cert.NotBefore.IsZero()) + assert.False(t, newPE.Cert.NotAfter.IsZero()) + + assert.Equal(t, pe.Cert.SerialNumber, newPE.Cert.SerialNumber) + assert.Equal(t, pe.Cert.Subject, newPE.Cert.Subject) + assert.Equal(t, pe.Cert.Issuer, newPE.Cert.Issuer) + + assert.False(t, newPE.IsDriver) + assert.False(t, newPE.IsDLL) + assert.True(t, newPE.IsExecutable) + assert.True(t, newPE.IsDotnet) + assert.Equal(t, pe.Imphash, newPE.Imphash) + assert.Equal(t, len(pe.Anomalies), len(newPE.Anomalies)) + assert.Contains(t, newPE.Anomalies, "image base is 0") } diff --git a/pkg/pe/parser.go b/pkg/pe/parser.go index c0ad3b3ae..6d88dc58d 100644 --- a/pkg/pe/parser.go +++ b/pkg/pe/parser.go @@ -46,6 +46,7 @@ var ( skippedImages = expvar.NewInt("pe.skipped.images") directoryParseErrors = expvar.NewInt("pe.directory.parse.errors") versionResourcesParseErrors = expvar.NewInt("pe.version.resources.parse.errors") + imphashErrors = expvar.NewInt("pe.imphash.errors") ) type opts struct { @@ -53,8 +54,10 @@ type opts struct { parseSections bool parseResources bool parseSecurity bool + parseCLR bool sectionEntropy bool sectionMD5 bool + calcImphash bool excludedImages []string } @@ -122,6 +125,20 @@ func WithSecurity() Option { } } +// WithImphash indicates if the import hash (imphash) is calculated as part of PE parsing. +func WithImphash() Option { + return func(o *opts) { + o.calcImphash = true + } +} + +// WithCLR indicates if CLR (Common Language Runtime) header is parsed. +func WithCLR() Option { + return func(o *opts) { + o.parseCLR = true + } +} + // ParseFile parses the PE given the file system path and parser options. func ParseFile(path string, opts ...Option) (*PE, error) { return parse(path, nil, opts...) @@ -183,14 +200,14 @@ func newParserOpts(opts opts) *peparser.Options { OmitSecurityDirectory: !opts.parseSecurity, OmitExceptionDirectory: true, OmitTLSDirectory: true, - OmitCLRHeaderDirectory: true, + OmitCLRHeaderDirectory: !opts.parseCLR, OmitDelayImportDirectory: true, OmitBoundImportDirectory: true, OmitArchitectureDirectory: true, OmitDebugDirectory: true, OmitRelocDirectory: true, OmitResourceDirectory: !opts.parseResources, - OmitImportDirectory: !opts.parseSymbols, + OmitImportDirectory: !opts.parseSymbols && !opts.calcImphash, OmitExportDirectory: true, OmitLoadConfigDirectory: true, OmitGlobalPtrDirectory: true, @@ -238,6 +255,7 @@ func parse(path string, data []byte, options ...Option) (*PE, error) { Imports: make([]string, 0), Sections: make([]Sec, 0), VersionResources: make(map[string]string), + filename: path, } switch pe.Is64 { case true: @@ -251,7 +269,7 @@ func parse(path string, data []byte, options ...Option) (*PE, error) { } // parse section header - if opts.parseSections { + if opts.parseSections || opts.parseResources { err = pe.ParseSectionHeader() if err != nil { return nil, err @@ -272,10 +290,18 @@ func parse(path string, data []byte, options ...Option) (*PE, error) { p.Sections = append(p.Sections, sec) } - // parse data directories - err = pe.ParseDataDirectories() - if err != nil { - directoryParseErrors.Add(1) + // retrieve basic PE data + p.IsDLL = pe.IsDLL() + p.IsDriver = pe.IsDriver() + p.IsExecutable = pe.IsEXE() + + if opts.parseSymbols || opts.parseResources || opts.parseSecurity || + opts.calcImphash || opts.parseCLR { + // parse data directories + err = pe.ParseDataDirectories() + if err != nil { + directoryParseErrors.Add(1) + } } // add imported symbols @@ -295,6 +321,7 @@ func parse(path string, data []byte, options ...Option) (*PE, error) { } } + // parse certificate info if opts.parseSecurity { p.IsSigned = pe.IsSigned if pe.HasCertificate { @@ -308,6 +335,17 @@ func parse(path string, data []byte, options ...Option) (*PE, error) { } } + // calculate imphash + if opts.calcImphash { + p.Imphash, err = pe.ImpHash() + if err != nil { + imphashErrors.Add(1) + } + } + + p.IsDotnet = pe.HasCLR + p.Anomalies = pe.Anomalies + return p, nil } diff --git a/pkg/pe/parser_test.go b/pkg/pe/parser_test.go index bc4644334..207c134c1 100644 --- a/pkg/pe/parser_test.go +++ b/pkg/pe/parser_test.go @@ -90,6 +90,12 @@ func TestParseFile(t *testing.T) { } } +func TestIsDotnet(t *testing.T) { + pe, err := ParseFile("./_fixtures/mscorlib.dll", WithCLR()) + require.NoError(t, err) + require.True(t, pe.IsDotnet) +} + func TestParseMem(t *testing.T) { var tests = []struct { executable string diff --git a/pkg/pe/types.go b/pkg/pe/types.go index 100e921ad..605f0e853 100644 --- a/pkg/pe/types.go +++ b/pkg/pe/types.go @@ -23,6 +23,8 @@ package pe import ( "fmt" + "github.com/rabbitstack/fibratus/pkg/sys" + "runtime" "time" ) @@ -51,8 +53,19 @@ type Sec struct { Md5 string } +// String returns the stirng representation of the section. +func (s Sec) String() string { + return fmt.Sprintf("Name: %s, Size: %d, Entropy: %f, Md5: %s", s.Name, s.Size, s.Entropy, s.Md5) +} + // Cert represents certificate information embedded in PE. type Cert struct { + // NotBefore specifies the certificate won't be valid before this timestamp. + NotBefore time.Time `json:"not_before"` + + // NotAfter specifies the certificate won't be valid after this timestamp. + NotAfter time.Time `json:"not_after"` + // Issuer represents the certificate authority (CA) that charges customers to issue // certificates for them. Issuer string `json:"issuer"` @@ -61,12 +74,6 @@ type Cert struct { // with (i.e. the "owner" of the certificate). Subject string `json:"subject"` - // NotBefore specifies the certificate won't be valid before this timestamp. - NotBefore time.Time `json:"not_before"` - - // NotAfter specifies the certificate won't be valid after this timestamp. - NotAfter time.Time `json:"not_after"` - // SerialNumber represents the serial number MUST be a positive integer assigned // by the CA to each certificate. It MUST be unique for each certificate issued by // a given CA (i.e., the issuer name and serial number identify a unique certificate). @@ -75,11 +82,6 @@ type Cert struct { SerialNumber string `json:"serial_number"` } -// String returns the stirng representation of the section. -func (s Sec) String() string { - return fmt.Sprintf("Name: %s, Size: %d, Entropy: %f, Md5: %s", s.Name, s.Size, s.Entropy, s.Md5) -} - // PE contains various headers that identifies the format and characteristics of the executable files. type PE struct { // NumberOfSections designates the total number of sections found withing the binary. @@ -100,14 +102,41 @@ type PE struct { Imports []string `json:"imports"` // VersionResources holds the version resources VersionResources map[string]string `json:"resources"` - // IsSigned determines if the PE contains the digital signature. + // IsSigned determines if the PE contains a digital signature. IsSigned bool `json:"is_signed"` + // IsTrusted determines if the PE certificate chain is trusted. + IsTrusted bool `json:"is_trusted"` // Cert contains certificate information. Cert *Cert `json:"cert"` + // IsDriver indicates if the PE contains driver code. + IsDriver bool `json:"is_driver"` + // IsDLL indicates if the PE is a DLL. + IsDLL bool `json:"is_dll"` + // IsExecutable indicates if the PE is an executable image. + IsExecutable bool `json:"is_executable"` + // IsDotnet indicates if the PE contains CLR data. + IsDotnet bool `json:"is_dotnet"` + // Imphash represents the PE import hash. + Imphash string `json:"imphash"` + // Anomalies contains PE parsing anomalies. + Anomalies []string `json:"anomalies"` + + filename string +} + +// VerifySignature checks if the embedded PE signature is trusted. +func (pe *PE) VerifySignature() { + if sys.IsWintrustFound() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + trust := sys.NewWintrustData(sys.WtdChoiceFile) + defer trust.Close() + pe.IsTrusted = trust.VerifyFile(pe.filename) + } } // String returns the string representation of the PE metadata. -func (pe PE) String() string { +func (pe *PE) String() string { return fmt.Sprintf(` Number of sections: %d Number of symbols: %d diff --git a/pkg/ps/types/marshaller_windows.go b/pkg/ps/types/marshaller_windows.go index 393e1bb21..95030e68c 100644 --- a/pkg/ps/types/marshaller_windows.go +++ b/pkg/ps/types/marshaller_windows.go @@ -84,11 +84,11 @@ func (ps *PS) Marshal() []byte { // write the PE metadata if ps.PE != nil { buf := ps.PE.Marshal() - sec := section.New(section.PE, kcapver.PESecV1, 0, uint32(len(buf))) + sec := section.New(section.PE, kcapver.PESecV2, 0, uint32(len(buf))) b = append(b, sec[:]...) b = append(b, buf...) } else { - sec := section.New(section.PE, kcapver.PESecV1, 0, 0) + sec := section.New(section.PE, kcapver.PESecV2, 0, 0) b = append(b, sec[:]...) } @@ -255,7 +255,7 @@ readpe: } var err error - ps.PE, err = pe.NewFromKcap(b[idx+offset:]) + ps.PE, err = pe.NewFromKcap(b[idx+offset:], sec.Version()) if err != nil { return err } diff --git a/pkg/util/convert/convert.go b/pkg/util/convert/convert.go new file mode 100644 index 000000000..0b2f787ff --- /dev/null +++ b/pkg/util/convert/convert.go @@ -0,0 +1,32 @@ +/* + * 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 convert + +// Btoi converts the provided bool value to uint8 integer. +func Btoi(b bool) uint8 { + if b { + return 1 + } + return 0 +} + +// Itob converts the provided uint8 integer to a bool value. +func Itob(i uint8) bool { + return i > 0 +} diff --git a/pkg/util/signature/cat.go b/pkg/util/signature/cat.go index ed798febb..03dda79c5 100644 --- a/pkg/util/signature/cat.go +++ b/pkg/util/signature/cat.go @@ -117,11 +117,11 @@ func (c *Cat) ParseCertificate() (*pe.Cert, error) { return nil, err } defer f.Close() - cert, err := io.ReadAll(f) + crt, err := io.ReadAll(f) if err != nil { return nil, err } - pkcs, err := pkcs7.Parse(cert) + pkcs, err := pkcs7.Parse(crt) if err != nil { return nil, err }