Skip to content

Commit

Permalink
Refactor the image signature validation to untangle signature check/v…
Browse files Browse the repository at this point in the history
…erification from certificate fetching. Also, add a new enumeration value for file verified signature types. (rabbitstack#180)
  • Loading branch information
rabbitstack authored Jul 13, 2023
1 parent 9380d58 commit 01f523d
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 82 deletions.
44 changes: 26 additions & 18 deletions pkg/kstream/processors/image_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,33 +81,41 @@ func (imageProcessor) Close() {}

// processSignature consults the signature cache and if the signature
// already exists for a particular image checksum, signature checking
// is skipped. On the contrary, the signature verification is performed
// and the cache is updated accordingly.
// is skipped and only certificate info is fetched. On the contrary,
// the signature verification is performed and the cache is updated
// accordingly.
func (m *imageProcessor) processSignature(e *kevent.Kevent) error {
checksum := e.Kparams.MustGetUint32(kparams.ImageCheckSum)
level := e.Kparams.MustGetUint32(kparams.ImageSignatureLevel)
typ := e.Kparams.MustGetUint32(kparams.ImageSignatureType)
sign, ok := m.signatures[checksum]
if !ok {
var opts []signature.Option
var filename = e.GetParamAsString(kparams.FileName)
if level != signature.UncheckedLevel {
opts = append(opts, signature.OnlyCert())
}
var err error
sign, err = signature.CheckWithOpts(filename, opts...)
if err != nil {
return err
}
if level == signature.UncheckedLevel && sign.IsSigned() {
sign.Verify()
if typ == signature.None {
var err error
sign, err = signature.Check(filename)
if err != nil {
return err
}
if sign.IsSigned() {
sign.Verify()
}
} else {
// signature checked, only obtain certificate info
sign = signature.Wrap(typ, level)
var err error
sign.Cert, err = signature.GetCertificate(filename)
if err != nil {
return err
}
}
m.signatures[checksum] = sign
}
if level == signature.UncheckedLevel {
// reset signature type/level parameters
_ = e.Kparams.SetValue(kparams.ImageSignatureType, sign.Type)
_ = e.Kparams.SetValue(kparams.ImageSignatureLevel, sign.Level)
}

// reset signature type/level parameters
_ = e.Kparams.SetValue(kparams.ImageSignatureLevel, sign.Level)
_ = e.Kparams.SetValue(kparams.ImageSignatureType, sign.Type)

// append certificate parameters
if sign.HasCertificate() {
e.AppendParam(kparams.ImageCertIssuer, kparams.UnicodeString, sign.Cert.Issuer)
Expand Down
96 changes: 45 additions & 51 deletions pkg/util/signature/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,66 +28,46 @@ import (
var isWintrustDLLFound bool
var once sync.Once

type opts struct {
onlyCert bool
// Wrap returns a new signature form signature type and level.
func Wrap(typ, level uint32) *Signature {
return &Signature{Type: typ, Level: level}
}

// Option represents the option that influences signature verification/checking process
type Option func(o *opts)

// OnlyCert indicates if only certificate info is fetched for the signature. If this
// option is set, it is assumed the signature was already checked.
func OnlyCert() Option {
return func(o *opts) {
o.onlyCert = true
// GetCertificate returns certificate details for the specific PE object.
func GetCertificate(filename string) (*pe.Cert, error) {
onceWintrustDLL()
f, err := pe.ParseFile(filename, pe.WithSecurity())
if err != nil {
return nil, err
}
if f.Cert != nil {
return f.Cert, nil
}
if !isWintrustDLLFound {
return nil, ErrWintrustUnavailable
}
// parse catalog certificate
catalog := NewCatalog()
if err := catalog.Open(filename); err != nil {
return nil, err
}
defer catalog.Close()
cert, err := catalog.ParseCertificate()
if err != nil {
return nil, err
}
return cert, nil
}

// CheckWithOpts determines if the provided executable image or DLL is signed.
// Check determines if the provided executable image or DLL is signed.
// 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 OnlyCert option is specified, then only certificate information is
// fetched for the particular executable image, DLL, or driver. If the function
// returns a nil value, this indicates the provided image is not signed.
func CheckWithOpts(filename string, options ...Option) (*Signature, error) {
once.Do(func() {
isWintrustDLLFound = sys.IsWintrustFound()
if !isWintrustDLLFound {
log.Warn("unable to find wintrust.dll library. This will lead to " +
"PE objects signature verification to be skipped possibly " +
"causing false positive samples in detection rules relying on " +
"image signature filter fields")
}
})
var opts opts
for _, opt := range options {
opt(&opts)
}
// the signature is assumed to be verified, so we just extract cert info
if opts.onlyCert {
f, err := pe.ParseFile(filename, pe.WithSecurity())
if err != nil {
return nil, err
}
if f.Cert != nil {
return &Signature{filename: filename, Cert: f.Cert}, nil
}
if !isWintrustDLLFound {
return nil, ErrWintrustUnavailable
}
// parse catalog certificate
catalog := NewCatalog()
if err := catalog.Open(filename); err != nil {
return nil, err
}
cert, err := catalog.ParseCertificate()
if err != nil {
return nil, err
}
return &Signature{filename: filename, Cert: cert}, nil
}

func Check(filename string) (*Signature, error) {
onceWintrustDLL()
// check if the signature is embedded in PE
f, err := pe.ParseFile(filename, pe.WithSecurity())
if err != nil {
Expand All @@ -106,6 +86,7 @@ func CheckWithOpts(filename string, options ...Option) (*Signature, error) {
if err := catalog.Open(filename); err != nil {
return nil, err
}
defer catalog.Close()
if catalog.IsCatalogSigned() {
cert, err := catalog.ParseCertificate()
if err != nil {
Expand All @@ -128,9 +109,22 @@ func (s *Signature) Verify() bool {
return false
}
s.Level = UnsignedLevel
if s.VerifyEmbedded() || s.VerifyCatalog() {
isTrusted := s.VerifyEmbedded() || s.VerifyCatalog()
if isTrusted {
s.Level = AuthenticodeLevel
return true
return isTrusted
}
return false
}

func onceWintrustDLL() {
once.Do(func() {
isWintrustDLLFound = sys.IsWintrustFound()
if !isWintrustDLLFound {
log.Warn("unable to find wintrust.dll library. This will lead to " +
"PE objects signature verification to be skipped possibly " +
"causing false positive samples in detection rules relying on " +
"image signature filter fields")
}
})
}
14 changes: 1 addition & 13 deletions pkg/util/signature/signature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,45 +36,33 @@ func TestSignature(t *testing.T) {
sigType uint32
sigLevel uint32
err error
opts []Option
}{
{
"PE embedded signature",
filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll"),
Embedded,
AuthenticodeLevel,
nil,
nil,
},
{
"catalog signature",
filepath.Join(os.Getenv("windir"), "notepad.exe"),
Catalog,
AuthenticodeLevel,
nil,
nil,
},
{
"unsigned binary",
executable,
None,
UnsignedLevel,
ErrNotSigned,
nil,
},
{
"PE only cert",
filepath.Join(os.Getenv("windir"), "System32", "kernel32.dll"),
None,
AuthenticodeLevel,
nil,
[]Option{OnlyCert()},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sig, err := CheckWithOpts(tt.filename, tt.opts...)
sig, err := Check(tt.filename)
assert.True(t, err == tt.err)
if sig != nil {
assert.Equal(t, tt.sigType, sig.Type)
Expand Down
1 change: 1 addition & 0 deletions pkg/util/signature/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ var Types = map[uint32]string{
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
}

// Levels enum defines all possible image signature levels at which the code was verified.
Expand Down

0 comments on commit 01f523d

Please sign in to comment.