09/07/2018
Blog technique
Portable Executable format, compilation timestamps and /Brepro flag
Equipe CERT
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;
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 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) != 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: %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;
break;
}
}
}
}
if (debugDirectoryOffset != 0 && debugDirectorySize < BUFF_SIZE) {
printf("tDebug Image Data Directory at offset %xn", 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%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);
break;
}
}
}
}
}
}
}
}
}
}
}
}
}
CloseHandle(hFile);
hFile = NULL;
return TRUE;
}