Manipulation and Detection of EOF
Fork me on Github
You can find a complete version of the project that is described in this paper on my Github account.
https://github.com/DarkCoderSc/peof-detectorDescription
This Delphi unit demonstrate how to manipulate EOF Data of a Valid Microsoft Windows Portable Executable (PE) File.
EOF (End Of File) is often used by Malware authors to offer their Malware users a way to edit Malware payload configuration (Ex: C2 informations) without having access to source code.
You often encounter such techniques in:
- Remote Access Tool/Trojan (RAT)
- File Wrapper / Binder
- Downloader
- Loader / Botnets
But not only.
Supported Features (32bit and 64bit)
- Write EOF Data to Valid PE File.
- Read EOF Data from Valid PE File.
- Clear EOF Data if Present in Valid PE File .
- Retrieve EOF Data Size if Present in Valid PE File.
- Detect EOF Data presence in Valid PE File.
Resources
C/C++ Implementation (Read / Extract)
https://github.com/DarkCoderSc/eof-reader
Code
(*******************************************************************************
Author:
-> Jean-Pierre LESUEUR (@DarkCoderSc)
https://github.com/DarkCoderSc
https://gist.github.com/DarkCoderSc
https://www.phrozen.io/
Description:
-> Unit for EOF manipulation on Portable Executable Files (x86/x64).
-> Detection and Removal of EOF Data (Often used by Malware to store configuration / files etc..).
Category:
-> Malware Research & Detection
License:
-> MIT
Functions:
-> WritePEOF() : Write extra data at the end of a PE File.
-> ReadPEOF() : Read extra data stored at the end of a PE File.
-> FileIsValidPE() : Check whether or not a file is a valid Portable Executable File.
-> ClearPEOF() : Remove / Sanitize / Disinfect a PE File from any extra data stored at it end.
-> GetPEOFSize() : Get the size of the extra data stored at the end of a PE File.
-> GetFileSize() : Get the expected file size of a PE File following PE Header description.
-> ContainPEOF() : Return True if some extra data is detected at the end of a PE File.
*******************************************************************************)
unit UntEOF;
interface
uses WinAPI.Windows, System.SysUtils, Classes;
type
TBasicPEInfo = record
Valid : Boolean; // True = Valid PE; False = Invalid PE
Arch64 : Boolean; // True = 64bit Image; False = 32bit Image
ImageSize : Int64;
end;
function WritePEOF(APEFile : String; ABuffer : PVOID; ABufferSize : Integer) : Boolean;
function ReadPEOF(APEFile : String; ABuffer : PVOID; ABufferSize : Integer; ABufferPos : Integer = 0) : Boolean;
function FileIsValidPE(AFileName : String) : Boolean;
function ClearPEOF(APEFile : String) : Boolean;
function GetPEOFSize(APEFile : String) : Int64;
function GetFileSize(AFileName : String) : Int64;
function ContainPEOF(APEFile : String) : Boolean;
implementation
{-------------------------------------------------------------------------------
Get File Size (Nothing more to say)
-------------------------------------------------------------------------------}
function GetFileSize(AFileName : String) : Int64;
var AFileInfo : TWin32FileAttributeData;
begin
result := 0;
if NOT FileExists(AFileName) then
Exit();
if NOT GetFileAttributesEx(
PWideChar(AFileName),
GetFileExInfoStandard,
@AFileInfo)
then
Exit();
///
result := (Int64(AFileInfo.nFileSizeLow) or Int64(AFileInfo.nFileSizeHigh shl 32));
end;
{-------------------------------------------------------------------------------
Is target file a 64bit PE file
-------------------------------------------------------------------------------}
function GetBasicPEInfo(APEFile : String; var ABasicPEInfo : TBasicPEInfo) : Boolean;
var hFile : THandle;
AImageDosHeader : TImageDosHeader;
dwBytesRead : DWORD;
AImageFileHeader : TImageFileHeader;
AImageNtHeaderSignature : DWORD;
AOptionalHeader32 : TImageOptionalHeader32;
AOptionalHeader64 : TimageOptionalHeader64;
I : Integer;
AImageSectionHeader : TimageSectionHeader;
begin
result := False;
ABasicPEInfo.Valid := False;
ABasicPEInfo.Arch64 := False;
ABasicPEInfo.ImageSize := 0;
// Open Target File (Must Exists)
hFile := CreateFile(
PChar(APEFile),
GENERIC_READ,
FILE_SHARE_READ,
nil,
OPEN_EXISTING,
0,
0
);
if hFile = INVALID_HANDLE_VALUE then
Exit;
try
SetFilePointer(hFile, 0, nil, FILE_BEGIN);
// Read the Image Dos Header
if NOT ReadFile(
hFile,
AImageDosHeader,
SizeOf(TImageDosHeader),
dwBytesRead,
nil
) then
Exit();
// To be considered as a valid PE file, e_magic must be $5A4D (MZ)
if (AImageDosHeader.e_magic <> IMAGE_DOS_SIGNATURE) then
Exit();
// Move the cursor to Image NT Signature
SetFilePointer(hFile, AImageDosHeader._lfanew, nil, FILE_BEGIN);
// Read the Image NT Signature
if NOT ReadFile(
hFile,
AImageNtHeaderSignature,
SizeOf(DWORD),
dwBytesRead,
nil
) then
Exit();
// To be considered as a valid PE file, Image NT Signature must be $00004550 (PE00)
if (AImageNtHeaderSignature <> IMAGE_NT_SIGNATURE) then
Exit();
ABasicPEInfo.Valid := True;
// Read the Image File Header
if NOT ReadFile(
hFile,
AImageFileHeader,
sizeOf(TImageFileHeader),
dwBytesRead,
0
) then
Exit();
// TImageDosHeader.Machine contains the architecture of the file
ABasicPEInfo.Arch64 := (AImageFileHeader.Machine = IMAGE_FILE_MACHINE_AMD64);
if ABasicPEInfo.Arch64 then begin
// For 64bit Image
if NOT ReadFile(
hFile,
AOptionalHeader64,
sizeOf(TImageOptionalHeader64),
dwBytesRead,
0
) then
Exit();
Inc(ABasicPEInfo.ImageSize, AOptionalHeader64.SizeOfHeaders);
Inc(ABasicPEInfo.ImageSize, AOptionalHeader64.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size);
end else begin
// For 32bit Image
if NOT ReadFile(
hFile,
AOptionalHeader32,
sizeOf(TImageOptionalHeader32),
dwBytesRead,
0
) then
Exit();
Inc(ABasicPEInfo.ImageSize, AOptionalHeader32.SizeOfHeaders);
Inc(ABasicPEInfo.ImageSize, AOptionalHeader32.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size);
end;
// Iterate through each section to get the size of each for ImageSize calculation
for I := 0 to AImageFileHeader.NumberOfSections -1 do begin
if NOT ReadFile(
hFile,
AImageSectionHeader,
SizeOf(TImageSectionHeader),
dwBytesRead, 0
) then
Exit(); // Fatal
Inc(ABasicPEInfo.ImageSize, AImageSectionHeader.SizeOfRawData);
end;
// All steps successfully passed
result := True;
finally
CloseHandle(hFile);
end;
end;
{-------------------------------------------------------------------------------
Is target file a valid Portable Executable
-------------------------------------------------------------------------------}
function FileIsValidPE(AFileName : String) : Boolean;
var ABasicPEInfo : TBasicPEInfo;
begin
result := False;
///
GetBasicPEInfo(AFileName, ABasicPEInfo);
result := ABasicPEInfo.Valid;
end;
{-------------------------------------------------------------------------------
Write Data to the End of a PE File.
-------------------------------------------------------------------------------}
function WritePEOF(APEFile : String; ABuffer : PVOID; ABufferSize : Integer) : Boolean;
var hFile : THandle;
ABytesWritten : Cardinal;
begin
result := false;
if NOT FileIsValidPE(APEFile) then
Exit();
hFile := CreateFile(
PWideChar(APEFile),
GENERIC_WRITE,
0,
nil,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
0
);
if hFile = INVALID_HANDLE_VALUE then
Exit;
try
SetFilePointer(hFile, 0, nil, FILE_END);
if NOT WriteFile(
hFile,
ABuffer^,
ABufferSize,
ABytesWritten,
0
) then
Exit();
result := true;
finally
CloseHandle(hFile);
end;
end;
{-------------------------------------------------------------------------------
Read Data from the End of a PE File.
-------------------------------------------------------------------------------}
function ReadPEOF(APEFile : String; ABuffer : PVOID; ABufferSize : Integer; ABufferPos : Integer = 0) : Boolean;
var hFile : THandle;
ABytesRead : Cardinal;
begin
result := false;
if NOT FileIsValidPE(APEFile) then
Exit();
hFile := CreateFile(
PWideChar(APEFile),
GENERIC_READ,
0,
nil,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
0
);
if hFile = INVALID_HANDLE_VALUE then
Exit();
try
SetFilePointer(
hFile,
(-ABufferSize + ABufferPos),
nil,
FILE_END
);
if NOT ReadFile(
hFile,
ABuffer^,
ABufferSize,
ABytesRead,
0
) then
Exit();
result := true;
finally
CloseHandle(hFile);
end;
end;
{-------------------------------------------------------------------------------
Get Target PE File EOF Size
return codes:
-------------
-1 : Error
>= 0 : The length of EOF data found
-------------------------------------------------------------------------------}
function GetPEOFSize(APEFile : String) : Int64;
var ABasicPEInfo : TBasicPEInfo;
begin
result := -1;
if NOT GetBasicPEInfo(APEFile, ABasicPEInfo) then
raise Exception.Create('Error: Invalid PE File');
result := (GetFileSize(APEFile) - ABasicPEInfo.ImageSize);
end;
{-------------------------------------------------------------------------------
Clear unexpected data at the end of a PE File
-------------------------------------------------------------------------------}
function ClearPEOF(APEFile : String) : Boolean;
var ABasicPEInfo : TBasicPEInfo;
AFileStream : TMemoryStream;
AFileSize : Int64;
AImageSize : Int64;
begin
result := False;
if NOT GetBasicPEInfo(APEFile, ABasicPEInfo) then
raise Exception.Create('Error: Invalid PE File');
AFileSize := GetFileSize(APEFile);
AImageSize := ABasicPEInfo.ImageSize;
// No EOF but no error so far so we return true
if (AFileSize - AImageSize) = 0 then begin
Exit(True);
end;
{
One technique to patch the file. Ignore content after the ImageSize
grabbed from PE info.
}
AFileStream := TMemoryStream.Create();
try
AFileStream.LoadFromFile(APEFile);
AFileStream.Position := 0;
AFileStream.SetSize(AImageSize);
AFileStream.SaveToFile(APEFile);
finally
AFileStream.Free;
end;
result := True;
end;
{-------------------------------------------------------------------------------
Detect if a PE file contain some data at the end of the file
-------------------------------------------------------------------------------}
function ContainPEOF(APEFile : String) : Boolean;
begin
result := (GetPEOFSize(APEFile) > 0);
end;
end.
EOF Reader (C++ Version)
An example about how to Read EOF Data in C++ can be find in the following Github repository: https://github.com/DarkCoderSc/eof-reader
/*
-----------------------------------------------------------------------------
Jean-Pierre LESUEUR (@DarkCoderSc)
jplesueur@phrozen.io
License : MIT
Read EOF (End Of File) Data from PE File.
Based on my previous work : https://github.com/DarkCoderSc/peof-detector/blob/master/UntEOF.pas
Compiled with : Visual Studio 2019 (Community)
Notice :
If you have any advices for improving the code or if you have any issues, feel free to contact me.
C++ is not yet my main language, always willing to learn ;-)
-----------------------------------------------------------------------------
*/
#include <iostream>
#include <fstream>
#include "windows.h"
#include <iomanip>
#include <sstream>
#include "termcolor/termcolor.hpp"
using namespace std;
/*
Log functions
*/
void log_error(const string &message) {
cerr << " " << termcolor::bloodred << "x" << termcolor::reset << " " << message << endl;
}
void log_debug(const string &message) {
cout << " " << "*" << " " << message << endl;
}
void log_success(const string &message) {
cout << " " << termcolor::lime << "*" << termcolor::reset << " " << message << endl;
}
void log_warn(const string &message = "") {
cout << " " << termcolor::yellow << "!" << termcolor::reset << " " << message << endl;
}
/*
Dump memory data to console.
*/
void HexDumpBufferToConsole(PVOID pBuffer, __int64 ABufferSize) {
cout << "| ------------------------------------------------|------------------|" << endl;
cout << "| 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F | |" << endl;
cout << "| ------------------------------------------------|------------------|" << endl;
for (int j = 0; j < ceil(ABufferSize / 16); j++) {
char AsciiColumns[17];
stringstream ARow;
for (int i = 0; i < 16; i++) {
unsigned char AChar = ((char*)pBuffer)[(j * 16) + i];
if (!isprint(AChar)) {
AChar = 46; // .
}
ARow << setfill('0') << setw(2) << hex << static_cast<unsigned int>(AChar) << " ";
AsciiColumns[i] = AChar;
}
AsciiColumns[16] = 0; // Add null terminated character.
cout << "| " << ARow.rdbuf() << "| " << AsciiColumns << " |" << endl;
}
cout << "| ------------------------------------------------|------------------|" << endl << endl;
}
/*
Dump memory data to file.
*/
bool WriteBufferToFile(PVOID pBuffer, __int64 ABufferSize, wstring ADestFile, PDWORD AErrorCode) {
SetLastError(0);
HANDLE hFile = CreateFile(ADestFile.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE) {
*AErrorCode = GetLastError();
return false;
}
DWORD dwBytesWritten = 0;
if (!WriteFile(hFile, pBuffer, ABufferSize, &dwBytesWritten, nullptr)) {
*AErrorCode = GetLastError();
CloseHandle(hFile);
return false;
}
CloseHandle(hFile);
return true;
}
/*
Basic way to read file size from disk
*/
__int64 GetFileSize(wchar_t AFileName[MAX_PATH]) {
LARGE_INTEGER AFileSize;
AFileSize.LowPart = 0;
AFileSize.HighPart = 0;
ifstream ifile(AFileName);
if (ifile) {
WIN32_FILE_ATTRIBUTE_DATA lpFileInfo;
if (GetFileAttributesExW(AFileName, GetFileExInfoStandard, &lpFileInfo)) {
AFileSize.HighPart = lpFileInfo.nFileSizeHigh;
AFileSize.LowPart = lpFileInfo.nFileSizeLow;
}
}
return AFileSize.QuadPart;
}
int main(int argc, char* argv[]) {
if (argc != 2) {
cout << "Usage : readeof.exe \"C:\\suspicious.exe\"" << endl;
return 0;
}
wchar_t AFileName[MAX_PATH] = { 0 };
for (int i = 0; i < strlen(argv[1]); i++) {
AFileName[i] = argv[1][i];
}
//GetModuleFileNameW(0, AFileName, MAX_PATH);
wcout << "Working on \"" << AFileName << "\" : " << endl << endl;
/*
Get target file size on disk.
*/
__int64 AFileSize = GetFileSize(AFileName);
if (AFileSize <= 0) {
log_error("Could not get target file size on disk. Abort.");
return 0;
}
log_success("File size on disk : " + to_string(AFileSize) + " bytes");
/*
Now we will compare with image size described by the PE Header.
*/
DWORD dwBytesRead = 0;
HANDLE hFile = CreateFile(AFileName, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, 0);
if (hFile == INVALID_HANDLE_VALUE) {
log_error("Could not open target file.");
return 0;
}
SetFilePointer(hFile, 0, nullptr, FILE_BEGIN);
/*
Read IMAGE_DOS_HEADER
*/
IMAGE_DOS_HEADER AImageDosHeader;
if (!ReadFile(hFile, &AImageDosHeader, sizeof(IMAGE_DOS_HEADER), &dwBytesRead, nullptr)) {
log_error("Could not read IMAGE_DOS_HEADER.");
CloseHandle(hFile);
return 0;
}
if (AImageDosHeader.e_magic != IMAGE_DOS_SIGNATURE) {
log_error("Not a valid PE File.");
CloseHandle(hFile);
return 0;
}
SetFilePointer(hFile, AImageDosHeader.e_lfanew, nullptr, FILE_BEGIN);
/*
Verify if if we match IMAGE_NT_SIGNATURE (0x4550)
*/
DWORD AImageNTSignature;
if (!ReadFile(hFile, &AImageNTSignature, sizeof(DWORD), &dwBytesRead, nullptr)) {
log_error("Could not read IMAGE_NT_SIGNATURE.");
CloseHandle(hFile);
return 0;
}
if (AImageNTSignature != IMAGE_NT_SIGNATURE) {
log_error("IMAGE_NT_SIGNATURE Doesn't match.");
CloseHandle(hFile);
return 0;
}
log_success("The file is likely a valid PE File.");
/*
At this point, we are enough sure we are facing a valid PE File.
Reading IMAGE_FILE_HEADER
*/
IMAGE_FILE_HEADER AImageFileHeader;
if (!ReadFile(hFile, &AImageFileHeader, sizeof(IMAGE_FILE_HEADER), &dwBytesRead, nullptr)) {
cout << "Could not read IMAGE_FILE_HEADER." << endl;
CloseHandle(hFile);
return 0;
}
// Checking if we are facing a x64 or x86 PE File.
bool x64 = (AImageFileHeader.Machine == IMAGE_FILE_MACHINE_AMD64);
log_debug(string("Facing ") + (x64 ? "64" : "32") + string("bit PE File."));
__int64 AImageSize = 0;
/*
Reading IMAGE_OPTIONAL_HEADER. Support both x64 and x64.
*/
if (x64) {
IMAGE_OPTIONAL_HEADER64 AOptionalHeader;
if (!ReadFile(hFile, &AOptionalHeader, sizeof(IMAGE_OPTIONAL_HEADER64), &dwBytesRead, nullptr)) {
log_error("Could not read IMAGE_OPTIONAL_HEADER64");
CloseHandle(hFile);
return 0;
}
/*
We don't forget to add the IMAGE_DIRERCTORY_ENTRY_SECURITY if target application is signed otherwise
the full image size wont match.
*/
AImageSize += (__int64(AOptionalHeader.SizeOfHeaders) + __int64(AOptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size));
}
else {
IMAGE_OPTIONAL_HEADER32 AOptionalHeader;
if (!ReadFile(hFile, &AOptionalHeader, sizeof(IMAGE_OPTIONAL_HEADER32), &dwBytesRead, nullptr)) {
log_error("Could not read IMAGE_OPTIONAL_HEADER32");
CloseHandle(hFile);
return 0;
}
// Same as above
AImageSize += (__int64(AOptionalHeader.SizeOfHeaders) + __int64(AOptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size));
}
/*
Enumerate each sections, and append to our current mesured image size.
*/
for (int i = 0; i < AImageFileHeader.NumberOfSections; i++) {
IMAGE_SECTION_HEADER AImageSectionHeader;
if (!ReadFile(hFile, &AImageSectionHeader, sizeof(IMAGE_SECTION_HEADER), &dwBytesRead, nullptr)) {
log_error("Fail to read section n°" + to_string(i));
CloseHandle(hFile);
return 0; // If one section fail to be read, then we loose.
}
AImageSize += AImageSectionHeader.SizeOfRawData;
}
log_success("Image Size successfully calculated : " + to_string(AImageSize) + " bytes");
/*
Checking if some EOF data is present in target file.
*/
unsigned AEOFSize = (AFileSize - AImageSize);
if (AEOFSize > 0) {
log_warn(to_string(AEOFSize) + " bytes of EOF Data detected.");
/*
Read EOF Data
*/
log_debug("Extracting / Printing EOF Data:");
cout << endl;
SetFilePointer(hFile, (AFileSize - AEOFSize), nullptr, FILE_BEGIN); // Could also use FILE_END
PVOID pBuffer = malloc(AEOFSize);
if (!ReadFile(hFile, pBuffer, AEOFSize, &dwBytesRead, nullptr)) {
log_error("Could not read EOF data.");
}
else {
/*
Print EOF data.
*/
HexDumpBufferToConsole(pBuffer, AEOFSize);
}
/*
Offering user to dump EOF Data to file
*/
cout << "Do you want to dump the content of EOF Data ? (y/n) : ";
string s = "";
cin.width(1); // we only take care of first character.
cin >> s;
if (s == "y") {
cout << "Output file path : ";
wstring AOutputPath;
cin.width(MAX_PATH);
wcin >> AOutputPath;
/*
Write EOF data to file
*/
DWORD AErrorCode = 0;
if (!WriteBufferToFile(pBuffer, AEOFSize, AOutputPath, &AErrorCode)) {
log_error("Could no write EOF data to file with error " + to_string(AErrorCode));
}
else
{
log_success("EOF data successfully dumped.");
};
}
free(pBuffer);
}
else {
log_success("No EOF data detected so far.");
}
CloseHandle(hFile);
return 0;
}
Written the Nov. 23, 2020, 10:19 a.m. by Jean-Pierre LESUEUR
Updated: ago.