Portable Executable format, compilation timestamps and /Brepro flag

Mon 09 July 2018
By CERT

Portable Executable binaries embed timestamps stored by the compiler, which may in some cases appear inconsistent. This article details the origin of these inconsistencies and gives a code sample which may be used to get only the correct timestamps for threat hunting purposes.

When performing threat hunting, a quick detection might be a comparison between compilation timestamps set by the compiler in the file itself and operating system timestamps: if the file has been compiled after its local filesystem creation, there is definitely something to investigate as something obvisouly changed this system creation date. Compilation timestamps are set by the compiler in the Portable Executable (PE) file in multiple locations: in its PE header itself (in the IMAGE_FILE_HEADER.TimeDateStamp field) or in several data directories which may also have a TimeDateStamp field.

While performing our tests, we actually found a lot of issues on a basic Windows installation. Almost all legitimate Microsoft PE files appeared having obvious invalid timestamps (i.e set to future or really old dates)... Moreover, any timestamp in the various data directories were set to the same invalid value (except for IMAGE_IMPORT_DESCRIPTOR/IMAGE_RESOURCE_DIRECTORY ones which are almost always set to 0x0 or 0xFFFFFFFF). We also found that loading these modules in the WinDbg Preview debugger and printing their information (e.g with lmDvmKERNEL32) spawned two messages, "Image was built with /Brepro flag." and "(This is a reproducible build file hash, not a timestamp)", indicating that this value is not a regular timestamp, and that there is something which indicates that it has been built with this particular linker flag.

In order to find this something, we tried setting this /Brepro flag in the VS2013 Express linker. The resulting PE files had an invalid timestamp, set to 0xFFFFFFFF. In VS2017 the /Brepro linker flag actually produces "Build IDs", not 0xFFFFFFFF ones. We did not try with VS2015 nor searched to find out how these "Build IDs" are computed. This flag actually strips any timestamp information in the final file, resulting in "reproductible" builds, but seems to remain undocumented by Microsoft.

As we're a little lazy and did not want to perform some WinDbg reverse engineering, and thought that this information should be embedded either in the Rich header, in the PE header itself or in its data directories. We quickly diffed the files with the pefile python tool and found that all of the /Brepro compiled ones had a custom IMAGE_DEBUG_DIRECTORY entry with a 0x10 type (undocumented). When changing this type, WinDbg Preview did not display the "/Brepro Build ID" message anymore.

