TL;DR:We’ve open-sourced a new library, μthenticode , for verifying Authenticode signatures on Windows PE binaries without a Windows machine. We’ve also integrated it into recent builds of Winchecksec , so that you can use it today to verify signatures on your Windows executables!
As a library, μthenticode aims to be a breeze to integrate: It’s written in cross-platform, modern C++ and avoids the complexity of the CryptoAPI interfaces it replaces (namely
CertVerifyCertificateChainPolicy ). You can use it now as a replacement for many of
SignTool ’s features, and more are on the way.
Authenticode is Microsoft’s code signing technology, comparable in spirit (but not implementation) to Apple’s Gatekeeper .
At its core, Authenticode supplies (or can supply, as optional features) a number of properties for signed programs:
Like all code signing technologies, there are things Authenticode can’t do or guarantee about a program:
Similarly, there are some things that Authenticode, like all PKI implementations, is susceptible to:
All told, Authenticode (and all other forms of code signing) add useful authenticity and integrity checks to binaries, provided that you trust the signer and their ability to store their key material .
With that said, let’s take a look at what makes Authenticode tick.
In a somewhat unusual move for 2000s-era Microsoft, most of the Authenticode format is actually documented and available for download . A few parts are conspicuously under-defined or marked as “out of scope”; we’ll cover some of them below.
At its core, Authenticode has two components:
SignedDataobjects, which are mostly normal PKCS#7 containers (marked with a content type of
SignedDataper RFC 2315 ).
The certificate table is the mechanism by which Authenticode signatures are embedded into PE files.
It has a few interesting properties:
RVAfield is not a virtual address—it’s a direct file offset. This is a reflection of the behavior of the Windows loader, which doesn’t actually load the certificates into the address space of the program.
Once located, actually parsing the certificate table is straightforward: It’s an 8 byte-aligned blob of
…with some fields of interest:
wRevision: the “revision” of the
WIN_CERT_REVISION_2_0=0x0200is the current version for Authenticode signatures;
WIN_CERT_REVISION_1_0=0x0100is for “legacy” signatures. I haven’t been able to find the latter in the wild.
wCertificateType: the kind of encapsulated certificate data.
wCertificateType, but we’re only interested in one:
bCertificate: the actual certificate data. For
WIN_CERT_TYPE_PKCS_SIGNED_DATA, this is the (mostly) PKCS#7
As you might have surmised, the structure of the certificate table allows for multiple independent Authenticode signatures. This is useful for deploying a program across multiple versions of Windows, particularly versions that might have legacy certificates in the Trusted Publishers store or that don’t trust a particular CA for whatever reason.
Microsoft helpfullysupplies this visualization of their
This is almost a normal PKCS#7
SignedData , with a few key deviations:
contentInfohas a type of
SPC_INDIRECT_DATA_OBJID, which Microsoft defines as
SpcIndirectDataContent. Microsoft conveniently provides its ASN.1 definition: (Observe that the custom
AlgorithmIdentifieris actually just X.509’s
AlgorithmIdentifier—see RFC 3279 and its updates).⚠ The code below does no error handling or memory management; read the μthenticode source for the full version. ⚠Given the ASN.1 definitions above, we can use OpenSSL’s (hellish and completely undocumented) ASN.1 macros to parse Microsoft’s custom structures:
With our structures in place, we can use OpenSSL’s (mostly) undocumented PKCS#7 API to parse our
SignedData and indirect data contents:
…and then validate them:
Voilà: the basics of Authenticode. Observe that we pass
PKCS7_NOVERIFY , as we don’t necessarily have access to the entire certificate chain—only Windows users with the relevant cert in their Trusted Publishers store will have that.
Now that we have authenticity (modulo the root certificate), let’s do integrity.
First, let’s grab the hash embedded in the Authenticode signature, for eventual comparison:
Next, we need to compute the binary’s actual hash. This is a little involved, thanks to a few different fields:
CheckSumfield that’s used for basic integrity purposes (i.e., accidental corruption). This field needs to be skipped when calculating the hash, as it’s calculated over the entire file and would change with the addition of certificates.
PointerToRawData, not the order of the section headers themselves. This is not particularly troublesome, but requires some additional bookkeeping.
μthenticode ’s implementation of the Authenticode hashing process is a little too long to duplicate below, but in pseudocode:
CheckSumfield from the buffer, in that order (to avoid rescaling the former’s offset).
IterSecAPI to construct a list of section buffers.
IterSecyields sections in file offset order as of #129 .
EVP_DigestUpdateand finish with
We haven’t discussed the two remaining major Authenticode features: page hashes and timestamp countersignatures.
As mentioned above, page hashes are conspicuously not documented in the Authenticode specification, and are described as stored in a “[…] binary structure [that] is outside the scope of this paper.”
Online information on said structure is limited to a few resources:
osslsigncodehas support for generating and validating page hashes, and grants us further insight:
SpcSerializedObjectis an ASN.1
SET, each member of which is an ASN.1
SEQUENCE, to the effect of: (The definitions above are my reconstruction from the body of
osslsigncodeconfusingly reuses the
Impl_SpcPageHashand constructs the rest of the contents of
As far as I can tell,
osslsigncode only inserts one
Impl_SpcPageHash for the entire PE, which it calculates in
pe_calc_page_hash . The code in that function is pretty dense, but it seems to generate a table of structures as follows:
IMPL_PAGE_HASH_SIZE is determined by the hash algorithm used (i.e., by
Impl_SpcPageHash.type ), and the very first entry in the table is a null-padded “page hash” for just the PE headers with
page_offset=0 . This table is not given an ASN.1 definition—it’s inserted directly into
Unlike page hashes, Authenticode’s timestamp countersignature format is relatively well documented, both in official and third-party sources.
Just as the Authenticode
SignedData is mostly a normal PKCS#7
SignedData , Authenticode’s timestamp format is mostly a normal PKCS#9 countersignature. Some noteworthy bits include:
countersignatureTypeis the custom Microsoft OID
contentis the original Authenticode PKCS#7
SignedData, from which the
SignerInfois extracted and embedded into the main Authenticode
SignedData. The certificates from the TSA response are similarly embedded into the certificate list as unauthenticated attributes.
We’ve covered all four major components of Authenticode above: verifying the signature, checking the integrity of the file against the verified hash, calculating page hashes, and verifying any timestamp countersignatures.
μthenticode itself is still a work in progress, and currently only has support for signatures and the main Authenticode hash. You can help us out by contributing support for page hash parsing and verification, as well as timestamp signature validation!
μthenticode ’s’ APIs are fully documented and hosted , and most can be used immediately with a
peparse::parsed_pe * :
Check out the
svcli command-line tool for an applied example, including retrieving the embedded Authenticode hash.
μthenticode was written completely from scratch and uses the official Authenticode document supplied by Microsoft as its primary reference. When that was found lacking, the following resources came in handy:
The following resources were not referenced, but were discovered while researching this post:
Want the scoop on our open-source projects and other security innovations?Contact us or sign up for our newsletter !