Verifying Windows binaries, without Windows

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!

svcli

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.

A quick Authenticode primer

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:

  • Authenticity: A program with a valid Authenticode signature contains a chain of certificates sufficient for validating that signature. Said chain is ultimately rooted in a certificate stored in the user’s Trusted Publishers store, preventing self-signed certificates without explicit opt-in by the user.
  • Integrity: Each Authenticode signature includes a cryptographic hash of the signed binary. This hash is compared against the binary’s in-memory representation at load time, preventing malicious modifications.
    • Authenticode can also embed cryptographic hashes for each page of memory. These are used with forced integrity signing, which is necessary for Windows kernel drivers and requires a special Microsoft cross-signed “Software Publisher Certificate” instead of a self-signed or independently trusted Certificate Authority (CA).
  • Timeliness: Authenticode supports countersignatures embedding from a Timestamping Authority (TSA), allowing the signature on a binary to potentially outlive the expiration dates of its signing certificates. Such countersignatures also prevent backdating of a valid signature, making it more difficult for an attacker to re-use an expired signing certificate.

Like all code signing technologies, there are things Authenticode can’t do or guarantee about a program:

  • That it has no bugs: Anybody can write buggy software and sign for it, either with a self-signed certificate or by purchasing one from a CA that’s been cross-signed by Microsoft.
  • That it runs no code other than itself: The Windows execution contract is notoriously lax (e.g., the DLL loading rules for desktop applications), and many applications support some form of code execution as a feature (scripts, plugins, sick WinAMP skins, etc). Authenticode has no ability to validate the integrity or intent of code executed outside of the initial signed binary.

Similarly, there are some things that Authenticode, like all PKI implementations, is susceptible to:

  • Misplaced trust: CAs want to sell as many certificates as possible, and thus have limited incentives to check the legitimacy of entities that purchase from them. Anybody can create a US LLC for a few hundred bucks.
  • Stolen certificates: Code-signing and HTTPS certificates are prime targets for theft; many real-world campaigns leverage stolen certificates to fool users into trusting malicious code. Companies regularly check their secret material into source control systems, and code signing certificates are no exception.
  • Fraudulent certificates: Flame infamously leveraged a novel chosen-prefix attack on MD5 to impersonate a Microsoft certificate that had been accidentally trusted for code signing. Similar attacks on SHA-1 are now tractable at prices reasonable for nation states and organized crime.

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.

Parsing Authenticode signatures: spicy PKCS#7

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:

  • The certificate table, which contains one or more entries, each of which may be a SignedData.
  • SignedData objects, which are mostly normal PKCS#7 containers (marked with a content type of SignedData per RFC 2315).

The certificate table

The certificate table is the mechanism by which Authenticode signatures are embedded into PE files.

It has a few interesting properties:

  • Accessing the certificate table involves reading the certificate table directory in the data directory table. Unlike every other entry in the data directory table, the certificate directory’s 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.
  • Despite this, real-world tooling appears to be inflexible about placement and subsequent parsing of the certificate table. Microsoft’s tooling consistently places the certificate table at the end of the PE (after all sections); many third-party tools naively seek to the certificate table offset and parse until EOF, allowing an attacker to trivially append additional certificates1.

Once located, actually parsing the certificate table is straightforward: It’s an 8 byte-aligned blob of WIN_CERTIFICATE structures:


win_certificate.png

…with some fields of interest:

  • wRevision: the “revision” of the WIN_CERTIFICATE.
    • MSDN only recently fixed the documentation for this field: 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.
    • MSDN documents four possible values for 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.

Authenticode’s SignedData

Microsoft helpfully2 supplies this visualization of their SignedData structure:

This is almost a normal PKCS#7 SignedData, with a few key deviations:

  • Instead of one of the RFC 2315 content types, the Authenticode SignedData’s contentInfo has a type of SPC_INDIRECT_DATA_OBJID, which Microsoft defines as 1.3.6.1.4.1.311.2.1.43.
  • The structure corresponding to this object identifier (OID) is documented as SpcIndirectDataContent. Microsoft conveniently provides its ASN.1 definition:
    spc_indirect(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:


    spc_indirect_macros

Actually checking the signature

With our structures in place, we can use OpenSSL’s (mostly) undocumented PKCS#7 API to parse our SignedData and indirect data contents:

verify1

…and then validate them:

verify2

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.

Calculating and checking the Authenticode hash

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:

authenticode_digest

Next, we need to compute the binary’s actual hash. This is a little involved, thanks to a few different fields:

  • Every PE has a 32-bit 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.
  • The certificate data directory entry itself needs to be skipped, since relocating and/or modifying the size of the certificate table should not require any changes to pre-existing signatures.
  • The certificate table (and constituent signatures) itself, naturally, cannot be part of the input to the hash.
  • To ensure a consistent hash, Authenticode stipulates that sections are hashed in ascending order by the value of each section header’s 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:

  1. Start with an empty buffer.
  2. Insert all PE headers (DOS, COFF, Optional, sections) into the buffer.
  3. Erase the certificate table directory entry and CheckSum field from the buffer, in that order (to avoid rescaling the former’s offset).
  4. Use pe-parse’s IterSec API to construct a list of section buffers. IterSec yields sections in file offset order as of #129.
  5. Skip past the certificate table and add trailing data to the buffer, if any exists.
  6. Create and initialize a new OpenSSL message digest context using the NID retrieved from the signature.
  7. Toss the buffer into EVP_DigestUpdate and finish with EVP_DigestFinal.
  8. Compare the result with the Authenticode-supplied hash.

Other bits and pieces

We haven’t discussed the two remaining major Authenticode features: page hashes and timestamp countersignatures.

Page hashes

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:

  • The VirtualBox source code references OIDs for two different versions of the page hashes structure:
    • 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 repository4, although they do appear in Wintrust.h.

  • At least one fork of osslsigncode has support for generating and validating page hashes, and grants us further insight:
    • The V1 OID represents SHA-1 page hashes; V2 represents SHA2-256.
    • The serializedData of each SpcSerializedObject is an ASN.1 SET, each member of which is an ASN.1 SEQUENCE, to the effect of:page_hash_asn1(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:

page_hash

…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.

Timestamp countersignatures

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:

  • When issuing a timestamp request (TSR) to a timestamp authority (TSA), the request takes the form of an HTTP 1.1 POST containing a DER-encoded, then base64-encoded ASN.1 message:timestamp_asn1…where 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.
  • The TSA response is a PKCS#7 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.

Wrapup

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 *:

uthenticode_example

Check out the svcli command-line tool for an applied example, including retrieving the embedded Authenticode hash.

Prior work and references

μ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:

  • jsign: A Java implementation of Authenticode

Want the scoop on our open-source projects and other security innovations? Contact us or sign up for our newsletter!

  1. Adding additional certificates without this parsing error is still relatively trivial, but requires the attacker to modify more parts of the PE rather than just append to it.↩
  2. For some definitions of helpful.↩
  3. The OID tree for Authenticode shows lots of other interesting OIDs, most of which aren’t publicly documented.↩
  4. I’ve submitted them, pending approval by the repository’s administrator.↩

Article Link: https://blog.trailofbits.com/2020/05/27/verifying-windows-binaries-without-windows/