How to Embed and Extract Custom PE Resources in C++

Author

Pavel Yosifovich has 25+ years as Software developer, trainer, consultant, author, and speaker. Co-author of “Windows Internals”. Author of “Windows Kernel Programming”, “Windows 10 System Programming, as well as System and kernel programming courses and “Windows Internals” series.

Pavel Yosifovich, Windows internals expert and author of the TrainSec Windows System Programming and Windows Internals course series, walks through everything you need to embed and extract custom resources in a C++ PE file using Visual Studio. This is Part 2 of the custom resources series. Part 1 covered what resources are and how Process Explorer uses the technique to bundle its own driver and 64-bit executable inside its binary. This post covers the code.

How Do You Add a Resource File to a Visual Studio C++ Project?

A console application created from the standard Visual Studio template starts with no resources at all. That is fine. You can add them.

There are two routes. The first is via Add > New Item, where you can select a resource file (.rc). The second is to right-click the project and use Add Resource directly, which creates both the .rc file and a corresponding resource.h header. Either way, the result is the same.

The .rc file is textual. It has its own language, compiled by the resource compiler (rc.exe), and you can edit it directly or let Visual Studio’s resource editor manage it for you. The resource editor is not always visible by default because it is considered uncommon for modern code. If you need it, go to View > Other Windows > Resource View.

One thing worth knowing: Visual Studio’s resource editor will not let you open resources while resource.h is open in the editor. Close one before opening the other.

What Are String Table Resources and How Do You Read Them at Runtime?

A string table is one of the standard resource types Windows understands. The resource editor gives you a table where each row has a numeric ID and a string value. Visual Studio assigns the next available ID starting from 101, and maps it to a #define in resource.h.

To read a string from the table at runtime, use LoadString:

#include "resource.h"

WCHAR text[32];
int count = LoadString(nullptr, IDS_HELLO, text, _countof(text));

The first parameter is an HINSTANCE, which is the base address of the loaded PE whose resources you want to access. Passing nullptr means “my own executable.” If you load a DLL with LoadLibrary, you can pass the returned HINSTANCE to access resources from that DLL instead.

The function returns the number of characters copied. If something goes wrong, you get zero back. Maybe the ID does not exist, maybe the instance is wrong. Either way, zero means nothing was loaded.

How Does LoadIcon Work, and What Is the MAKEINTRESOURCE Macro?

Icon resources are slightly different from string tables. You load them with LoadIcon, but the second parameter is not a numeric ID. It expects a string, specifically a resource name as a pointer.

This is where the MAKEINTRESOURCE macro comes in. It does something that looks aggressive but is intentional: it takes a numeric ID and casts it to a pointer. Not a pointer to anything, just the number shoved into pointer-sized storage.

The resource APIs use a convention to distinguish names from IDs: if the high word of the pointer is zero, it is treated as a numeric ID. If not, it is treated as a pointer to a string. MAKEINTRESOURCE produces the zero-high-word form. That is all it does.

So to load an icon whose ID in resource.h is IDI_PIE:

HICON hIcon = LoadIcon(nullptr, MAKEINTRESOURCE(IDI_PIE));

You can also use the pound-sign syntax L"#103" which the resource APIs interpret the same way, but MAKEINTRESOURCE is the straightforward approach. Passing the raw number “103" directly as a pointer does not work, because the high word would not be zero.

This same pattern applies to LoadBitmap, LoadMenu, and every other Load* resource function. Worth understanding once so you stop wondering why it fails when you pass the number directly.

The Resource View also shows you the RC file structure if you right-click the .rc file and choose View Code. Standard resource entries look like:

STRINGTABLE
BEGIN
    IDS_HELLO  "Hello Resources"
END

IDI_PIE   ICON   "pie_chart.ico"

If you reference an icon by full path, consider changing that to a relative path. Full paths make the project non-portable if you move the directory.

How Do You Create a Custom Resource Type and Import a Binary File?

Custom resource types are where things get interesting. Windows has no concept of them, which is the point.

In the resource editor, right-click and choose Add Resource, then select Custom. Give the type a name, such as BIN. Visual Studio creates a new resource entry of that type and lets you fill in hex values manually. That works for small amounts of data, but it is not how you would embed a full binary.

For that, use the Import option in the Add Resource dialog. Browse to any file, and when it asks what resource type to use, give it your custom type name (BIN, BINRES, whatever you prefer). The file’s entire content is embedded verbatim as a resource. If you open the result in a PE viewer, you will see the raw bytes. The PE viewer does not know what they are, and it does not need to.

You can also edit the .rc file directly to point at a file:

IDR_OBJEXP  BIN  "ObjExp.exe"

Paths are relative to the project folder. Relative paths are better than absolute ones for the same portability reason as with icons.

The Visual Studio resource editor will assign a default name like IDR_BIN1 to new custom entries. You can rename it to whatever makes sense. This goes into resource.h as a #define.

What Is the FindResource / LoadResource / SizeofResource / LockResource Sequence?

None of the Load* functions work for custom types, because Windows has no idea what a BIN resource is. You use the lower-level sequence instead.

