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 WinVerifyTrust
and 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:
SignedData
. SignedData
objects, which are mostly normal PKCS#7 containers (marked with a content type of SignedData
per RFC 2315 ). The certificate table is the mechanism by which Authenticode signatures are embedded into PE files.
It has a few interesting properties:
RVA
field 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 WIN_CERTIFICATE
structures:
…with some fields of interest:
wRevision
: the “revision” of the WIN_CERTIFICATE
. WIN_CERT_REVISION_2_0=0x0200
is the current version for Authenticode signatures; WIN_CERT_REVISION_1_0=0x0100
is 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: WIN_CERT_TYPE_PKCS_SIGNED_DATA
. bCertificate
: the actual certificate data. For WIN_CERT_TYPE_PKCS_SIGNED_DATA
, this is the (mostly) PKCS#7 SignedData
mentioned above. 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 SignedData
structure:
This is almost a normal PKCS#7 SignedData
, with a few key deviations:
SignedData
’s contentInfo
has a type of SPC_INDIRECT_DATA_OBJID
, which Microsoft defines as 1.3.6.1.4.1.311.2.1.4
. SpcIndirectDataContent
. Microsoft conveniently provides its ASN.1 definition: (Observe that the custom AlgorithmIdentifier
is 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:
CheckSum
field 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:
CheckSum
field from the buffer, in that order (to avoid rescaling the former’s offset). IterSec
API to construct a list of section buffers. IterSec
yields sections in file offset order as of #129 . EVP_DigestUpdate
and finish with EVP_DigestFinal
. 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:
SPC_PE_IMAGE_PAGE_HASHES_V1_OBJID
: 1.3.6.1.4.1.311.2.3.1
SPC_PE_IMAGE_PAGE_HASHES_V2_OBJID
: 1.3.6.1.4.1.311.2.3.2
These OIDs are not listed in Microsoft’s OID reference or in the OID repository, although they do appear in Wintrust.h
.
osslsigncode
has support for generating and validating page hashes, and grants us further insight: serializedData
of each SpcSerializedObject
is 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 get_page_hash_link
; osslsigncode
confusingly reuses the SpcAttributeTypeAndOptionalValue
type for Impl_SpcPageHash
and constructs the rest of the contents of SpcSerializedObject
manually.) 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:
…where 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 Impl_SpcPageHash.pageHashes
.
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:
countersignatureType
is the custom Microsoft OID 1.3.6.1.4.1.311.3.2.1
(i.e., SPC_TIME_STAMP_REQUEST_OBJID
) and content
is the original Authenticode PKCS#7 ContentInfo
. SignedData
, from which the SignerInfo
is 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 !