Vous êtes victime d’un incident de sécurité ? Contactez notre CERT


Blog technique

Portable Executable format, compilation timestamps and /Brepro flag

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

					#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;
    SIZE_T currentMaxAddr = 0;

    hFile = CreateFileA(fPath,
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("CreateFileA errorn");
        return FALSE;
    printf("%sn", 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: %xn", peHeaderPtr->FileHeader.TimeDateStamp);

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

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

                        if (pImageDataDirectory&nbsp;!= NULL) {

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

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

                                if (debugDirectoryRVA&nbsp;!= 0) {
                                    printf("tDebug Image Data Directory found, RVA: %x, Size: %xn", 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;

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

                                    printf("tDebug Image Data Directory at offset %xn", debugDirectoryOffset);
                                    if (SetFilePointer(hFile, debugDirectoryOffset, 0, FILE_BEGIN)&nbsp;!= 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%xn", 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);

    hFile = NULL;
    return TRUE;


Voir les derniers articles du Blog technique

4 décembre 2023
Find here the crypto and reverse challenges that our teams created for the European Cyber Week pre-qualification and qualification tests of CTF, a recognized cybersecurity event that took place in Rennes from November 21 to 23, 2023.
29 mars 2023
On the 24th of January, AMOSSYS and Malizen put together a Blue Team CTF, for the SupSec seminar organized by Inria. In this blog post, we explain how we, at AMOSSYS, generated the dataset used in this challenge.
20 décembre 2022
Find here the crypto and web challenges that our teams created for the European Cyber Week pre-qualification tests of CTF, a recognized cybersecurity event that took place in Rennes from November 15 to 17, 2022.
22 novembre 2022
Find here the write-ups of the crypto and web challenges that our teams created for the European Cyber Week pre-qualification tests of CTF
14 septembre 2022
This article starts with a quick overview on NIDS (Network Intrusion Detection System) evasions to remind what it is and why it could happen.
31 mars 2021
Essor du numérique, diversification des surfaces d’exposition, multiplication des cyberattaques… Depuis plusieurs années, la sécurité informatique est devenue une composante essentielle de l'administration d'un Système d'Information (SI).
18 septembre 2020
Depuis plusieurs années, l'écosystème informatique a dû faire face à une recrudescence de compromissions de systèmes d'informations par des rançongiciels, ou cryptolockers, qui s'introduisent principalement par des méthodes automatiques (_spear phishing_, etc.).
12 août 2020
We will discuss the feasibility in real world of the Spectre V1 flaw from a cross-process, userland perspective.
29 juillet 2020
This article details the behavior of the Sodinokibi ransomware using static analysis with IDA Pro. Sodinokibi, also called REvil, [...]
14 janvier 2020
Focus on the architecture of the Linux random number generator, also known as `/dev/urandom`. How does it work? Is it secure?