How to Safely Preserve PE Overlay Data in Packers

This article explains how to preserve PE Overlay data without loss during executable packing. It focuses on locating the end of the last section, extracting appended file-tail data, and safely writing it back after adding a new packer section. The goal is to prevent issues such as lost configuration, broken resources, and post-packing runtime regressions. Keywords: PE files, Overlay, packing.

Technical specification snapshot

Parameter Description
Language C++
Platform Windows
File Format PE / Portable Executable
Key Structures IMAGE_DOS_HEADER, IMAGE_NT_HEADERS, IMAGE_SECTION_HEADER
Core Dependencies windows.h, fstream, vector
Core Standard PE file format specification
Stars Not provided in the source

Overlay data is not part of the PE image, but it often determines whether the program is complete

Overlay is the raw byte stream appended after the last section of a PE file. It is not loaded as part of the standard image, but applications often use it to store configuration, patch data, multilingual resources, or secondary payloads.

For a packer, the biggest risk is usually not “broken stub logic” but “rewriting only the image and forgetting the file tail.” Once Overlay is truncated or discarded, the program may still launch, but its business logic can fail silently.

The formula for identifying Overlay is straightforward

DWORD overlayStart = lastSection.PointerToRawData + lastSection.SizeOfRawData; // Calculate the file end of the last section
DWORD overlaySize  = fileSize - overlayStart; // Subtract the image end from the total file size

This logic determines whether extra data exists at the end of the file that is not declared in the section table.

Overlay must be located by file offsets rather than memory addresses

The key to detecting Overlay is finding the end position of the last section from the file’s perspective. You must use PointerToRawData and SizeOfRawData, not VirtualAddress or VirtualSize.

The reason is simple: Overlay exists at the end of the on-disk file, while the PE loader operates on the in-memory image. If you use memory-based fields to calculate the file tail, the result is often too small or too large, which leads to incorrect Overlay boundaries.

A practical Overlay extraction implementation looks like this

#include <windows.h>
#include 
<fstream>
#include 
<vector>

bool ExtractOverlayData(const char* peFilePath, std::vector
<BYTE>& overlayData) {
    std::ifstream file(peFilePath, std::ios::binary | std::ios::ate);
    if (!file.is_open()) return false;

    DWORD fileSize = static_cast
<DWORD>(file.tellg()); // Get the total file size
    file.seekg(0, std::ios::beg);

    IMAGE_DOS_HEADER dosHeader;
    file.read(reinterpret_cast<char*>(&dosHeader), sizeof(dosHeader));
    if (dosHeader.e_magic != IMAGE_DOS_SIGNATURE) return false; // Validate the DOS header

    file.seekg(dosHeader.e_lfanew, std::ios::beg);
    IMAGE_NT_HEADERS ntHeaders;
    file.read(reinterpret_cast<char*>(&ntHeaders), sizeof(ntHeaders));
    if (ntHeaders.Signature != IMAGE_NT_SIGNATURE) return false; // Validate the NT header

    file.seekg(dosHeader.e_lfanew + sizeof(IMAGE_NT_HEADERS), std::ios::beg);

    DWORD lastSectionEndOffset = 0;
    for (int i = 0; i < ntHeaders.FileHeader.NumberOfSections; ++i) {
        IMAGE_SECTION_HEADER sectionHeader;
        file.read(reinterpret_cast<char*>(&sectionHeader), sizeof(sectionHeader));

        DWORD sectionEnd = sectionHeader.PointerToRawData + sectionHeader.SizeOfRawData; // Calculate the file end of the section
        if (sectionEnd > lastSectionEndOffset) {
            lastSectionEndOffset = sectionEnd; // Keep the furthest section end position
        }
    }

    if (fileSize <= lastSectionEndOffset) return false; // No Overlay present

    DWORD overlaySize = fileSize - lastSectionEndOffset;
    overlayData.resize(overlaySize);
    file.seekg(lastSectionEndOffset, std::ios::beg);
    file.read(reinterpret_cast<char*>(overlayData.data()), overlaySize); // Read the Overlay data
    return true;
}

