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;
}
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;
}
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;
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;
}
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;
}
⚠️ 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;
}
⚠️ 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
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
*/
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);
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)
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;
⚠️ 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
andfileSize3
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
}
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:
- Offset = Sum of sizes of all preceding files
- 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);
📌 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.
🔄 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
}
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
}
⚠️ 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 callFreeMappedAppendedData()
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
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;
}
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
⚠️ 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
- Using
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)