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;
}
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
deletewhenever 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,fileSize2andfileSize3with 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
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:
- 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);
🔁 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
MappedAppendedDatastruct.
📌 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);
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
externdeclarations 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
MappedAppendedDatastructure, which can be cleanly released at any time with a single call toFreeMappedAppendedData().
💡 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
}
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
}
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
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
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
MappedAppendedDatafor 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
MappedAppendedDatastructure 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.
⚠️ 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;
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
⚠️ 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.
⚖️ 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)