This article covers 4 methods to embed arbitrary binary files (images, data, etc.) inside an executable.
From standard approaches (byte arrays) to low-level hacks like binary appending (copy /b
), each method includes ready-to-use examples in Visual C++.
Use cases:
- Single-file deployment (no external resources).
- Hiding data inside .exe/.elf (no encryption).
- Quick prototyping or educational purposes.
β οΈ Note:
- Methods 1-3 are "clean" and portable.
- Method 4 (
copy /b
) is a hack and may cause problems with checksums or antivirus. - All examples assume x86_64 context (adaptable for other architectures).
Method 1: Win32 Resource Files (.rc)
Best for: Native Windows executables where you want integrated resource handling
Step 1: Add resource files in Visual Studio
- Right-click project β Add β Resource...
- Choose "Import..." and select your binary file (e.g.,
data.bin
) - Set resource type as
RCDATA
and assign an ID (e.g.,IDR_MY_DATA
)
Visual Studio will automatically:
- Generate/modify
resource.h
with your ID - Create/update the
.rc
file with:
IDR_MY_DATA RCDATA "file.bin"
Step 2: Access the resource from code
This will be the code to access the resources and use them in memory:
main.cpp
#include <windows.h>
#include "resource.h" // Auto-generated by VS
void LoadEmbeddedData()
{
HRSRC hRes = FindResource(nullptr, MAKEINTRESOURCE(IDR_MY_DATA), RT_RCDATA);
DWORD hResSize = SizeofResource(nullptr, hRes);
HGLOBAL hData = LoadResource(nullptr, hRes);
const BYTE* pData = static_cast<const BYTE*>(LockResource(hData));
if (pData)
{
// At this point, pData pointer contains the embedded data
// You can do anything with the buffer and size
// Example: processing or writing to disk
}
}
Key Notes:
- π§ No manual compilation β VS handles
.rc
β.res
conversion during build - π Data is read-only in memory (safe from modification)
- π¦ Supports any binary format (use
RT_RCDATA
for raw data) - β Possible external changes - Resources can be replaced by external tools
- β Avoid too large files - When using
LoadResource
andLockResource
, Windows loads the ENTIRE resource into RAM at once. So if the resource is very large (for example, hundreds of megabytes or gigabytes), it will take up a lot of memory, potentially running out of RAM or causing slowdowns.
Method 2: Manual Hex Offset on Source Code
Best for: Precise binary embedding without compiler dependencies
Required Tools
HxD Editor - A utility to export the binary file array offset as a C/C++ source file
Step 1: Export binary to C++ offset array
- Open your binary file (
.bin
,.dat
, etc.) in HxD Editor - Go to File β Export β C and choose a path to save the source
.cpp
file - Move the source
.cpp
file into your project folder and include it
The source file should look like:
/* [FILENAME] ([DATE])
StartOffset(h): 00000000, EndOffset(h): 000003FF, Length(h): 00000400 */
unsigned char rawData[1024] =
{
0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00, // ELF magic bytes
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0x00, 0x3E, 0x00, 0x01, 0x00, 0x00, 0x00,
// ... (truncated for brevity) ...
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
Step 2: Accessing the offset from the code
Now create a new H header file from Visual Studio and insert the following declarations:
embedded_data.h
#pragma once
extern unsigned char embedded_data[];
extern const size_t embedded_data_size;
After remember to move the size of the bytes array into a const variable and assign it to the size of the array, do not forget to include the header with the declarations
embedded_data.cpp
#pragma once
#include "embedded_data.h" // include the header
const size_t embedded_data_size = 1024; // this is the value of the array size offset below
unsigned char embedded_data[embedded_data_size] =
{
0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00, // ELF magic bytes
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0x00, 0x3E, 0x00, 0x01, 0x00, 0x00, 0x00,
// ... (truncated for brevity) ...
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
At this point from any part of our application code we include the header that declares our offset and declare a pointer of the binary data in the code to be able to use it in memory
main.cpp
#include "windows.h"
#include "embedded_data.h"
void LoadEmbeddedData()
{
const BYTE* embedded_data_ptr = embedded_data;
const size_t embedded_data_ptr_size = embedded_data_size;
if (embedded_data_ptr)
{
// At this point, embedded_data_ptr pointer contains the embedded data
// You can do anything with the buffer and size
// Example: processing or writing to disk
}
}
Key Notes:
- β Manual binary precision β It allows direct embedding of binary data into source code, eliminating dependencies on automated tools.
- π Data is read-only in memory (safe from modification)
- π¦ Immutable data Embedded data is read-only, ensuring safety and integrity during execution.
- β Manual updates required - To change or update embedded data, you need to regenerate the offset and recompile the source code.
- β Avoid too large files - Embedding large files can increase the size of the source code, reduce readability, and cause compilation slowdowns or even errors.
Method 3: COFF Object Binary Linking
Best for: Windows applications (MSVC or MinGW) that need tightly integrated, read-only binary data.
Required Tools
-
bin2obj - Includes a preconfigured portable MinGW (ucrt64) setup with
objcopy
and a small utility GUI tool to simplify conversion.
Step 1: Convert Binary to COFF Object File
Option A: Manual objcopy Command
To manually convert a binary file to a COFF object, open a terminal (CMD or Git Bash) inside the ucrt64
folder and run one of the following commands depending on your target architecture:
For 32-bit COFF:
objcopy -I binary -O pe-i386 -B i386 binary.bin binary.obj
For 64-bit COFF:
objcopy -I binary -O pe-x86-64 -B i386:x86-64 binary.bin binary.obj
-
binary.bin
is your input file (raw binary). -
binary.obj
is the output object file that you'll link into your project.
Option B: Use the bin2obj
Utility (Recommended)
The GUI tool allows for quick conversion:
1. Select your binary file.
2. Choose a destination .obj
file.
1. Click "Convert to COFF".
Be sure to leave the "Export C++ header file" checkbox enabled to automatically generate a header file with symbol declarations.
Generated Header Example
The tool will generate a binary_data.h
like the following:
For 32-bit:
#pragma once
#include <cstdint>
// include this header in your application and link the COFF as additional dependencies
extern "C"
{
extern uint8_t binary_filename_start[];
extern uint8_t binary_filename_end[];
const size_t binary_filename_size = binary_filename_end - binary_filename_start;
}
For 64-bit:
#pragma once
#include <cstdint>
// include this header in your application and link the COFF as additional dependencies
extern "C"
{
extern uint8_t _binary_filename_start[];
extern uint8_t _binary_filename_end[];
const size_t binary_filename_size = _binary_filename_end - _binary_filename_start;
}
π You can reuse the same header on every build if the binary file name doesnβt change.
Step 2 (Optional): Manually Create Header Using dumpbin
If you didnβt use the utility and created the .obj
manually, youβll need to extract symbol names:
1. Open Developer Command Prompt for Visual Studio
2. Navigate to the folder with the .obj
file
3. Run:
dumpbin /symbols binary.obj
Youβll see output like:
000 00000000 SECT1 notype External | _binary_filename_start
001 000069DD SECT1 notype External | _binary_filename_end
Header Template:
For 32-bit (remove underscores):
#pragma once
#include <cstdint>
extern "C"
{
extern uint8_t binary_filename_start[];
extern uint8_t binary_filename_end[];
const size_t binary_filename_size = binary_filename_end - binary_filename_start;
}
For 64-bit (keep underscores):
#pragma once
#include <cstdint>
extern "C"
{
extern uint8_t _binary_filename_start[];
extern uint8_t _binary_filename_end[];
const size_t binary_filename_size = _binary_filename_end - _binary_filename_start;
}
βΉοΈ Tip: Symbol names always reflect the original file name, so using consistent filenames helps with version control and updates.
βΉοΈ Tip: You can rename the
.obj
symbols always with the GUI utility bin2obj without having too much difficulty with the command lines
Step 3: Linking and Accessing Embedded Data
- Copy the
.obj
file into your project. - Add it to your linker input (Additional Dependencies in Visual Studio).
- Include the generated
.h
header in your source code.
Usage Example:
main.cpp
#include "windows.h"
#include "binary_data.h"
void LoadEmbeddedData()
{
const BYTE* buffer = binary_filename_start;
const size_t bufferSize = binary_filename_size;
if (buffer)
{
// buffer contains the embedded binary data
// You can process it in memory, decompress, write to disk, etc.
}
}
For 64-bit:
const BYTE* binary_filename_ptr = _binary_filename_start;
β οΈRemember to compile your application in 32 bit / 64 bit depending on your COFF architecture
Key Notes:
- β Efficient binary embedding β Converts binary data into a COFF object, allowing direct linking in Windows executables (ideal for MSVC/MinGW).
- π Data is read-only in memory (safe from modification)
- π¦ Immutable data Embedded data is read-only, ensuring safety and integrity during execution.
- β Manual updates required - To change or update embedded data, you need to regenerate the COFF and recompile the source code.
- β
Symbol consistency - If the binary file has the same name, the generated symbols (e.g.
binary_filename_start
) will be identical, allowing easy updates without changes to the source code. - βοΈ Custom section alignment possible - You can specify additional options with objcopy to control the alignment of the section, which is useful for certain data types or alignment-sensitive binary structures.
Method 4: Binary concatenation of files into executable (copy /b
)
Best for: Perfect for portable or self-extracting programs: no need to attach external files
This is a "dirty but effective" method that allows you to append binary files (ZIP, DLLs, images, etc.) to the end of a Windows executable using the copy /b
command. The resulting executable remains fully functional and can load and use the appended data at runtime.
β No external tools are required β everything is done via the Windows command line and API.
Step 1: Declare helper functions in your code
Add the following helper functions to your C++ application to:
1. Determine the real size of the executable (excluding any appended binary data).
2. Load the appended binary data into memory.
main.cpp
#include <windows.h>
// Returns the size of the actual executable without appended 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)
{
TCHAR exePath[MAX_PATH] = { 0 };
GetModuleFileName(NULL, exePath, MAX_PATH);
DWORD exeSize = GetRealExeSize(exePath);
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 <= exeSize) { CloseHandle(hFile); return nullptr; }
DWORD appendedSize = (DWORD)(fileSize.QuadPart - exeSize);
BYTE* data = new BYTE[appendedSize];
SetFilePointer(hFile, exeSize, 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)
{
TCHAR exePath[MAX_PATH] = { 0 };
GetModuleFileName(NULL, exePath, MAX_PATH);
DWORD exeSize = GetRealExeSize(exePath);
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 - exeSize);
if (appendedSize == 0) {
UnmapViewOfFile(pBase);
CloseHandle(hMapping);
CloseHandle(hFile);
return nullptr;
}
outSize = appendedSize;
return pBase + exeSize; // Direct pointer to the mapped binary data
}
Usage example
Now you can declare a pointer to data and work in memory:
void LoadEmbeddedData()
{
DWORD dataSize = 0;
BYTE* data = LoadAppendedData(dataSize);
if (data)
{
// Use the embedded binary data here
// e.g., save to disk, decompress, process, etc.
// Only use delete[] if allocated on the heap (Version 1)
// delete[] data;
}
}
Once this is done, compile the application and reopen the command prompt in the build folder (Debug/Release)
Step 2: Append binary data to executable
Use this command in a command prompt (inside the output folder of your build):
copy /b app.exe + mydata.bin final_app.exe
β οΈ Important: Donβt overwrite the original
.exe
. Always create a copy (final_app.exe
) with the appended data.
Key Notes:
- β Easy loading of concatenated data β It uses the Windows API to read data added to an executable, allowing you to manipulate combined files without additional dependencies like MFC.
- β No dependencies β all code uses pure WinAPI.
- π¦ Works best with a single binary file.
- β Manual updates may be required β Every time you change the binary data you will need to re-run the
copy /b
command every time after compilation - β Does not support multiple files unless structured properly:
- Use a container format (e.g., ZIP) to store multiple files.
- Alternatively, insert unique markers (magic bytes) between files to identify boundaries.
- π Appended data must be reattached after each rebuild using
copy /b
.
π Advanced: Protect the embedded data with digital signature
You can enhance this method by validating the appended data through the executableβs digital signature. This ensures integrity and authenticity of both the executable and the binary payload.πRead: How to Protect Binary Data Appended into an Executable and Validate it with a Digital Signature
Concatenate and manage multiple appended files in Win32 executable
I have discovered a fairly simple method to distinguish the various files concatenated together and access them separately. πRead: How to Easily Distinguish Multiple Binary Data Files Concatenated to a Win32 Executable
Methods Summary
Both methods have their pros and cons, below is the summary table for everything
Method | Best For | Pros | Cons | Example Use Cases |
---|---|---|---|---|
Method 1: Win32 Resource Files (.rc) | Native Windows executables where integrated resource handling is preferred | β
No manual compilation β VS handles .rc β .res conversionβ Data is read-only (safe from modification) β Supports any binary format |
β Possible external changes β Resources can be replaced by external tools β Avoid too large files - When using LoadResource and LockResource , Windows loads the ENTIRE resource into RAM at once. |
Embedding configuration files, certificates, or game assets; Storing default data for single-file apps |
Method 2: Manual Hex Offset on Source Code | Precise binary embedding without compiler dependencies | β
Manual binary precision β Directly embeds binary data into source code β Data is read-only β Immutable data β No additional dependencies β Custom section alignment possible |
β Manual updates required β Must regenerate offsets and recompile β Avoid too large files β Large files reduce readability and cause compilation slowdowns |
Embedding small binary data (e.g., logos, icons, or small assets) for small projects |
Method 3: COFF Object Binary Linking | Windows executables (MSVC/MinGW) requiring direct binary embedding | β
Efficient binary embedding β Converts binary into a COFF object for direct linking β Symbol consistency β Same file name results in identical symbols β Custom section alignment possible |
β Manual updates required β Regenerate the COFF and recompile for updates β External tool dependency β Requires bin2obj utility or manual symbol creation |
Embedding large binary files in executables, such as libraries or other large assets |
Method 4: Binary Concatenation with copy /b |
Self-extracting programs, or when no external files are allowed | β
Easy loading of concatenated data β No dependencies like MFC β Portable β Doesnβt need additional external libraries β Fast to implement β Simple concatenation command |
β Manual updates may be required β Every time you change the binary data you will need to re-run the copy /b command every time after compilationβ "Dirty" method β Can cause issues with checksums or antivirus detection |
Embedding data like ZIP files, DLLs, or other large data into executables for self-extraction |
Conclusion
In this article, we've covered four distinct methods for embedding binary data into a Win32 executable, each offering its own strengths and trade-offs:
- Method 1: Win32 Resource Files (.rc): Ideal for straightforward integration with Windows resource handling. Best for applications where the embedded data does not change often and is part of the resource management system.
- Method 2: Manual Hex Offset on Source Code: Provides precise control over the embedded data but requires manual management of offsets. Best suited for small to medium binary data embedded directly within the source code.
- Method 3: COFF Object Binary Linking: A more sophisticated method for embedding large binary objects, ideal for developers using MSVC or MinGW who need to efficiently manage binary assets.
-
Method 4: Binary Concatenation with
copy /b
: A "dirty" but quick solution for appending data directly to an executable. This method is great for self-extracting applications but requires careful handling of offsets and file sizes.
Best Practices & Recommendations
- Keep Your Executable Small: While embedding binary data can make your application self-contained, avoid adding too much data to prevent bloating the executable and affecting performance.
- Use Proper Memory Management: Always ensure to free any allocated memory once you're done using the embedded data to prevent memory leaks.
- Consider Security: If you're planning to hide sensitive data (e.g., encryption keys), be mindful that these methods are not inherently secure and can be easily extracted by someone with access to the executable. You may want to consider additional security measures, like encryption.
-
Avoid loading very large files entirely into memory: For large binaries (e.g. ZIP archives, videos, DLLs), consider block processing (streaming) or memory mapping (
CreateFileMapping
+MapViewOfFile
) instead of allocating a huge buffer, this can reduce RAM usage and make everything more performant
Final Thoughts
Each of these methods is suitable for different scenarios, and the best choice depends on your specific needs: portability, ease of use, and how frequently the embedded data needs to be updated. For quick prototyping or simple use cases, copy /b
might be the easiest choice, but for more robust applications, using Win32 resource files or manual hex offsets could be more effective.
Feel free to experiment with these methods and choose the one that fits best with your project goals!
Contributions & Questions
If you have any suggestions, improvements, or questions, feel free to contribute or reach out! I'm open to discussing different techniques and ways to enhance the methods presented here.
Happy coding and stay creative! π
Top comments (0)