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-detector

Description

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.