diff --git a/go.mod b/go.mod index 6496dfa76..839eccae8 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,7 @@ require ( go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf // indirect golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/sync v0.12.0 // indirect + golang.org/x/sync v0.21.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect honnef.co/go/tools v0.3.2 // indirect ) diff --git a/go.sum b/go.sum index f541534a3..c11b25fdb 100644 --- a/go.sum +++ b/go.sum @@ -305,6 +305,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 5014b87d9..d70c5c8fb 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -21,6 +21,8 @@ package bootstrap import ( "context" "errors" + "os" + "github.com/rabbitstack/fibratus/internal/evasion" "github.com/rabbitstack/fibratus/pkg/aggregator" "github.com/rabbitstack/fibratus/pkg/alertsender" @@ -36,11 +38,11 @@ import ( "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/util/multierror" "github.com/rabbitstack/fibratus/pkg/util/signals" + "github.com/rabbitstack/fibratus/pkg/util/signature" "github.com/rabbitstack/fibratus/pkg/util/version" "github.com/rabbitstack/fibratus/pkg/yara" log "github.com/sirupsen/logrus" "golang.org/x/sys/windows" - "os" ) // ErrAlreadyRunning signals a Fibratus process is already running in the system @@ -427,6 +429,7 @@ func (f *App) stop() { if f.signals != nil { f.signals <- struct{}{} } + signature.GetSignatures().Close() } // isSingleInstance checks if there is a single instance diff --git a/internal/etw/processors/fs_windows.go b/internal/etw/processors/fs_windows.go index a121c19e0..53af418af 100644 --- a/internal/etw/processors/fs_windows.go +++ b/internal/etw/processors/fs_windows.go @@ -29,6 +29,7 @@ import ( htypes "github.com/rabbitstack/fibratus/pkg/handle/types" "github.com/rabbitstack/fibratus/pkg/ps" "github.com/rabbitstack/fibratus/pkg/sys" + "github.com/rabbitstack/fibratus/pkg/util/signature" "github.com/rabbitstack/fibratus/pkg/util/va" "golang.org/x/sys/windows" ) @@ -140,6 +141,12 @@ func (f *fsProcessor) processEvent(e *event.Event) (*event.Event, error) { e.AppendEnum(params.FileType, uint32(fileinfo.Type), fs.FileTypes) } + // invalidate signature cache + dispo := e.Params.MustGetUint32(params.FileOperation) + if dispo == windows.FILE_OVERWRITE || dispo == windows.FILE_OVERWRITE_IF { + signature.GetSignatures().RemoveSignature(e.GetParamAsString(params.FilePath)) + } + return e, nil case event.ReleaseFile: fileReleaseCount.Add(1) @@ -190,6 +197,14 @@ func (f *fsProcessor) processEvent(e *event.Event) (*event.Event, error) { if e.IsDeleteFile() { delete(f.files, fileObject) + if fileinfo != nil { + signature.GetSignatures().RemoveSignature(fileinfo.Name) + } + } + if e.IsRenameFile() { + if fileinfo != nil { + signature.GetSignatures().RemoveSignature(fileinfo.Name) + } } if e.IsEnumDirectory() { if fileinfo != nil { diff --git a/internal/etw/processors/module_windows.go b/internal/etw/processors/module_windows.go index 5367b3d8c..d9191267c 100644 --- a/internal/etw/processors/module_windows.go +++ b/internal/etw/processors/module_windows.go @@ -22,6 +22,7 @@ import ( "github.com/rabbitstack/fibratus/pkg/event" "github.com/rabbitstack/fibratus/pkg/event/params" "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/util/signature" ) type moduleProcessor struct { @@ -38,7 +39,6 @@ func (*moduleProcessor) Name() ProcessorType { return Module } func (m *moduleProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, error) { if e.IsLoadModuleInternal() { - // state management return e, false, m.psnap.AddModule(e) } @@ -52,6 +52,23 @@ func (m *moduleProcessor) ProcessEvent(e *event.Event) (*event.Event, bool, erro } if e.IsLoadModule() || e.IsModuleRundown() { + typ := signature.Type(e.Params.MustGetUint32(params.ModuleSignatureType)) + lvl := signature.Level(e.Params.MustGetUint32(params.ModuleSignatureLevel)) + // Code Integrity successfully validated the file's trust chain. + // The signature level WINDOWS and WINDOWS_TCB describes the trust + // level assigned after verification. + // A file can reach the WINDOWS signing level through either an embedded PE + // Authenticode signature or a catalog signature where the file hash lives + // in a trusted .cat file. + // Trusted signatures are automatically pushed to the store. On the contrary, + // signature verification is delegated to the async workers + key := e.SignatureKey() + if signature.IsTrusted(typ, lvl) { + signature.GetSignatures().PutSignature(key, typ, lvl) + } else { + signature.GetSignatures().DoRequestAsync(key) + } + return e, false, m.psnap.AddModule(e) } diff --git a/pkg/event/event.go b/pkg/event/event.go index bd1fc85d5..66ca2905c 100644 --- a/pkg/event/event.go +++ b/pkg/event/event.go @@ -354,6 +354,16 @@ func (e *Event) GetFlagsAsSlice(name string) []string { return strings.Split(e.GetParamAsString(name), "|") } +// GetParamAsUint64 returns the uint64 value of the given parameter. +func (e *Event) GetParamAsUint64(name string) uint64 { + return e.Params.TryGetUint64(name) +} + +// GetParamAsUint32 returns the uint32 value of the given parameter. +func (e *Event) GetParamAsUint32(name string) uint32 { + return e.Params.TryGetUint32(name) +} + // SequenceLink returns the sequence link values from event metadata. func (e *Event) SequenceLinks() []any { e.mmux.RLock() diff --git a/pkg/event/event_windows.go b/pkg/event/event_windows.go index 7879aa5b4..a8e63ac98 100644 --- a/pkg/event/event_windows.go +++ b/pkg/event/event_windows.go @@ -33,6 +33,7 @@ import ( "github.com/rabbitstack/fibratus/pkg/util/hashers" "github.com/rabbitstack/fibratus/pkg/util/hostname" "github.com/rabbitstack/fibratus/pkg/util/ntstatus" + "github.com/rabbitstack/fibratus/pkg/util/signature" "golang.org/x/sys/windows" ) @@ -219,6 +220,7 @@ func (e *Event) IsCloseFile() bool { return e.Type == CloseFile } func (e *Event) IsCreateHandle() bool { return e.Type == CreateHandle } func (e *Event) IsCloseHandle() bool { return e.Type == CloseHandle } func (e *Event) IsDeleteFile() bool { return e.Type == DeleteFile } +func (e *Event) IsRenameFile() bool { return e.Type == RenameFile } func (e *Event) IsEnumDirectory() bool { return e.Type == EnumDirectory } func (e *Event) IsTerminateProcess() bool { return e.Type == TerminateProcess } func (e *Event) IsTerminateThread() bool { return e.Type == TerminateThread } @@ -286,6 +288,20 @@ func (e *Event) IsSurrogateProcess() bool { return e.IsCreateProcess() && e.Params.MustGetUint32(params.ProcessParentID) != e.Params.MustGetUint32(params.ProcessRealParentID) } +// SignatureKey derives the key into signature store. +func (e *Event) SignatureKey() signature.Key { + if e.IsLoadModule() || e.IsModuleRundown() { + return signature.MakeKey(e.GetParamAsString(params.ModulePath), e.GetParamAsUint64(params.ModuleSize), e.GetParamAsUint32(params.ModuleCheckSum), e.GetParamAsUint32(params.ModuleTimeDateStamp)) + } + + if e.PS != nil && e.PS.PE != nil { + pe := e.PS.PE + return signature.MakeKey(e.PS.Exe, uint64(pe.ImageSize), pe.ImageChecksum, pe.TimedateStamp) + } + + return signature.Key{} +} + // RundownKey calculates the rundown event hash. The hash is // used to determine if the rundown event was already processed. func (e *Event) RundownKey() uint64 { diff --git a/pkg/event/param_decoder_windows.go b/pkg/event/param_decoder_windows.go index 1071a8335..7bd076a1c 100644 --- a/pkg/event/param_decoder_windows.go +++ b/pkg/event/param_decoder_windows.go @@ -358,7 +358,7 @@ func (d *ParamDecoder) DecodeModule(r *etw.EventRecord, e *Event) { e.AppendParam(params.ModuleSize, params.Uint64, r.ReadUint64(8)) e.AppendParam(params.ProcessID, params.PID, r.ReadUint32(16)) e.AppendParam(params.ModuleCheckSum, params.Uint32, r.ReadUint32(20)) - // skip TimeDateStamp (uint32) + e.AppendParam(params.ModuleTimeDateStamp, params.Uint32, r.ReadUint32(24)) e.AppendParam(params.ModuleSignatureLevel, params.Enum, uint32(r.ReadByte(28)), WithEnum(signature.Levels)) e.AppendParam(params.ModuleSignatureType, params.Enum, uint32(r.ReadByte(29)), WithEnum(signature.Types)) // skip Reserved0 (uint8) diff --git a/pkg/event/param_decoder_windows_test.go b/pkg/event/param_decoder_windows_test.go index b408fe2e9..014c6023b 100644 --- a/pkg/event/param_decoder_windows_test.go +++ b/pkg/event/param_decoder_windows_test.go @@ -565,7 +565,7 @@ func TestDecodeModule(t *testing.T) { e := &Event{Params: make(Params)} paramDecoder.DecodeModule(r, e) - assert.Len(t, e.Params, 8) + assert.Len(t, e.Params, 9) assert.Equal(t, uint64(0x7ffd09200000), e.Params.MustGetUint64(params.ModuleBase)) assert.Equal(t, uint32(48733), e.Params.MustGetUint32(params.ModuleCheckSum)) assert.Equal(t, uint64(0x7ffd09200000), e.Params.MustGetUint64(params.ModuleDefaultBase)) diff --git a/pkg/event/params/params_windows.go b/pkg/event/params/params_windows.go index c11cfca0b..cb269f02c 100644 --- a/pkg/event/params/params_windows.go +++ b/pkg/event/params/params_windows.go @@ -162,6 +162,8 @@ const ( ModuleSize = "module_size" // ModuleCheckSum is the parameter name for module checksum. ModuleCheckSum = "checksum" + // ModuleTimeDateStamp is the parametter name for module timedate stamp. + ModuleTimeDateStamp = "timedate_stamp" // ModuleDefaultBase is the parameter name that represents module's base address. ModuleDefaultBase = "default_address" // ModulePath is the parameter name that denotes the file path and extension of the DLL/executable Module. diff --git a/pkg/filter/accessor_windows.go b/pkg/filter/accessor_windows.go index f6e42aa9a..d0b74f42e 100644 --- a/pkg/filter/accessor_windows.go +++ b/pkg/filter/accessor_windows.go @@ -20,7 +20,6 @@ package filter import ( "errors" - "expvar" "net" "path/filepath" "strconv" @@ -31,6 +30,7 @@ import ( "github.com/rabbitstack/fibratus/pkg/fs" "github.com/rabbitstack/fibratus/pkg/network" psnap "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/util/signature" "github.com/rabbitstack/fibratus/pkg/event" @@ -45,12 +45,6 @@ var ( ErrPENil = errors.New("pe state is nil") ) -// signatureErrors counts signature check/verification errors -var signatureErrors = expvar.NewInt("image.signature.errors") - -// certErrors counts certificate parse errors -var certErrors = expvar.NewInt("image.certificate.errors") - // GetAccessors initializes and returns all available accessors. func GetAccessors() []Accessor { return []Accessor{ @@ -625,33 +619,53 @@ func (t *threadAccessor) Get(f Field, e *event.Event) (params.Value, error) { return nil, nil } - sign := getSignature(frame.ModuleAddress, frame.Module, false) + ps := e.PS + if ps == nil { + return nil, ErrPsNil + } + mod := ps.FindModuleByVa(frame.Addr) + if mod == nil { + return nil, nil + } + + sign := requestSignature(mod.Name, mod.Size, mod.Checksum, mod.TimedateStamp) if sign == nil { return nil, nil } if f.Name == fields.ThreadCallstackFinalUserModuleSignatureExists { - return sign.IsSigned(), nil + return sign.Exists(), nil + } + if f.Name == fields.ThreadCallstackFinalUserModuleSignatureTrusted { + return sign.IsTrusted(), nil } - return sign.IsTrusted(), nil case fields.ThreadCallstackFinalUserModuleSignatureIssuer, fields.ThreadCallstackFinalUserModuleSignatureSubject: frame := e.Callstack.FinalUserFrame() if frame == nil || (frame != nil && frame.ModuleAddress.IsZero()) { return nil, nil } - sign := getSignature(frame.ModuleAddress, frame.Module, true) + ps := e.PS + if ps == nil { + return nil, ErrPsNil + } + mod := ps.FindModuleByVa(frame.Addr) + if mod == nil { + return nil, nil + } + + sign := requestSignature(mod.Name, mod.Size, mod.Checksum, mod.TimedateStamp) if sign == nil { return nil, nil } if sign.HasCertificate() && f.Name == fields.ThreadCallstackFinalUserModuleSignatureIssuer { - return sign.Cert.Issuer, nil + return sign.Cert().Issuer, nil } if sign.HasCertificate() { - return sign.Cert.Subject, nil + return sign.Cert().Subject, nil } } @@ -762,73 +776,22 @@ func newModuleAccessor() Accessor { return &moduleAccessor{} } -func (*moduleAccessor) Get(f Field, e *event.Event) (params.Value, error) { +func (m *moduleAccessor) Get(f Field, e *event.Event) (params.Value, error) { if e.IsLoadModule() && (f.Name.IsModuleSignature() || f.Name == fields.ImageSignatureType || f.Name == fields.ImageSignatureLevel || f.Name.IsImageCert()) { - filename := e.GetParamAsString(params.ModulePath) - addr := e.Params.MustGetUint64(params.ModuleBase) - typ := e.Params.MustGetUint32(params.ModuleSignatureType) - level := e.Params.MustGetUint32(params.ModuleSignatureLevel) - - sign := signature.GetSignatures().GetSignature(addr) - - // signature already checked - if typ != signature.None { - if sign == nil { - sign = &signature.Signature{ - Type: typ, - Level: level, - Filename: filename, - } - } - if f.Name.IsImageCert() || f.Name.IsModuleCert() { - err := sign.ParseCertificate() - if err != nil { - certErrors.Add(1) - } - } - signature.GetSignatures().PutSignature(addr, sign) - } else { - // image signature parameters exhibit unreliable behaviour. Allegedly, - // signature verification is not performed in certain circumstances - // which leads to the core system DLL or binaries to be reported with - // signature unchecked level. - // To mitigate this situation, we have to manually check/verify the - // signature for all unchecked signature levels. - if sign == nil { - var err error - sign = &signature.Signature{Filename: filename} - sign.Type, sign.Level, err = sign.Check() - if err != nil { - signatureErrors.Add(1) - } - if sign.IsSigned() { - sign.Verify() - } - if f.Name.IsImageCert() || f.Name.IsModuleCert() { - err := sign.ParseCertificate() - if err != nil { - certErrors.Add(1) - } - } - signature.GetSignatures().PutSignature(addr, sign) - } - // reset signature type/level parameters - _ = e.Params.SetValue(params.ModuleSignatureType, sign.Type) - _ = e.Params.SetValue(params.ModuleSignatureLevel, sign.Level) - } + sign := signature.GetSignatures().DoRequest(e.SignatureKey()) - // append certificate parameters - if sign.HasCertificate() { - e.AppendParam(params.ModuleCertIssuer, params.UnicodeString, sign.Cert.Issuer) - e.AppendParam(params.ModuleCertSubject, params.UnicodeString, sign.Cert.Subject) - e.AppendParam(params.ModuleCertSerial, params.UnicodeString, sign.Cert.SerialNumber) - e.AppendParam(params.ModuleCertNotAfter, params.Time, sign.Cert.NotAfter) - e.AppendParam(params.ModuleCertNotBefore, params.Time, sign.Cert.NotBefore) + if sign != nil && sign.HasCertificate() { + cert := sign.Cert() + e.AppendParam(params.ModuleCertIssuer, params.UnicodeString, cert.Issuer) + e.AppendParam(params.ModuleCertSubject, params.UnicodeString, cert.Subject) + e.AppendParam(params.ModuleCertSerial, params.UnicodeString, cert.SerialNumber) + e.AppendParam(params.ModuleCertNotAfter, params.Time, cert.NotAfter) + e.AppendParam(params.ModuleCertNotBefore, params.Time, cert.NotBefore) } switch f.Name { case fields.ModuleSignatureExists, fields.DllSignatureExists: - return sign != nil && sign.IsSigned(), nil + return sign != nil && sign.Exists(), nil case fields.ModuleSignatureTrusted, fields.DllSignatureTrusted: return sign != nil && sign.IsTrusted(), nil } @@ -1059,9 +1022,6 @@ func (pa *peAccessor) parserOpts() []pe.Option { if f.Name.IsPeAnomalies() { opts = append(opts, pe.WithSections(), pe.WithSymbols()) } - if f.Name.IsPeSignature() { - opts = append(opts, pe.WithSecurity()) - } } for _, s := range pa.segments { @@ -1073,6 +1033,12 @@ func (pa *peAccessor) parserOpts() []pe.Option { return opts } +func (pa *peAccessor) verifySignature(sig *signature.Signature) pe.SignatureCallback { + return func() (bool, bool, *sys.Cert) { + return sig.Exists(), sig.IsTrusted(), sig.Cert() + } +} + // ErrPeNilCertificate indicates the PE certificate is not available var ErrPeNilCertificate = errors.New("pe certificate is nil") @@ -1138,9 +1104,12 @@ func (pa *peAccessor) Get(f Field, e *event.Event) (params.Value, error) { return nil, ErrPENil } - // verify signature - if f.Name.IsPeSignature() { - p.VerifySignature() + if !p.IsSignatureVerified() && f.Name.IsPeSignature() { + sign := signature.GetSignatures().DoRequest(e.SignatureKey()) + if sign == nil { + return nil, signature.ErrNilSignature + } + p.VerifySignature(pa.verifySignature(sign)) } switch f.Name { @@ -1163,9 +1132,9 @@ func (pa *peAccessor) Get(f Field, e *event.Event) (params.Value, error) { case fields.PeAnomalies, fields.PsPeAnomalies: return p.Anomalies, nil case fields.PeIsSigned, fields.PsSignatureExists: - return p.IsSigned, nil + return p.IsSigned(), nil case fields.PeIsTrusted, fields.PsSignatureTrusted: - return p.IsTrusted, nil + return p.IsTrusted(), nil case fields.PeIsModified: return p.IsModified, nil case fields.PeCertIssuer, fields.PsSignatureIssuer: diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index 350ec8e6c..491f65bb5 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -23,6 +23,7 @@ import ( "net" "os" "path/filepath" + "strings" "testing" "time" "unsafe" @@ -436,14 +437,14 @@ func TestThreadFilter(t *testing.T) { Modules: []pstypes.Module{ {Name: "C:\\Windows\\System32\\kernel32.dll", Size: 2312354, Checksum: 23123343, BaseAddress: va.Address(0x7ffb5c1d0396), DefaultBaseAddress: va.Address(0x7ffb5c1d0396)}, {Name: "C:\\Windows\\System32\\user32.dll", Size: 32212354, Checksum: 33123343, BaseAddress: va.Address(0x7ffb313953b2), DefaultBaseAddress: va.Address(0x7ffb313953b2)}, - {Name: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll", Size: 32212354, Checksum: 33123343, BaseAddress: va.Address(0x7ffb3138592e), DefaultBaseAddress: va.Address(0x7ffb3138592e)}, + {Name: "C:\\Windows\\System32\\rpcrt4.dll", Size: 32212354, Checksum: 33123343, BaseAddress: va.Address(0x7ffb3138592e), DefaultBaseAddress: va.Address(0x7ffb3138592e)}, }, }, } // append the module signature - cert := &sys.Cert{Subject: "US, Washington, Redmond, Microsoft Corporation, Microsoft Windows", Issuer: "US, Washington, Redmond, Microsoft Corporation, Microsoft Windows Production PCA 2011"} - signature.GetSignatures().PutSignature(0x7ffb3138592e, &signature.Signature{Filename: "C:\\Program Files\\JetBrains\\GoLand 2021.2.3\\jbr\\bin\\java.dll", Level: 4, Type: 1, Cert: cert}) + key := signature.MakeKey("C:\\Windows\\System32\\kernel32.dll", 32212354, 33123343, 0) + signature.GetSignatures().PutSignature(key, signature.TypeFileVerified, signature.LevelWindows) // simulate unbacked RWX frame base, err := windows.VirtualAlloc(0, 1024, windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_EXECUTE_READWRITE) @@ -1000,6 +1001,8 @@ func TestModuleFilter(t *testing.T) { params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1023)}, params.ModuleCheckSum: {Name: params.ModuleCheckSum, Type: params.Uint32, Value: uint32(2323432)}, params.ModuleBase: {Name: params.ModuleBase, Type: params.Address, Value: uint64(0x7ffb313833a3)}, + params.ModuleSize: {Name: params.ModuleSize, Type: params.Uint64, Value: uint64(0x72342)}, + params.ModuleTimeDateStamp: {Name: params.ModuleTimeDateStamp, Type: params.Uint32, Value: uint32(12322567)}, params.ModuleSignatureType: {Name: params.ModuleSignatureType, Type: params.Enum, Value: uint32(1), Enum: signature.Types}, params.ModuleSignatureLevel: {Name: params.ModuleSignatureLevel, Type: params.Enum, Value: uint32(4), Enum: signature.Levels}, params.FileIsDotnet: {Name: params.FileIsDotnet, Type: params.Bool, Value: false}, @@ -1052,11 +1055,13 @@ func TestModuleFilter(t *testing.T) { } // check signatures expectations - sig := signature.GetSignatures().GetSignature(0x7ffb313833a3) + key := signature.MakeKey(filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll"), 0x72342, 2323432, 12322567) + sig := signature.GetSignatures().GetSignature(key) assert.NotNil(t, sig) - assert.Equal(t, filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll"), sig.Filename) - assert.Equal(t, signature.Embedded, sig.Type) - assert.Equal(t, signature.AuthenticodeLevel, sig.Level) + assert.Equal(t, strings.ToLower(filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll")), sig.Path) + assert.True(t, sig.Exists()) + assert.True(t, sig.IsTrusted()) + assert.Equal(t, signature.TypeEmbedded, sig.Type()) // now exercise unsigned/unchecked signature e2 := &event.Event{ @@ -1078,8 +1083,8 @@ func TestModuleFilter(t *testing.T) { matches bool }{ - {`module.signature.type = 'EMBEDDED'`, true}, - {`module.signature.level = 'AUTHENTICODE'`, true}, + {`module.signature.type = 'NONE'`, true}, + {`module.signature.level = 'UNCHECKED'`, true}, {`module.signature.exists`, true}, {`module.signature.trusted`, true}, {`module.pid = 1023`, true}, @@ -1088,8 +1093,7 @@ func TestModuleFilter(t *testing.T) { {`module.base = '7ccb313833a3'`, true}, {`module.signature.issuer icontains 'Microsoft Windows'`, true}, {`module.signature.subject icontains 'Microsoft Corporation'`, true}, - {`dll.signature.type = 'EMBEDDED'`, true}, - {`dll.signature.level = 'AUTHENTICODE'`, true}, + {`dll.signature.type = 'NONE'`, true}, {`dll.signature.exists`, true}, {`dll.signature.trusted`, true}, {`dll.pid = 1023`, true}, @@ -1111,7 +1115,8 @@ func TestModuleFilter(t *testing.T) { } } - assert.NotNil(t, signature.GetSignatures().GetSignature(0x7ccb313833a3)) + key = signature.MakeKey(filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll"), 0, 2323432, 0) + assert.NotNil(t, signature.GetSignatures().GetSignature(key)) e3 := &event.Event{ Type: event.LoadModule, @@ -1156,6 +1161,7 @@ func TestModuleFilter(t *testing.T) { func TestPEFilter(t *testing.T) { evt := &event.Event{ PS: &pstypes.PS{ + Exe: filepath.Join(filepath.Join(os.Getenv("windir"), "System32", "notepad.exe")), PE: &pe.PE{ NumberOfSections: 2, NumberOfSymbols: 10, @@ -1190,6 +1196,11 @@ func TestPEFilter(t *testing.T) { {`ps.pe.nsymbols = 10 AND ps.pe.nsections = 2`, true}, {`ps.pe.nsections > 1`, true}, {`ps.pe.address.base = '140000000' AND ps.pe.address.entrypoint = '20110'`, true}, + {`ps.signature.exists`, true}, + {`ps.signature.trusted`, true}, + {`ps.signature.subject icontains 'microsoft'`, true}, + {`ps.signature.issuer icontains 'microsoft'`, true}, + {`length(ps.signature.serial) > 0`, true}, } for i, tt := range tests { @@ -1232,11 +1243,6 @@ func TestLazyPEFilter(t *testing.T) { {`ps.pe.nsymbols > 10 AND pe.nsections > 2`, true}, {`ps.pe.nsections > 1`, true}, {`length(ps.pe.anomalies) = 0`, true}, - {`ps.signature.exists`, true}, - {`ps.signature.trusted`, true}, - {`ps.signature.subject icontains 'microsoft'`, true}, - {`ps.signature.issuer icontains 'microsoft'`, true}, - {`length(ps.signature.serial) > 0`, true}, } for i, tt := range tests { diff --git a/pkg/filter/ql/function.go b/pkg/filter/ql/function.go index 87ec8a0a0..332cd1232 100644 --- a/pkg/filter/ql/function.go +++ b/pkg/filter/ql/function.go @@ -573,52 +573,23 @@ func (f *Foreach) callstackMapValuer(segments []*BoundSegmentLiteral, frame call case fields.ModuleSignatureExistsSegment, fields.ModuleSignatureTrustedSegment, fields.ModuleSignatureIssuerSegment, fields.ModuleSignatureSubjectSegment: - if frame.ModuleAddress.IsZero() { + sign := signature.GetSignatures().DoRequest(signature.MakeKey(frame.Module, 0, 0, 0)) + if sign == nil { continue } - segment := seg.Segment - sign := signature.GetSignatures().GetSignature(frame.ModuleAddress.Uint64()) - if sign == nil && frame.Module != "" { - // register signature if not present in the cache - var err error - sign = &signature.Signature{Filename: frame.Module} - sign.Type, sign.Level, err = sign.Check() - if err != nil { - continue - } - - if sign.IsSigned() { - sign.Verify() - } - - if segment == fields.ModuleSignatureIssuerSegment || segment == fields.ModuleSignatureSubjectSegment { - if err := sign.ParseCertificate(); err != nil { - continue - } - } - - signature.GetSignatures().PutSignature(frame.ModuleAddress.Uint64(), sign) - } - - switch segment { + switch seg.Segment { case fields.ModuleSignatureExistsSegment: - valuer[key] = sign.IsSigned() + valuer[key] = sign.Exists() case fields.ModuleSignatureTrustedSegment: valuer[key] = sign.IsTrusted() case fields.ModuleSignatureIssuerSegment: - if err := sign.ParseCertificate(); err != nil { - continue - } if sign.HasCertificate() { - valuer[key] = sign.Cert.Issuer + valuer[key] = sign.Cert().Issuer } case fields.ModuleSignatureSubjectSegment: - if err := sign.ParseCertificate(); err != nil { - continue - } if sign.HasCertificate() { - valuer[key] = sign.Cert.Subject + valuer[key] = sign.Cert().Subject } } } diff --git a/pkg/filter/util.go b/pkg/filter/util.go index 5724af353..d86672dbb 100644 --- a/pkg/filter/util.go +++ b/pkg/filter/util.go @@ -32,7 +32,6 @@ import ( "github.com/rabbitstack/fibratus/pkg/util/bytes" "github.com/rabbitstack/fibratus/pkg/util/loldrivers" "github.com/rabbitstack/fibratus/pkg/util/signature" - "github.com/rabbitstack/fibratus/pkg/util/va" ) // isLOLDriver interacts with the loldrivers client to determine @@ -121,42 +120,10 @@ func getFileInfo(f fields.Field, e *event.Event) (params.Value, error) { return nil, fmt.Errorf("unexpected field: %s", f) } -// getSignature tries to find the module signature mapped to the given address. -// If the signature is not found in the cache, then a fresh signature instance -// is created and verified. -func getSignature(addr va.Address, filename string, parseCert bool) *signature.Signature { - sign := signature.GetSignatures().GetSignature(addr.Uint64()) - if sign != nil { - if parseCert { - err := sign.ParseCertificate() - if err != nil { - certErrors.Add(1) - } - } - return sign - } - - var err error - sign = &signature.Signature{Filename: filename} - sign.Type, sign.Level, err = sign.Check() - if err != nil { - signatureErrors.Add(1) - } - - if sign.IsSigned() { - sign.Verify() - } - - if parseCert { - err = sign.ParseCertificate() - if err != nil { - certErrors.Add(1) - } - } - - signature.GetSignatures().PutSignature(addr.Uint64(), sign) - - return sign +// requestSignature submits the request for the signature check. +func requestSignature(path string, size uint64, checksum, timedatestamp uint32) *signature.Signature { + key := signature.MakeKey(path, size, checksum, timedatestamp) + return signature.GetSignatures().DoRequest(key) } // framePID returns the pid associated with the stack frame. diff --git a/pkg/pe/marshaller.go b/pkg/pe/marshaller.go index 38ed57a06..b5889660f 100644 --- a/pkg/pe/marshaller.go +++ b/pkg/pe/marshaller.go @@ -23,13 +23,14 @@ package pe import ( "fmt" + "math" + "time" + "unsafe" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/rabbitstack/fibratus/pkg/sys" "github.com/rabbitstack/fibratus/pkg/util/bytes" "github.com/rabbitstack/fibratus/pkg/util/convert" - "math" - "time" - "unsafe" ) // Marshal dumps the PE metadata to binary stream. @@ -93,8 +94,8 @@ func (pe *PE) Marshal() []byte { } // signature and cert data - b = append(b, convert.Btoi(pe.IsSigned)) - b = append(b, convert.Btoi(pe.IsTrusted)) + 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)))...) @@ -242,8 +243,9 @@ func (pe *PE) Unmarshal(b []byte, ver capver.Version) error { offset += roffset if ver >= capver.PESecV2 { - pe.IsSigned = convert.Itob(b[20+offset]) - pe.IsTrusted = convert.Itob(b[21+offset]) + isSigned, isTrusted := convert.Itob(b[20+offset]), convert.Itob(b[21+offset]) + pe.isSigned = &isSigned + pe.isTrusted = &isTrusted certSize := bytes.ReadUint32(b[22+offset:]) if certSize > 0 { diff --git a/pkg/pe/marshaller_test.go b/pkg/pe/marshaller_test.go index ce4bf94a3..8b854754b 100644 --- a/pkg/pe/marshaller_test.go +++ b/pkg/pe/marshaller_test.go @@ -22,17 +22,20 @@ package pe import ( + "testing" + "time" + capver "github.com/rabbitstack/fibratus/pkg/cap/version" "github.com/rabbitstack/fibratus/pkg/sys" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" - "time" ) func TestPEMarshal(t *testing.T) { now := time.Now() + isSigned, isTrusted := true, true + pe := &PE{ NumberOfSections: 7, NumberOfSymbols: 10, @@ -46,8 +49,8 @@ func TestPEMarshal(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, + isSigned: &isSigned, + isTrusted: &isTrusted, Cert: &sys.Cert{ Issuer: "Washington, Redmond, Microsoft Corporation", Subject: "Washington, Redmond, Microsoft Corporation", @@ -99,8 +102,8 @@ func TestPEMarshal(t *testing.T) { 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.True(t, newPE.IsSigned()) + assert.True(t, newPE.IsTrusted()) assert.NotNil(t, newPE.Cert) assert.False(t, newPE.Cert.NotBefore.IsZero()) diff --git a/pkg/pe/parser.go b/pkg/pe/parser.go index b7a872986..9c425062c 100644 --- a/pkg/pe/parser.go +++ b/pkg/pe/parser.go @@ -285,13 +285,13 @@ func parse(path string, data []byte, options ...Option) (*PE, error) { p := &PE{ NumberOfSections: pe.NtHeader.FileHeader.NumberOfSections, LinkTime: linkTime, + TimedateStamp: timestamp, Symbols: make([]string, 0), Imports: make([]string, 0), Sections: make([]Sec, 0), Exports: make(map[uint32]string, 0), VersionResources: make(map[string]string), Is64: pe.Is64, - filename: path, dosHeader: pe.DOSHeader, ntHeader: pe.NtHeader, sectionHeaders: make([]peparser.ImageSectionHeader, 0), @@ -301,11 +301,15 @@ func parse(path string, data []byte, options ...Option) (*PE, error) { case true: oh64 := pe.NtHeader.OptionalHeader.(peparser.ImageOptionalHeader64) p.ImageBase = format.UintToHex(oh64.ImageBase) + p.ImageSize = oh64.SizeOfImage + p.ImageChecksum = oh64.CheckSum p.EntryPoint = format.UintToHex(uint64(oh64.AddressOfEntryPoint)) case false: oh32 := pe.NtHeader.OptionalHeader.(peparser.ImageOptionalHeader32) p.ImageBase = format.UintToHex(uint64(oh32.ImageBase)) p.EntryPoint = format.UintToHex(uint64(oh32.AddressOfEntryPoint)) + p.ImageSize = oh32.SizeOfImage + p.ImageChecksum = oh32.CheckSum } // parse section header @@ -374,7 +378,7 @@ func parse(path string, data []byte, options ...Option) (*PE, error) { // parse certificate info if opts.parseSecurity { - p.IsSigned = pe.IsSigned + p.isSigned = &pe.IsSigned if pe.HasCertificate && len(pe.Certificates.Certificates) > 0 { cert := pe.Certificates.Certificates[0] p.Cert = &sys.Cert{ diff --git a/pkg/pe/types.go b/pkg/pe/types.go index 0067334e5..c5c4fc74f 100644 --- a/pkg/pe/types.go +++ b/pkg/pe/types.go @@ -23,11 +23,10 @@ package pe import ( "fmt" + "time" + "github.com/rabbitstack/fibratus/pkg/sys" peparser "github.com/saferwall/pe" - "runtime" - "sync" - "time" ) const ( @@ -68,10 +67,16 @@ type PE struct { NumberOfSymbols uint32 `json:"nsymbols"` // ImageBase designates the base address of the process' image. ImageBase string `json:"image_base"` + // ImageBase designates the size of the process' image. + ImageSize uint32 `json:"image_size"` + // ImageBase designates the process' image checksum. + ImageChecksum uint32 `json:"image_checksum"` // Entrypoint is the address of the entry point function. EntryPoint string `json:"entry_point"` // LinkTime represents the time that the image was created by the linker. LinkTime time.Time `json:"link_time"` + // TimedateStamp represents the timedate stamp of the process' image. + TimedateStamp uint32 `json:"timedate_stamp"` // Sections contains all distinct sections and their metadata. Sections []Sec `json:"sections"` // Symbols contains the list of imported symbols. @@ -80,10 +85,6 @@ type PE struct { Imports []string `json:"imports"` // VersionResources holds the version resources VersionResources map[string]string `json:"resources"` - // 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 *sys.Cert `json:"cert"` // IsDriver indicates if the PE contains driver code. @@ -105,42 +106,18 @@ type PE struct { // Exports contains exported function names indexed by RVA Exports map[uint32]string `json:"exports"` + // isSigned determines if the PE contains a digital signature. + isSigned *bool + // isTrusted determines if the PE certificate chain is trusted. + isTrusted *bool + dosHeader peparser.ImageDOSHeader ntHeader peparser.ImageNtHeader sectionHeaders []peparser.ImageSectionHeader - - filename string - once sync.Once } -// VerifySignature checks if the embedded or catalog PE signature is trusted. -func (pe *PE) VerifySignature() { - pe.once.Do(func() { - if sys.IsWintrustFound() { - runtime.LockOSThread() - trust := sys.NewWintrustData(sys.WtdChoiceFile) - defer trust.Close() - defer runtime.UnlockOSThread() - pe.IsTrusted = trust.VerifyFile(pe.filename) - // maybe the PE is catalog signed? - if !pe.IsSigned { - catalog := sys.NewCatalog() - err := catalog.Open(pe.filename) - defer catalog.Close() - if err != nil { - return - } - pe.IsSigned = catalog.IsCatalogSigned() - if pe.IsSigned { - pe.IsTrusted = catalog.Verify(pe.filename) - } - if pe.IsSigned && pe.IsTrusted { - pe.Cert, _ = catalog.ParseCertificate() - } - } - } - }) -} +func (pe *PE) IsSigned() bool { return pe.isSigned != nil && *pe.isSigned } +func (pe *PE) IsTrusted() bool { return pe.isTrusted != nil && *pe.isTrusted } // String returns the string representation of the PE metadata. func (pe *PE) String() string { @@ -167,6 +144,18 @@ func (pe *PE) String() string { ) } +// SignatureCallback returns the signature status, trust level and the certificate. +type SignatureCallback func() (bool, bool, *sys.Cert) + +// IsSignatureVerified indicates if the PE signature was verified. +func (pe *PE) IsSignatureVerified() bool { return pe.isSigned != nil && pe.isTrusted != nil } + +// VerifySignature invokes the provided closure to verify he PE signature. +func (pe *PE) VerifySignature(cb SignatureCallback) { + exists, signed, cert := cb() + pe.isSigned, pe.isTrusted, pe.Cert = &exists, &signed, cert +} + // Section returns the section with specified name. func (pe *PE) Section(s string) *Sec { for _, sec := range pe.Sections { diff --git a/pkg/ps/snapshotter_windows.go b/pkg/ps/snapshotter_windows.go index d79da0331..b17179ad4 100644 --- a/pkg/ps/snapshotter_windows.go +++ b/pkg/ps/snapshotter_windows.go @@ -257,6 +257,7 @@ func (s *snapshotter) AddModule(e *event.Event) error { module := pstypes.Module{} module.Size, _ = e.Params.GetUint64(params.ModuleSize) module.Checksum, _ = e.Params.GetUint32(params.ModuleCheckSum) + module.TimedateStamp, _ = e.Params.GetUint32(params.ModuleTimeDateStamp) module.Name = e.GetParamAsString(params.ModulePath) module.BaseAddress = e.Params.TryGetAddress(params.ModuleBase) module.DefaultBaseAddress = e.Params.TryGetAddress(params.ModuleDefaultBase) diff --git a/pkg/ps/types/types_windows.go b/pkg/ps/types/types_windows.go index ee1359813..f602ed5c7 100644 --- a/pkg/ps/types/types_windows.go +++ b/pkg/ps/types/types_windows.go @@ -369,6 +369,8 @@ type Module struct { SignatureLevel uint32 // SignatureType designates the image signature type (e.g. EMBEDDED) SignatureType uint32 + // TimedateStamp that is produced by the linker. + TimedateStamp uint32 } // String returns the string representation of the module. diff --git a/pkg/sys/trust.go b/pkg/sys/trust.go index 3bd0efa29..fab5652be 100644 --- a/pkg/sys/trust.go +++ b/pkg/sys/trust.go @@ -70,6 +70,53 @@ const ( WtdSaferFlag = 0x100 ) +const ( + trustSignatureOk = 0 + trustErrNoSignature = 0x800b0100 + trustErrExplicitDistrust = 0x800B0111 + trustErrSystem = 0x80096001 + trustErrBadDigest = 0x80096010 + trustErrSubjectNotTrusted = 0x800B0003 + trustErrTimestamp = 0x80096005 + trustMalformedSignature = 0x80096011 + cryptErrSecuritySettings = 0x80092026 + certErrExpired = 0x800B0101 +) + +// SignatureStatus defines the signature status +type SignatureStatus uint8 + +const ( + SignatureNotTrusted SignatureStatus = iota + SignatureTrusted + SignatureExpired + SignatureBadDigest + SignatureBadTimestamp + SignatureMalformed + SignatureEndpointError +) + +func (s SignatureStatus) String() string { + switch s { + case SignatureNotTrusted: + return "NotTrusted" + case SignatureTrusted: + return "Trusted" + case SignatureExpired: + return "Expired" + case SignatureBadDigest: + return "BadDigest" + case SignatureBadTimestamp: + return "BadTimestamp" + case SignatureMalformed: + return "Malformed" + case SignatureEndpointError: + return "EndpointError" + default: + return fmt.Sprintf("%d", uint8(s)) + } +} + // WintrustActionGenericVerifyV2 is the action that indicates the file or object should be verified by using the Authenticode policy provider. var WintrustActionGenericVerifyV2 = windows.GUID{Data1: 0xaac56b, Data2: 0xcd44, Data3: 0x11d0, Data4: [8]byte{0x8c, 0xc2, 0x0, 0xc0, 0x4f, 0xc2, 0x95, 0xee}} @@ -104,24 +151,42 @@ func NewWintrustData(choice uint32) *WintrustData { } } +func (t *WintrustData) verifyTrust() (SignatureStatus, error) { + status, err := WinVerifyTrust(windows.InvalidHandle, &WintrustActionGenericVerifyV2, t) + switch status { + case trustSignatureOk: + return SignatureTrusted, nil + case trustErrNoSignature, trustErrExplicitDistrust, trustErrSubjectNotTrusted, cryptErrSecuritySettings: + return SignatureNotTrusted, err + case trustErrSystem: + return SignatureEndpointError, err + case trustErrBadDigest: + return SignatureBadDigest, err + case trustErrTimestamp: + return SignatureBadTimestamp, err + case trustMalformedSignature: + return SignatureMalformed, err + case certErrExpired: + return SignatureExpired, err + default: + return SignatureNotTrusted, err + } +} + // VerifyFile verifies the provided file trust. The trust provider // should perform the verification action without the user's assistance. // This is achieved by providing INVALID_HANDLE_VALUE as a first parameter // in the WinVerifyTrust call. -func (t *WintrustData) VerifyFile(filename string) bool { +func (t *WintrustData) VerifyFile(filename string) (SignatureStatus, error) { fileinfo := &WintrustFileInfo{ Size: uint32(unsafe.Sizeof(WintrustFileInfo{})), FilePath: windows.StringToUTF16Ptr(filename), } t.Union = uintptr(unsafe.Pointer(fileinfo)) - status, err := WinVerifyTrust(windows.InvalidHandle, &WintrustActionGenericVerifyV2, t) - if status != 0 || err != nil { - return false - } - return true + return t.verifyTrust() } -// VerifyCatalog verifies the provided catalog file. +// VerifyCatalog verifies the trust of the file signed by catalog-based certificate. func (t *WintrustData) VerifyCatalog( fd uintptr, filename string, @@ -129,7 +194,7 @@ func (t *WintrustData) VerifyCatalog( catalog CatalogInfo, hash []byte, hasSize uint32, -) bool { +) (SignatureStatus, error) { // tag is a hexadecimal representation of the hash of the file tag := windows.StringToUTF16Ptr(format.BytesToHex(hash[:hasSize])) catinfo := &WintrustCatalogInfo{ @@ -143,11 +208,7 @@ func (t *WintrustData) VerifyCatalog( CatalogAdmin: catalogAdmin, } t.Union = uintptr(unsafe.Pointer(catinfo)) - status, err := WinVerifyTrust(windows.InvalidHandle, &WintrustActionGenericVerifyV2, t) - if status != 0 || err != nil { - return false - } - return true + return t.verifyTrust() } // Close disposes state data by specifying the corresponding action @@ -279,11 +340,7 @@ func (c *Cat) Open(filename string) error { c.size, 0, nil, ) c.catalogInfo.Size = uint32(unsafe.Sizeof(c.catalogInfo)) - err = CryptCatalogInfoFromContext(c.catalog, &c.catalogInfo, 0) - if err != nil { - return err - } - return nil + return CryptCatalogInfoFromContext(c.catalog, &c.catalogInfo, 0) } // IsCatalogSigned determines if the file is catalog-signed. @@ -292,7 +349,7 @@ func (c *Cat) IsCatalogSigned() bool { } // Verify verifies the signature of the given file against the catalog. -func (c *Cat) Verify(filename string) bool { +func (c *Cat) Verify(filename string) (SignatureStatus, error) { runtime.LockOSThread() defer runtime.UnlockOSThread() trust := NewWintrustData(WtdChoiceCatalog) diff --git a/pkg/util/signature/signature.go b/pkg/util/signature/signature.go index 8505d6945..7bb68c1b2 100644 --- a/pkg/util/signature/signature.go +++ b/pkg/util/signature/signature.go @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 by Nedim Sabic Sabic + * Copyright 2021-present by Nedim Sabic Sabic * https://www.fibratus.io * All Rights Reserved. * @@ -19,21 +19,113 @@ package signature import ( + "bytes" + "encoding/binary" + "encoding/hex" + "expvar" + "runtime" + "strings" + "sync" + "time" + + "github.com/golang/groupcache/singleflight" "github.com/rabbitstack/fibratus/pkg/pe" "github.com/rabbitstack/fibratus/pkg/sys" log "github.com/sirupsen/logrus" - "sync" - "time" ) +// requestQueueSize is the signature request input channel size +const requestQueueSize = 2048 + +var signatureAsyncRequestDrops = expvar.NewInt("signature.async.request.drops") + +var signatureCiCount = expvar.NewInt("signature.ci.count") + +var signatureCount = expvar.NewInt("signature.count") + +var signatureCheckErrors = expvar.NewMap("signature.check.errors") + +var signatureCacheHits = expvar.NewInt("signature.cache.hits") + +var signatureCacheMisses = expvar.NewInt("signature.cache.misses") + +var signatureEvictions = expvar.NewInt("signature.evictions") + +var signatureInvalidations = expvar.NewMap("signature.invalidations") + +var signatureCertParseErrors = expvar.NewMap("signature.cert.parse.errors") + +// Key stores the attributes used for mapping the executable/DLL to its signature info. +type Key struct { + // Path is the normalized NT device path of the executable/DLL file. + Path string + + // ImageSize is the mapped image size from IMAGE_INFO. Free discriminator. + // Distinguishes builds with identical timestamps (rare but possible + // with reproducible builds or timestamp stripping). + ImageSize uint64 + + // CheckSum is PE optional header checksum. Included when non-zero. + // Many debug/internal builds leave this as 0. + CheckSum uint32 + + // TimeDateStamp is the PE optional header attribute. Written by the + // linker and changes on every rebuild. + TimeDateStamp uint32 +} + +func (k Key) String() string { + var b bytes.Buffer + b.Grow(len(k.Path) + 16) + + b.WriteString(k.Path) + b.Write(binary.LittleEndian.AppendUint32(nil, k.CheckSum)) + b.Write(binary.LittleEndian.AppendUint64(nil, k.ImageSize)) + b.Write(binary.LittleEndian.AppendUint32(nil, k.TimeDateStamp)) + + return hex.EncodeToString(b.Bytes()) +} + +// IsDegenerate returns true when the key lacks enough entropy to be +// a reliable discriminator. In this case fall through to a synchronous +// inline check rather than risking a stale cache hit. +func (k Key) IsDegenerate() bool { + return k.TimeDateStamp == 0 && k.CheckSum == 0 +} + +// MakeKey produces a new signature store key. +func MakeKey(path string, imageSize uint64, checksum, timeDateStamp uint32) Key { + return Key{ + Path: strings.ToLower(path), + ImageSize: imageSize, + CheckSum: checksum, + TimeDateStamp: timeDateStamp, + } +} + +// Request contains the data necessary for querying the module signature info. +type Request struct { + Key Key + Response chan<- *Signature // caller can optionally wait +} + // Signatures manages and caches DLL and executable signatures. type Signatures struct { - signatures map[uint64]*Signature - mux sync.Mutex - purger *time.Ticker + signatures map[Key]*Signature + + requests chan Request + certs chan Request + + mux sync.RWMutex + purger *time.Ticker + + group singleflight.Group + + stop chan struct{} } var sigs *Signatures +var once sync.Once // sigTTL maximum time for the signature to remain in the // internal store before it is purged. @@ -41,134 +133,323 @@ var sigTTL = 10 * time.Minute // GetSignatures creates a new signatures singleton. func GetSignatures() *Signatures { - if sigs != nil { - return sigs - } + once.Do(func() { + sigs = newSignatures() + }) + + return sigs +} + +func newSignatures() *Signatures { sigs = &Signatures{ - signatures: make(map[uint64]*Signature), + signatures: make(map[Key]*Signature, 512), purger: time.NewTicker(time.Minute), + requests: make(chan Request, requestQueueSize), + certs: make(chan Request, requestQueueSize), + stop: make(chan struct{}), + } + + workerCount := max(runtime.NumCPU()/2, 4) + for range workerCount { + go sigs.runWorker() } - go sigs.gcSignatures() + go sigs.runGC() return sigs } -// GetSignature retrieves the signature by base address. If +// Close shuts down the GC and worker goroutines. Call during graceful shutdown. +func (s *Signatures) Close() { + s.purger.Stop() + close(s.stop) +} + +// DoRequest submits a signature check and blocks until the result is ready. +// Use this when the caller must make an allow/deny decision such as +// in the field accessors. +func (s *Signatures) DoRequest(key Key) *Signature { + if key.IsDegenerate() { + // degenerate keys bypass the cache entirely. We still want + // LockOSThread on a worker, so route through the channel. + // Fall through to the channel path below. + } else { + // hot path returns immediately if the signature is already resolved + if sig := s.get(key); sig != nil { + if sig.IsTrusted() && !sig.HasCertificate() { + s.parseCertificate(sig) + } + return sig + } + } + + ch := make(chan *Signature, 1) + select { + case s.requests <- Request{Key: key, Response: ch}: + default: + // queue full: fall back to inline check rather than + // dropping a decision that has a security consequence. + signatureAsyncRequestDrops.Add(1) + return s.getOrCheck(key) + } + r := <-ch + return r +} + +// DoRequestAsync submits a fire-and-forget signature check. The callback +// returns immediately and the result lands in the cache when ready. +// This is useful for the module-load hot path where we don't need +// to block the event. +func (s *Signatures) DoRequestAsync(key Key) { + if s.contains(key) { + return + } + select { + case s.requests <- Request{Key: key}: + default: + // drop rather than block the event processing thread + signatureAsyncRequestDrops.Add(1) + } +} + +// GetSignature retrieves the signature by the key. If // the signature exists, its accessed timestamp is updated -// to prevent it being purged by the gc. -func (s *Signatures) GetSignature(addr uint64) *Signature { +// to prevent it being purged by the gc. If the signature is +// not found in the store, this method returns nil. +func (s *Signatures) GetSignature(key Key) *Signature { + return s.get(key) +} + +// RemoveSignature removes the signature from the store for the specified path. +func (s *Signatures) RemoveSignature(path string) { + var stale []Key + p := strings.ToLower(path) + + s.mux.RLock() + for k := range s.signatures { + if k.Path == p { + stale = append(stale, k) + } + } + s.mux.RUnlock() + + if len(stale) == 0 { + return + } + s.mux.Lock() - defer s.mux.Unlock() - sign, ok := s.signatures[addr] - if !ok { - return nil + for _, k := range stale { + delete(s.signatures, k) + signatureCount.Add(-1) + signatureInvalidations.Add(k.Path, 1) } - sign.keepalive() - return sign + s.mux.Unlock() } -// PutSignature links the signature data to the specified base address. -func (s *Signatures) PutSignature(addr uint64, sign *Signature) { +// PutSignature puts the signature where the signature type and level are +// typically attributed to trusted signatures. For this reason, we can skip +// WinTrust verification and directly proceed to certificate parsing. +func (s *Signatures) PutSignature(key Key, sigType Type, sigLevel Level) { + if s.contains(key) { + return + } + s.mux.Lock() - defer s.mux.Unlock() - if s.signatures[addr] == nil { - s.signatures[addr] = sign + s.signatures[key] = newSignature(key.Path, sigType, sigLevel) + s.mux.Unlock() + signatureCount.Add(1) + signatureCiCount.Add(1) + + select { + case s.certs <- Request{Key: key}: + default: } } -func (s *Signatures) gcSignatures() { +// get is the shared read path. RLock allows many concurrent readers. +// The keepalive write is atomic so we never need to upgrade to a write lock. +func (s *Signatures) get(key Key) *Signature { + s.mux.RLock() + sig := s.signatures[key] + s.mux.RUnlock() + if sig != nil { + sig.keepalive() // atomic write, no lock needed + signatureCacheHits.Add(1) + } + return sig +} + +// contains returns true if the signature with the given key exists in the store. +// If the signature doesn't exist, this method returns false. +func (s *Signatures) contains(key Key) bool { + return s.get(key) != nil +} + +func (s *Signatures) getOrCheck(key Key) *Signature { + if sig := s.get(key); sig != nil { + return sig + } + + // singleflight.Do ensures that if N goroutines miss the same key + // concurrently, only one of them does the PE/catalog work, while the + // rest block and share the result. This is critical during process + // startup when many threads load the same DLL simultaneously + v, err := s.group.Do(key.String(), func() (any, error) { + sig := newUncheckedSignature(key.Path) + if err := sig.check(); err != nil { + return sig, err + } + return sig, nil + }) + + if err != nil { + signatureCheckErrors.Add(err.Error(), 1) + } + + // put the signature in the store + s.mux.Lock() + sign := v.(*Signature) + s.signatures[key] = sign + s.mux.Unlock() + signatureCount.Add(1) + signatureCacheMisses.Add(1) + + return sign +} + +func (s *Signatures) runWorker() { for { - <-s.purger.C - s.mux.Lock() - for addr, sig := range s.signatures { - if time.Since(sig.accessed) > sigTTL { - log.Debugf("removing signature info for file %s", sig.Filename) - delete(s.signatures, addr) + select { + case req := <-s.requests: + s.processRequest(req) + case req := <-s.certs: + sig := s.get(req.Key) + if sig != nil { + continue } + s.parseCertificate(sig) + case <-s.stop: + return } - s.mux.Unlock() } } -// ParseCertificate parses the certificate data for catalog-based -// signatures. -func (s *Signature) ParseCertificate() error { - // the certificate exists in the PE security directory - if s.Cert != nil { - return nil +func (s *Signatures) processRequest(req Request) { + sign := s.getOrCheck(req.Key) + if req.Response != nil { + req.Response <- sign } - if !sys.IsWintrustFound() { - return ErrWintrustUnavailable +} + +// gc removes entries that have not been accessed within sigTTL. +// It reads the accessed timestamp via an atomic load, so it does not +// need to hold the write lock while computing ages. +func (s *Signatures) runGC() { + for { + select { + case <-s.purger.C: + s.gcSignatures() + case <-s.stop: + return + } } - // parse catalog certificate - catalog := sys.NewCatalog() - if err := catalog.Open(s.Filename); err != nil { - return err +} + +func (s *Signatures) gcSignatures() { + now := time.Now() + + // collect stale keys under a read lock to minimize write-lock hold time + s.mux.RLock() + var stale []Key + for key, sig := range s.signatures { + if now.Sub(sig.lastAccessed()) > sigTTL { + stale = append(stale, key) + } } - defer catalog.Close() - var err error - s.Cert, err = catalog.ParseCertificate() + s.mux.RUnlock() + + if len(stale) == 0 { + return + } + + s.mux.Lock() + for _, key := range stale { + sig := s.signatures[key] + // re-check under the write lock: the entry may have been + // refreshed between the RUnlock above and this Lock + if sig != nil && now.Sub(sig.lastAccessed()) > sigTTL { + log.Debugf("evicting signature for %s", sig.Path) + delete(s.signatures, key) + signatureCount.Add(-1) + signatureEvictions.Add(1) + } + } + s.mux.Unlock() +} + +func (s *Signatures) parseCertificate(sig *Signature) { + cert, err := sig.parseCertificate(false) if err != nil { - return err + signatureCertParseErrors.Add(err.Error(), 1) + return } - return nil + sig.setCert(cert) } -// Check determines if the provided executable image or DLL is signed. +// check determines if the executable image or DLL is signed and trusted. // It first parses the PE security directory to look for the signature // information. If the certificate is not embedded inside the PE object -// then this method will try to locate the hash in the catalog file. If -// the certificate parsing is successful, this function returns the -// signature structure containing the signature type and certificate info. -// If the signature is not present, this function returns ErrNotSigned error. -// To verify the signature, call the Verify method of the Signature structure. -// On success, this method returns the signature type and the signature level. -// The signature level is either unchecked or unsigned. It is necessary to -// call the Verify method to determine the signature chain trust. -func (s *Signature) Check() (uint32, uint32, error) { +// then this method will try to locate the hash in the catalog file. +// If the signature is not present, this function returns ErrNoSignature +// error. On the contrary, the signature chain trust is verified and +// the catalog certificates are parsed. +func (s *Signature) check() error { + if s == nil { + return ErrNilSignature + } + // check if the signature is embedded in PE - f, err := pe.ParseFile(s.Filename, pe.WithSecurity()) + f, err := pe.ParseFile(s.Path, pe.WithSecurity()) if err != nil { - return None, UncheckedLevel, err + return err } - if f.IsSigned { - s.Cert = f.Cert - return Embedded, UncheckedLevel, nil + + if f.IsSigned() { + s.setExists() + s.setType(TypeEmbedded) + s.setCert(f.Cert) + if !sys.IsWintrustFound() { + return ErrWintrustUnavailable + } + return s.verifyFile() } if !sys.IsWintrustFound() { - return None, UncheckedLevel, ErrWintrustUnavailable + return ErrWintrustUnavailable } // maybe the signature is in the catalog? catalog := sys.NewCatalog() - if err := catalog.Open(s.Filename); err != nil { - return None, UncheckedLevel, err + if err := catalog.Open(s.Path); err != nil { + return ErrNoSignature } defer catalog.Close() - if catalog.IsCatalogSigned() { - return Catalog, UncheckedLevel, nil + + if !catalog.IsCatalogSigned() { + return ErrNoSignature } - return None, UnsignedLevel, ErrNotSigned // image not signed -} + s.setExists() + s.setType(TypeCatalogCached) -// Verify verifies the DLL or executable image signature. -// The signature is verified via Authenticode policy provider. -// Windows must verify the trust chain by following the certificates -// to a trusted root certificate. -// If the verification fails on the PE object, then the attempt to -// verify the signature in the catalog file is made. This method -// returns a bool value indicating if the signature is trusted. -func (s *Signature) Verify() bool { - if !sys.IsWintrustFound() { - return false + if err := s.verifyCatalog(catalog); err != nil { + return err } - s.Level = UnsignedLevel - isTrusted := s.VerifyEmbedded() || s.VerifyCatalog() - if isTrusted { - s.Level = AuthenticodeLevel - return isTrusted + cert, err := catalog.ParseCertificate() + if err != nil { + signatureCertParseErrors.Add(err.Error(), 1) + return err } - return false + s.setCert(cert) + + return nil } diff --git a/pkg/util/signature/signature_test.go b/pkg/util/signature/signature_test.go index e9c783c8a..adfa2fe9e 100644 --- a/pkg/util/signature/signature_test.go +++ b/pkg/util/signature/signature_test.go @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 by Nedim Sabic Sabic + * Copyright 2021-present by Nedim Sabic Sabic * https://www.fibratus.io * All Rights Reserved. * @@ -19,56 +19,652 @@ package signature import ( - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/sys/windows" "os" "path/filepath" + "sync" "testing" + "time" + + "github.com/rabbitstack/fibratus/pkg/pe" + "github.com/rabbitstack/fibratus/pkg/sys" + "github.com/stretchr/testify/require" ) -func TestSignature(t *testing.T) { - executable, err := os.Executable() +func systemDir(path string) string { + return filepath.Join(os.Getenv("windir"), "system32", path) +} + +// wellKnownDLLs are catalog-signed system DLLs present on every +// supported Windows version. They provide a reliable test corpus +// without requiring specific test fixtures. +var wellKnownDLLs = []string{ + systemDir("kernel32.dll"), + systemDir("kernelbase.dll"), + systemDir("ntdll.dll"), + systemDir("user32.dll"), +} + +var embeddedSignedDLL = systemDir("kernel32.dll") + +// freshSignatures returns a clean Signatures instance and registers +// a cleanup that closes it and resets the singleton. +func freshSignatures(t *testing.T) *Signatures { + t.Helper() + s := newSignatures() + t.Cleanup(func() { + s.Close() + }) + return s +} + +func freshSignaturesWithoutCleanup(t *testing.T) *Signatures { + t.Helper() + return newSignatures() +} + +// makeKeyForFile builds a Key by reading the PE header of a real file. +// If the file cannot be read the test is skipped. +func makeKeyForFile(t *testing.T, path string) Key { + t.Helper() + hdr, err := pe.ParseFile(path, pe.WithSections()) + if err != nil { + t.Skipf("cannot read PE header for %s: %v", path, err) + } + return MakeKey(path, uint64(hdr.ImageSize), hdr.ImageChecksum, hdr.TimedateStamp) +} + +func TestKeyNormalisation(t *testing.T) { + // keys built with different casing must be equal. Windows paths + // are case-insensitive and the rule engine may supply either form. + k1 := MakeKey(`C:\Windows\System32\NTDLL.DLL`, 0x100000, 0xABCD, 0x12345678) + k2 := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x12345678) + if k1 != k2 { + t.Errorf("keys with different casing must be equal\n k1=%+v\n k2=%+v", k1, k2) + } +} + +func TestKeyDistinctForDifferentTimestamps(t *testing.T) { + // Same path, different build results in different key. + // This is the core property that prevents stale cache hits after + // a DLL update (e.g. Windows Update replaces ntdll.dll). + k1 := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0, 0x11111111) + k2 := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0, 0x22222222) + if k1 == k2 { + t.Error("keys with different TimeDateStamp must not be equal") + } +} + +func TestKeyDistinctForDifferentImageSizes(t *testing.T) { + // same path + timestamp but different image size has different key. + // Guards against reproducible builds that strip the timestamp + k1 := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0, 0) + k2 := MakeKey(`c:\windows\system32\ntdll.dll`, 0x200000, 0, 0) + if k1 == k2 { + t.Error("keys with different ImageSize must not be equal") + } +} + +func TestKeyDistinctForDifferentPaths(t *testing.T) { + k1 := MakeKey(`c:\windows\system32\kernel32.dll`, 0x100000, 0xABCD, 0x12345678) + k2 := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x12345678) + if k1 == k2 { + t.Error("keys with different paths must not be equal") + } +} + +func TestKeyStringDeterministic(t *testing.T) { + // Key.String() is used as the singleflight group key. + // The same Key must always produce the same string. + k := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x12345678) + if k.String() != k.String() { + t.Error("Key.String() must be deterministic") + } +} + +func TestKeyStringDistinctForDifferentKeys(t *testing.T) { + k1 := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x11111111) + k2 := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x22222222) + if k1.String() == k2.String() { + t.Error("distinct keys must produce distinct strings") + } +} + +func TestIsDegenerateReturnsTrueWhenBothZero(t *testing.T) { + k := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0, 0) + if !k.IsDegenerate() { + t.Error("key with zero CheckSum and TimeDateStamp must be degenerate") + } +} + +func TestIsDegenerateReturnsFalseWhenTimestampPresent(t *testing.T) { + k := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0, 0x12345678) + if k.IsDegenerate() { + t.Error("key with non-zero TimeDateStamp must not be degenerate") + } +} + +func TestIsDegenerateReturnsFalseWhenChecksumPresent(t *testing.T) { + k := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0) + if k.IsDegenerate() { + t.Error("key with non-zero CheckSum must not be degenerate") + } +} + +func TestGetSignaturesReturnsSameInstance(t *testing.T) { + s1 := GetSignatures() + s2 := GetSignatures() + if s1 != s2 { + t.Error("GetSignatures must return the same singleton instance") + } + s1.Close() +} + +func TestPutAndGetSignature(t *testing.T) { + s := freshSignatures(t) + key := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x12345678) + + s.PutSignature(key, TypeEmbedded, LevelWindows) + sig := s.GetSignature(key) + if sig == nil { + t.Fatal("GetSignature returned nil after PutSignature") + } + if sig.Type() != TypeEmbedded { + t.Errorf("expected Type=%v, got %v", TypeEmbedded, sig.Type()) + } +} + +func TestPutSignatureIdempotent(t *testing.T) { + // a second PutSignature for the same key must not overwrite + // the existing entry because the cache entry is immutable once set + s := freshSignatures(t) + key := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x12345678) + + s.PutSignature(key, TypeEmbedded, LevelWindows) + s.PutSignature(key, TypeNone, LevelUnsigned) // must be ignored + + sig := s.GetSignature(key) + if sig.Type() != TypeEmbedded { + t.Errorf("second PutSignature must not overwrite: got Type=%v", sig.Type()) + } +} + +func TestGetSignatureReturnsNilOnMiss(t *testing.T) { + s := freshSignatures(t) + key := MakeKey(`c:\nonexistent\path\foo.dll`, 0x1000, 0, 0x12345678) + if s.GetSignature(key) != nil { + t.Error("GetSignature must return nil for unknown key") + } +} + +func TestPutSignatureUpdatesCount(t *testing.T) { + s := freshSignatures(t) + before := signatureCount.Value() + key := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x12345678) + s.PutSignature(key, TypeEmbedded, LevelWindows) + after := signatureCount.Value() + if after != before+1 { + t.Errorf("signatureCount should have increased by 1: before=%d after=%d", before, after) + } +} + +func TestRemoveSignatureDeletesEntry(t *testing.T) { + s := freshSignatures(t) + key := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x12345678) + s.PutSignature(key, TypeEmbedded, LevelWindows) + + s.RemoveSignature(`C:\Windows\System32\ntdll.dll`) // different casing + if s.GetSignature(key) != nil { + t.Error("RemoveSignature must delete the entry regardless of path casing") + } +} + +// Bug regression: RemoveSignature only deleted the first matching key. +// If the same path appeared with two different timestamps (two builds of +// the same DLL loaded at different times), the second entry survived. +func TestRemoveSignatureDeletesAllMatchingPaths(t *testing.T) { + s := freshSignatures(t) + path := `c:\windows\system32\ntdll.dll` + + k1 := MakeKey(path, 0x100000, 0xABCD, 0x11111111) + k2 := MakeKey(path, 0x100000, 0xABCD, 0x22222222) + s.PutSignature(k1, TypeEmbedded, LevelWindows) + s.PutSignature(k2, TypeEmbedded, LevelWindows) + + s.RemoveSignature(path) + + if s.GetSignature(k1) != nil || s.GetSignature(k2) != nil { + t.Error("RemoveSignature must delete all entries sharing the same path") + } +} + +func TestRemoveSignatureNoopForUnknownPath(t *testing.T) { + s := freshSignatures(t) + // must not panic or return an error for a path not in the store. + s.RemoveSignature(`c:\nonexistent\foo.dll`) +} + +func TestKeepAliveUpdatesTimestamp(t *testing.T) { + sig := newUncheckedSignature(`c:\windows\system32\ntdll.dll`) + before := sig.accessed.Load() + + time.Sleep(2 * time.Millisecond) + sig.keepalive() + after := sig.accessed.Load() + + if after <= before { + t.Error("keepalive must advance the accessed timestamp") + } +} + +func TestGCEvictsStaleEntries(t *testing.T) { + s := freshSignatures(t) + + // Shrink TTL so the GC fires during the test. + oldTTL := sigTTL + sigTTL = 50 * time.Millisecond + t.Cleanup(func() { sigTTL = oldTTL }) + + key := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x12345678) + s.PutSignature(key, TypeEmbedded, LevelWindows) + + // Wait beyond TTL then trigger GC directly. + time.Sleep(100 * time.Millisecond) + s.gcSignatures() + + if s.GetSignature(key) != nil { + t.Error("GC must evict entries older than sigTTL") + } +} + +func TestGCDoesNotEvictRecentlyAccessed(t *testing.T) { + s := freshSignatures(t) + + oldTTL := sigTTL + sigTTL = 50 * time.Millisecond + t.Cleanup(func() { sigTTL = oldTTL }) + + key := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x12345678) + s.PutSignature(key, TypeEmbedded, LevelWindows) + + // Touch the entry just before TTL expires. + time.Sleep(30 * time.Millisecond) + s.GetSignature(key) // triggers keepalive + time.Sleep(30 * time.Millisecond) + s.gcSignatures() + + if s.GetSignature(key) == nil { + t.Error("GC must not evict recently accessed entries") + } +} + +func TestConcurrentGetAndPut(t *testing.T) { + s := freshSignatures(t) + key := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x12345678) + + var wg sync.WaitGroup + const workers = 64 + + // Writers + for i := 0; i < workers/2; i++ { + wg.Add(1) + go func() { + defer wg.Done() + s.PutSignature(key, TypeEmbedded, LevelWindows) + }() + } + + // Readers + for i := 0; i < workers/2; i++ { + wg.Add(1) + go func() { + defer wg.Done() + s.GetSignature(key) // must not race with writers + }() + } + + wg.Wait() +} + +func TestConcurrentDoRequestAsync(t *testing.T) { + // Verifies that concurrent async requests for the same key do not + // enqueue duplicate work and that the cache reaches a consistent state. + s := freshSignatures(t) + key := MakeKey(`c:\windows\system32\ntdll.dll`, 0x100000, 0xABCD, 0x12345678) + + var wg sync.WaitGroup + const goroutines = 32 + wg.Add(goroutines) + for i := 0; i < goroutines; i++ { + go func() { + defer wg.Done() + s.DoRequestAsync(key) + }() + } + wg.Wait() +} + +func TestDoRequestAsyncSkipsKnownUnsigned(t *testing.T) { + s := freshSignatures(t) + + // Pre-populate with an unsigned entry. + key := MakeKey(`c:\windows\system32\fake.dll`, 0x1000, 0, 0x12345678) + s.mux.Lock() + s.signatures[key] = newSignature(key.Path, TypeNone, LevelUnsigned) + s.mux.Unlock() + + before := len(s.requests) + s.DoRequestAsync(key) + after := len(s.requests) + + if after != before { + t.Errorf("DoRequestAsync must not enqueue a request for a known-unsigned DLL: queue grew by %d", after-before) + } +} + +func TestParsePEHeaderKernel32(t *testing.T) { + hdr, err := pe.ParseFile(`C:\Windows\System32\kernel32.dll`, pe.WithSections()) + if err != nil { + t.Fatalf("parsePEHeader failed: %v", err) + } + if hdr.TimedateStamp == 0 { + t.Error("kernel32.dll must have a non-zero TimeDateStamp") + } + if hdr.ImageSize == 0 { + t.Error("kernel32.dll must have a non-zero ImageSize") + } +} + +func TestSignatureCheckEmbedded(t *testing.T) { + if !isWintrustAvailable() { + t.Skip("wintrust not available") + } + sig := newUncheckedSignature(embeddedSignedDLL) + if err := sig.check(); err != nil { + t.Fatalf("check() failed for %s: %v", embeddedSignedDLL, err) + } + if !sig.Exists() { + t.Error("notepad.exe must have Exists=true") + } + if sig.Type() != TypeEmbedded { + t.Errorf("notepad.exe must have TypeEmbedded, got %v", sig.Type()) + } +} + +func TestSignatureCheckCatalogSigned(t *testing.T) { + if !isWintrustAvailable() { + t.Skip("wintrust not available") + } + // notepad is catalog-signed on most Windows installations. + sig := newUncheckedSignature(systemDir("notepad.exe")) + if err := sig.check(); err != nil && err != ErrNoSignature { + t.Fatalf("check() unexpected error for notepad.exe: %v", err) + } + if sig.Exists() && sig.Type() == TypeNone { + t.Error("notepad.exe Exists=true but Type=None is inconsistent") + } +} + +func TestSignatureCheckNonExistentFile(t *testing.T) { + sig := newUncheckedSignature(`C:\nonexistent\ghost.dll`) + err := sig.check() + if err == nil { + t.Error("check() must return an error for a non-existent file") + } +} + +func TestSignatureCheckUnsignedFile(t *testing.T) { + exe, err := os.Executable() require.NoError(t, err) + sig := newUncheckedSignature(exe) + err = sig.check() + if err == nil { + t.Error("check() must return an error for unsigned file") + } +} - var tests = []struct { - name string - filename string - sigType uint32 - sigLevel uint32 - err error - }{ - { - "PE embedded signature", - filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll"), - Embedded, - AuthenticodeLevel, - nil, - }, - { - "catalog signature", - filepath.Join(os.Getenv("windir"), "notepad.exe"), - Catalog, - AuthenticodeLevel, - nil, - }, - { - "unsigned binary", - executable, - None, - UnsignedLevel, - windows.ERROR_INVALID_PARAMETER, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sign := &Signature{Filename: tt.filename} - typ, _, err := sign.Check() - assert.True(t, err == tt.err) - sign.Verify() - assert.Equal(t, tt.sigType, typ) - assert.Equal(t, tt.sigLevel, sign.Level) +func TestDoRequestReturnsCachedResult(t *testing.T) { + s := freshSignatures(t) + key := makeKeyForFile(t, `C:\Windows\System32\kernel32.dll`) + + // Warm the cache. + sig1 := s.DoRequest(key) + if sig1 == nil { + t.Fatal("first DoRequest returned nil") + } + + // Second call must return the cached entry, not re-check. + sig2 := s.DoRequest(key) + if sig1 != sig2 { + t.Error("DoRequest must return the same *Signature pointer on cache hit") + } +} + +func TestDoRequestDeduplicatesConcurrentMisses(t *testing.T) { + // Verifies that singleflight collapses N concurrent misses for the + // same key into a single check() invocation. We cannot directly count + // check() calls without instrumentation, so we verify the observable + // invariant: all callers receive an identical *Signature pointer. + s := freshSignatures(t) + key := makeKeyForFile(t, systemDir("kernel32.dll")) + + const goroutines = 16 + results := make([]*Signature, goroutines) + var wg sync.WaitGroup + wg.Add(goroutines) + for i := 0; i < goroutines; i++ { + i := i + go func() { + defer wg.Done() + results[i] = s.DoRequest(key) + }() + } + wg.Wait() + + first := results[0] + for i, r := range results { + if r != first { + t.Errorf("goroutine %d got a different *Signature — singleflight not working", i) + } + } +} + +func TestDoRequestAsyncEventuallyPopulatesCache(t *testing.T) { + s := freshSignatures(t) + key := makeKeyForFile(t, systemDir("kernel32.dll")) + + s.DoRequestAsync(key) + + // Poll until the cache entry appears or timeout. + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + if sig := s.GetSignature(key); sig != nil { + return // success + } + time.Sleep(10 * time.Millisecond) + } + t.Error("DoRequestAsync did not populate the cache within 5 seconds") +} + +func TestWellKnownDLLsResolveWithinBudget(t *testing.T) { + // all well-known system DLLs must resolve within 500ms each. + // This guards against pathological catalog hash slowness. + if !isWintrustAvailable() { + t.Skip("wintrust not available") + } + + s := freshSignatures(t) + + for _, path := range wellKnownDLLs { + t.Run(path, func(t *testing.T) { + t.Parallel() + key := makeKeyForFile(t, path) + + start := time.Now() + sig := s.DoRequest(key) + elapsed := time.Since(start) + + if sig == nil { + t.Logf("DoRequest returned nil for %s", path) + } + if elapsed > 500*time.Millisecond { + t.Logf("WARN: DoRequest took %v for %s (budget: 500ms)", elapsed, path) + } }) } } + +func TestPrewarmReducesLatencyForSubsequentRequests(t *testing.T) { + if !isWintrustAvailable() { + t.Skip("wintrust not available") + } + + s := freshSignatures(t) + + // Prewarm a well-known DLL. + path := `C:\Windows\System32\kernel32.dll` + key := makeKeyForFile(t, path) + s.DoRequest(key) // blocks until resolved — this is the prewarm + + // Subsequent request must hit the cache and return within 1ms. + start := time.Now() + sig := s.GetSignature(key) + elapsed := time.Since(start) + + if sig == nil { + t.Fatal("GetSignature returned nil after prewarm") + } + if elapsed > time.Millisecond { + t.Errorf("cache hit took %v (expected <1ms)", elapsed) + } +} + +func TestIsTrustedWindowsSigned(t *testing.T) { + cases := []struct { + sigType Type + level Level + expected bool + }{ + {TypeEmbedded, LevelWindows, true}, + {TypeEmbedded, LevelWindowsTCB, true}, + {TypeFileVerified, LevelWindows, true}, + {TypeEmbedded, LevelAuthenticode, false}, // Authenticode is not Windows-level trust + {TypeCatalogCached, LevelWindows, false}, + {TypeNone, LevelUnsigned, false}, + {TypeEmbedded, LevelUnsigned, false}, + } + for _, tc := range cases { + got := IsTrusted(tc.sigType, tc.level) + if got != tc.expected { + t.Errorf("IsTrusted(%v, %v) = %v, want %v", tc.sigType, tc.level, got, tc.expected) + } + } +} + +func TestNewSignatureTrustedSetsStatus(t *testing.T) { + sig := newSignature(`c:\windows\system32\ntdll.dll`, TypeEmbedded, LevelWindows) + if !sig.IsTrusted() { + t.Error("newSignature with trusted type+level must set IsTrusted=true") + } + if !sig.Exists() { + t.Error("newSignature with trusted type+level must set Exists=true") + } +} + +func TestNewUncheckedSignatureIsNotTrusted(t *testing.T) { + sig := newUncheckedSignature(`c:\windows\system32\ntdll.dll`) + if sig.IsTrusted() { + t.Error("newUncheckedSignature must not be trusted") + } + if sig.Type() != TypeNone { + t.Errorf("newUncheckedSignature must have TypeNone, got %v", sig.Type()) + } +} + +func TestSignatureCountConsistencyAfterGC(t *testing.T) { + s := freshSignatures(t) + + oldTTL := sigTTL + sigTTL = 50 * time.Millisecond + t.Cleanup(func() { sigTTL = oldTTL }) + + before := signatureCount.Value() + + k1 := MakeKey(`c:\a.dll`, 0x1000, 0, 0x11111111) + k2 := MakeKey(`c:\b.dll`, 0x1000, 0, 0x22222222) + s.PutSignature(k1, TypeEmbedded, LevelWindows) + s.PutSignature(k2, TypeEmbedded, LevelWindows) + + time.Sleep(100 * time.Millisecond) + s.gcSignatures() + + after := signatureCount.Value() + + // Both entries added then GC'd: net change should be zero. + if after != before { + t.Errorf("signatureCount after add+GC: expected %d, got %d", before, after) + } +} + +func TestCloseStopsWorkersCleanly(t *testing.T) { + _ = freshSignatures(t) + + // give workers a moment to start + time.Sleep(10 * time.Millisecond) + + done := make(chan struct{}) + go func() { + close(done) + }() + + select { + case <-done: + // success + case <-time.After(2 * time.Second): + t.Error("Close() did not return within 2 seconds. Worker goroutines may be blocked") + } +} + +func TestKeepaliveIsRaceFree(t *testing.T) { + // Run with -race to verify atomic.Int64 is sufficient. + sig := newUncheckedSignature(`c:\windows\system32\ntdll.dll`) + var wg sync.WaitGroup + const goroutines = 128 + wg.Add(goroutines) + for i := 0; i < goroutines; i++ { + go func() { + defer wg.Done() + sig.keepalive() + _ = sig.lastAccessed() + }() + } + wg.Wait() +} + +func TestDoRequestAsyncDropsWhenQueueFull(t *testing.T) { + // fill the request queue to capacity then verify the drop counter + // increments rather than the call blocking + s := freshSignaturesWithoutCleanup(t) + + // stop workers so the queue fills + s.Close() + time.Sleep(10 * time.Millisecond) + + var dropsBefore int64 + // read current drop count via expvar string (expvar.Int has no direct Value method in all versions) + dropsBefore = signatureAsyncRequestDrops.Value() + + // overflow the queue + for i := 0; i < requestQueueSize+10; i++ { + key := MakeKey(`c:\a.dll`, uint64(i), 0, uint32(i)) + s.DoRequestAsync(key) + } + + dropsAfter := signatureAsyncRequestDrops.Value() + if dropsAfter <= dropsBefore { + t.Error("DoRequestAsync must increment drop counter when queue is full") + } +} + +func isWintrustAvailable() bool { + return sys.IsWintrustFound() +} diff --git a/pkg/util/signature/types.go b/pkg/util/signature/types.go index 7529454ca..4c3b51e06 100644 --- a/pkg/util/signature/types.go +++ b/pkg/util/signature/types.go @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 by Nedim Sabic Sabic + * Copyright 2021-present by Nedim Sabic Sabic * https://www.fibratus.io * All Rights Reserved. * @@ -20,106 +20,297 @@ package signature import ( "errors" - "github.com/rabbitstack/fibratus/pkg/sys" + "fmt" "runtime" + "sync/atomic" "time" + + "github.com/rabbitstack/fibratus/pkg/pe" + "github.com/rabbitstack/fibratus/pkg/sys" ) +// Type defines the signature verification type. +type Type uint32 + const ( - // UncheckedLevel specifies signature unchecked level - UncheckedLevel uint32 = 0 - // UnsignedLevel specifies signature unsigned level - UnsignedLevel uint32 = 1 - // AuthenticodeLevel determines the object is Authenticode signed - AuthenticodeLevel uint32 = 4 - - // None indicates non-existent signature - None uint32 = 0 - // Embedded indicates the signature is baked into the PE object - Embedded uint32 = 1 - // Catalog indicates the executable or DLL signature is stored in the catalog - Catalog uint32 = 3 + TypeNone Type = 0 // unsigned or verification hasn't been attempted + TypeEmbedded Type = 1 // embedded signature + TypeCached Type = 2 // cached signature; presence of a CI EA means the file was previously verified + TypeCatalogCached Type = 3 // cached catalog verified via Catalog Database or searching catalog directly + TypeCatalogUncached Type = 4 // uncached catalog verified via Catalog Database or searching catalog directly + TypeCatalogHint Type = 5 // successfully verified using an EA that informs CI that catalog to try first + TypePackageCatalog Type = 6 // AppX / MSIX package catalog verified + TypeFileVerified Type = 7 // the file was verified ) -// ErrNotSigned represents the error which is raised when the image lacks the signature -var ErrNotSigned = errors.New("image is not signed") +func (t Type) String() string { + switch t { + case TypeNone: + return "None" + case TypeEmbedded: + return "Embedded" + case TypeCached: + return "Cached" + case TypeCatalogCached: + return "CatalogCached" + case TypeCatalogUncached: + return "CatalogUncached" + case TypeCatalogHint: + return "CatalogHint" + case TypePackageCatalog: + return "PackageCatalog" + case TypeFileVerified: + return "FileVerified" + default: + return fmt.Sprintf("%d", uint32(t)) + } +} -// ErrWintrustUnavailable represents the error which is raised when wintrust platfrom is not available -var ErrWintrustUnavailable = errors.New("wintrust is not available") +// Level defines the image signing level. +type Level uint32 + +const ( + LevelUnchecked Level = 0 // signing level hasn't yet been checked + LevelUnsigned Level = 1 // file is unsigned or has no signature that passes the active policies + LevelEnterprise Level = 2 // trusted by Windows Defender Application Control policy + LevelDeveloper Level = 3 // developer signed code + LevelAuthenticode Level = 4 // Authenticode signed + LevelStorePPL Level = 5 // Microsoft Store signed app PPL (Protected Process Light) + LevelStore Level = 6 // Microsoft Store-signed + LevelAntimalware Level = 7 // signed by an Antimalware vendor whose product is using AMPPL + LevelMicrosoft Level = 8 // Microsoft signed + LevelCustom4 Level = 9 + LevelCustom5 Level = 10 + LevelDynamicCodeGen Level = 11 // only used for signing of the .NET NGEN compiler + LevelWindows Level = 12 // Windows signed + LevelCustom7 Level = 13 + LevelWindowsTCB Level = 14 // Windows Trusted Computing Base signed + LevelCustom6 Level = 15 +) -// Types enum defines signature types which verified the image. var Types = map[uint32]string{ - 0: "NONE", // unsigned or verification hasn't been attempted - 1: "EMBEDDED", // embedded signature - 2: "CACHED", // cached signature; presence of a CI EA means the file was previously verified - 3: "CATALOG_CACHED", // cached catalog verified via Catalog Database or searching catalog directly - 4: "CATALOG_UNCACHED", // uncached catalog verified via Catalog Database or searching catalog directly - 5: "CATALOG_HINT", // successfully verified using an EA that informs CI that catalog to try first - 6: "PACKAGE_CATALOG", // AppX / MSIX package catalog verified - 7: "FILE_VERIFIED", // the file was verified + uint32(TypeNone): "NONE", + uint32(TypeEmbedded): "EMBEDDED", + uint32(TypeCached): "CACHED", + uint32(TypeCatalogCached): "CATALOG_CACHED", + uint32(TypeCatalogUncached): "CATALOG_UNCACHED", + uint32(TypeCatalogHint): "CATALOG_HINT", + uint32(TypePackageCatalog): "PACKAGE_CATALOG", + uint32(TypeFileVerified): "FILE_VERIFIED", } -// Levels enum defines all possible image signature levels at which the code was verified. var Levels = map[uint32]string{ - 0: "UNCHECKED", // signing level hasn't yet been checked - 1: "UNSIGNED", // file is unsigned or has no signature that passes the active policies - 2: "ENTERPRISE", // trusted by Windows Defender Application Control policy - 3: "DEVELOPER", // developer signed code - 4: "AUTHENTICODE", // Authenticode signed - 5: "STORE_PPL", // Microsoft Store signed app PPL (Protected Process Light) - 6: "STORE", // Microsoft Store-signed - 7: "ANTIMALWARE", // signed by an Antimalware vendor whose product is using AMPPL - 8: "MICROSOFT", // Microsoft signed - 9: "CUSTOM_4", - 10: "CUSTOM_5", - 11: "DYNAMIC_CODEGEN", // only used for signing of the .NET NGEN compiler - 12: "WINDOWS", // Windows signed - 13: "CUSTOM_7", - 14: "WINDOWS_TCB", // Windows Trusted Computing Base signed - 15: "CUSTOM_6", -} - -// Signature represents the signature status. + uint32(LevelUnchecked): "UNCHECKED", + uint32(LevelUnsigned): "UNSIGNED", + uint32(LevelEnterprise): "ENTERPRISE", + uint32(LevelDeveloper): "DEVELOPER", + uint32(LevelAuthenticode): "AUTHENTICODE", + uint32(LevelStorePPL): "STORE_PPL", + uint32(LevelStore): "STORE", + uint32(LevelAntimalware): "ANTIMALWARE", + uint32(LevelMicrosoft): "MICROSOFT", + uint32(LevelCustom4): "CUSTOM_4", + uint32(LevelCustom5): "CUSTOM_5", + uint32(LevelDynamicCodeGen): "DYNAMIC_CODEGEN", + uint32(LevelWindows): "WINDOWS", + uint32(LevelCustom7): "CUSTOM_7", + uint32(LevelWindowsTCB): "WINDOWS_TCB", + uint32(LevelCustom6): "CUSTOM_6", +} + +// ErrNoSignature represents the error which is raised when the executable image lacks the signature +var ErrNoSignature = errors.New("image is not signed") + +// ErrNilSignature represents the error that is signaled when the operation is attempted on a nil signature +var ErrNilSignature = errors.New("the signature is not initialized") + +// ErrWintrustUnavailable represents the error which is raised when wintrust platform is not available +var ErrWintrustUnavailable = errors.New("wintrust is not available") + +// Signature represents the signature state. type Signature struct { - // Type specifies the signature type. If the image is not signed, the - // type is equal to None. - Type uint32 - // Level specifies the signature level at which the code was signed. - Level uint32 - // Cert represents certificate information for the particular signature. - Cert *sys.Cert - // Filename represents the name of the executable image/DLL/driver - Filename string - // accessed the timestamp of the signature access by field extractor - accessed time.Time + // Path represents the name of the executable/DLL. + Path string + + // typ indicates the signature type. + typ atomic.Uint32 + + // exists indicates if the signature exists in the PE security directory + // or a system-wide catalog. + exists atomic.Bool + + // status represents the signature trust status. + status atomic.Uint32 + + // cert represents certificate information for the particular signature. + cert atomic.Pointer[sys.Cert] + + // accessed the timestamp of the signature access by field extractor. + accessed atomic.Int64 +} + +func (s *Signature) String() string { + var cert string + if s.HasCertificate() { + c := s.Cert() + cert = c.Issuer + " | " + c.Subject + } + + var accessed string + if ts := s.accessed.Load(); ts != 0 { + accessed = time.Unix(0, ts).Format(time.RFC3339Nano) + } + + return fmt.Sprintf( + "Exists: %t, Type: %s, Status: %s, Path: %s, Cert: %s, Accessed: %s}", + s.Exists(), + s.Type(), + s.Status(), + s.Path, + cert, + accessed, + ) +} + +// IsTrusted returns true if Code Integrity successfully validates the file's trust chain. +func IsTrusted(sigType Type, sigLevel Level) bool { + return (sigType == TypeEmbedded && + (sigLevel == LevelWindows || sigLevel == LevelWindowsTCB)) || + (sigType == TypeFileVerified && sigLevel == LevelWindows) +} + +// newSignature returns a signature initialized with file name and signature type. +// If the signature type is known upfront, then we can skip the signature +// check phase to save system resources. +func newSignature(path string, sigType Type, sigLevel Level) *Signature { + s := &Signature{ + Path: path, + } + s.setType(sigType) + s.setStatus(sys.SignatureNotTrusted) + + s.exists.Store(false) + + if IsTrusted(sigType, sigLevel) { + s.setExists() + s.setStatus(sys.SignatureTrusted) + } + + return s +} + +// newUncheckedSignature creates a new signature with none type and unchecked signature level. +// This is the default constructor for any non-trusted signature verification. +func newUncheckedSignature(path string) *Signature { + return newSignature(path, TypeNone, LevelUnchecked) } func (s *Signature) keepalive() { - s.accessed = time.Now() + s.accessed.Store(time.Now().UnixNano()) +} + +func (s *Signature) lastAccessed() time.Time { + return time.Unix(0, s.accessed.Load()) +} + +func (s *Signature) Status() sys.SignatureStatus { + return sys.SignatureStatus(s.status.Load()) +} + +func (s *Signature) Type() Type { + return Type(s.typ.Load()) +} + +func (s *Signature) Exists() bool { + return s.exists.Load() +} + +func (s *Signature) setStatus(v sys.SignatureStatus) { + s.status.Store(uint32(v)) +} + +func (s *Signature) setType(t Type) { + s.typ.Store(uint32(t)) +} + +func (s *Signature) setExists() { + s.exists.Store(true) } -func (s *Signature) IsSigned() bool { return s.Type != None } -func (s *Signature) IsTrusted() bool { return s.Level != UncheckedLevel && s.Level != UnsignedLevel } -func (s *Signature) HasCertificate() bool { return s.Cert != nil } +func (s *Signature) IsTrusted() bool { + return s.Status() == sys.SignatureTrusted +} + +// Cert returns the certificate, or nil if not yet parsed. +func (s *Signature) Cert() *sys.Cert { + return s.cert.Load() +} + +// setCert uses a aingle atomic store to initialize a valid pointer +// cert pointer exactly once. Any reader seeing non-nil is guaranteed +// to see the fully initialised Cert struct. +func (s *Signature) setCert(cert *sys.Cert) { + s.cert.CompareAndSwap(nil, cert) +} -// VerifyEmbedded performs a trust verification action on the PE file +// HasCertificate returns true if this signature holds a certificate. +func (s *Signature) HasCertificate() bool { + return s.cert.Load() != nil +} + +// verifyFile performs a trust verification action on the PE file // by passing the inquiry to a trust provider that supports the action // identifier. -func (s *Signature) VerifyEmbedded() bool { +func (s *Signature) verifyFile() error { runtime.LockOSThread() defer runtime.UnlockOSThread() trust := sys.NewWintrustData(sys.WtdChoiceFile) defer trust.Close() - return trust.VerifyFile(s.Filename) + status, err := trust.VerifyFile(s.Path) + s.setStatus(status) + return err +} + +// verifyCatalog verifies the catalog-based file signature. +func (s *Signature) verifyCatalog(catalog sys.Cat) error { + status, err := catalog.Verify(s.Path) + s.setStatus(status) + return err } -// VerifyCatalog verifies the catalog-based file signature. -func (s *Signature) VerifyCatalog() bool { +// parseCertificate parses the certificate data for catalog-based +// or PE signatures if the parameter is set to false. +func (s *Signature) parseCertificate(onlyCatalog bool) (*sys.Cert, error) { + if s == nil { + return nil, ErrNilSignature + } + + // the certificate already exists + if s.HasCertificate() { + return s.Cert(), nil + } + if !s.Exists() { + return nil, ErrNoSignature + } + + if !onlyCatalog { + // parse PE certificate + f, err := pe.ParseFile(s.Path, pe.WithSecurity()) + if err != nil { + goto cat + } + return f.Cert, nil + } + +cat: + if !sys.IsWintrustFound() { + return nil, ErrWintrustUnavailable + } + // parse catalog certificate catalog := sys.NewCatalog() - err := catalog.Open(s.Filename) - if err != nil { - return false + if err := catalog.Open(s.Path); err != nil { + return nil, err } defer catalog.Close() - return catalog.Verify(s.Filename) + return catalog.ParseCertificate() } diff --git a/pkg/yara/scanner.go b/pkg/yara/scanner.go index 823975511..951246685 100644 --- a/pkg/yara/scanner.go +++ b/pkg/yara/scanner.go @@ -199,27 +199,13 @@ func (s scanner) Scan(e *event.Event) (bool, error) { // scan the process loading unsigned/untrusted module // or loading the module from unbacked memory region pid := e.PID - addr := e.Params.MustGetUint64(params.ModuleBase) - typ := e.Params.MustGetUint32(params.ModuleSignatureType) - if typ != signature.None { - return false, nil - } filename := e.GetParamAsString(params.ModulePath) if s.config.ShouldSkipFile(filename) { return false, nil } - // get module signature - sign := signature.GetSignatures().GetSignature(addr) - if sign == nil { - sign = &signature.Signature{Filename: filename} - sign.Type, sign.Level, err = sign.Check() - if sign.IsSigned() { - sign.Verify() - } - } - - if !sign.IsSigned() || !sign.IsTrusted() || (!e.Callstack.IsEmpty() && e.Callstack.ContainsUnbacked()) { + sign := signature.GetSignatures().GetSignature(e.SignatureKey()) + if (sign != nil && (!sign.Exists() || !sign.IsTrusted())) || (!e.Callstack.IsEmpty() && e.Callstack.ContainsUnbacked()) { log.Debugf("scanning suspicious module loading. pid: %d, module: %s", pid, filename) matches, err = s.scan(pid) moduleScans.Add(1) @@ -295,13 +281,8 @@ func (s scanner) Scan(e *event.Event) (bool, error) { size := e.Params.MustGetUint64(params.FileViewSize) if e.PID != 4 && size >= 4096 && ((prot&sys.SectionRX) != 0 && (prot&sys.SectionRWX) != 0) && !s.isMmapMatched(pid) { filename := e.GetParamAsString(params.FilePath) - // skip mappings of signed Modules addr := e.Params.MustGetUint64(params.FileViewBase) - sign := signature.GetSignatures().GetSignature(addr) - if sign != nil && sign.IsSigned() && sign.IsTrusted() { - return false, nil - } - // data/Module file was mapped? + // data/module file was mapped? if filename != "" { if s.config.ShouldSkipFile(filename) { return false, nil diff --git a/pkg/yara/scanner_test.go b/pkg/yara/scanner_test.go index d3f3f66b9..0912f1187 100644 --- a/pkg/yara/scanner_test.go +++ b/pkg/yara/scanner_test.go @@ -281,15 +281,22 @@ func TestScan(t *testing.T) { } psnap.On("Find", pid).Return(true, proc) + exe, err := os.Executable() + require.NoError(t, err) + + key := signature.MakeKey(exe, 0, 2323432, 0) + signature.GetSignatures().PutSignature(key, 0, 0) + e := &event.Event{ Type: event.LoadModule, Name: "LoadModule", Tid: 2484, PID: pid, Params: event.Params{ - params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: "tests.exe"}, + params.FilePath: {Name: params.FilePath, Type: params.UnicodeString, Value: exe}, params.ModuleBase: {Name: params.ModuleBase, Type: params.Uint64, Value: uint64(0x74888fd99)}, - params.ModuleSignatureType: {Name: params.ModuleSignatureType, Type: params.Uint32, Value: signature.None}, + params.ModuleCheckSum: {Name: params.ModuleCheckSum, Type: params.Uint32, Value: uint32(2323432)}, + params.ModuleSignatureType: {Name: params.ModuleSignatureType, Type: params.Uint32, Value: signature.TypeNone}, params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: pid}, }, Metadata: make(map[event.MetadataKey]any), @@ -755,57 +762,6 @@ func TestScan(t *testing.T) { }, true, }, - { - "scan rx pagefile mmap address for signed module", - func() (*event.Event, error) { - proc := &pstypes.PS{ - Name: "tests.exe", - PID: 1123, - Ppid: uint32(os.Getppid()), - Exe: `C:\ProgramData\tests.exe`, - Cmdline: `C:\ProgramData\tests.exe`, - SID: "S-1-1-18", - Cwd: `C:\ProgramData\`, - SessionID: 1, - } - psnap.On("Find", 1123).Return(true, proc) - - signature.GetSignatures().PutSignature(uint64(0x7f3e1000), &signature.Signature{Level: signature.AuthenticodeLevel, Type: signature.Catalog}) - - e := &event.Event{ - Type: event.MapViewFile, - Name: "MapViewFile", - Category: event.File, - Tid: 2484, - PID: 565, - Params: event.Params{ - params.ProcessID: {Name: params.ProcessID, Type: params.PID, Value: uint32(1123)}, - params.FileViewBase: {Name: params.FileViewBase, Type: params.Address, Value: uint64(0x7f3e1000)}, - params.FileViewSize: {Name: params.FileViewSize, Type: params.Uint64, Value: uint64(12333)}, - params.MemProtect: {Name: params.MemProtect, Type: params.Flags, Value: uint32(sys.SectionRX), Flags: event.ViewProtectionFlags}, - }, - Metadata: make(map[event.MetadataKey]any), - PS: proc, - } - return e, nil - }, - func() (Scanner, error) { - return NewScanner(psnap, config.Config{ - Enabled: true, - ScanTimeout: time.Minute, - Rule: config.Rule{ - Paths: []config.RulePath{ - { - Namespace: "default", - Path: "_fixtures/rules", - }, - }, - }, - }) - }, - alertsender.Alert{}, - false, - }, { "scan rx pagefile readonly mmap", func() (*event.Event, error) {