Select Page

## Introduction

The H3 Collective is releasing an alpha version of a PE cert fuzzing tool. We use this internally (among other tools) to test some of our software products and wanted to let others leverage it for their use. Rolling out your own ASN.1 parsing and certificate validation tools can be tricky at best, potentially introducing very serious security bugs if they have not been thoroughly tested. Sometimes leveraging an existing fuzzing framework isn’t possible because of how the software is built or what platform it runs on.

This tool takes an existing binary and mutates components related to the authenticode signature. Currently it can mutate the security data directory, the WIN_CERTIFICATE struct inside of the attribute certificate table, and bCertificate field. You can use it to swap in the signature from another signed binary before mutations are made, in case you have a valid binary from which you’d like to leverage the signature. It also allows you to enable only specific modes in order to limit the number of generated artifacts in the event you’re narrowing focus on a certain test mode. And naturally you can control the number of artifacts that are generated from this tool, including the ability to generate no artifacts and simply run test binaries as they are generated.

After mutated binaries are output, a helper script is also dropped to run each of the binaries and can be configured to run the binaries using a custom command to facilitate your testing. It’s easy to get started if you already have a signed binary:

> python cert_mutator.py -b test.exe -o test_dir
> cd test_dir
> .\_run_all.bat

What the tool does not currently support is mutating PKCS 7 fields and then re-encoding them correctly back into the file, nor can it simply swap in individual certificates (which also requires re-encoding the entire block.) These features may come later, but other improvements are likely to be focused more on quality-of-life updates, making the tool easier to use in different contexts.

## Background

Binary signature validation is an important component of establishing trust in a computer. There are many standards and systems that do this, including parts of the Windows operating system. Performing this validation can be tricky at best, and rolling out your own or trusting a third-party library can introduce very serious security bugs if they have not been thoroughly tested. A good complement to any software development shop includes fuzzing, basically producing anomalous data, either at random or in a targeted fashion, in order to trigger bugs in the code being tested.

Unfortunately, it’s not always possible to instrument a piece of code using a fuzzing library such as AFL, since that code may run in the kernel or on an architecture that is not yet supported. For such cases, we’ve written a small PE mutator that will produce numerous variants with changes targeting portions used for signatures specifically. The pre 1.0 release features include:

• Several mutation modes for the following fields:
• Security data directory
• Attribute certificate table
• bCertificate
• Authenticode signature truncation
• Authenticode substitution from an existing binary
• Ability to control the number of mutated variants to produce
• Helper script output that will run all of the binaries
• Ability to run variants as they are generated to prevent a large number of files from being dropped to disk at the same time.

The code runs on a binary that has been signed already, so it doesn’t matter if it’s self-signed or legitimately signed. A few quick steps to sign a binary and then some examples of running the tool are provided here.

## Signing a binary with a test cert

This assumes you already have a binary that can be signed. If not, just write a small program that returns 0 and compile it in release mode.

### Generate the PFX

This was adapted from https://www.petri.com/create-self-signed-certificate-using-powershell. Run the following commands in an elevated powershell prompt (substituting the domain, password, and pfx path):

$cert = New-SelfSignedCertificate -CertStoreLocation cert:\localmachine\my -dnsname yourtestdomain.com -KeyUsage digitalsignature,CertSign,CRLSign -KeyUsageProperty All -SmimeCapabilities -KeyExportPolicy Exportable -HashAlgorithm sha1 -keyalgorithm rsa -Type CodeSigningCert$pwd = ConvertTo-SecureString -String '1234' -Force -AsPlainText
$path = 'cert:\localmachine\my\' +$cert.Thumbprint
Export-PfxCertificate -cert $path -FilePath c:\preferred\path\to\your.pfx -Password$pwd

### Sign a binary

Next, you need to run the signtool command, which comes with the windows SDK:

signtool sign /debug /f c:\path\to\your.pfx /p 1234 c:\path\to\your\binary.exe

