Skip to content

Commit

Permalink
feature: parse all certificates from multi-signed files (#95)
Browse files Browse the repository at this point in the history
* feat: parse multiple certificates in section

Multiple certificates are not, like the current code assumes,
appended; instead, a specific (unsigned) attribute is nested
inside the PKCS7 struct that contains the next PKCS7 struct.

Also improve the returned certificates to include structs for all
parsed, PKCS7 structs.

* fix: Extract signature algorithm from authenticode

The signature algorithm listed in the signing certificate is not
necessarily the signature algorithm used for the file signature.
Extract the algorithm for the file signature from the authemticode
instead.
  • Loading branch information
secDre4mer authored May 16, 2024
1 parent 8846571 commit f9f7d40
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 161 deletions.
20 changes: 12 additions & 8 deletions cmd/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,14 +575,18 @@ func parsePE(filename string, cfg config) {
w.Flush()
fmt.Print("\n ---Raw Certificate dump---\n")
hexDump(cert.Raw)
fmt.Print("\n---Certificate ---\n\n")
fmt.Fprintf(w, "Issuer Name:\t %s\n", cert.Info.Issuer)
fmt.Fprintf(w, "Subject Name:\t %s\n", cert.Info.Subject)
fmt.Fprintf(w, "Serial Number:\t %x\n", cert.Info.SerialNumber)
fmt.Fprintf(w, "Validity From:\t %s to %s\n", cert.Info.NotBefore.String(), cert.Info.NotAfter.String())
fmt.Fprintf(w, "Signature Algorithm:\t %s\n", cert.Info.SignatureAlgorithm.String())
fmt.Fprintf(w, "PublicKey Algorithm:\t %s\n", cert.Info.PublicKeyAlgorithm.String())
w.Flush()
for _, cert := range cert.Certificates {
fmt.Print("\n---Certificate ---\n\n")
fmt.Fprintf(w, "Issuer Name:\t %s\n", cert.Info.Issuer)
fmt.Fprintf(w, "Subject Name:\t %s\n", cert.Info.Subject)
fmt.Fprintf(w, "Serial Number:\t %x\n", cert.Info.SerialNumber)
fmt.Fprintf(w, "Validity From:\t %s to %s\n", cert.Info.NotBefore.String(), cert.Info.NotAfter.String())
fmt.Fprintf(w, "Signature Algorithm:\t %s\n", cert.Info.SignatureAlgorithm.String())
fmt.Fprintf(w, "PublicKey Algorithm:\t %s\n", cert.Info.PublicKeyAlgorithm.String())
fmt.Fprintf(w, "Certificate valid:\t %v\n", cert.Verified)
fmt.Fprintf(w, "Signature valid:\t %v\n", cert.SignatureValid)
w.Flush()
}

// Calculate the PE authentihash.
pe.Authentihash()
Expand Down
2 changes: 1 addition & 1 deletion file.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type File struct {
TLS TLSDirectory `json:"tls,omitempty"`
LoadConfig LoadConfig `json:"load_config,omitempty"`
Exceptions []Exception `json:"exceptions,omitempty"`
Certificates Certificate `json:"certificates,omitempty"`
Certificates CertificateSection `json:"certificates,omitempty"`
DelayImports []DelayImport `json:"delay_imports,omitempty"`
BoundImports []BoundImportDescriptorData `json:"bound_imports,omitempty"`
GlobalPtr uint32 `json:"global_ptr,omitempty"`
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ go 1.15

require (
github.com/edsrzf/mmap-go v1.1.0
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d
golang.org/x/text v0.7.0
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d h1:RQqyEogx5J6wPdoxqL132b100j8KjcVHO1c0KLRoIhc=
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d/go.mod h1:PegD7EVqlN88z7TpCqH92hHP+GBpfomGCCnw1PFtNOA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
Expand Down
213 changes: 112 additions & 101 deletions security.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ import (
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"time"

"go.mozilla.org/pkcs7"
"github.com/secDre4mer/pkcs7"
)

// The options for the WIN_CERTIFICATE Revision member include
Expand Down Expand Up @@ -66,13 +65,18 @@ var (
`invalid certificate header in security directory`)
)

type CertificateSection struct {
Header WinCertificate `json:"header"`
Raw []byte `json:"-"`

Certificates []Certificate
}

// Certificate directory.
type Certificate struct {
Header WinCertificate `json:"header"`
Content pkcs7.PKCS7 `json:"-"`
SignatureContent AuthenticodeContent `json:"-"`
SignatureValid bool `json:"signature_valid"`
Raw []byte `json:"-"`
Info CertInfo `json:"info"`
Verified bool `json:"verified"`
}
Expand Down Expand Up @@ -309,101 +313,68 @@ func (pe *File) AuthentihashExt(hashers ...hash.Hash) [][]byte {
// bind an Authenticode-signed file to the identity of a software publisher.
// This data are not loaded into memory as part of the image file.
func (pe *File) parseSecurityDirectory(rva, size uint32) error {

var pkcs *pkcs7.PKCS7
var certValid bool
certInfo := CertInfo{}
certHeader := WinCertificate{}
var certHeader WinCertificate
certSize := uint32(binary.Size(certHeader))
signatureContent := AuthenticodeContent{}
var signatureValid bool
var certContent []byte

// The virtual address value from the Certificate Table entry in the
// Optional Header Data Directory is a file offset to the first attribute
// certificate entry.
fileOffset := rva

// PE file can be dual signed by applying multiple signatures, which is
// strongly recommended when using deprecated hashing algorithms such as MD5.
for {
err := pe.structUnpack(&certHeader, fileOffset, certSize)
if err != nil {
return ErrOutsideBoundary
}
err := pe.structUnpack(&certHeader, fileOffset, certSize)
if err != nil {
return ErrOutsideBoundary
}

if fileOffset+certHeader.Length > pe.size {
return ErrOutsideBoundary
}
if certHeader.Length > size {
return ErrOutsideBoundary
}

if certHeader.Length == 0 {
return ErrSecurityDataDirInvalid
}
if fileOffset+certHeader.Length > pe.size {
return ErrOutsideBoundary
}

certContent = pe.data[fileOffset+certSize : fileOffset+certHeader.Length]
pkcs, err = pkcs7.Parse(certContent)
if certHeader.Length == 0 {
return ErrSecurityDataDirInvalid
}

pe.HasCertificate = true
pe.Certificates.Header = certHeader
pe.Certificates.Raw = pe.data[fileOffset+certSize : fileOffset+certHeader.Length]

certContent := pe.Certificates.Raw
for {
pkcs, err := pkcs7.Parse(certContent)
if err != nil {
pe.Certificates = Certificate{Header: certHeader, Raw: certContent}
pe.HasCertificate = true
return err
}

// The pkcs7.PKCS7 structure contains many fields that we are not
// interested to, so create another structure, similar to _CERT_INFO
// structure which contains only the important information.
serialNumber := pkcs.Signers[0].IssuerAndSerialNumber.SerialNumber
for _, cert := range pkcs.Certificates {
if !reflect.DeepEqual(cert.SerialNumber, serialNumber) {
continue
}

certInfo.SerialNumber = hex.EncodeToString(cert.SerialNumber.Bytes())
certInfo.PublicKeyAlgorithm = cert.PublicKeyAlgorithm
certInfo.SignatureAlgorithm = cert.SignatureAlgorithm

certInfo.NotAfter = cert.NotAfter
certInfo.NotBefore = cert.NotBefore

// Issuer infos
if len(cert.Issuer.Country) > 0 {
certInfo.Issuer = cert.Issuer.Country[0]
}

if len(cert.Issuer.Province) > 0 {
certInfo.Issuer += ", " + cert.Issuer.Province[0]
}

if len(cert.Issuer.Locality) > 0 {
certInfo.Issuer += ", " + cert.Issuer.Locality[0]
}

certInfo.Issuer += ", " + cert.Issuer.CommonName

// Subject infos
if len(cert.Subject.Country) > 0 {
certInfo.Subject = cert.Subject.Country[0]
}
var signerCertificate = pkcs.GetOnlySigner()
if signerCertificate == nil {
return errors.New("could not find signer certificate")
}

if len(cert.Subject.Province) > 0 {
certInfo.Subject += ", " + cert.Subject.Province[0]
}
var certInfo CertInfo

if len(cert.Subject.Locality) > 0 {
certInfo.Subject += ", " + cert.Subject.Locality[0]
}
certInfo.SerialNumber = hex.EncodeToString(signerCertificate.SerialNumber.Bytes())
certInfo.PublicKeyAlgorithm = signerCertificate.PublicKeyAlgorithm

if len(cert.Subject.Organization) > 0 {
certInfo.Subject += ", " + cert.Subject.Organization[0]
}
certInfo.NotAfter = signerCertificate.NotAfter
certInfo.NotBefore = signerCertificate.NotBefore

certInfo.Subject += ", " + cert.Subject.CommonName
// Issuer infos
certInfo.Issuer = formatPkixName(signerCertificate.Issuer)

break
}
// Subject infos
certInfo.Subject = formatPkixName(signerCertificate.Subject)

// Let's mark the file as signed, then we verify if the signature is valid.
pe.IsSigned = true

var certValid bool
// Let's load the system root certs.
if !pe.opts.DisableCertValidation {
var certPool *x509.CertPool
Expand All @@ -425,6 +396,7 @@ func (pe *File) parseSecurityDirectory(rva, size uint32) error {
}
}

var signatureValid bool
signatureContent, err = parseAuthenticodeContent(pkcs.Content)
if err != nil {
pe.logger.Errorf("could not parse authenticode content: %v", err)
Expand All @@ -434,24 +406,30 @@ func (pe *File) parseSecurityDirectory(rva, size uint32) error {
signatureValid = bytes.Equal(authentihash, signatureContent.HashResult)
}

// Subsequent entries are accessed by advancing that entry's dwLength
// bytes, rounded up to an 8-byte multiple, from the start of the
// current attribute certificate entry.
nextOffset := certHeader.Length + fileOffset
nextOffset = ((nextOffset + 8 - 1) / 8) * 8
certInfo.SignatureAlgorithm = signatureContent.Algorithm

// Check if we walked the entire table.
if nextOffset == fileOffset+size {
break
}
pe.Certificates.Certificates = append(pe.Certificates.Certificates, Certificate{
Content: *pkcs,
SignatureContent: signatureContent,
SignatureValid: signatureValid,
Info: certInfo,
Verified: certValid,
})

fileOffset = nextOffset
// Subsequent certificates are an (unsigned) attribute of the PKCS#7
var newCert asn1.RawValue
nestedSignatureOid := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 4, 1}
err = pkcs.UnmarshalUnsignedAttribute(nestedSignatureOid, &newCert)
if err != nil {
var attributeNotFound pkcs7.AttributeNotFoundError
if errors.As(err, &attributeNotFound) {
break // No further nested certificates
}
return err
}
certContent = newCert.FullBytes
}

pe.Certificates = Certificate{Header: certHeader, Content: *pkcs,
Raw: certContent, Info: certInfo, Verified: certValid,
SignatureContent: signatureContent, SignatureValid: signatureValid}
pe.HasCertificate = true
return nil
}

Expand Down Expand Up @@ -542,26 +520,35 @@ type DigestInfo struct {
}

// Translation of algorithm identifier to hash algorithm, copied from pkcs7.getHashForOID
func parseHashAlgorithm(identifier pkix.AlgorithmIdentifier) (crypto.Hash, error) {
func parseHashAlgorithm(identifier pkix.AlgorithmIdentifier) (crypto.Hash, x509.SignatureAlgorithm, error) {
oid := identifier.Algorithm
switch {
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA1), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA1),
oid.Equal(pkcs7.OIDDigestAlgorithmDSA), oid.Equal(pkcs7.OIDDigestAlgorithmDSASHA1),
oid.Equal(pkcs7.OIDEncryptionAlgorithmRSA):
return crypto.SHA1, nil
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA256), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA256):
return crypto.SHA256, nil
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA384), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA384):
return crypto.SHA384, nil
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA512), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA512):
return crypto.SHA512, nil
}
return crypto.Hash(0), pkcs7.ErrUnsupportedAlgorithm
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA1), oid.Equal(pkcs7.OIDEncryptionAlgorithmRSA):
return crypto.SHA1, x509.SHA1WithRSA, nil
case oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA1):
return crypto.SHA1, x509.ECDSAWithSHA1, nil
case oid.Equal(pkcs7.OIDDigestAlgorithmDSA), oid.Equal(pkcs7.OIDDigestAlgorithmDSASHA1):
return crypto.SHA1, x509.DSAWithSHA1, nil
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA256):
return crypto.SHA256, x509.SHA256WithRSA, nil
case oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA256):
return crypto.SHA256, x509.ECDSAWithSHA256, nil
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA384):
return crypto.SHA384, x509.SHA256WithRSA, nil
case oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA384):
return crypto.SHA384, x509.ECDSAWithSHA384, nil
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA512):
return crypto.SHA512, x509.ECDSAWithSHA512, nil
case oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA512):
return crypto.SHA512, x509.ECDSAWithSHA512, nil
}
return 0, 0, pkcs7.ErrUnsupportedAlgorithm
}

