Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Manually sign a PE file

I'm trying to sign an existing Portable Executable manually.

I'm following instructions found in this document:

  1. Load the image header into memory.
  2. Initialize a hash algorithm context.
  3. Hash the image header from its base to immediately before the start of the checksum address, as specified in Optional Header Windows-Specific Fields.
  4. Skip over the checksum, which is a 4-byte field.
  5. Hash everything from the end of the checksum field to immediately before the start of the Certificate Table entry, as specified in Optional Header Data Directories.
  6. Get the Attribute Certificate Table address and size from the Certificate Table entry. For details, see section 5.7 of the PE/COFF specification.
  7. Exclude the Certificate Table entry from the calculation and hash everything from the end of the Certificate Table entry to the end of image header, including Section Table (headers).The Certificate Table entry is 8 bytes long, as specified in Optional Header Data Directories.
  8. Create a counter called SUM_OF_BYTES_HASHED, which is not part of the signature. Set this counter to the SizeOfHeaders field, as specified in Optional Header Windows-Specific Field.
  9. Build a temporary table of pointers to all of the section headers in the image. The NumberOfSections field of COFF File Header indicates how big the table should be. Do not include any section headers in the table whose SizeOfRawData field is zero.
  10. Using the PointerToRawData field (offset 20) in the referenced SectionHeader structure as a key, arrange the table's elements in ascending order. In other words, sort the section headers in ascending order according to the disk-file offset of the sections.
  11. Walk through the sorted table, load the corresponding section into memory, and hash the entire section. Use the SizeOfRawData field in the SectionHeader structure to determine the amount of data to hash.
  12. Add the section’s SizeOfRawData value to SUM_OF_BYTES_HASHED.
  13. Repeat steps 11 and 12 for all of the sections in the sorted table.
  14. Create a value called FILE_SIZE, which is not part of the signature. Set this value to the image’s file size, acquired from the underlying file system. If FILE_SIZE is greater than SUM_OF_BYTES_HASHED, the file contains extra data that must be added to the hash. This data begins at the SUM_OF_BYTES_HASHED file offset, and its length is: (File Size) – ((Size of AttributeCertificateTable) + SUM_OF_BYTES_HASHED) Note: The size of Attribute Certificate Table is specified in the second ULONG value in the Certificate Table entry (32 bit: offset 132, 64 bit: offset 148) in Optional Header Data Directories.
  15. Finalize the hash algorithm context. Note: This procedure uses offset values from the PE/COFF specification, version 8.1 . For authoritative offset values, refer to the most recent version of the PE/COFF specification.

The following code tries to get the part-to-be-hashed from the image:

        // Variables
        // full: vector<char> holding the image
        // d: vector<char> where to store the data-to-be-hashed   
        // sections: vector of the sections, ensuring size > 0
        // nt/pnt* : pointer inside full that points to the beginning of NT header


        // Sort Sections
        std::sort(sections.begin(), sections.end(), [](const section& s1, const section& s2) -> bool
            {
                if (s1.sec->PointerToRawData < s2.sec->PointerToRawData)
                    return true;
                return false;
            });

        // Up to where?
        size_t BytesUpToLastSection = ((char*)(sections[sections.size() - 1].sec) - full.data()) + sizeof(image_section_header);
        d.resize(BytesUpToLastSection);
        memcpy(d.data(), full.data(), BytesUpToLastSection);

        // We remove the certificate table entry (8 bytes)
        size_t offset = 0;
        if (nt.Is32())
        {
            offset = offsetof(optional_header_32, DataDirectory[DIR_SECURITY]);
        }
        else
        {
            offset = offsetof(optional_header_64, DataDirectory[DIR_SECURITY]);
        }
        offset += sizeof(nt.FileHeader) + sizeof(nt.Signature);
        offset += pnt - full.data();
        d.erase(d.begin() + offset, d.begin() + offset + 8);

        // We remove the checksum (4 bytes)
        if (nt.Is32())
            offset = offsetof(optional_header_32,CheckSum);
        else
            offset = offsetof(optional_header_64,CheckSum);
        offset += sizeof(nt.FileHeader) + sizeof(nt.Signature);
        offset += pnt - full.data();
        d.erase(d.begin() + offset, d.begin() + offset + 4);

        // Counter
        size_t SUM_OF_BYTES_HASHED = 0;
        if (nt.Is32())
            SUM_OF_BYTES_HASHED = std::get<optional_header_32>(nt.OptionalHeader).SizeOfHeaders;
        else
            SUM_OF_BYTES_HASHED = std::get<optional_header_64>(nt.OptionalHeader).SizeOfHeaders;

        for (auto& ss : sections)
        {
            if (ss.sectionData.sz == 0)
                continue;
            s = d.size();
            d.resize(d.size() + ss.sectionData.sz);
            memcpy(d.data() + s, ss.sectionData.p, ss.sectionData.sz);
            SUM_OF_BYTES_HASHED += ss.sec->SizeOfRawData;
        }
        size_t FILE_SIZE = full.size();
        if (FILE_SIZE > SUM_OF_BYTES_HASHED)
        {
        // Not entering here, test executable does not have extra data
        }

There must be a problem somewhere. Signing this data and then updating the executable Certifcate Entry and appending the PCKS#7 signature results in an executable that is not recognized by Windows. Right click-> "Invalid Signature".

When comparing with the result of signtool.exe, the signature is different. When I try to verify this signature with CryptVerifyDetachedMessageSignature, there's an error 0x80091007 which means that the hash is incorrect.

Which means that I don't calculate correctly the "what to be signed" buffer. What do I miss?

I even hardcoded the removal of the entries:

d = full;
d.erase(d.begin() + 296, d.begin() + 296 + 8);
d.erase(d.begin() + 216, d.begin() + 216 + 4);

Thanks a lot.

like image 568
Michael Chourdakis Avatar asked Jul 22 '19 17:07

Michael Chourdakis


People also ask

What is the file signature of a PE file?

Signature (Image Only) After the MS-DOS stub, at the file offset specified at offset 0x3c, is a 4-byte signature that identifies the file as a PE format image file. This signature is "PE\0\0" (the letters "P" and "E" followed by two null bytes).

What is entry point of a PE executable?

The PE entry point is defined in the IMAGE_OPTIONAL_HEADER structure, in the AddressOfEntryPoint field: A pointer to the entry point function, relative to the image base address. For executable files, this is the starting address. For device drivers, this is the address of the initialization function.

What is a Microsoft PE file?

The Portable Executable (PE) format is a file format for executables, object code, DLLs and others used in 32-bit and 64-bit versions of Windows operating systems. The PE format is a data structure that encapsulates the information necessary for the Windows OS loader to manage the wrapped executable code.

What is PE file in malware analysis?

The PE file format is a data structure that contains the information necessary for the Windows OS loader to manage the wrapped executable code. Nearly every file with executable code that is loaded by Windows is in the PE file format, though some legacy file formats do appear on rare occasion in malware.


1 Answers

I found the solution.

The signature inside the PE is not a typical PKCS#7, it contains also specific authenticated and unauthenticated attributes described in the document.

Once these are satisfied, the signature is OK. However I can't use CAdES because Windows will not accept any other authenticated attributed in the list, which is required for CAdES to be validated.

like image 194
Michael Chourdakis Avatar answered Nov 14 '22 22:11

Michael Chourdakis