github.com/saferwall/pe@v1.5.2/security.go (about)

     1  // Copyright 2018 Saferwall. All rights reserved.
     2  // Use of this source code is governed by Apache v2 license
     3  // license that can be found in the LICENSE file.
     4  
     5  package pe
     6  
     7  import (
     8  	"bytes"
     9  	"crypto"
    10  	"crypto/x509"
    11  	"crypto/x509/pkix"
    12  	"encoding/asn1"
    13  	"encoding/binary"
    14  	"encoding/hex"
    15  	"errors"
    16  	"fmt"
    17  	"hash"
    18  	"io"
    19  	"os"
    20  	"os/exec"
    21  	"path/filepath"
    22  	"reflect"
    23  	"runtime"
    24  	"sort"
    25  	"strings"
    26  	"time"
    27  
    28  	"go.mozilla.org/pkcs7"
    29  )
    30  
    31  // The options for the WIN_CERTIFICATE Revision member include
    32  // (but are not limited to) the following.
    33  const (
    34  	// WinCertRevision1_0 represents the WIN_CERT_REVISION_1_0 Version 1,
    35  	// legacy version of the Win_Certificate structure.
    36  	// It is supported only for purposes of verifying legacy Authenticode
    37  	// signatures
    38  	WinCertRevision1_0 = 0x0100
    39  
    40  	// WinCertRevision2_0 represents the WIN_CERT_REVISION_2_0. Version 2
    41  	// is the current version of the Win_Certificate structure.
    42  	WinCertRevision2_0 = 0x0200
    43  )
    44  
    45  // The options for the WIN_CERTIFICATE CertificateType member include
    46  // (but are not limited to) the items in the following table. Note that some
    47  // values are not currently supported.
    48  const (
    49  	// Certificate contains an X.509 Certificate (Not Supported)
    50  	WinCertTypeX509 = 0x0001
    51  
    52  	// Certificate contains a PKCS#7 SignedData structure.
    53  	WinCertTypePKCSSignedData = 0x0002
    54  
    55  	// Reserved.
    56  	WinCertTypeReserved1 = 0x0003
    57  
    58  	// Terminal Server Protocol Stack Certificate signing (Not Supported).
    59  	WinCertTypeTSStackSigned = 0x0004
    60  )
    61  
    62  var (
    63  	// ErrSecurityDataDirInvalidCertHeader is reported when the certificate
    64  	// header in the security directory is invalid.
    65  	ErrSecurityDataDirInvalid = errors.New(
    66  		`invalid certificate header in security directory`)
    67  )
    68  
    69  // Certificate directory.
    70  type Certificate struct {
    71  	Header           WinCertificate      `json:"header"`
    72  	Content          pkcs7.PKCS7         `json:"-"`
    73  	SignatureContent AuthenticodeContent `json:"-"`
    74  	SignatureValid   bool                `json:"signature_valid"`
    75  	Raw              []byte              `json:"-"`
    76  	Info             CertInfo            `json:"info"`
    77  	Verified         bool                `json:"verified"`
    78  }
    79  
    80  // WinCertificate encapsulates a signature used in verifying executable files.
    81  type WinCertificate struct {
    82  	// Specifies the length, in bytes, of the signature.
    83  	Length uint32 `json:"length"`
    84  
    85  	// Specifies the certificate revision.
    86  	Revision uint16 `json:"revision"`
    87  
    88  	// Specifies the type of certificate.
    89  	CertificateType uint16 `json:"certificate_type"`
    90  }
    91  
    92  // CertInfo wraps the important fields of the pkcs7 structure.
    93  // This is what we what keep in JSON marshalling.
    94  type CertInfo struct {
    95  	// The certificate authority (CA) that charges customers to issue
    96  	// certificates for them.
    97  	Issuer string `json:"issuer"`
    98  
    99  	// The subject of the certificate is the entity its public key is associated
   100  	// with (i.e. the "owner" of the certificate).
   101  	Subject string `json:"subject"`
   102  
   103  	// The certificate won't be valid before this timestamp.
   104  	NotBefore time.Time `json:"not_before"`
   105  
   106  	// The certificate won't be valid after this timestamp.
   107  	NotAfter time.Time `json:"not_after"`
   108  
   109  	// The serial number MUST be a positive integer assigned by the CA to each
   110  	// certificate. It MUST be unique for each certificate issued by a given CA
   111  	// (i.e., the issuer name and serial number identify a unique certificate).
   112  	// CAs MUST force the serialNumber to be a non-negative integer.
   113  	// For convenience, we convert the big int to string.
   114  	SerialNumber string `json:"serial_number"`
   115  
   116  	// The identifier for the cryptographic algorithm used by the CA to sign
   117  	// this certificate.
   118  	SignatureAlgorithm x509.SignatureAlgorithm `json:"signature_algorithm"`
   119  
   120  	// The Public Key Algorithm refers to the public key inside the certificate.
   121  	// This certificate is used together with the matching private key to prove
   122  	// the identity of the peer.
   123  	PublicKeyAlgorithm x509.PublicKeyAlgorithm `json:"public_key_algorithm"`
   124  }
   125  
   126  type RelRange struct {
   127  	Start  uint32
   128  	Length uint32
   129  }
   130  
   131  type byStart []RelRange
   132  
   133  func (s byStart) Len() int      { return len(s) }
   134  func (s byStart) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
   135  func (s byStart) Less(i, j int) bool {
   136  	return s[i].Start < s[j].Start
   137  }
   138  
   139  type Range struct {
   140  	Start uint32
   141  	End   uint32
   142  }
   143  
   144  func (pe *File) parseLocations() (map[string]*RelRange, error) {
   145  	location := make(map[string]*RelRange, 3)
   146  
   147  	fileHdrSize := uint32(binary.Size(pe.NtHeader.FileHeader))
   148  	optionalHeaderOffset := pe.DOSHeader.AddressOfNewEXEHeader + 4 + fileHdrSize
   149  
   150  	var (
   151  		oh32 ImageOptionalHeader32
   152  		oh64 ImageOptionalHeader64
   153  
   154  		optionalHeaderSize uint32
   155  	)
   156  
   157  	switch pe.Is64 {
   158  	case true:
   159  		oh64 = pe.NtHeader.OptionalHeader.(ImageOptionalHeader64)
   160  		optionalHeaderSize = oh64.SizeOfHeaders
   161  	case false:
   162  		oh32 = pe.NtHeader.OptionalHeader.(ImageOptionalHeader32)
   163  		optionalHeaderSize = oh32.SizeOfHeaders
   164  	}
   165  
   166  	if optionalHeaderSize > pe.size-optionalHeaderOffset {
   167  		msgF := "the optional header exceeds the file length (%d + %d > %d)"
   168  		return nil, fmt.Errorf(msgF, optionalHeaderSize, optionalHeaderOffset, pe.size)
   169  	}
   170  
   171  	if optionalHeaderSize < 68 {
   172  		msgF := "the optional header size is %d < 68, which is insufficient for authenticode"
   173  		return nil, fmt.Errorf(msgF, optionalHeaderSize)
   174  	}
   175  
   176  	// The location of the checksum
   177  	location["checksum"] = &RelRange{optionalHeaderOffset + 64, 4}
   178  
   179  	var rvaBase, certBase, numberOfRvaAndSizes uint32
   180  	switch pe.Is64 {
   181  	case true:
   182  		rvaBase = optionalHeaderOffset + 108
   183  		certBase = optionalHeaderOffset + 144
   184  		numberOfRvaAndSizes = oh64.NumberOfRvaAndSizes
   185  	case false:
   186  		rvaBase = optionalHeaderOffset + 92
   187  		certBase = optionalHeaderOffset + 128
   188  		numberOfRvaAndSizes = oh32.NumberOfRvaAndSizes
   189  	}
   190  
   191  	if optionalHeaderOffset+optionalHeaderSize < rvaBase+4 {
   192  		pe.logger.Debug("The PE Optional Header size can not accommodate for the NumberOfRvaAndSizes field")
   193  		return location, nil
   194  	}
   195  
   196  	if numberOfRvaAndSizes < uint32(5) {
   197  		pe.logger.Debugf("The PE Optional Header does not have a Certificate Table entry in its "+
   198  			"Data Directory; NumberOfRvaAndSizes = %d", numberOfRvaAndSizes)
   199  		return location, nil
   200  	}
   201  
   202  	if optionalHeaderOffset+optionalHeaderSize < certBase+8 {
   203  		pe.logger.Debug("The PE Optional Header size can not accommodate for a Certificate Table" +
   204  			"entry in its Data Directory")
   205  		return location, nil
   206  	}
   207  
   208  	// The location of the entry of the Certificate Table in the Data Directory
   209  	location["datadir_certtable"] = &RelRange{certBase, 8}
   210  
   211  	var address, size uint32
   212  	switch pe.Is64 {
   213  	case true:
   214  		dirEntry := oh64.DataDirectory[ImageDirectoryEntryCertificate]
   215  		address = dirEntry.VirtualAddress
   216  		size = dirEntry.Size
   217  	case false:
   218  		dirEntry := oh32.DataDirectory[ImageDirectoryEntryCertificate]
   219  		address = dirEntry.VirtualAddress
   220  		size = dirEntry.Size
   221  	}
   222  
   223  	if size == 0 {
   224  		pe.logger.Debug("The Certificate Table is empty")
   225  		return location, nil
   226  	}
   227  
   228  	if int64(address) < int64(optionalHeaderSize)+int64(optionalHeaderOffset) ||
   229  		int64(address)+int64(size) > int64(pe.size) {
   230  		pe.logger.Debugf("The location of the Certificate Table in the binary makes no sense and "+
   231  			"is either beyond the boundaries of the file, or in the middle of the PE header; "+
   232  			"VirtualAddress: %x, Size: %x", address, size)
   233  		return location, nil
   234  	}
   235  
   236  	// The location of the Certificate Table
   237  	location["certtable"] = &RelRange{address, size}
   238  	return location, nil
   239  }
   240  
   241  // Authentihash generates the SHA256 pe image file hash.
   242  // The relevant sections to exclude during hashing are:
   243  //   - The location of the checksum
   244  //   - The location of the entry of the Certificate Table in the Data Directory
   245  //   - The location of the Certificate Table.
   246  func (pe *File) Authentihash() []byte {
   247  	results := pe.AuthentihashExt(crypto.SHA256.New())
   248  	if len(results) > 0 {
   249  		return results[0]
   250  	}
   251  	return nil
   252  }
   253  
   254  // AuthentihashExt generates pe image file hashes using the given hashers.
   255  // The relevant sections to exclude during hashing are:
   256  //   - The location of the checksum
   257  //   - The location of the entry of the Certificate Table in the Data Directory
   258  //   - The location of the Certificate Table.
   259  func (pe *File) AuthentihashExt(hashers ...hash.Hash) [][]byte {
   260  
   261  	locationMap, err := pe.parseLocations()
   262  	if err != nil {
   263  		return nil
   264  	}
   265  
   266  	locationSlice := make([]RelRange, 0, len(locationMap))
   267  	for k, v := range locationMap {
   268  		if stringInSlice(k, []string{"checksum", "datadir_certtable", "certtable"}) {
   269  			locationSlice = append(locationSlice, *v)
   270  		}
   271  	}
   272  	sort.Sort(byStart(locationSlice))
   273  
   274  	ranges := make([]*Range, 0, len(locationSlice))
   275  	start := uint32(0)
   276  	for _, r := range locationSlice {
   277  		ranges = append(ranges, &Range{Start: start, End: r.Start})
   278  		start = r.Start + r.Length
   279  	}
   280  	ranges = append(ranges, &Range{Start: start, End: pe.size})
   281  
   282  	var rd io.ReaderAt
   283  	if pe.f != nil {
   284  		rd = pe.f
   285  	} else {
   286  		rd = bytes.NewReader(pe.data)
   287  	}
   288  
   289  	for _, v := range ranges {
   290  		for _, hasher := range hashers {
   291  			sr := io.NewSectionReader(rd, int64(v.Start), int64(v.End)-int64(v.Start))
   292  			io.Copy(hasher, sr)
   293  			sr.Seek(0, io.SeekStart)
   294  		}
   295  	}
   296  
   297  	var ret [][]byte
   298  	for _, hasher := range hashers {
   299  		ret = append(ret, hasher.Sum(nil))
   300  	}
   301  
   302  	return ret
   303  }
   304  
   305  // The security directory contains the authenticode signature, which is a digital
   306  // signature format that is used, among other purposes, to determine the origin
   307  // and integrity of software binaries. Authenticode is based on the Public-Key
   308  // Cryptography Standards (PKCS) #7 standard and uses X.509 v3 certificates to
   309  // bind an Authenticode-signed file to the identity of a software publisher.
   310  // This data are not loaded into memory as part of the image file.
   311  func (pe *File) parseSecurityDirectory(rva, size uint32) error {
   312  
   313  	var pkcs *pkcs7.PKCS7
   314  	var certValid bool
   315  	certInfo := CertInfo{}
   316  	certHeader := WinCertificate{}
   317  	certSize := uint32(binary.Size(certHeader))
   318  	signatureContent := AuthenticodeContent{}
   319  	var signatureValid bool
   320  	var certContent []byte
   321  
   322  	// The virtual address value from the Certificate Table entry in the
   323  	// Optional Header Data Directory is a file offset to the first attribute
   324  	// certificate entry.
   325  	fileOffset := rva
   326  
   327  	// PE file can be dual signed by applying multiple signatures, which is
   328  	// strongly recommended when using deprecated hashing algorithms such as MD5.
   329  	for {
   330  		err := pe.structUnpack(&certHeader, fileOffset, certSize)
   331  		if err != nil {
   332  			return ErrOutsideBoundary
   333  		}
   334  
   335  		if fileOffset+certHeader.Length > pe.size {
   336  			return ErrOutsideBoundary
   337  		}
   338  
   339  		if certHeader.Length == 0 {
   340  			return ErrSecurityDataDirInvalid
   341  		}
   342  
   343  		certContent = pe.data[fileOffset+certSize : fileOffset+certHeader.Length]
   344  		pkcs, err = pkcs7.Parse(certContent)
   345  		if err != nil {
   346  			pe.Certificates = Certificate{Header: certHeader, Raw: certContent}
   347  			pe.HasCertificate = true
   348  			return err
   349  		}
   350  
   351  		// The pkcs7.PKCS7 structure contains many fields that we are not
   352  		// interested to, so create another structure, similar to _CERT_INFO
   353  		// structure which contains only the important information.
   354  		serialNumber := pkcs.Signers[0].IssuerAndSerialNumber.SerialNumber
   355  		for _, cert := range pkcs.Certificates {
   356  			if !reflect.DeepEqual(cert.SerialNumber, serialNumber) {
   357  				continue
   358  			}
   359  
   360  			certInfo.SerialNumber = hex.EncodeToString(cert.SerialNumber.Bytes())
   361  			certInfo.PublicKeyAlgorithm = cert.PublicKeyAlgorithm
   362  			certInfo.SignatureAlgorithm = cert.SignatureAlgorithm
   363  
   364  			certInfo.NotAfter = cert.NotAfter
   365  			certInfo.NotBefore = cert.NotBefore
   366  
   367  			// Issuer infos
   368  			if len(cert.Issuer.Country) > 0 {
   369  				certInfo.Issuer = cert.Issuer.Country[0]
   370  			}
   371  
   372  			if len(cert.Issuer.Province) > 0 {
   373  				certInfo.Issuer += ", " + cert.Issuer.Province[0]
   374  			}
   375  
   376  			if len(cert.Issuer.Locality) > 0 {
   377  				certInfo.Issuer += ", " + cert.Issuer.Locality[0]
   378  			}
   379  
   380  			certInfo.Issuer += ", " + cert.Issuer.CommonName
   381  
   382  			// Subject infos
   383  			if len(cert.Subject.Country) > 0 {
   384  				certInfo.Subject = cert.Subject.Country[0]
   385  			}
   386  
   387  			if len(cert.Subject.Province) > 0 {
   388  				certInfo.Subject += ", " + cert.Subject.Province[0]
   389  			}
   390  
   391  			if len(cert.Subject.Locality) > 0 {
   392  				certInfo.Subject += ", " + cert.Subject.Locality[0]
   393  			}
   394  
   395  			if len(cert.Subject.Organization) > 0 {
   396  				certInfo.Subject += ", " + cert.Subject.Organization[0]
   397  			}
   398  
   399  			certInfo.Subject += ", " + cert.Subject.CommonName
   400  
   401  			break
   402  		}
   403  
   404  		// Let's mark the file as signed, then we verify if the signature is valid.
   405  		pe.IsSigned = true
   406  
   407  		// Let's load the system root certs.
   408  		if !pe.opts.DisableCertValidation {
   409  			var certPool *x509.CertPool
   410  			if runtime.GOOS == "windows" {
   411  				certPool, err = loadSystemRoots()
   412  			} else {
   413  				certPool, err = x509.SystemCertPool()
   414  			}
   415  
   416  			// Verify the signature. This will also verify the chain of trust of the
   417  			// the end-entity signer cert to one of the root in the trust store.
   418  			if err == nil {
   419  				err = pkcs.VerifyWithChain(certPool)
   420  				if err == nil {
   421  					certValid = true
   422  				} else {
   423  					certValid = false
   424  				}
   425  			}
   426  		}
   427  
   428  		signatureContent, err = parseAuthenticodeContent(pkcs.Content)
   429  		if err != nil {
   430  			pe.logger.Errorf("could not parse authenticode content: %v", err)
   431  			signatureValid = false
   432  		} else if !pe.opts.DisableSignatureValidation {
   433  			authentihash := pe.AuthentihashExt(signatureContent.HashFunction.New())[0]
   434  			signatureValid = bytes.Equal(authentihash, signatureContent.HashResult)
   435  		}
   436  
   437  		// Subsequent entries are accessed by advancing that entry's dwLength
   438  		// bytes, rounded up to an 8-byte multiple, from the start of the
   439  		// current attribute certificate entry.
   440  		nextOffset := certHeader.Length + fileOffset
   441  		nextOffset = ((nextOffset + 8 - 1) / 8) * 8
   442  
   443  		// Check if we walked the entire table.
   444  		if nextOffset == fileOffset+size {
   445  			break
   446  		}
   447  
   448  		fileOffset = nextOffset
   449  	}
   450  
   451  	pe.Certificates = Certificate{Header: certHeader, Content: *pkcs,
   452  		Raw: certContent, Info: certInfo, Verified: certValid,
   453  		SignatureContent: signatureContent, SignatureValid: signatureValid}
   454  	pe.HasCertificate = true
   455  	return nil
   456  }
   457  
   458  // loadSystemsRoots manually downloads all the trusted root certificates
   459  // in Windows by spawning certutil then adding root certs individually
   460  // to the cert pool. Initially, when running in windows, go SystemCertPool()
   461  // used to enumerate all the certificate in the Windows store using
   462  // (CertEnumCertificatesInStore). Unfortunately, Windows does not ship
   463  // with all of its root certificates installed. Instead, it downloads them
   464  // on-demand. As a consequence, this behavior leads to a non-deterministic
   465  // results. Go team then disabled the loading Windows root certs.
   466  func loadSystemRoots() (*x509.CertPool, error) {
   467  
   468  	needSync := true
   469  	roots := x509.NewCertPool()
   470  
   471  	// Create a temporary dir in the OS temp folder
   472  	// if it does not exists.
   473  	dir := filepath.Join(os.TempDir(), "certs")
   474  	info, err := os.Stat(dir)
   475  	if os.IsNotExist(err) {
   476  		if err = os.Mkdir(dir, 0755); err != nil {
   477  			return roots, err
   478  		}
   479  	} else {
   480  		now := time.Now()
   481  		modTime := info.ModTime()
   482  		diff := now.Sub(modTime).Hours()
   483  		if diff < 24 {
   484  			needSync = false
   485  		}
   486  	}
   487  
   488  	// Use certutil to download all the root certs.
   489  	if needSync {
   490  		cmd := exec.Command("certutil", "-syncWithWU", dir)
   491  		hideWindow(cmd)
   492  		out, err := cmd.Output()
   493  		if err != nil {
   494  			return roots, err
   495  		}
   496  		if !strings.Contains(string(out), "command completed successfully") {
   497  			return roots, err
   498  		}
   499  	}
   500  
   501  	files, err := os.ReadDir(dir)
   502  	if err != nil {
   503  		return roots, err
   504  	}
   505  
   506  	for _, f := range files {
   507  		if !strings.HasSuffix(f.Name(), ".crt") {
   508  			continue
   509  		}
   510  		certPath := filepath.Join(dir, f.Name())
   511  		certData, err := os.ReadFile(certPath)
   512  		if err != nil {
   513  			return roots, err
   514  		}
   515  
   516  		if crt, err := x509.ParseCertificate(certData); err == nil {
   517  			roots.AddCert(crt)
   518  		}
   519  	}
   520  
   521  	return roots, nil
   522  }
   523  
   524  type SpcIndirectDataContent struct {
   525  	Data          SpcAttributeTypeAndOptionalValue
   526  	MessageDigest DigestInfo
   527  }
   528  
   529  type SpcAttributeTypeAndOptionalValue struct {
   530  	Type  asn1.ObjectIdentifier
   531  	Value SpcPeImageData `asn1:"optional"`
   532  }
   533  
   534  type SpcPeImageData struct {
   535  	Flags asn1.BitString
   536  	File  asn1.RawValue
   537  }
   538  
   539  type DigestInfo struct {
   540  	DigestAlgorithm pkix.AlgorithmIdentifier
   541  	Digest          []byte
   542  }
   543  
   544  // Translation of algorithm identifier to hash algorithm, copied from pkcs7.getHashForOID
   545  func parseHashAlgorithm(identifier pkix.AlgorithmIdentifier) (crypto.Hash, error) {
   546  	oid := identifier.Algorithm
   547  	switch {
   548  	case oid.Equal(pkcs7.OIDDigestAlgorithmSHA1), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA1),
   549  		oid.Equal(pkcs7.OIDDigestAlgorithmDSA), oid.Equal(pkcs7.OIDDigestAlgorithmDSASHA1),
   550  		oid.Equal(pkcs7.OIDEncryptionAlgorithmRSA):
   551  		return crypto.SHA1, nil
   552  	case oid.Equal(pkcs7.OIDDigestAlgorithmSHA256), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA256):
   553  		return crypto.SHA256, nil
   554  	case oid.Equal(pkcs7.OIDDigestAlgorithmSHA384), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA384):
   555  		return crypto.SHA384, nil
   556  	case oid.Equal(pkcs7.OIDDigestAlgorithmSHA512), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA512):
   557  		return crypto.SHA512, nil
   558  	}
   559  	return crypto.Hash(0), pkcs7.ErrUnsupportedAlgorithm
   560  }
   561  
   562  // AuthenticodeContent provides a simplified view on SpcIndirectDataContent, which specifies the ASN.1 encoded values of
   563  // the authenticode signature content.
   564  type AuthenticodeContent struct {
   565  	HashFunction crypto.Hash
   566  	HashResult   []byte
   567  }
   568  
   569  func parseAuthenticodeContent(content []byte) (AuthenticodeContent, error) {
   570  	var authenticodeContent SpcIndirectDataContent
   571  	content, err := asn1.Unmarshal(content, &authenticodeContent.Data)
   572  	if err != nil {
   573  		return AuthenticodeContent{}, err
   574  	}
   575  	_, err = asn1.Unmarshal(content, &authenticodeContent.MessageDigest)
   576  	if err != nil {
   577  		return AuthenticodeContent{}, err
   578  	}
   579  	hashFunction, err := parseHashAlgorithm(authenticodeContent.MessageDigest.DigestAlgorithm)
   580  	if err != nil {
   581  		return AuthenticodeContent{}, err
   582  	}
   583  	return AuthenticodeContent{
   584  		HashFunction: hashFunction,
   585  		HashResult:   authenticodeContent.MessageDigest.Digest,
   586  	}, nil
   587  }