Verify your binary is signed by right-clicking and selecting properties. There should be a ‘Digital Signatures’ tab with your self-signed cert in it. If that’s present, congratulations, you’ve just signed a binary.

## Mutating the signed binary

Next comes the fun part. Using the PE cert mutator python script, you can produce numerous variants of that binary. Let’s analyze some of how the signature is stored in the file itself.

### Security Data Directory

The first indicator that a binary has a signature is that the offset to the security data directory is > 0. You can verify this using CFF Explorer (https://ntcore.com/?page_id=388)

These are two dwords in the optional header that we want to try changing to 0x00000000, 0xFFFFFFFF, and random bytes. The mutator will produce two variants for the null and FF bytes, and then use the “count” parameter to produce that number of variants with random values for each field.

The size field is special: 1/2 of the random variants will be at or under the existing size, in this case between 0x0-0x600, and the second half will be random within the size of a dword, e.g. 0x0-0xFFFFFFFF.

If a parser has made assumptions about the fact that these values point to valid locations in the binary, or that a full/parseable authenticode signature exists at that location, then this may trip that up.

### Attribute Certificate Table

The start of the Attribute Certificate Table is a WIN_CERTIFICATE struct which contains these fields:

 DWORD dwLength WORD wRevision WORD wCertificateType BYTE bCertificate[ANYSIZE_ARRAY]

dwLength refers to the length of the bCertificate array. wRevision must be set to 2, but can be set to 1 if there’s a legacy signature present (not covered here.) wCertificateType should be set to 2, but has three other defined options in the spec which should not be used. bCertificate is special, which will be covered shortly.

The first 3 fields are all susceptible to fuzzing that could trip a parser up, especially since the wRevision and wCertificateType fields can change the interpretation of the bCertificate array. The utility will do the standard null, ff, and random byte permutations for each of these three fields with extra variants for valid options (e.g. 0x1-0x4 for wCertificateType).

bCertificate is where all the magic happens. This is an ASN.1 DER encoded field that includes such information as certificates, CRLs, signature and encryption algorithms, information about each signer that has signed this file, attributes of the signature, etc.

### bCertificate

This array holds all security data needed to perform verification and is for all essential purposes the authenticode signature. The encoding can be quite complex and difficult to decode properly/fully if done manually. The format follows the PKCS #7 v1.5 standard, which is described in RFC 2315 (https://tools.ietf.org/html/rfc2315) and also described (in part) in Microsoft’s Authenticode documentation. This piece will not go in-depth on the details of all nested fields and will not provide more than a cursory description of ASN.1 formatting.

#### ASN.1 Description

ASN.1 formatting is encoded in groups of Tag-Length-Value (TLV) triplets. The tag specifies the element type (e.g. INTEGER, BIT STRING, SEQUENCE, OBJECT IDENTIFIER), length specifies the number of bytes necessary to encode the value, and the value is dependent on the element type. The size is special. If the high bit on size is set, that indicates that the lower seven bits describe a number of bytes that follow which represent the size, which is done when the size of an element is > 127. There are more complexities behind the tag, length, and value fields. If you want to learn more, check out this tutorial: https://www.obj-sys.com/asn1tutorial/asn1only.html.

#### SignedData element (top element)

The top element is known as a ContentInfo sequence, shown by this description:

ContentInfo ::= SEQUENCE {
contentType ContentType,
content
[0] EXPLICIT ANY DEFINED BY contentType OPTIONAL }

ContentType ::= OBJECT IDENTIFIER

Per the Authenticode spec, that contentType must be that of SignedData, which according to the RFC states that the object identifiers are defined as:

pkcs-7 OBJECT IDENTIFIER ::=
pkcs(1) 7 }

signedData OBJECT IDENTIFIER ::= { pkcs-7 2 }

And the actual SignedData definition looks like this:

