rea-reverse

Inspect PE Icons, Version Resources and Embedded Data with REA

Written for REA 6.8.0Published by rea-reverseUpdated 3 min read
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.

Inspect PE resources with REASilent walkthrough with English captions.

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.