DEV Community

Stefor07
Stefor07

Posted on • Edited on

How to Easily Distinguish Multiple Binary Payloads Files Appended to a Win32 Executable

In this article, you'll learn how to reliably identify and extract multiple binary payloads appended to the end of an executable file using only offsets and size calculations.

While experimenting with binary file concatenation on Windows executables, I discovered a simple and practical method to distinguish and load multiple appended files — without relying on any extra markers, headers, or metadata.

Unlike the technique shown in the fourth method How to Embed Binary Data into a Win32 Executable File in 4 Methods, which supports only a single appended file, this approach scales naturally to multiple appended files, with a clean and straightforward logic.


Step 1: Declaring Helper Functions in Code

Add these helper functions in your code:

1. Determine the real size of the executable (excluding any appended binary data).
2. Load the appended binary data into memory using the new memory mapping helper function which:

  • Accepts a struct argument to store all necessary handles for proper cleanup;
  • Takes a DWORD argument to specify how many bytes to ignore at the beginning (the executable + any previously handled appended files);
  • Ensures safe and efficient resource management by allowing explicit freeing of memory-mapped views and handles.

Gets the actual size of the EXE excluding appended binary data

DWORD GetRealExeSize(const TCHAR* exePath)
{
    HANDLE hFile = CreateFile(exePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return 0;

    HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
    if (!hMapping) { CloseHandle(hFile); return 0; }

    LPBYTE pBase = (LPBYTE)MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
    if (!pBase) { CloseHandle(hMapping); CloseHandle(hFile); return 0; }

    IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)pBase;
    if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
    {
        UnmapViewOfFile(pBase);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        return 0;
    }

    IMAGE_NT_HEADERS* pNtHeaders = (IMAGE_NT_HEADERS*)(pBase + pDosHeader->e_lfanew);
    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
    {
        UnmapViewOfFile(pBase);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        return 0;
    };

    DWORD maxEnd = 0;
    IMAGE_SECTION_HEADER* pSection = IMAGE_FIRST_SECTION(pNtHeaders);
    for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; ++i)
    {
        DWORD sectionEnd = pSection[i].PointerToRawData + pSection[i].SizeOfRawData;
        if (sectionEnd > maxEnd) maxEnd = sectionEnd;
    }


    UnmapViewOfFile(pBase);
    CloseHandle(hMapping);
    CloseHandle(hFile);
    return maxEnd;
}
Enter fullscreen mode Exit fullscreen mode

Version 1: Heap allocation (for small/medium data)

