Extracting Icons From A Windows DLL Or EXE With C#
Table of Contents
The other day, Tom Warren bluesky-d (I am still not certain what term we should be using here - maybe “skeeted”, after all) that he loves the fact that Windows 11 still has icons from the MS-DOS era.
He’s right - there are plenty of places around Windows that have a lot of old icons, and especially if you take a peek inside C:\Windows\System32
. I love looking there every once in a while for that rush of nostalgia - so many memories unlocked just by looking at this icon alone:
Now, I am by no means a Windows historian or have any connections to the original developers, so I can’t provide in-depth insights on why certain icon decisions were made or what is the story behind a certain format. For that, I’d recommend looking at Raymond Chen’s writing. I am just someone who, since early childhood, loved tinkering with icons. Mainly because I wanted to write my own applications in Visual Basic 6, was not a good icon designer, and had access to a Windows 98 box. I thought I’d take whatever icons Windows offered and run with those, because why not.
To do that, however, I had to learn how to extract the icons from all sorts of artifacts, including libraries and executables bundled with Windows or from whatever video games I was playing at the time. I will share with you an updated version of my early (ugly) code that does just that - dumps icons into a local folder.
The code #
I’ll save you the “My grandma was born in Italy in 1876 and really loved walks near the piazza…” spiel in this recipe and get to the core you care about - here is the code that you can, quite literally, copy and paste in your editor and build with .NET SDK on Windows (macOS and Linux would need a completely different approach as the binary format is different from Windows) to extract icons from a Dynamically Linked Library (DLL) or executable, written entirely in C#:
using System.Drawing;
using System.Runtime.InteropServices;
namespace IconExtractor
{
internal class Program
{
private const int RT_GROUP_ICON = 14;
private const int RT_ICON = 3;
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern IntPtr LoadLibraryEx(
string lpFileName,
IntPtr hFile,
uint dwFlags);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool FreeLibrary(
IntPtr hModule);
[DllImport("kernel32", CharSet = CharSet.Unicode)]
private static extern IntPtr FindResource(
IntPtr hModule,
IntPtr lpName,
IntPtr lpType);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LoadResource(
IntPtr hModule,
IntPtr hResInfo);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LockResource(
IntPtr hResData);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint SizeofResource(
IntPtr hModule,
IntPtr hResInfo);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool DestroyIcon(
IntPtr hIcon);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr CreateIconFromResourceEx(
IntPtr presbits,
uint dwResSize,
bool fIcon,
uint dwVer,
int cxDesired,
int cyDesired,
uint uFlags);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool EnumResourceNames(
IntPtr hModule,
int lpszType,
EnumResNameProc lpEnumFunc,
IntPtr lParam);
private delegate bool EnumResNameProc(IntPtr hModule, IntPtr lpszType, IntPtr lpszName, IntPtr lParam);
static void Main(string[] args)
{
string dllPath = @"PATH_TO_DLL";
string outputFolder = @"PATH_TO_DESTINATION_FOLDER";
if (!Directory.Exists(outputFolder))
{
Directory.CreateDirectory(outputFolder);
}
IntPtr hModule = LoadLibraryEx(dllPath, IntPtr.Zero, 0x00000002); // LOAD_LIBRARY_AS_DATAFILE
if (hModule == IntPtr.Zero)
{
Console.WriteLine("Failed to load the file.");
return;
}
try
{
ExtractIconsFromModule(hModule, outputFolder);
}
finally
{
FreeLibrary(hModule);
}
Console.WriteLine("Icon extraction complete.");
}
private static void ExtractIconsFromModule(IntPtr hModule, string outputFolder)
{
bool result = EnumResourceNames(hModule, RT_GROUP_ICON, (hModule, lpszType, lpszName, lParam) =>
{
Console.WriteLine($"Found group icon resource: {lpszName}");
IntPtr hGroupIcon = FindResource(hModule, lpszName, lpszType);
if (hGroupIcon == IntPtr.Zero) return true;
IntPtr pResData = LoadAndLockResource(hModule, hGroupIcon);
if (pResData == IntPtr.Zero) return true;
uint resSize = SizeofResource(hModule, hGroupIcon);
byte[] groupData = new byte[resSize];
Marshal.Copy(pResData, groupData, 0, (int)resSize);
int iconCount = BitConverter.ToUInt16(groupData, 4);
Console.WriteLine($"Group icon resource {lpszName} contains {iconCount} icons.");
for (int j = 0; j < iconCount; j++)
{
int iconID = BitConverter.ToUInt16(groupData, 6 + j * 14 + 12);
IntPtr hIcon = FindResource(hModule, (IntPtr)iconID, RT_ICON);
IntPtr pIconData = LoadAndLockResource(hModule, hIcon);
if (pIconData == IntPtr.Zero) continue;
uint iconSize = SizeofResource(hModule, hIcon);
string outputPath = Path.Combine(outputFolder, $"icon_{lpszName}_{j}.png");
SaveIcon(pIconData, iconSize, outputPath);
}
return true;
}, IntPtr.Zero);
if (!result)
{
Console.WriteLine("Failed to enumerate resources.");
}
}
private static IntPtr LoadAndLockResource(IntPtr hModule, IntPtr resourceHandle)
{
IntPtr hResData = LoadResource(hModule, resourceHandle);
if (hResData == IntPtr.Zero) return IntPtr.Zero;
return LockResource(hResData);
}
private static void SaveIcon(IntPtr iconData, uint iconSize, string outputPath)
{
try
{
IntPtr hIcon = CreateIconFromResourceEx(iconData, iconSize, true, 0x00030000, 0, 0, 0);
if (hIcon != IntPtr.Zero)
{
using (Icon icon = Icon.FromHandle(hIcon))
using (Bitmap bitmap = icon.ToBitmap())
{
bitmap.Save(outputPath, System.Drawing.Imaging.ImageFormat.Png);
}
DestroyIcon(hIcon);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error saving icon: {ex.Message}");
}
}
}
}
I thought for some time that I would wrap all of this in a PowerShell script and make it easy for those that want to use a .ps1
file to get the icons, but the resulting code would be such an abomination of C# and PowerShell hacks that I would rather have you write that in C#, or, if you so desire, in C++ or Rust (the latter two are left as an exercise to the reader - for now).
The step-by-step breakdown #
To give you an idea of what this code actually does, let’s start with understanding where icons are stored. In DLLs (libraries) and executables on Windows there is a dedicated resources section that stores resources, such as icons (what we’re after here). Windows offers native APIs to load a library or executable and parse its resources section, so we don’t need to do that from scratch.
To do that, first we’re going to be using LoadLibraryEx
to get the contents of the binary into the memory. When loading the binary, we’re passing LOAD_LIBRARY_AS_DATAFILE
(0x00000002
in the snippet above) to tell the API that we are only trying to load resources from the binary and do not want to execute any code in it.
Next, we use the ExtractIconsFromModule
wrapper function to do the heavy lifting of getting the icons out. It uses EnumResourceNames
to enumerate all resources of type RT_GROUP_ICON
from the binary.
For every resource that is then identified, a call to FindResource
is made to find the location of the resource in the binary file. If the call succeeds, we’ll get a handle to the resource information block. The result can then be used with the LoadAndLockResource
wrapper function, that does two things:
- Loads the resources with
LoadResource
, which doesn’t actually load the resource in any typical fashion - it just gives us a pointer to where the resources is located in memory. - If the data from the previous call is not a null pointer, we will use
LockResource
to get direct access to the resource data.
Moving on. We now reached the point where we need to call SizeofResource
to get the, you guessed it, size of the resource being loaded, in bytes. That result is then promptly used to allocate a byte array and copy the native resource data into it with the help of Marshal.Copy
.
The number of icons in the group (remember, we’re looking at RT_GROUP_ICON
as the target resource) is stored as a 16-bit unsigned integer at byte offset 4 in the data array we just created. This is the standard format for a group icon resource. We grab the two bytes with the help of BitConverter.ToUInt16
at the designated position to get the count.
With the icon count now handy, we can parse each icon out. To do that, for each icon we need to obtain its ID. There is a formula for that, that I will explain the origins of shortly! For each icon, its ID is stored in the groupData
array at a specific offset.
The offset is calculated as 6 + <ICON_COUNTER> * 14 + 12
:
6
is the starting offset after the number of icons field.<ICON_COUNTER> * 14
accounts for each icon’s entry in the group (each icon takes 14 bytes).12
is the offset within each entry that points to the icon ID.
This all sounds like a bunch of gibberish, considering that there is no context provided to the numbers above. The secret here is that we’re actually operating on underlying data structures that are well-defined but I didn’t bother implementing to keep the code short. Raymond Chen talks about them in his blog post from 2012:
The icon directory (the header plus the directory entries) is stored as a resource of type RT_GROUP_ICON. The format of the icon directory in resources is slightly different from the format on disk:
typedef struct GRPICONDIR { WORD idReserved; WORD idType; WORD idCount; GRPICONDIRENTRY idEntries[]; } GRPICONDIR; typedef struct GRPICONDIRENTRY { BYTE bWidth; BYTE bHeight; BYTE bColorCount; BYTE bReserved; WORD wPlanes; WORD wBitCount; DWORD dwBytesInRes; WORD nId; } GRPICONDIRENTRY;
All the members mean the same thing as in the corresponding
ICONDIR
andIconDirectoryEntry
structures, except for that mysteriousnId
(which replaces thedwImageOffset
from theIconDirectoryEntry
).
Inside the array, we’re actually dealing with instances of GRPICONDIRENTRY
, so the offset that I mentioned above is an effective way to skip through the bytes that we don’t need and get to what we do need. Because we start with a GRPICONDIR
, we skip through 6
bytes (WORD
is a fancy Windows API way of saying UInt16
in C#) and land on idEntries
. The total number of bytes to get to nId
is 12
(that’s the ending offset), and the total number of bytes per GRPICONDIRENTRY
structure is 14
(BYTE
is just that - one byte, while DWORD
is four bytes).
Wouldn't it be easier to just implement the structs in C# and not have to deal with all this offset magic? Feels like this is not exactly maintainable code.
You are correct - if this would be something production-ready we would definitely have the struct implemented in C# and marshal the native data into it for easier manipulation. However, for this quick proof-of-concept it works as-is.
By the way, all the documentation on GRPICONDIR
and GRPICONDIRENTRY
can be found in the archived section of Microsoft Learn that talks about icons. I am not sure why there is no up-to-date documentation on the same subject.
I digress, however - let’s wrap this up. Lastly, once we have the information about the individual icon resources, we use the exact same approach as we did for extracting icon groups to extract individual icons. The only difference there is that I am also invoking SaveIcon
, which is a wrapper for a few steps:
- Create the icon from the raw data with
CreateIconFromResourceEx
. This will give us a native handle to an icon object - aHICON
, if you will. - The native data is then converted to a managed representation of an icon with
Icon.FromHandle
. - That icon is then converted to a bitmap with
Icon.ToBitmap
, ensuring that all the fancy alpha-channel data is properly preserved. - Finally, the icon is written to disk as a PNG file.
There you go! You can now use the code above, or a derivative of it, to extract Windows icons from any of the native DLLs and executables. Running the library on C:\Windows\SystemResources\imageres.dll.mun
will give us this beautiful output:
Hold up! What's up with the .mun
extension? I thought we're looking for DLLs and EXEs here, but this is neither!
.mun
files are just compressed resource files for the associated libraries and executables. Starting with the most recent Windows releases, icons are not actually embedded in the previously-famous DLLs, like shell32
, but rather in their .mun
counterparts found in C:\Windows\SystemResources
. That is where you can look to get the icons you missed. You can also use a tool like Resource Hacker to get an idea of what icons are located in what locations.
If you are a Windows API expert and would love to chime in here with any additional context, drop your notes in the comments! I am positive that I left something out of my “hacky” solution.