From dcc755f0b574c393ab6c02833cf039fabb701003 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 23 Jun 2026 18:03:38 +0200 Subject: [PATCH 1/4] chore(event): Parse module timedate stamp parameter --- pkg/event/param_decoder_windows.go | 2 +- pkg/event/param_decoder_windows_test.go | 2 +- pkg/event/params/params_windows.go | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) 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. From 44c82e74dc7ebb98d665c729b21d684d43bb1e51 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 23 Jun 2026 18:07:14 +0200 Subject: [PATCH 2/4] chore(ps): Add module timedate stamp attribute --- pkg/ps/snapshotter_windows.go | 1 + pkg/ps/types/types_windows.go | 2 ++ 2 files changed, 3 insertions(+) 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. From 5eac3a81a25353c8435852b884612812293d2002 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Tue, 23 Jun 2026 18:16:20 +0200 Subject: [PATCH 3/4] refactor(sys): Better WinTrust error handling --- pkg/sys/trust.go | 95 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 19 deletions(-) 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) From ea67c6aeb018cc853bbcc985c13d2dc6ef560023 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Wed, 24 Jun 2026 17:14:12 +0200 Subject: [PATCH 4/4] refactor(signature): Overhaul cache keying and async verification pipeline The signature verification subsystem has been redesigned to address correctness, performance, and observability concerns that accumulated as the codebase grew. Cache keying was fundamentally broken by relying on the module base address as the primary index. ASLR randomises base addresses across processes, causing every process spawn to produce a full cache miss for system DLLs that were already verified moments earlier. The new key is derived entirely from data available in the image-load event including a normalised NT device path, PE timestamp, checksum, and mapped image size. This requires zero additional syscalls and no file handle acquisition at key construction time. It also eliminates stale hit and false miss scenarios caused by DLL unload/reload at the same virtual address, and collapses redundant verification work for the same physical file loaded across multiple processes. The synchronisation model has been reworked throughout. The global mutex has been replaced with a reader-writer lock, and the signature accessed timestamp is updated via an atomic store, eliminating the need to upgrade to a write lock on every cache hit. All fields on the Signature struct that are written by worker goroutines and read by the rule engine are now accessed through the appropriate atomic primitives, closing a class of data races that the previous implementation left undetected. Certificate parsing no longer holds the write lock across file I/O and WinTrust calls, which previously serialised all cache readers for the full duration of a catalog hash operation. Cache invalidation on CreateFile events with FILE_OVERWRITE and FILE_OVERWRITE_IF dispositions remove stale signatures from the store. The async verification pipeline has been designed around the observation that the enrichment stages between raw ETW delivery and rule engine evaluation (stack walk correlation, symbol resolution, and evasion scanner execution) provide a natural latency budget that covers the median catalog hash duration. Image rundown events emitted at session start are used to prewarm the cache during bootstrap, ensuring that common system modules are fully verified before the first live event reaches the rule engine. --- go.mod | 2 +- go.sum | 2 + internal/bootstrap/bootstrap.go | 5 +- internal/etw/processors/fs_windows.go | 15 + internal/etw/processors/module_windows.go | 19 +- pkg/event/event.go | 10 + pkg/event/event_windows.go | 16 + pkg/filter/accessor_windows.go | 133 ++--- pkg/filter/filter_test.go | 40 +- pkg/filter/ql/function.go | 41 +- pkg/filter/util.go | 41 +- pkg/pe/marshaller.go | 16 +- pkg/pe/marshaller_test.go | 15 +- pkg/pe/parser.go | 8 +- pkg/pe/types.go | 65 +- pkg/util/signature/signature.go | 457 ++++++++++++--- pkg/util/signature/signature_test.go | 684 ++++++++++++++++++++-- pkg/util/signature/types.go | 335 ++++++++--- pkg/yara/scanner.go | 25 +- pkg/yara/scanner_test.go | 62 +- 20 files changed, 1485 insertions(+), 506 deletions(-) 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/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/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) {