用 REA 检查 Windows EXE 的图标、版本资源和嵌入数据
本页目录
一个 EXE 里除了代码,还可能放着图标、版本信息和应用数据。比较两个构建产物时,先弄清楚资源里有什么、哪一份数据发生了变化,往往比直接打开程序更有用。
REA 的 inspect-pe-resources 可以列出 PE 资源树、计算每个资源数据的哈希,并把图标组关联到候选图标资源。它不会解码版本字符串,也不能证明 Windows 最终会显示哪个图标;这些问题需要其他解析工具或真实 Windows 环境来回答。
本文自己构造了一个 EXE,只检查文件,没有运行它。测试环境是 macOS、REA 6.8.0、Node 26.12.1、Zig 0.17.0 和 pefile 2024.8.26。REA 6.8.0 要求 Node 22.x 至少 22.23.4、24.x 至少 24.22.1,或 26.12.1 及以上;Node 23 和 25 不受支持。换一套工具链或构建参数,EXE 的哈希和偏移量可能不同。
构造一个带资源的 EXE
先创建目录、程序和数据文件:
mkdir pe-demo && cd pe-demo
printf 'int main(void) { return 0; }\n' > main.c
printf 'REA resource demonstration\n' > payload.bin
用 Python 标准库生成一个 16×16、内部使用 PNG 的 ICO。把以下代码保存为 make_icon.py,然后运行 python3 make_icon.py:
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)
再创建 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
编译资源对象并链接到 Windows PE。这两条命令只生成文件,不会执行它:
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
我们的 file 输出显示它是 PE32+ x86-64 控制台程序;REA 也将其识别为 PE32+。
查看资源树
安装 REA 并使用受支持的 Node 版本后,运行:
rea inspect-pe-resources demo.exe --format json > rea-resources.json
查看 normalized_result.resources。每一项都有类型、名称、语言,以及资源数据的 RVA、文件偏移、长度和 SHA-256。我们的构建得到四项,coverage.status 为 "complete":
| 类型 ID | 名称 ID | 语言 ID | 数据长度 | 含义 |
|---|---|---|---|---|
| 3 | 1 | 1033 | 82 字节 | RT_ICON 图像数据 |
| 10 | 7 | 1033 | 27 字节 | RT_RCDATA,即我们放入的数据 |
| 14 | 1 | 1033 | 20 字节 | RT_GROUP_ICON 图标组目录 |
| 16 | 1 | 1033 | 344 字节 | RT_VERSION 版本资源 |
语言 ID 1033 即 0x0409。它是文件中记录的标识,并不能证明 Windows 最终会选择哪种语言。REA 遇到具名资源时也会保留名称,因此不要假定所有资源都只有数字 ID。
理解图标组和图像的关系
资源编译器没有把整个 .ico 原样存成一项,而是把我们的输入拆成图标图像和图标组目录。REA 在 normalized_result.icon_groups 中报告一个 16×16、32 位条目,resource_id 为 1;candidate_resource_indices 包含 0,same_language_resource_index 为 0,size_matches 为 true。
这些字段描述的是文件里的静态关联,不能证明 Windows 运行时选中了哪个图标、图像能否解码,或者资源管理器最终显示了什么。需要确认显示效果时,请在实际 Windows 环境中测试。
核对数据,并用其他工具读取版本
我们这次构建的 RCDATA 条目,payload.location 是偏移 220064、长度 27;SHA-256 为:
574cc5c002054f36046755069ea9433cbe0d363054a2a0d3ca54f64a506d68b6
运行 shasum -a 256 payload.bin 得到相同结果。还可以直接核对本次构建的文件区间:dd if=demo.exe bs=1 skip=220064 count=27 2>/dev/null | shasum -a 256。换一个构建时,应使用该文件自己的 REA 结果中的偏移和长度。
要用独立工具复核,可以在 Python 虚拟环境中安装 pefile==2024.8.26,然后运行:
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)
本次 pefile 与 REA 对四个资源的偏移、长度和哈希全部一致;pefile 还读出固定文件版本 1 2 3 4。这个版本号来自 pefile 的独立检查;REA 6.8.0 的资源清单把版本资源内容作为不透明数据处理。
如果清单无法完成
REA 的检查有调用者可设置的限制。例如,故意把目录条目预算设得很小:
rea inspect-pe-resources demo.exe --max-entries 1 --format json
我们的运行返回 resource_constraint,说明目录条目预算已耗尽,没有返回部分清单。确有需要时再提高限制;检查失败或没有结果,并不等于文件里没有资源。
这里的结论只针对这个文件的静态内容,不能证明发布者身份、签名有效性、运行行为或程序安全。若下一步想确认 PE 的 32 位或 64 位架构,可以继续阅读 检查 EXE 和 DLL 的架构。