Therefore we think that:

  • IMAGE_DEBUG_DIRECTORY entries should definitely be considered when looking at compilation timestamps;
  • attackers who spoof their compilation timestamps manually (i.e without the /Brepro linker flag) may forget changing the timestamps present in some data directories;
  • timestamps of files without "/Brepro" IMAGE_DEBUG_DIRECTORY entries should be considered as valid ones. However we did not check if other compilers set invalid compilation timestamps without this particular debug directory, so let us know if you find ones;
  • a PE file with a "/Brepro" IMAGE_DEBUG_DIRECTORY entry should also embed other debugging information (as its main purpose is debugging) and have been built by Microsoft (as it's not a widely documented feature).

You may find a quick and dirty C code below which parses a PE file in order to print the compilation timestamp and searchs for this particular debug directory.

And for french readers: si vous êtes intéressés pour bosser sur des sujets sympas tout en restant loin de Paris, n'hésitez pas à nous envoyer votre CV à rh@amossys.fr ;)

#define BUFF_SIZE 0x1000
BOOL GetPEFileCompilationTimeStamp(
    __in LPCSTR fPath) {
    UCHAR fileData[BUFF_SIZE];
    HANDLE hFile = NULL;
    ULONG cbRead = 0, peHeaderOffset = 0, i = 0, sectionsCount = 0, debugDirectoryRVA = 0, debugDirectoryOffset = 0, debugDirectorySize = 0;
    PIMAGE_NT_HEADERS64 peHeaderPtr = NULL;
    PIMAGE_DATA_DIRECTORY pImageDataDirectory = NULL;
    PIMAGE_SECTION_HEADER pCurrentSection = NULL;
    PIMAGE_DEBUG_DIRECTORY pDebugDirectory = NULL;
    SIZE_T currentMaxAddr = 0;

    hFile = CreateFileA(fPath,
        GENERIC_READ,
        FILE_SHARE_READ,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        0
        );
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("CreateFileA error\n");
        return FALSE;
    }
    printf("%s\n", fPath);

    if (ReadFile(hFile, fileData, BUFF_SIZE, &cbRead, NULL) == TRUE) {
        if (cbRead > 0xF0) {
            currentMaxAddr = (SIZE_T)fileData + cbRead;

            if (fileData[0] == 'M' && fileData[1] == 'Z') {
                peHeaderOffset = *(PULONG)(fileData + 0x3C);

                if (peHeaderOffset + sizeof(IMAGE_NT_HEADERS64) < cbRead) {
                    peHeaderPtr = (PIMAGE_NT_HEADERS64)(peHeaderOffset + fileData);

                    if (peHeaderPtr->Signature == 0x4550) {
                        printf("\tPE.TimeDateStamp: %x\n", peHeaderPtr->FileHeader.TimeDateStamp);

                        // is there any debug data directory?
                        pImageDataDirectory = NULL;
                        if ((peHeaderPtr->FileHeader.Machine & IMAGE_FILE_MACHINE_AMD64) != 0) {
                            sectionsCount = peHeaderPtr->FileHeader.NumberOfSections;
                            pCurrentSection = (PIMAGE_SECTION_HEADER)((SIZE_T)&(peHeaderPtr->OptionalHeader) + peHeaderPtr->FileHeader.SizeOfOptionalHeader);

                            if (peHeaderPtr->FileHeader.SizeOfOptionalHeader != 0) {
                                pImageDataDirectory = (PIMAGE_DATA_DIRECTORY)((SIZE_T)(&peHeaderPtr->OptionalHeader) + 160);
                            }
                        }
                        else if ((peHeaderPtr->FileHeader.Machine & IMAGE_FILE_MACHINE_I386) != 0) {
                            if (peHeaderPtr->FileHeader.SizeOfOptionalHeader != 0) {
                                pImageDataDirectory = (PIMAGE_DATA_DIRECTORY)((SIZE_T)(&peHeaderPtr->OptionalHeader) + 144);
                            }
                        }

                        if (pImageDataDirectory != NULL) {

                            if ((SIZE_T)pImageDataDirectory + sizeof(IMAGE_DATA_DIRECTORY) < currentMaxAddr) {

                                debugDirectoryRVA = pImageDataDirectory->VirtualAddress;
                                debugDirectorySize = pImageDataDirectory->Size;

                                if (debugDirectoryRVA != 0) {
                                    printf("\tDebug Image Data Directory found, RVA: %x, Size: %x\n", debugDirectoryRVA, debugDirectorySize);

                                    // let's find its raw file offset
                                    if (sectionsCount * sizeof(IMAGE_SECTION_HEADER) + (SIZE_T)pCurrentSection < currentMaxAddr) {
                                        for (i = 0; i < sectionsCount; i++) {
                                            if (pCurrentSection[i].VirtualAddress < debugDirectoryRVA &&
                                                (pCurrentSection[i].SizeOfRawData + pCurrentSection[i].VirtualAddress) > debugDirectoryRVA) {
                                                debugDirectoryOffset = debugDirectoryRVA - pCurrentSection[i].VirtualAddress + pCurrentSection[i].PointerToRawData;
                                                break;
                                            }
                                        }
                                    }
                                }

                                if (debugDirectoryOffset != 0 && debugDirectorySize < BUFF_SIZE) {

                                    printf("\tDebug Image Data Directory at offset %x\n", debugDirectoryOffset);
                                    if (SetFilePointer(hFile, debugDirectoryOffset, 0, FILE_BEGIN) != INVALID_SET_FILE_POINTER) {

                                        // let's read the whole data directory and walk it
                                        if (ReadFile(hFile, fileData, debugDirectorySize, &cbRead, NULL) == TRUE) {

                                            pDebugDirectory = (PIMAGE_DEBUG_DIRECTORY)fileData;
                                            for (i = 0; i < debugDirectorySize / sizeof(IMAGE_DEBUG_DIRECTORY); i++) {
                                                if ((SIZE_T)&pDebugDirectory[i] < (SIZE_T)fileData + cbRead) {
                                                    //printf("\tDebug directory #%d: type 0x%x\n", i, pDebugDirectory[i].Type);
                                                    if (pDebugDirectory[i].Type == 0x10) {
                                                        printf("\tDebug Image Data Directory with 0x10 type found, /Brepro flag was set (TimeDateStamp: %x)\n", pDebugDirectory[i].TimeDateStamp);
                                                        break;
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    CloseHandle(hFile);
    hFile = NULL;
    return TRUE;
}