Step 1: Locate the resource

FindResource takes the instance, a resource name, and a resource type. Both name and type use the same MAKEINTRESOURCE convention if they are numeric.

HRSRC rs = FindResource(nullptr, MAKEINTRESOURCE(IDR_OBJEXP), L"BIN");
assert(rs != nullptr);

The returned HRSRC is not a kernel handle. It is an opaque internal identifier. Do not try to close it.

Step 2: Get the resource data

LoadResource takes the instance and the HRSRC and returns an HGLOBAL. You then call LockResource on the HGLOBAL to get a pointer to the actual bytes.

HGLOBAL h = LoadResource(nullptr, rs);
void* p = LockResource(h);

The HGLOBAL type is a 16-bit artifact. In the original 16-bit Windows memory model, dynamic allocations returned handles that you had to lock into place before accessing. In 32-bit and 64-bit Windows, none of that applies. LoadResource returns the actual pointer already, and LockResource is effectively a no-op cast. If you compare h and p at runtime, they will have the same value.

You should still call LockResource for correctness, since the API requires it. You do not need to call a matching GlobalUnlock for resources, and there is no UnloadResource. The resource data lives inside the mapped PE image and is available for the lifetime of the process. Just take the pointer.

Step 3: Get the size

SizeofResource returns the size of the resource in bytes as a DWORD. You need this to know how much data to work with.

DWORD size = SizeofResource(nullptr, rs);

Now you have a pointer and a size. What you do with them is up to you.

How Do You Write the Extracted Resource to Disk?

This is standard Win32 file I/O. For illustration, writing the embedded binary to disk looks like this:

HANDLE hFile = CreateFile(L"D:\\test.exe", GENERIC_WRITE, 0, nullptr,
                          OPEN_ALWAYS, 0, nullptr);
DWORD written;
WriteFile(hFile, p, size, &written, nullptr);
CloseHandle(hFile);

CreateFile with OPEN_ALWAYS creates the file if it does not exist, or opens it if it does. WriteFile copies the resource bytes to disk. After that you have a standalone copy of whatever was embedded.

Run it, and if the embedded file was an executable, it runs. If it was a driver, you can load it. The bytes came from the PE’s resource section and are now a file.

This is exactly what Process Explorer does when it extracts its 64-bit variant and its driver from the BINRES resources in the 32-bit binary. The mechanism is not special. It is four API calls.

What Does This Mean for Security Research?

The same technique shows up throughout malware. A second-stage payload, encrypted shellcode, or a configuration blob can sit in the resource section of a binary that otherwise looks clean, be extracted at runtime with FindResource/LoadResource, decrypted in memory, and never touch disk as a recognizable file. A scanner that checks only imports, section names, or visible strings can miss this entirely.

The pattern is also used in legitimate dual-architecture tools like Process Explorer, installer stubs, and any application that needs to deploy a dependency without relying on an external file. Understanding the Writing Control Panel Applications pattern is a good parallel: .cpl files also embed icons and string tables in exactly this way.

If you are doing malware analysis and see a binary with large custom resource entries, particularly ones starting with 4D 5A (MZ), treat them as embedded PEs. Extract them, analyze them separately, and check whether the outer binary calls FindResource and then CreateFile/WriteFile or directly maps the bytes into memory. For cases where the embedded payload is mapped without touching disk at all, the When Process Hollowing Isn’t Process Hollowing post covers how that technique works.

What This Means Practically

  • Add resources to any PE project type, not only Windows GUI applications. A console application works just as well. Modify the .rc file directly or use Visual Studio’s resource editor, whichever is more convenient.
  • Use MAKEINTRESOURCE for any Load* or FindResource call that takes a resource name or type as a number. Passing the number directly will fail silently.
  • The complete runtime extraction sequence is exactly four functions: FindResource, LoadResource, LockResource, SizeofResource. There is no cleanup needed for the resource itself.
  • The HGLOBAL returned by LoadResource is not a real heap allocation. It is the pointer to the resource bytes inside the mapped PE image. Do not pass it to GlobalFree or HeapFree.
  • When importing a file as a custom resource, use a relative path in the .rc file so the project stays portable.
  • On the analysis side: resource entries with high entropy or unexpectedly large size for a named custom type are worth extracting and inspecting as standalone binaries.

Keep Learning

The Win32 resource APIs are part of the broader Windows system programming model. If you want to understand the full picture, including how the PE loader maps sections, how the resource section is laid out in memory, and how the same loading mechanism works for DLL dependencies, Windows System Programming 1 covers all of this with working code.

For a deeper look at how Windows actually loads PE files, what happens in memory when a binary is mapped, and how the loader resolves imports and relocations, the Windows Internals course series goes well beyond the documentation.


blue depth

About the author

Pavel Yosifovich has 25+ years as Software developer, trainer, consultant, author, and speaker. Co-author of “Windows Internals”. Author of “Windows Kernel Programming”, “Windows 10 System Programming, as well as System and kernel programming courses and “Windows Internals” series.