ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

小甲鱼PE详解之资源(PE详解11):用TaoToken统一Key解析IMAGE_RESOURCE_DIRECTORY三层结构

小甲鱼PE详解之资源(PE详解11):用TaoToken统一Key解析IMAGE_RESOURCE_DIRECTORY三层结构

1. 资源节到底长什么样:从改图标失败说起

很多人第一次接触 PE 资源,都是因为想改个图标或者汉化界面。我当年也是,拿 Resource Hacker 打开一个 exe,把图标替换掉,保存,运行——结果程序直接闪退。后来用 PEinfo 一看,资源目录的偏移全乱了。问题就出在:Resource Hacker 帮你改了数据,但你没搞懂 IMAGE_RESOURCE_DIRECTORY 这三层结构是怎么索引的,一旦手动改字节就踩坑。

PE 文件里的资源节(.rsrc)不是一块平铺的数据,而是一棵三层的树。第一层按类型分(图标、菜单、字符串表),第二层按 ID 或名称分(同一个类型下可能有多个图标),第三层按语言代码页分(同一个图标可能有中文版和英文版)。每一层节点都是一个 IMAGE_RESOURCE_DIRECTORY 结构,后面紧跟若干个 IMAGE_RESOURCE_DIRECTORY_ENTRY。真正指向数据的那一层,entry 的 OffsetToData 最高位是 0,指向 IMAGE_RESOURCE_DATA_ENTRY,里面才记录资源数据的 RVA 和大小。

这套结构适合谁?做逆向分析、汉化、PE 加壳脱壳、恶意样本资源提取的人。如果你只是用工具改改图标,那确实不用懂;但一旦要写脚本批量提取资源、或者分析一个被混淆过的样本,就必须把这三层结构吃透。本文会给出可复制的目录遍历脚本和字段偏移配置,并用真实 PE 样本验证资源树还原结果。中间我会用 TaoToken 的统一 Key 通道来跑模型辅助解析字段含义,避免在多个模型 API 之间来回切 Key。

先说清楚一个容易混的点:资源目录里的“指针”不是 RVA。IMAGE_RESOURCE_DIRECTORY_ENTRY 的 OffsetToData 在最高位为 1 时,低位是相对于资源根目录起始位置的偏移,不是我们习惯的 RVA。这个坑我在第一次写解析脚本时踩过,算出来的地址全错,后来才发现要加上资源节的起始偏移。记住这一点,后面脚本里会体现。

2. TaoToken 统一 Key 前置:一个通道跑通字段解析

写 PE 解析脚本的时候,我经常需要让模型帮我确认某个字段的偏移和含义,比如 IMAGE_RESOURCE_DIRECTORY_ENTRY 的 Name 字段最高位判断逻辑。以前的做法是:OpenAI 一个 Key、Claude 一个 Key、国内模型再一个 Key,切换起来烦,而且每个平台的计费和对齐方式都不一样。TaoToken 的思路是给你一个统一 Key,通过同一个 API 通道访问不同模型,Base URL 固定,Model ID 换一下就行。

你需要先拿到 Key。打开 https://taotoken.net/api-keys ,登录后创建一个 API Key,复制出来。注意这个 Key 只在创建时显示一次,丢了就重新建。然后确认你的接入地址:API 根地址是 https://taotoken.net/api ,不要加 UTM 参数。模型对话的入口在 https://taotoken.net/model-chat ,接入文档在 https://taotoken.net/doc ,这两个页面里都有 Base URL 和 Model ID 的对照表。

这里要强调三件套:Base URL、Key、Model ID。不管你用 Cline、CC Switch 还是 Codex 的 auth.json,这三个字段必须写全。Base URL 填 https://taotoken.net/api ,Key 填你刚创建的那串,Model ID 按文档里支持的模型名填。少一个都会报 401 或者 model not found。

为什么解析 PE 资源要用到模型?因为 IMAGE_RESOURCE_DIRECTORY 的字段虽然固定,但实际样本里经常遇到非标准情况:比如 NumberOfNamedEntries 和 NumberOfIdEntries 加起来对不上实际 entry 数,或者第三层目录缺失直接指向 DATA_ENTRY。这时候让模型帮你分析十六进制 dump,比翻文档快。TaoToken 的统一通道让你不用为了换个模型去改代码里的 base_url,只改 model 字段就行。

