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

While the way you calculate and access appended file blocks (via offsets and sizes) doesn't change,
the updated LoadAppendedData() function now accepts a MappedAppendedData struct to keep track of system resources —
allowing safe cleanup with a single call to FreeMappedAppendedData().

📂 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

📌 Alternative Access Strategy

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

  • Manually set the file size, if it is already known;
  • Use pointer arithmetic to access the starting point of each file by summing the sizes of the previous ones.

This approach is much cleaner and more efficient when the sizes of all appended files are known in advance.

✅ Example

// You can declare a global or extern pointer that maps the entire appended data block in memory
DWORD fullAppendedDataSize = 0; // Stores the total size of all appended data
BYTE* dataPtr = nullptr;        // Will point to the beginning of the appended data

// Somewhere in your initialization function, map the data after the executable
dataPtr = LoadAppendedData(g_mappedAppendedData, fullAppendedDataSize, 0);

// Known sizes of the appended files (in bytes)
DWORD fileSize1 = 160432128;
DWORD fileSize2 = 53477376;
DWORD fileSize3 = 82837504;

// Access each file using pointer arithmetic
BYTE* file1 = dataPtr;                                 // First file starts at the base
BYTE* file2 = dataPtr + fileSize1;                     // Second file starts after the first
BYTE* file3 = dataPtr + (fileSize1 + fileSize2);         // Third file starts after the second

// Always call FreeMappedAppendedData before exiting or when no longer needed, to release mapped memory and handles.
Enter fullscreen mode Exit fullscreen mode

🔄 This strategy eliminates the need for dynamic size calculations and is ideal when the file sizes are fixed or hardcoded.

The method for accessing appended files remains the same — using consistent offset and size calculations. The key difference is that LoadAppendedData now takes a MappedAppendedData struct to track pointers and system resources, which can be safely released at the end with a single call.


💡 Bonus: Check the integrity of appended files

You can verify the integrity of appended files by summing their sizes and comparing this total with the value returned by the LoadAppendedData function. This comparison helps detect any tampering.

Here is a practical example:

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

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

Additionally, you should verify the validity of the pointers referencing the appended data. You can update the condition to check for null pointers as well:

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

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

DWORD signSize = GetAuthenticodeSignatureSize();

const DWORD totalAppendedDataSize = (fileSize1 + fileSize2 + fileSize3) + signSize; // Also add the size of the signature

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


⚠️ 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.

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 app.exe + file1.bin + file2.bin + file3.bin final_app.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.


⚠️ 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;

BYTE* embedded_data = LoadAppendedData(embedded_data_size, offset);

offset -= GetAuthenticodeSignatureSize(); // 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.


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.


⚖️ 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!

Top comments (0)