Reading Time: 5 minutes

Following our previous post, in which Microsoft Authenticode was introduced, in this post we’ll use OpenSSL to verify embedded signatures into PE files.

Note that this post was tested using Python 3 and OpenSSL 1.1.0l, so it may vary in your own setup. You should always use official Microsoft’s sigcheck tool whenever it is possible: what we explain here is just a way to overcome limitations when working with other operating systems than Windows.

Prerequisites

  • Python 3 (>= 3.5.3)
    • pefile (>= 2019.4.18)
  • OpenSSL 1.1.0l

Extracting embedded Authenticode signature

As we already know, the Security directory entry within the Data directories array of the PE optional header stores the file offset and size of the Authenticode signature. So, we can try the following Python code snippet to extract the signature (we are using the Microsoft’s sigcheck tool as a signed file example):

In [1]: import pefile

In [2]: pe = pefile.PE('sigcheck64.exe', fast_load=True)    # fast_load avoids to load all the directories information for best parse performance

In [3]: security_directory = pe.OPTIONAL_HEADER.DATA_DIRECTORY[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_SECURITY']]    # Extract Security directory

In [4]: win_certificate = pe.__data__[security_directory.VirtualAddress:security_directory.VirtualAddress+security_directory.Size]  # Extract WIN_CERTIFICATE

Now we have the WIN_CERTIFICATE. If you remember from the previous post, it has the following structure:

typedef struct _WIN_CERTIFICATE {
  DWORD dwLength;
  WORD  wRevision;
  WORD  wCertificateType;
  BYTE  bCertificate[ANYSIZE_ARRAY];
} WIN_CERTIFICATE, *LPWIN_CERTIFICATE;

That means that we need to skip first bytes to only work on the PKCS#7 SignedData (corresponding to bCertificate in the struct). We can save it in a file simply with:

In [5]: signature = win_certificate[0x4+0x2+0x2:]   # Skip WIN_CERTIFICATE information

In [6]: with open('sigcheck64.exe.cert', 'wb') as f: 
   ...:     f.write(signature)

Reading PKCS#7 SignedData

Now we have extracted the signature in a separated file. Since it follows the binary DER-encoded ASN.1 format, we can read the signature file providing the appropriate options to openssl tool (part of the tool suite of OpenSSL):

$ openssl asn1parse -inform DER -in sigcheck64.exe.cert | head -n 25
    0:d=0  hl=4 l=9067 cons: SEQUENCE          
    4:d=1  hl=2 l=   9 prim: OBJECT            :pkcs7-signedData
   15:d=1  hl=4 l=9052 cons: cont [ 0 ]        
   19:d=2  hl=4 l=9048 cons: SEQUENCE          
   23:d=3  hl=2 l=   1 prim: INTEGER           :01
   26:d=3  hl=2 l=  15 cons: SET               
   28:d=4  hl=2 l=  13 cons: SEQUENCE          
   30:d=5  hl=2 l=   9 prim: OBJECT            :sha256
   41:d=5  hl=2 l=   0 prim: NULL              
   43:d=3  hl=2 l=  92 cons: SEQUENCE          
   45:d=4  hl=2 l=  10 prim: OBJECT            :1.3.6.1.4.1.311.2.1.4
   57:d=4  hl=2 l=  78 cons: cont [ 0 ]        
   59:d=5  hl=2 l=  76 cons: SEQUENCE          
   61:d=6  hl=2 l=  23 cons: SEQUENCE          
   63:d=7  hl=2 l=  10 prim: OBJECT            :1.3.6.1.4.1.311.2.1.15
   75:d=7  hl=2 l=   9 cons: SEQUENCE          
   77:d=8  hl=2 l=   1 prim: BIT STRING        
   80:d=8  hl=2 l=   4 cons: cont [ 0 ]        
   82:d=9  hl=2 l=   2 cons: cont [ 2 ]        
   84:d=10 hl=2 l=   0 prim: cont [ 0 ]        
   86:d=6  hl=2 l=  49 cons: SEQUENCE          
   88:d=7  hl=2 l=  13 cons: SEQUENCE          
   90:d=8  hl=2 l=   9 prim: OBJECT            :sha256
  101:d=8  hl=2 l=   0 prim: NULL              
  103:d=7  hl=2 l=  32 prim: OCTET STRING      [HEX DUMP]:1A63DA481D6954EBE6FAAD6771B28AC621CAA92855FBF8B6B5FF1FDE29CF1AAC

As specified in the documentation, in each line of this example we have:

  • A file offset, expressed in decimal.
  • d=XX specifies the current depth. The depth is increased within the scope of any SET or SEQUENCE.
  • hl=XX gives the header length (in bytes) of the current type.
  • l=XX gives the length of the content, also in bytes.

Calculating hash

You probably noticed at least two interest strings in the previous output: sha256 and HEX DUMP data. As you imagine, the hex dump data corresponds to the SHA-256 hash (as indicated by the other string, easy, huh?) of the embedded signature corresponding to the signed-file. Right, but… how do we calculate the hash of the file to check that both hashes match?

Well, we already know which parts are skipped during the Authenticode hash calculation process (we explained that also in our previous post). Therefore, we can calculate it by ourselves as follows:

In [7]: checksum_offset = pe.OPTIONAL_HEADER.dump_dict()['CheckSum']['FileOffset']  # CheckSum file offset

In [8]: certificate_table_offset = security_directory.dump_dict()['VirtualAddress']['FileOffset'] # IMAGE_DIRECTORY_ENTRY_SECURITY file offset

In [9]: certificate_virtual_addr = security_directory.VirtualAddress

In [10]: certificate_size = security_directory.Size

In [11]: raw_data = pe.__data__

In [12]: hash_data = raw_data[:checksum_offset] + raw_data[checksum_offset+0x04:certificate_table_offset]   # Skip OptionalHeader.CheckSum field and continue until IMAGE_DIRECTORY_ENTRY_SECURITY

In [13]: hash_data += raw_data[certificate_table_offset+0x08:certificate_virtual_addr] + raw_data[certificate_virtual_addr+certificate_size:]   # Skip IMAGE_DIRECTORY_ENTRY_SECURITY and certificate

In [14]: import hashlib

In [15]: hashlib.sha256(hash_data).hexdigest()
Out[15]: '1a63da481d6954ebe6faad6771b28ac621caa92855fbf8b6b5ff1fde29cf1aac'

Hurrah! We have calculated the SHA-256 hash and it matches with the content provided in the embedded signature. So now we can completely trust that the binary file was unmodified after it was signed. However, our work has not finished yet. We also need to validate the signature.

Verifying signature

This is the trickiest part. Here, we can rely on OpenSSL’s smime command to verify the signature. The problem is that the command expects, besides signature, the signed content data. In the case of Authenticode, this content corresponds to the Object Identifier (OID) 1.3.6.1.4.1.311.2.1.15, called SPC_PE_IMAGE_DATA_OBJID by Microsoft.

In the previous output (the parsing of the signature using openssl asn1parse command), we can see such specific OID in the signature at offset 63. However, we need to provide the whole SEQUENCE, which it usually starts 2 depth above SPC_PE_IMAGE_DATA_OBJID object. According to our output, the object is in d=7, but the sequence starts in d=5, with a header length of 2, and an object length of 76. So, we can use this information to extract the whole sequence:

In [16]: signed_data = signature[59+2:59+2+76]

In [17]: with open('sigcheck64.exe.signed.data', 'wb') as f: 
    ...:     f.write(signed_data) 

Finally, we can use both the signature and the signed data to verify the signature as:

$ openssl smime -verify -inform DER -in sigcheck64.exe.cert -binary -content sigcheck64.exe.signed.data -purpose any -CApath /etc/ssl/certs/ -out /tmp/dummy.txt
Verification failure
140429820977216:error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error:../crypto/pkcs7/pk7_smime.c:285:Verify error:certificate has expired

Strictly, the certificate is no longer valid since it has expired (in fact, almost all system PE files in Windows have already their certificate expired). We can provide -no_check_time argument to avoid temporary restrictions to the validation process:

$ openssl smime -verify -inform DER -in sigcheck64.exe.cert -binary -content sigcheck64.exe.signed.data -purpose any -CApath /etc/ssl/certs/ -no_check_time -out /tmp/dummy.txt 
Verification successful

¡Tachán!

Error: “unable to get local issuer certificate”

A typical problem is that openssl is unable to find the certificate associated with a particular signature, giving an error like:

140649320939584:error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error:../crypto/pkcs7/pk7_smime.c:285:Verify error:unable to get local issuer certificate

If we run the command under strace we can quickly identify which certificate is missed. In our case, it is 523e3c59.0, which corresponds to “Microsoft Root Certificate Authority 2011”:

[... redacted ...]
stat("/etc/ssl/certs/523e3c59.0", 0x7ffc5cae25e0) = -1 ENOENT (No such file or directory)
stat("/etc/ssl/certs/523e3c59.0", 0x7ffc5cae25e0) = -1 ENOENT (No such file or directory)
write(2, "Verification failure\n", 21Verification failure
)  = 21
write(2, "140490832396352:error:21075075:P"..., 168140490832396352:error:21075075:PKCS7 routines:PKCS7_verify:certificate verify error:../crypto/pkcs7/pk7_smime.c:285:Verify error:unable to get local issuer certificate
) = 168
close(3)                                = 0
close(4)                                = 0
close(5)                                = 0
futex(0x7fc691e20aac, FUTEX_WAKE_PRIVATE, 2147483647) = 0
exit_group(4)                           = ?
+++ exited with 4 +++

A quick search on the Internet can provide you with the missing certificate (remember to always double-check the website from where you get the root certificate). Once downloaded, the error should be gone.


So far so good? At this moment, we have verified the integrity of the signed file plus the validity of the certificate. However, we have not completed all the necessary steps to validate a digital certificate. Currently, the openssl binary tool binary does not perform any certificate revocation check. In our upcoming blog, we will fix this issue to fully complete the validation!