SignedData ::= SEQUENCE {
version Version,
digestAlgorithms DigestAlgorithmIdentifiers,
contentInfo ContentInfo,
certificates            [0] IMPLICIT ExtendedCertificatesAndCertificates OPTIONAL,
Crls   [1] IMPLICIT CertificateRevocationLists OPTIONAL,
signerInfos SignerInfos
}

DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier

The object identifier for SignedData technically looks like this: 1.2.840.113549.1.7.2 and is encoded in a very unique way. Please reference https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier for a more complete description of the data transformations used to encode/decode this data.

So the start of a ContentInfo block representing a SignedData element might look very similar to:

30 82 05 F1            SEQUENCE: length of 0x5F1 bytes.
06 09                OBJECT IDENTIFIER: length of 9 bytes
2A 86 48 86 F7 0D 01 07 02    Signed data OID: 1.2.840.113549.1.7.2
A0 82 05 E2       A Context-specific tag indicating an array of types, length: 0x5E2.   Basically this is the 'content' variable.
30 82 05 DE   SEQUENCE - length of 0x5DE
02 01       INTEGER - length 0x1
01       Value: 1.   This is the value of the 'version' variable, which must be 1.
31 0B       SET: length 0xB
....digestAlgorithms definition would follow....


In this short example we can see the outer block of a ContentInfo sequence that describes a SignedData sequence. The Version is 1, which is required by the authenticode spec. Hopefully this adequately demonstrates how complex the encoding/decoding of ASN.1 formatted data can be. There are numerous places to trick a parser, or the application, that may be relying on the data from such a parser.

### Mutations on buffers

In the first phase of this tool, simple edits on top of the bCertificate field (and other buffers) are being done blindly without regard to the encoded values. As a buffer, the null, ff, and random byte mutations are being performed, but in such a way that the number and location of edits is random, though with an increasing chance of covering more as the variant count reaches its max. At this stage, the mutation is more about finding bugs in ASN.1 parsing attempts than to the certificate field validation code.

Recall that the mutation mode takes a count, which by default is 5 but can be anything. Let’s assume it is 10 for the purposes of illustration, and that the program is currently generating null byte mutations. First a number of mutations is determined, which is:

mum_mutations = rand ( (current_count / total_count) * len(buffer) )

This states that the number of overwrites possible is a percentage of the size of the buffer based on how many variants had been generated so far. So variant 5 has a chance of overwriting as much as 50% of the buffer, while variant 10 has a chance overwriting as much as 100% of the buffer (though it may not, since it’s random.) This was done to ensure that there are definitely smaller sets of edits applied. The logic being that if most of the buffer is unmodified, the logic to reach a more nested area may be more likely to be executed. So we force the random number of edits to be smaller in earlier variants.

Next, the overwrites are applied at random locations inside of a loop:

overwrite_offset = rand( len(buffer) )

This may result in certain offsets being overwritten multiple times, but this should not be much of a problem. The higher the count, the more variants are generated that exhibit small and large sets of overwrites. This process is repeated for 0xff and random bytes.

## Example Uses

View help and information about parameters:

python cert_mutator.py -h

Take an existing signed binary named test.exe and generate mutations in the ‘test_dir’ folder based on the default modes

python cert_mutator.py -b test.exe -o test_dir

Generate mutations from test.exe using only the ‘security_data_directory’ mode, with a count of 20 and output files in the ‘test_dir’ folder

python cert_mutator.py -b test.exe -o test_dir -c 20 -m security_data_directory

Replace the certificate in test.exe with that of alt.exe and then generate mutations based the default modes (can be combined with other options):

Python cert_mutator.py -b test.exe -r alt.exe

Perform mutations on test.exe and then run each variant as it is generated, not leaving any artifacts on disk (rag == run after generation):

Python cert_mutator.py -b test.exe -rag

–References–

GitHub Repo:
https://github.com/h3collective/pe_cert_mutator﻿

Windows PE format – Attribute Certificate Table:
https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#the-attribute-certificate-table-image-only

RFC 2315: Cryptographic message syntax:
https://tools.ietf.org/html/rfc2315