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 (with a new DWORD argument for the total size in bytes of the appended files to ignore).
#include "windows.h"
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)
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;
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;
}
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 LoadEmbeddedData
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 = LoadEmbeddedData(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 = LoadEmbeddedData(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 LoadEmbeddedData
handles the size internally
DWORD embedded_data_size = 0;
DWORD offset = fileSize1;
BYTE* embedded_data = LoadEmbeddedData(embedded_data_size, offset);
// Remove trailing files to isolate only File 2
embedded_data_size -= fileSize3;
⚠️ 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
LoadEmbeddedData
.🔁 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
}
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 could potentially corrupt or change the bounds of the added binary content, especially if you attempt to parse it later.
To ensure correct offset calculation and avoid accidental inclusion of the Authenticode signature in your embedded data:
Before reading the final 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 = LoadEmbeddedData(embedded_data_size, offset);
offset -= GetAuthenticodeSignatureSize(); // Exclude signature from the tail
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.
This approach is:
- Robust: Works regardless of how many files you append.
- Simple: Requires only file size information and offset calculations.
- Flexible: Supports different loading strategies, such as heap allocation or memory mapping.
However, be aware that this method is sensitive to any modification of the executable or the appended files.
If any embedded file is altered, removed, or corrupted, the offset and size calculations will become invalid, potentially breaking the loading process.
Therefore, ensure that the executable and its appended files remain intact and unmodified for reliable operation.
Whether you’re packaging resources, data files, or other binaries with your executable, this technique offers a clean and efficient solution that keeps your executable self-contained and easy to manage.
Feel free to adapt and extend this method based on your specific needs!
Further Reading
If you want to enhance this method by adding integrity checks and validation, I suggest reading my other article:
How to Protect Binary Data Appended into an Executable and Validate it with a Digital Signature
This follow-up explains how to secure appended data against tampering by using digital signatures, which can help avoid the fragility issues discussed here.
Top comments (0)