BYTE* LoadAppendedData(DWORD& outSize, DWORD32 ignoredSize)
{
    TCHAR exePath[MAX_PATH] = { 0 };
    GetModuleFileName(NULL, exePath, MAX_PATH);

    DWORD exeSize = GetRealExeSize(exePath);

    DWORD totalIgnoredSize = exeSize + ignoredSize;

    HANDLE hFile = CreateFile(exePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return nullptr;

    LARGE_INTEGER fileSize;
    if (!GetFileSizeEx(hFile, &fileSize)) { CloseHandle(hFile); return nullptr; }

    if (fileSize.QuadPart <= totalIgnoredSize) { CloseHandle(hFile); return nullptr; }

    DWORD appendedSize = (DWORD)(fileSize.QuadPart - totalIgnoredSize);
    BYTE* data = new BYTE[appendedSize];

    SetFilePointer(hFile, totalIgnoredSize, NULL, FILE_BEGIN);
    DWORD bytesRead = 0;
    ReadFile(hFile, data, appendedSize, &bytesRead, NULL);
    CloseHandle(hFile);

    if (bytesRead != appendedSize) { delete[] data; return nullptr; }

    outSize = appendedSize;
    return data;
}
Enter fullscreen mode Exit fullscreen mode

Version 2: Memory-mapped (recommended for large files)

Before declaring the helper function, you should define a structure to store the necessary handles for releasing memory-mapped resources safely:

struct MappedAppendedData
{
    BYTE* mappedView = nullptr;   // Pointer to the full mapped view (used in UnmapViewOfFile)
    BYTE* basePtr = nullptr;      // Offset to where the appended data starts
    size_t totalSize = 0;
    HANDLE hFile = NULL;
    HANDLE hMapping = NULL;
};

// Declare an instance of the structure (ideally as extern if used across multiple files)
MappedAppendedData g_mappedAppendedData;
Enter fullscreen mode Exit fullscreen mode

Now, declare the helper function as follows:

BYTE* LoadAppendedData(MappedAppendedData& mappedDataStruct, DWORD& outSize, DWORD ignoredSize)
{
    TCHAR exePath[MAX_PATH] = { 0 };
    GetModuleFileName(NULL, exePath, MAX_PATH);

    DWORD exeSize = GetRealExeSize(exePath);

    DWORD totalIgnoredSize = exeSize + ignoredSize;

    HANDLE hFile = CreateFile(exePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return nullptr;

    HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
    if (!hMapping) { CloseHandle(hFile); return nullptr; }

    LPBYTE pBase = (LPBYTE)MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
    if (!pBase) { CloseHandle(hMapping); CloseHandle(hFile); return nullptr; }

    LARGE_INTEGER fileSize;
    if (!GetFileSizeEx(hFile, &fileSize)) {
        UnmapViewOfFile(pBase);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        return nullptr;
    }

    DWORD appendedSize = (DWORD)(fileSize.QuadPart - totalIgnoredSize);
    if (appendedSize == 0) {
        UnmapViewOfFile(pBase);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        return nullptr;
    }

    mappedDataStruct.mappedView = pBase;
    mappedDataStruct.basePtr = (pBase + totalIgnoredSize);
    mappedDataStruct.hFile = hFile;
    mappedDataStruct.hMapping = hMapping;
    mappedDataStruct.totalSize = appendedSize;

    outSize = appendedSize;
    return pBase + totalIgnoredSize;
}
Enter fullscreen mode Exit fullscreen mode

Then you will need another function to clean up the mapped memory:

Clean Mapped Memory

void FreeMappedAppendedData(MappedAppendedData& data)
{
    if (data.mappedView)
    {
        UnmapViewOfFile(data.mappedView);
        data.mappedView = nullptr;
        data.basePtr = nullptr;
    }

    if (data.hMapping)
    {
        CloseHandle(data.hMapping);
        data.hMapping = NULL;
    }

    if (data.hFile)
    {
        CloseHandle(data.hFile);
        data.hFile = NULL;
    }

    data.totalSize = 0;
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Deprecated Variant (Simplified)

This is an older, simplified version of the memory mapping function.
It works for quick testing, but does not store or release the file and mapping handles, which can result in serious memory issues.

BYTE* LoadAppendedData(DWORD& outSize, DWORD ignoredSize)
{
    TCHAR exePath[MAX_PATH] = { 0 };
    GetModuleFileName(NULL, exePath, MAX_PATH);

    DWORD exeSize = GetRealExeSize(exePath);

    DWORD totalIgnoredSize = exeSize + ignoredSize;

    HANDLE hFile = CreateFile(exePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return nullptr;

    HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
    if (!hMapping) { CloseHandle(hFile); return nullptr; }

    LPBYTE pBase = (LPBYTE)MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
    if (!pBase) { CloseHandle(hMapping); CloseHandle(hFile); return nullptr; }

    LARGE_INTEGER fileSize;
    if (!GetFileSizeEx(hFile, &fileSize)) {
        UnmapViewOfFile(pBase);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        return nullptr;
    }

    DWORD appendedSize = (DWORD)(fileSize.QuadPart - totalIgnoredSize);
    if (appendedSize == 0) {
        UnmapViewOfFile(pBase);
        CloseHandle(hMapping);
        CloseHandle(hFile);
        return nullptr;
    }

    outSize = appendedSize;
    return pBase + totalIgnoredSize;
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Note:

This version does not track or release system handles (HANDLE, MapViewOfFile).

Mapping multiple files with this function can quickly exhaust system resources, leading to ERROR_NOT_ENOUGH_MEMORY (8) or even application instability.

Do not use in production environments.

Later in this guide, I’ll explain how to map all the appended data in memory at once, and how to access each segment by declaring pointers and calculating their respective sizes.


Step 2: Files Size Retrieving

Suppose we now want to embed multiple files into our application—for example, three files appended at the end of the executable.

Let's imagine that the various files have different sizes:

  • The first file is 153 MB with a size in 160432128 bytes
  • The second file is 51 MB with a size in 53477376 bytes
  • The third file is 79 MB with a size in 82837504 bytes

The best way to determine the exact size of the files you want to embed is as follows:

1. Compile the application with the minimal executable (without any appended data).
2. Copy or move the files to be appended to the executable in the build folder (Debug or Release)
3. Open PowerShell in the current folder and run this command: dir

The output will look like this:

    Directory: [Build Directory Path]

Mode     Length       Name
----     ------       ----
-a----   160432128    file1.bin
-a----    53477376    file2.bin
-a----    82837504    file3.bin
-a----    12345678    your_application.exe
-a----    12345678    your_application.pdb
Enter fullscreen mode Exit fullscreen mode

In this way we can note the size of all the various files, copy them or keep the Powershell window open

Step 3: Accessing Embedded Files at Runtime

Once we know the sizes and order of the files to append on the executable, we can access them at runtime by calculating the correct offset and expected size.

The process depends on whether the target file is the first, intermediate, or last in the concatenated bundle.

Example: Appended Files Overview

/*
    Appended files (in order):

    File 1 size: 160,432,128 bytes
    File 2 size:  53,477,376 bytes
    File 3 size:  82,837,504 bytes
*/
Enter fullscreen mode Exit fullscreen mode

Accessing the First Appended File (Simplest Case)

Offset is zero. If LoadAppendedData gives you the full remaining data, you must subtract the sizes of the files that come after.

DWORD embedded_data_size = 0;
BYTE* embedded_data = LoadAppendedData(embedded_data_size, 0);

// Adjust to only include the first file
embedded_data_size -= (fileSize2 + fileSize3);
Enter fullscreen mode Exit fullscreen mode

Accessing the Last Appended File (Simplest Case)

Only sum the sizes of the files that come before it to calculate the offset. You don't need to subtract anything.

DWORD embedded_data_size = 0;
DWORD offset = (fileSize1 + fileSize2);

BYTE* embedded_data = LoadAppendedData(embedded_data_size, offset);

// embedded_data_size is automatically the size of File 3 (last file)
Enter fullscreen mode Exit fullscreen mode

Accessing a File in the Middle

To access a file that has others before and after it, you need to:

1. Add the sizes of the files that come before it → this gives you the correct offset
2. Subtract the sizes of the files that come after it

→ This is required to determine the correct size, unless LoadAppendedData handles the size internally

DWORD embedded_data_size = 0;
DWORD offset = fileSize1;

BYTE* embedded_data = LoadAppendedData(embedded_data_size, offset);

// Remove trailing files to isolate only File 2
embedded_data_size -= fileSize3;
Enter fullscreen mode Exit fullscreen mode

⚠️ Important: ALWAYS remember to call delete whenever the pointer pointing to the added data is no longer needed to avoid memory leaks if you are using the heap

⚠️ Tip: To be better and to avoid confusion, enter the size of each file in the order in which they will be appended.

ℹ️ Tip: Obviously replace fileSize1, fileSize2 and fileSize3 with the various sizes of our files that will be appended, there can be even more than three

✅ General Rule

To access a specific file from a concatenated bundle, you need to know:

  • "How much to skip (offset = sum of previous files)"
  • "How much to read (size = total size − sum of following files)"

Then pass these to LoadAppendedData.

🔁 This logic doesn't change, no matter how many files are appended.

💡 Optional Tip

To make this robust, define constants or an array of sizes:

const DWORD fileSizes[] = { fileSize1, fileSize2, fileSize3 };

DWORD GetOffset(int fileIndex) {
    DWORD offset = 0;
    for (int i = 0; i < fileIndex; ++i)
        offset += fileSizes[i];
    return offset;
}

DWORD GetSize(int fileIndex) {
    DWORD size = 0;
    for (int i = fileIndex + 1; i < std::size(fileSizes); ++i)
        size += fileSizes[i];
    return fileSizes[fileIndex]; // or subtract from total if needed
}
Enter fullscreen mode Exit fullscreen mode

Improved method for memory mapping

The calculation and access logic for appended file blocks (using offsets and sizes) remains the same.

However, the updated LoadAppendedData() function now takes a MappedAppendedData struct to manage system resources, enabling safe cleanup with a single FreeMappedAppendedData() call.

📂 Accessing Appended Files (General Case)

To access any appended file, follow this consistent logic:

  1. Offset = Sum of sizes of all preceding files
  2. Size = Known size of the file (or total appended size minus the sizes of the trailing files)

Use LoadAppendedData() with a MappedAppendedData struct to manage resources safely.

🧩 Example

DWORD embedded_data_size = 0;
DWORD offset = /* sum of previous files */;

BYTE* embedded_data = LoadAppendedData(g_mappedAppendedData, embedded_data_size, offset);

// If needed, subtract the size of subsequent files
// embedded_data_size -= /* size of trailing files */;

// After use, free resources
// FreeMappedAppendedData(g_mappedAppendedData);
Enter fullscreen mode Exit fullscreen mode

🔁 This is essentially the same strategy as previously described; however, the procedure must be repeated for each individual file by declaring a separate instance of the MappedAppendedData struct.

📌 Alternative Access Strategy

Instead of calculating each appended file’s size by subtracting the sizes of subsequent files, you can:

  • Manually specify file sizes, if already known.
  • Use pointer arithmetic to locate each file by summing the sizes of the previous ones.

This method is cleaner and more efficient when all appended file sizes are known in advance.

✅ Example

// Global or extern pointer to the full appended data block
DWORD fullAppendedDataSize = 0;
BYTE* dataPtr = nullptr;

// Map the entire appended section once (after the executable)
dataPtr = LoadAppendedData(g_mappedAppendedData, fullAppendedDataSize, 0);

// Known file sizes (in bytes)
const DWORD fileSize1 = 160432128;
const DWORD fileSize2 = 53477376;
const DWORD fileSize3 = 82837504;

// Access each file using pointer arithmetic
const BYTE* file1 = dataPtr;                          // First file
const BYTE* file2 = dataPtr + fileSize1;              // Second file
const BYTE* file3 = dataPtr + (fileSize1 + fileSize2);  // Third file

// Clean up mapped memory and handles when done
// FreeMappedAppendedData(g_mappedAppendedData);
Enter fullscreen mode Exit fullscreen mode

This approach eliminates the need for runtime size calculations and is most effective when the sizes of the appended files are known or fixed.

You can also expose global pointers using extern declarations in a dedicated header file, allowing both the appended data and the helper functions to be accessed throughout the application.

In both scenarios, resource handling is centralized through the MappedAppendedData structure, which can be cleanly released at any time with a single call to FreeMappedAppendedData().


💡 Bonus: Check the integrity of appended files

Verify the integrity of the added files by adding their sizes and comparing the total to the size returned by the LoadAppendedData function. If they don't match, tampering may have occurred.

Here is a practical example:

const DWORD expectedAppendedDataSize = (fileSize1 + fileSize2 + fileSize3); // sum of all appended files

if (expectedAppendedDataSize != fullAppendedDataSize)
{
    // Possible tampering detected — take appropriate action or close the application
}
Enter fullscreen mode Exit fullscreen mode

Additionally, you can check the validity of pointers that reference the added data by updating the condition to also check for null pointers:

if (!file1 || !file2 || !file3 || expectedAppendedDataSize != fullAppendedDataSize)
{
    // Possible tampering detected — take appropriate action or close the application
}
Enter fullscreen mode Exit fullscreen mode

If the application is also protected with an Authenticode signature, include its size in the total:

 DWORD signSize = GetAuthenticodeSignatureSize();

 const DWORD expectedAppendedDataSize = (fileSize1 + fileSize2 + fileSize3) + signSize; // Also add the size of the signature
Enter fullscreen mode Exit fullscreen mode

You can find the GetAuthenticodeSignatureSize function below, which returns the size of the Authenticode signature if your application is protected by one.

⚠️ Note: This technique only detects coarse tampering, such as deleting, truncating, or adding files. It does not verify the integrity of the file content itself. For more effective protection, i recommend using cryptographic hashes or digital signature integrity checks.


Step 4: Binary File Concatenation

Once the application has been compiled, open a Command Prompt in the build directory and run the following command to append the binary files expected by the application:

copy /b stub.exe + file1.bin + file2.bin + file3.bin final_stub.exe
Enter fullscreen mode Exit fullscreen mode

This command creates a new executable (final_app.exe) that contains the original application followed by the three binary files appended in order.

At this point, the files have been embedded into the executable, and the application is ready to be tested.


Comparison of Memory Mapping Access Strategies

Aspect Version 1 (single offset) Version 2 (full mapping)
Data loaded Only the required portion Entire appended section
Handling multiple files Need to calculate offset for each file Pointer arithmetic allows direct access
Complexity More flexible if you only need 1 file More efficient if using all files
Resources Possibly lighter in memory Uses more memory but avoids multiple mappings
Updating embedded_data_size Yes, you can trim Not needed, file sizes are known

As shown in the comparison table above, we can understand the optimal data-access strategy by considering how memory is mapped into the application's virtual address space.

Version 1 — Single-Offset Mapping

Version 1 performs a single mapping by supplying an offset to skip a given number of bytes past the end of the PE structure. Once the BYTE* pointer and the local variable storing the file size are returned by LoadAppendedData, the size of any trailing files can be subtracted to avoid accidentally reading beyond the intended block.

This approach has the advantage of creating a per-file mapping, but quickly becomes impractical when many added files are involved. Multiple simultaneous mappings increase the risk of memory leaks, make the code more difficult to maintain, and can exhaust the available virtual address space, especially in 32-bit applications, where mapping large files (approaching or exceeding 4 GB) becomes unreliable, whereas 64-bit applications typically do not have this limitation.

Version 2 — Full Mapping + Pointer Arithmetic

Version 2 is far more convenient because it performs a single mapping of the entire appended data block. By passing an offset of 0, the function returns a BYTE* pointer and a DWORD variable containing the total size of all appended files.

Once this memory area is mapped, the returned pointer naturally points to the first file. Accessing the subsequent files becomes trivial: simply use pointer arithmetic by adding the sizes of the preceding files to move to the correct memory location.

This allows all appended files to be accessed during runtime without requiring multiple mappings. It also makes it possible to declare global pointers to each appended file—alongside the helper function FreeMappedAppendedData—which can be called from anywhere in the application to release the associated system resources.

Recommendation for 32-bit Applications

In 32-bit applications, this method is highly recommended, because using a single mapping for the entire appended blob avoids fragmentation of the virtual address space and ensures stable access to all files, provided the total appended data does not exceed the 4 GB limit of the 32-bit address space.


⚠️ Memory Mapping Pitfalls in 32-bit Applications

I found that repeatedly calling LoadAppendedData (using the deprecated helper function) and declaring multiple BYTE* pointers to map different sections eventually caused a system error after loading approximately 600 MB of data.

As mentioned earlier, this led to an ERROR_NOT_ENOUGH_MEMORY (8) due to unreleased memory-mapped handles.

⚠️ While this issue typically doesn’t occur in 64-bit processes, it can become a serious limitation in 32-bit applications, where virtual address space is constrained.

✅ Why a Single Memory Mapping is Better

One key advantage of using a single memory-mapped block (via MappedAppendedData) is that it is more efficient and manageable in both design and cleanup.

Without this approach, you would need to:

  • Declare one instance of MappedAppendedData for each appended file;
  • Manually call FreeMappedAppendedData() on each instance to release its resources (mapped view, handles, memory).

This adds complexity and increases the risk of resource leaks — especially when working with multiple files.

In contrast, with a single memory-mapped block, you can:

  • Map all appended data at once;
  • Simply declare BYTE* pointers and calculate offsets to access each embedded file;
  • Call FreeMappedAppendedData() just once, making memory management significantly easier and safer.

✅ Using a single MappedAppendedData structure for all appended content is not only more efficient — it also results in cleaner, more maintainable code.

Otherwise, you'd need one struct per file and call FreeMappedAppendedData() on each, increasing the risk of memory/resource leaks.

🧠 Summary

  • ❌ Avoid repeated memory mappings without freeing them — especially in 32-bit applications.
  • ✅ Prefer a single, centralized memory-mapped block.
  • ✅ Always free resources using FreeMappedAppendedData() once done.

⚠️ Warning: Authenticode Signature and Appended Data

If you add binary data to the executable after compilation and before signing, be aware that applying an Authenticode signature (e.g., with signtool.exe) will slightly bloat the final executable. This may corrupt the binary payloads appended after the executable, or shift their offsets, making them unreadable at runtime.

To ensure correct offset calculation and avoid accidental inclusion of the Authenticode signature in your embedded data:

Before reading all the appended binary content, subtract the size of the Authenticode signature.

Here is a helper function to obtain the size of the embedded Authenticode signature:

DWORD GetAuthenticodeSignatureSize()
{
    HMODULE hModule = GetModuleHandle(nullptr);
    if (!hModule)
        return 0;

    PIMAGE_DOS_HEADER pDosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(hModule);

    if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
        return 0;

    PIMAGE_NT_HEADERS pNtHeaders = reinterpret_cast<PIMAGE_NT_HEADERS>(
        reinterpret_cast<BYTE*>(hModule) + pDosHeader->e_lfanew);

    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
        return 0;

    const IMAGE_DATA_DIRECTORY& certDir =
        pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY];

    return certDir.Size;
}
Enter fullscreen mode Exit fullscreen mode

Use the returned size to adjust your file offset before extracting or reading embedded binary data:

DWORD embedded_data_size = 0;
DWORD offset = fileSize1 + fileSize2;
DWORD signSize = GetAuthenticodeSignatureSize(); // Returns the Authenticode signature size from the helper function

BYTE* embedded_data = LoadAppendedData(embedded_data_size, offset);

offset -= signSize; // Exclude signature from the tail
Enter fullscreen mode Exit fullscreen mode

⚠️ Warning : When calculating the size of previous files to access a specific file in memory, you must always subtract the size of the Authenticode signature. This is true even if, technically, the Authenticode signature is located immediately after the last file appended.

🔄 Tip : If you use pointer arithmetic on the added files and define the size of each file in a dedicated variable, you will not need to subtract the length of the Authenticode digital signature unless you perform runtime tampering checks.

This step is not mandatory but it is recommended if you want to keep everything clean and avoid working with extra useless signature, it is however advisable to subtract the size of the signature always to exclude it from the actual total size of the appended files to avoid discrepancies

✅ Why This Matters

  • Authenticode signatures live outside the PE sections, in a structure called WIN_CERTIFICATE, appended at the file's end.

  • If you read embedded content from the end of the file without accounting for the signature, you'll include that signature garbage, corrupting your parsing logic.

  • This issue is especially relevant when:

    • Using copy /b app.exe + data.bin final.exe
    • Loading or mapping data from the tail of a signed executable

How does this method work?

The core idea is to first determine the size of the clean executable, meaning the actual size defined by the PE structure (i.e. the end of the last section). For this, we use a function that parses the PE header and finds where the real executable content ends.

Once we know the original executable size:

  • The first appended file starts exactly after this point.
  • To retrieve a file in the middle, we calculate its offset by summing the sizes of all files before it.
  • To determine its size, we subtract the sizes of all the files appended after it (unless the function handles this internally).

This logic is simple, scalable, and does not require any metadata or markers inside the executable — just plain math using file sizes.


⚖️ Pros and Cons of This Approach

✔️ Advantages

  • Simple: No need for custom file formats, headers, or metadata
  • Clean: No need to modify the PE structure or embed resources
  • Scalable: Works with any number of appended files
  • Self-contained: All files are bundled into a single executable
  • Portable: No dependencies or external tools required at runtime

❌ Disadvantages

  • Fragile: Any mismatch in size or order breaks data access
  • No autodetection: You must hardcode or externally manage offsets and sizes
  • No security: Appended data can be easily extracted or modified
  • No integrity checks: Corruption or tampering is not detectable by default
  • Sensitive to signing: Authenticode signatures alter the file size and must be accounted for

✅ When to Use This

This method is ideal for:

  • Internal tools or portable utilities
  • Controlled environments
  • Self-extracting utilities or installers
  • Situations where simplicity and size matter more than security

It is not recommended for:

  • Untrusted or public distribution scenarios
  • Applications that require data integrity or confidentiality
  • Systems needing runtime discoverability or flexibility

🛡️ How to Make It More Robust

To mitigate the risks and improve reliability, consider:

  • Adding a small metadata block (e.g. magic number, count, size table)
  • Using hashes or checksums for each embedded file
  • Encrypting sensitive data before appending
  • Signing the executable after appending data, and adjusting for signature size in the reader

🔐 Further Reading

If you want to enhance this method by adding integrity checks and validation, check out the follow-up article:

🔗 How to Protect Binary Data Appended into an Executable and Validate it with a Digital Signature

It explains how to prevent tampering by using digital signatures, providing a much stronger foundation for production use.


In summary, this technique offers a clean, zero-overhead way to append and load multiple binary resources directly from your executable. It shines in simplicity and flexibility — but like all minimalist solutions, it comes with trade-offs in safety and robustness.

🔁 Important: Using markers may seem convenient for a few files, but it quickly becomes unmanageable as the number of files grows. Each file would require its own marker, resulting in increased complexity and error potential.

🧠 Unlike other approaches that use metadata structures to describe embedded files (e.g., names, offsets, sizes), this method relies only on a priori known offsets and sizes, making it simpler, more robust, and free of parsing or complex logic.

This method, on the other hand, is scalable, deterministic, and cleaner, making it ideal even for dozens of embedded files.

Adapt it wisely to your context!


Conclusion

In this article we've explored a straightforward and reliable method to embed and access multiple binary files appended sequentially to a Windows executable — all without relying on additional markers, headers, or metadata.

By understanding the real size of the executable (based on PE section data) and applying simple arithmetic with the sizes of the appended files, you can calculate offsets and sizes to correctly extract each embedded payload at runtime.

Top comments (0)