Inspect PE Icons, Version Resources and Embedded Data with REA
On this page
An EXE can carry much more than executable code: version metadata, icon images and application data all live in its resource section. If you are comparing two builds, you may want to know which resource changed before opening either executable.
REA’s inspect-pe-resources inventories the resource tree and hashes each payload. It also connects icon groups to candidate icon image resources. It does not decode version strings or prove that Windows will display a particular icon. For that second question, you need another parser or a Windows test.
The example below is an executable we built for this article, not an application downloaded from someone else. We inspected it without running it. Our test used REA 6.8.0 with Node 26.12.1, Zig 0.17.0 and pefile 2024.8.26 on macOS. REA 6.8.0 requires Node 22.23.4 or later in the 22.x series, 24.22.1 or later in the 24.x series, or 26.12.1 or later; Node 23 and 25 are unsupported. Your offsets and EXE hash can differ with the toolchain or build settings.
Build an executable with three kinds of resources
Create a directory, then write the small program and data file:
mkdir pe-demo && cd pe-demo
printf 'int main(void) { return 0; }\n' > main.c
printf 'REA resource demonstration\n' > payload.bin
Make a simple 16×16 PNG-backed ICO using only Python’s standard library:
import struct, zlib
from pathlib import Path
w = h = 16
raw = b''.join(b'\0' + bytes([28, 110, 190, 255]) * w for _ in range(h))
def chunk(kind, data):
return (struct.pack('>I', len(data)) + kind + data
+ struct.pack('>I', zlib.crc32(kind + data) & 0xffffffff))
png = (b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 6, 0, 0, 0))
+ chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b''))
header = struct.pack('<HHH', 0, 1, 1)
entry = struct.pack('<BBBBHHII', w, h, 0, 0, 1, 32, len(png), 22)
Path('demo.ico').write_bytes(header + entry + png)
Save that as make_icon.py and run python3 make_icon.py. Next, create demo.rc:
1 ICON "demo.ico"
7 RCDATA "payload.bin"
1 VERSIONINFO
FILEVERSION 1,2,3,4
PRODUCTVERSION 1,2,3,4
FILEOS 0x40004
FILETYPE 1
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "FileDescription", "REA resource demo"
VALUE "FileVersion", "1.2.3.4"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
Compile the resource object and link it into a Windows PE. These commands create the file; they do not execute it:
zig rc /:auto-includes none /:output-format coff -- demo.rc demo.res.o
zig cc -target x86_64-windows-gnu main.c demo.res.o -o demo.exe
file demo.exe
On our machine, file reported a PE32+ x86-64 console executable. REA identified the file as PE32+ as well.
Inspect the resource tree
With REA installed under a supported Node version, run:
rea inspect-pe-resources demo.exe --format json > rea-resources.json
Look in normalized_result.resources. Each entry identifies a type, name and language, plus the payload’s RVA, file offset, byte count and SHA-256. Our build returned four entries and coverage.status: "complete":
| Type ID | Name ID | Language ID | Payload bytes | Meaning |
|---|---|---|---|---|
| 3 | 1 | 1033 | 82 | RT_ICON image payload |
| 10 | 7 | 1033 | 27 | RT_RCDATA, our arbitrary data |
| 14 | 1 | 1033 | 20 | RT_GROUP_ICON directory |
| 16 | 1 | 1033 | 344 | RT_VERSION payload |
Language 1033 is 0x0409; it is an identifier in this file, not evidence of which language Windows would choose. REA also preserves named resource identities when present, so do not assume every resource is a number.
Follow the icon relationship
An ICO file is not stored as one unchanged .ico resource. The resource compiler split our input into an image and a group directory. In normalized_result.icon_groups, REA reported a 16×16, 32-bit entry with resource_id: 1. Its candidate_resource_indices contained 0, same_language_resource_index was 0, and size_matches was true.
Those are relationships in the file’s resource table. They do not show what Windows chose at runtime, whether an image decoder accepts the payload, or what appears in Explorer. Check an actual Windows environment if the displayed icon matters.
Verify the payload and read the version separately
In our REA result, the RCDATA entry’s payload.location was offset 220064, length 27. Its SHA-256 was:
574cc5c002054f36046755069ea9433cbe0d363054a2a0d3ca54f64a506d68b6
shasum -a 256 payload.bin produced the same digest. To verify the reported range directly on this specific build, run dd if=demo.exe bs=1 skip=220064 count=27 2>/dev/null | shasum -a 256. Use the offsets from your own result for a different build.
For an independent parser, install pefile==2024.8.26 in a Python virtual environment and inspect the entries:
import hashlib, pefile
p = pefile.PE('demo.exe')
for t in p.DIRECTORY_ENTRY_RESOURCE.entries:
for n in t.directory.entries:
for lang in n.directory.entries:
data = lang.data.struct
offset = p.get_offset_from_rva(data.OffsetToData)
payload = p.__data__[offset:offset + data.Size]
print(t.id, n.id, lang.id, offset, data.Size,
hashlib.sha256(payload).hexdigest())
for version in p.VS_FIXEDFILEINFO:
print('version', version.FileVersionMS >> 16, version.FileVersionMS & 65535,
version.FileVersionLS >> 16, version.FileVersionLS & 65535)
Our pefile output agreed with REA on all four resource offsets, sizes and hashes; it read the fixed file version as 1 2 3 4. That version comes from the separate pefile check. REA’s 6.8.0 resource inventory leaves the version payload opaque.
When the inventory cannot finish
REA has caller-selected limits. For example, this deliberately small budget:
rea inspect-pe-resources demo.exe --max-entries 1 --format json
returned resource_constraint and said the directory-entry budget was exceeded; it returned no partial inventory. Raise the limit only when appropriate for the file. A missing or failed result is not an empty resource tree.
These are static observations of a specific file. They do not establish publisher identity, signature validity, runtime behavior or the safety of the executable. For the next question—whether a PE is 32-bit or 64-bit—see Check EXE and DLL Architecture.