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 anySET
orSEQUENCE
.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!