// AuthenticodeContent provides a simplified view on SpcIndirectDataContent, which specifies the ASN.1 encoded values of
// the authenticode signature content.
type AuthenticodeContent struct {
Algorithm x509.SignatureAlgorithm
HashFunction crypto.Hash
HashResult []byte
}
Expand All @@ -576,12 +563,36 @@ func parseAuthenticodeContent(content []byte) (AuthenticodeContent, error) {
if err != nil {
return AuthenticodeContent{}, err
}
hashFunction, err := parseHashAlgorithm(authenticodeContent.MessageDigest.DigestAlgorithm)
hashFunction, algorithmId, err := parseHashAlgorithm(authenticodeContent.MessageDigest.DigestAlgorithm)
if err != nil {
return AuthenticodeContent{}, err
}
return AuthenticodeContent{
Algorithm: algorithmId,
HashFunction: hashFunction,
HashResult: authenticodeContent.MessageDigest.Digest,
}, nil
}

func formatPkixName(name pkix.Name) string {
var formattedName string
if len(name.Country) > 0 {
formattedName = name.Country[0]
}

if len(name.Province) > 0 {
formattedName += ", " + name.Province[0]
}

if len(name.Locality) > 0 {
formattedName += ", " + name.Locality[0]
}

if len(name.Organization) > 0 {
formattedName += ", " + name.Organization[0]
}

formattedName += ", " + name.CommonName

return formattedName
}
Loading

0 comments on commit f9f7d40

Please sign in to comment.