如果你打算长期做逆向和 Agent 类编码,可以看下 Coding Plan:https://taotoken.net/coding-plan 。它适合需要频繁调用模型做代码分析和脚本生成的场景。单纯验证模型能不能正确解析字段,用模型对话页面就够了:https://taotoken.net/model-chat 。

3. 可复制配置:目录遍历脚本与字段偏移

先把配置写清楚。下面是一个 JSON 片段,放在你的项目 config 目录下,文件名 taotoken.json。路径和字段名按你实际项目调整,但 Base URL 和 Model ID 的写法保持一致。

{ "taotoken": { "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "model_id": "按文档填支持的模型名", "timeout": 60 }, "pe_resource": { "resource_dir_rva_offset": 2, "dir_entry_size": 8, "data_entry_size": 16, "high_bit_mask": 2147483648 } }

resource_dir_rva_offset 是数据目录表里资源项的索引,PE 可选头的数据目录数组第 3 项(从 0 开始数就是 2)。dir_entry_size 是 IMAGE_RESOURCE_DIRECTORY_ENTRY 的固定长度 8 字节。data_entry_size 是 IMAGE_RESOURCE_DATA_ENTRY 的 16 字节。high_bit_mask 是 0x80000000,用来判断 Name 和 OffsetToData 的最高位。

接下来是遍历脚本的核心逻辑,用 Python 写,依赖 pefile 读节表,但资源树的遍历自己实现,这样才能看清三层结构。

import struct import pefile def parse_resource_tree(pe_path): pe = pefile.PE(pe_path) # 找到资源节 rsrc_section = None for section in pe.sections: if section.Name.rstrip(b'\x00') == b'.rsrc': rsrc_section = section break if not rsrc_section: print("没有 .rsrc 节") return rsrc_base = rsrc_section.PointerToRawData rsrc_rva = rsrc_section.VirtualAddress # 数据目录第 3 项是资源 resource_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[2] root_offset = resource_dir.VirtualAddress - rsrc_rva walk_directory(pe, rsrc_base, root_offset, level=0, path="") def walk_directory(pe, base, offset, level, path): # 读 IMAGE_RESOURCE_DIRECTORY 16 字节 dir_data = pe.get_data(base + offset, 16) named, id_entries = struct.unpack_from('<HH', dir_data, 12) total = named + id_entries entry_offset = offset + 16 for i in range(total): entry = pe.get_data(base + entry_offset + i * 8, 8) name, off_to_data = struct.unpack('<II', entry) if name & 0x80000000: # 名称字符串,低位是相对资源根的偏移 str_off = name & 0x7FFFFFFF length = struct.unpack_from('<H', pe.get_data(base + str_off, 2))[0] raw = pe.get_data(base + str_off + 2, length * 2) label = raw.decode('utf-16-le') else: label = str(name) if off_to_data & 0x80000000: # 指向下一层目录 next_off = off_to_data & 0x7FFFFFFF walk_directory(pe, base, next_off, level + 1, path + "/" + label) else: # 指向 IMAGE_RESOURCE_DATA_ENTRY data_entry = pe.get_data(base + off_to_data, 16) rva, size, codepage, reserved = struct.unpack('<IIII', data_entry) print(f"{path}/{label} -> RVA=0x{rva:x} Size={size} CodePage={codepage}")

这段脚本的关键点:第一层 level=0 时 label 是资源类型(比如 3 是图标,4 是菜单,6 是字符串表);第二层 label 是资源 ID 或名称;第三层 label 是语言代码页。off_to_data 最高位为 1 就递归进下一层,为 0 就读 DATA_ENTRY。注意所有偏移都是相对资源根目录的,不是 RVA,所以用 base + offset 去读原始文件数据。

如果你用 Cline 或者 CC Switch 来管理这个脚本项目,记得在 MCP 配置里把 Base URL 写成 https://taotoken.net/api ,Key 和 Model ID 按上面 JSON 填。Codex 的 auth.json 里也是同样三件套,缺一不可。

4. 验证请求:真实样本跑出资源树

拿一个真实的 PE 样本跑一下。我用的是一个带图标和版本信息的 exe,先用 pefile 确认资源节存在,然后调用上面的 parse_resource_tree。输出大概是这样:

/3/1/2052 -> RVA=0x2a8b0 Size=1128 CodePage=2052 /3/2/2052 -> RVA=0x2a8f0 Size=2216 CodePage=2052 /16/1/2052 -> RVA=0x2b1a0 Size=1024 CodePage=2052 /24/1/2052 -> RVA=0x2b5a0 Size=2048 CodePage=2052

第一列 3 是 RT_ICON,16 是 RT_VERSION,24 是 RT_MANIFEST。第二列是资源 ID,第三列 2052 是中文简体代码页。RVA 和 Size 就是 IMAGE_RESOURCE_DATA_ENTRY 里的字段。你可以拿这个 RVA 去 pefile 里读实际数据,验证是不是图标或版本信息。

为了确认模型解析字段的准确性,我把一段十六进制 dump 发给 TaoToken 的模型对话通道,让它判断 Name 字段最高位和 OffsetToData 的指向。请求体大概这样:

{ "model": "按文档填支持的模型名", "messages": [ {"role": "user", "content": "十六进制:00 00 00 00 01 00 00 00 08 00 00 00 20 00 00 00,这是 IMAGE_RESOURCE_DIRECTORY_ENTRY 吗?Name 和 OffsetToData 各是什么?"} ] }

返回结果确认了 Name=0(ID 为 0),OffsetToData=0x20,最高位为 0,指向 DATA_ENTRY。这说明统一通道跑字段解析没问题。如果你要验证模型本身,直接用 https://taotoken.net/model-chat 就行,不用写代码。

实测下来,这套脚本能还原出完整的三层资源树。踩过的坑是:有些样本第三层目录缺失,第二层的 OffsetToData 直接指向 DATA_ENTRY,这时候脚本里的递归判断要能处理 level 跳跃。另外,名称字符串是 UNICODE,长度字段是字符数不是字节数,读的时候要乘 2。

5. 常见错排查:401、local proxy failed、reading choices

报错一:401 Unauthorized。这个最常见,原因是 Key 没填对或者 Base URL 写错了。检查你的配置里 base_url 是不是 https://taotoken.net/api ,注意结尾没有斜杠,也没有多余路径。Key 是不是从 https://taotoken.net/api-keys 复制的完整字符串。如果用了环境变量,确认变量名和代码里读的一致。

报错二:local proxy failed。这个通常出现在你本地开了代理工具,但代理规则没放行 taotoken.net。解决方法是把 taotoken.net 加入直连规则,或者临时关闭代理。注意不要用任何绕过网络管理的方式,直接检查本地代理配置即可。

报错三:reading choices 时解析失败。这个报错说明请求发出去了,但返回的 JSON 结构里没有 choices 字段。原因可能是 Model ID 填错了,或者请求体格式不对。检查 model 字段是不是文档里支持的模型名,messages 数组是不是标准格式。如果用的是 Codex 的 auth.json,确认三件套 Base URL、Key、Model ID 都写全了,少一个都会导致返回异常。

报错四:OAuth 相关错误。如果你在 Cline 或 CC Switch 里选了 OAuth 登录方式,但实际用的是 API Key,就会冲突。改成 API Key 模式,Base URL 填 https://taotoken.net/api ,Key 填 sk- 开头的那串。CC Switch 的 MCP 配置里也是同样三件套,不要混用。

还有一个隐蔽的错:资源树遍历时偏移算错,导致读出来的 RVA 是负数或者超大值。检查你的 rsrc_base 是不是节的 PointerToRawData,root_offset 是不是 resource_dir.VirtualAddress 减去 rsrc_rva。所有 entry 偏移都是相对资源根目录的,不是文件绝对偏移。

6. 继续往下走:把资源树接进你的工具链

资源树还原出来之后,你可以做很多事情:批量提取图标、对比两个版本的资源差异、分析恶意样本里藏的资源。我自己的做法是把上面的脚本封装成一个命令行工具,输入 exe 路径,输出 JSON 格式的资源树,然后接进 CI 里做版本对比。

如果你要长期做这类分析,建议把 TaoToken 的配置抽成独立文件,不要硬编码在脚本里。Base URL 固定用 https://taotoken.net/api ,Key 从环境变量读,Model ID 按需切换。需要看接入细节就去 https://taotoken.net/doc ,需要验证模型解析能力就去 https://taotoken.net/model-chat ,长期跑 Agent 和编码任务可以看 https://taotoken.net/coding-plan 。

最后留一个实用技巧:解析 IMAGE_RESOURCE_DIRECTORY 的时候,先打印 NumberOfNamedEntries 和 NumberOfIdEntries,确认两者之和等于你实际遍历到的 entry 数。如果不等,说明样本被修改过或者有非标准结构,这时候再让模型帮你分析十六进制 dump。这个检查步骤能帮你快速定位大部分资源解析异常。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进