This code completes three core steps: PE header validation, section traversal, and Overlay extraction.

During packer write-back, Overlay must be handled as a separate asset

The correct workflow is not to “modify the original file tail in place,” but to “cache the Overlay first, then rebuild the new file.” Adding a new packer section changes the section table, file length, and last-section boundary. In-place modification can easily overwrite appended data.

The recommended process is: parse the original PE, cache the Overlay, insert the new section, write the new image, and finally append the original Overlay bytes to the end of the new file. This is the most stable approach and the easiest to debug and compare.

A safe write-back order can be abstracted as follows

// Pseudocode: show the file generation order after packing
WriteHeaders(newFile);              // Write the modified DOS/NT headers and section table
WriteOriginalSections(newFile);     // Write the original section data
WriteStubSection(newFile);          // Write the newly added packer section
WriteOverlay(newFile, overlayData); // Append the original Overlay data

This order ensures that Overlay always remains at the end of the final file and is never overwritten by the packer section.

Alignment rules must strictly follow the PE specification when adding a new section

The most common error when adding a new section is incorrect alignment handling. PointerToRawData and SizeOfRawData must comply with FileAlignment, while VirtualAddress and VirtualSize must follow SectionAlignment.

One subtle but important detail is that Overlay itself usually does not participate in section alignment. It only needs to be placed immediately after the last written section. In other words, you align the packer section, not the Overlay itself.

A common alignment helper can be implemented like this

DWORD AlignUp(DWORD value, DWORD alignment) {
    return (value + alignment - 1) / alignment * alignment; // Round up to the alignment boundary
}

Use this function to calculate the file offset and size of the new section and avoid generating an invalid PE file.

You should verify both structure and behavior when validating Overlay preservation

A larger output file alone does not prove that Overlay was preserved correctly. A more reliable method is double validation: first inspect the appended data after the last section with a PE analysis tool, then run the target program and confirm that configuration, resources, or custom logic still behave as expected.

If the packed program still starts but behaves incorrectly, especially when it depends on external configuration, language packs, or embedded resources, check first whether the Overlay was truncated, duplicated, or written back at the wrong offset.

Common troubleshooting checklist

// Debug checkpoints
// 1. Verify that lastSectionEndOffset is the maximum end offset across all sections
// 2. Verify that the new section's PointerToRawData is aligned to FileAlignment
// 3. Verify that Overlay is appended only after all section data has been written
// 4. Verify that the final output file size equals image size + Overlay size

This checklist works well as a regression baseline during early packer development.

A robust packer must explicitly model the Overlay lifecycle

From an engineering perspective, Overlay is not incidental data that you preserve “if convenient.” It is a file asset that matters just as much as sections, the entry point, and relocations. The safest design is to model it explicitly as a three-step lifecycle: extract -> cache -> append.

The benefits are clear: you avoid functional regressions, reduce debugging complexity, and make the packer more compatible with real-world production samples.

FAQ

1. Why can’t I use VirtualAddress and VirtualSize to locate Overlay?

Because Overlay belongs to the end of the on-disk file, not to the loaded memory image. You must locate it with the file-offset fields PointerToRawData and SizeOfRawData.

2. Does Overlay need to be realigned after I add a new packer section?

Usually no. The new section itself must be aligned according to FileAlignment, while Overlay only needs to be appended directly after all section data has been written.

3. If the packed program launches but behaves incorrectly, what should I check first?

Check first whether Overlay was preserved intact, including whether its starting offset was calculated correctly, whether the write-back order places it after the packer section, and whether the final number of written Overlay bytes matches the original file.

Summary

This article systematically breaks down how to locate, extract, cache, and write back PE Overlay data in packing scenarios. It focuses on file-offset calculation, section insertion, alignment handling, and compatibility validation, helping developers avoid functional issues caused by missing or corrupted Overlay data.