文章目录
前言
在Windows应用程序开发与逆向工程中,PE(Portable Executable)文件格式是最基础也是最复杂的领域之一。本文将深入探讨一个具体的实践场景:如何从PE文件的RT_MANIFEST资源中提取程序集名称(Assembly Name)。
这个问题源于我在分析某知名开源项目时遇到的需求——需要快速判断一个可执行文件所属的程序集,而无需完整加载或执行它。虽然看似简单,但在实现过程中却涉及了PE文件解析、资源目录遍历、数据偏移计算等多个技术难点。
本文将带你一步步实现这个功能,并分享在代码优化与错误处理方面的一些思考。
技术背景
PE文件结构简述
PE文件格式是Windows可执行文件的标准格式,主要由以下部分组成:
- DOS头部 - 包含DOS存根和指向PE头部的偏移
- PE头部 - 包括文件头和可选头部
- 节表 - 描述各个节的属性与位置
- 节数据 - 实际的代码、数据、资源等
资源目录结构
PE资源采用三层目录结构进行组织:
资源根目录
├── 类型目录 (如RT_MANIFEST = 24)
│ ├── 名称目录 (资源名称或ID)
│ │ ├── 语言目录 (如LANG_NEUTRAL)
│ │ │ └── 资源数据入口
RT_MANIFEST(类型ID 24)存储了应用程序的清单信息,其中<assemblyIdentity>标签的name属性就是我们要提取的目标。
核心实现分析
1. 地址转换:RVA到文件偏移
PE文件在内存中的虚拟地址(RVA)与文件中的物理偏移不同,我们需要实现转换函数:
static DWORD RvaToFileOffset(PIMAGE_SECTION_HEADER sections, WORD numSections,
DWORD rva) {
for (WORD i = 0; i < numSections; i++) {
if (rva >= sections[i].VirtualAddress &&
rva < sections[i].VirtualAddress + sections[i].Misc.VirtualSize) {
return rva - sections[i].VirtualAddress +
sections[i].PointerToRawData;
}
}
return 0;
}
设计思考:这里选择遍历节表而非使用ImageRvaToVa,是为了完全控制读取过程,避免额外的内存映射开销。
2. 资源目录读取优化
原始实现存在一个关键问题:多次小尺寸ReadFile调用。我将其优化为单次读取整个目录条目数组:
static bool ReadResourceDirectory(
HANDLE hFile, DWORD resourceBaseOffset, DWORD relativeOffset,
PIMAGE_RESOURCE_DIRECTORY dir,
std::vector<IMAGE_RESOURCE_DIRECTORY_ENTRY> &entries) {
// 单次读取目录头部
DWORD offset = resourceBaseOffset + relativeOffset;
SetFilePointer(hFile, offset, NULL, FILE_BEGIN);
// ... 读取头部 ...
// 单次读取所有条目
entries.resize(totalEntries);
ReadFile(hFile, entries.data(), entriesSize, ...);
}
3. 三层资源遍历
资源解析的核心是三层嵌套循环,分别对应类型 → 名称 → 语言:
// 第一层:遍历资源类型
for (size_t i = 0; i < type_entries.size(); i++) {
// 只处理RT_MANIFEST (ID = 24)
if (type_entries[i].Id != 24) continue;
// 第二层:遍历资源名称
// 第三层:遍历语言
// └─ 读取实际资源数据
}
关键点:必须检查DataIsDirectory标志,确保目录结构的完整性。
4. XML解析:轻量级提取策略
我们没有引入完整的XML解析库,而是采用字符串搜索策略:
static std::string ExtractAssemblyName(const char *manifestData, DWORD size) {
// 1. 定位 <assemblyIdentity 标签
// 2. 查找 name=" 属性
// 3. 提取引号之间的值
// 大小写不敏感比较
if (_strnicmp(pos, assemblyTag, assemblyTagLen) == 0) {
// ...
}
}
这种方式的优势在于零依赖、高性能,适合资源受限的环境。
代码优化与陷阱规避
在完善这段代码的过程中,我处理了几个隐蔽的问题:
问题1:资源条目验证不足
原始代码直接信任目录条目,可能导致越界读取。
解决方案:添加条目数量上限检查和Offset合理性验证:
if (totalEntries > 1000) return false; // 防止DOS攻击
for (size_t i = entries.size(); i > 0; i--) {
if ((entries[i - 1].OffsetToData & 0x7FFFFFFF) > 0x1000000) {
entries.erase(...); // 移除无效条目
}
}
问题2:内存管理风险
原始代码存在多处malloc后异常退出的风险。
解决方案:采用do-while(false)结构和集中释放策略:
int ret = -1;
do {
// 分配资源...
if (error) break;
// 处理逻辑...
ret = 0;
} while(false);
// 统一释放
if (ptr) free(ptr);
if (handle) CloseHandle(handle);
问题3:Unicode路径支持
原始代码使用char*路径,无法处理非ASCII字符。
解决方案:全面切换到宽字符API:
CreateFileW(w_file_path.c_str(), ...);
fopen_s(&file, w_file_path, L"rb"); // 使用fopen_s增强安全性
完整代码演示
以下是经过优化的完整实现(关键部分已在上文展示,完整代码见下文)。
执行示例:
> PEExtractor.exe "Clash.Verge_2.0.3_x64-setup.exe"
解析Manifest资源成功 msg:Clash.Verge
扩展与应用
这个工具可以进一步扩展:
- 批量扫描:集成到病毒分析流程,快速识别已知程序集
- 版本提取:同时提取
version="..."属性 - 完整性校验:验证数字签名与清单的匹配性
代码
源代码如下:
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <vector>
#include <string>
// Helper function: RVA to file offset (using pre-read sections)
static DWORD RvaToFileOffset(PIMAGE_SECTION_HEADER sections, WORD numSections,
DWORD rva) {
for (WORD i = 0; i < numSections; i++) {
if (rva >= sections[i].VirtualAddress &&
rva < sections[i].VirtualAddress + sections[i].Misc.VirtualSize) {
return rva - sections[i].VirtualAddress +
sections[i].PointerToRawData;
}
}
return 0;
}
// Helper function: Read resource directory (optimized - single ReadFile)
static bool ReadResourceDirectory(
HANDLE hFile, DWORD resourceBaseOffset, DWORD relativeOffset,
PIMAGE_RESOURCE_DIRECTORY dir,
std::vector<IMAGE_RESOURCE_DIRECTORY_ENTRY> &entries) {
DWORD offset = resourceBaseOffset + relativeOffset;
if (SetFilePointer(hFile, offset, NULL, FILE_BEGIN) ==
INVALID_SET_FILE_POINTER) {
return false;
}
DWORD bytesRead;
unsigned int headerSize = 2 * sizeof(DWORD) + 4 * sizeof(WORD);
if (!ReadFile(hFile, dir, headerSize, &bytesRead, NULL) ||
bytesRead != headerSize) {
return false;
}
entries.clear();
DWORD totalEntries = dir->NumberOfNamedEntries + dir->NumberOfIdEntries;
if (totalEntries == 0) {
return true;
}
if (totalEntries > 1000) {
return false;
}
entries.resize(totalEntries);
unsigned int entriesSize = totalEntries * 2 * sizeof(DWORD);
if (!ReadFile(hFile, entries.data(), entriesSize, &bytesRead, NULL) ||
bytesRead != entriesSize) {
entries.clear();
return false;
}
for (size_t i = entries.size(); i > 0; i--) {
if ((entries[i - 1].OffsetToData & 0x7FFFFFFF) > 0x1000000) {
entries.erase(entries.begin() + (i - 1));
}
}
return true;
}
// 读取资源字符串名称(参考MANA_PE的实现方式)
bool ReadResourceStringName(FILE* file, DWORD resourceBaseOffset,
DWORD nameOffset, std::wstring& nameStr) {
DWORD offset = resourceBaseOffset + (nameOffset & 0x7FFFFFFF);
if (fseek(file, offset, SEEK_SET) != 0) {
return false;
}
WORD length;
if (fread(&length, sizeof(WORD), 1, file) != 1) {
return false;
}
nameStr.resize(length);
if (fread(&nameStr[0], sizeof(WCHAR), length, file) != length) {
return false;
}
return true;
}
// Helper function: Extract assembly name from manifest XML
// Searches for <assemblyIdentity name="..." pattern in the manifest
static std::string ExtractAssemblyName(const char *manifestData, DWORD size) {
if (!manifestData || size == 0) {
return "";
}
const char *dataEnd = manifestData + size;
const char *assemblyTag = "assemblyIdentity";
const size_t assemblyTagLen = 16;
const char *nameAttr = "name="";
const size_t nameAttrLen = 6;
// Step 1: Find "assemblyIdentity" tag (case-insensitive)
const char *pos = manifestData;
while (pos + assemblyTagLen <= dataEnd) {
if (_strnicmp(pos, assemblyTag, assemblyTagLen) == 0) {
break;
}
pos++;
}
if (pos + assemblyTagLen > dataEnd) {
return "";
}
// Step 2: Search for "name="" attribute after assemblyIdentity tag
pos += assemblyTagLen;
while (pos + nameAttrLen <= dataEnd) {
if (_strnicmp(pos, nameAttr, nameAttrLen) == 0) {
break;
}
pos++;
}
if (pos + nameAttrLen > dataEnd) {
return "";
}
// Step 3: Extract the value between quotes
const char *nameStart = pos + nameAttrLen;
const char *nameEnd = nameStart;
while (nameEnd < dataEnd && *nameEnd != '"' && *nameEnd != '\0') {
nameEnd++;
}
if (nameEnd > nameStart && nameEnd <= dataEnd) {
return std::string(nameStart, nameEnd - nameStart);
}
return "";
}
// 解析Manifest资源(参考MANA_PE的实现方式,使用三层循环)
int ParseManifestResource(std::wstring w_file_path, std::string& msg) {
HANDLE h_file = INVALID_HANDLE_VALUE;
char* manifest_data = nullptr;
LPBYTE p_opt_hdr_buffer = nullptr;
PIMAGE_SECTION_HEADER p_sections = nullptr;
int ret = -1;
do {
h_file =
CreateFileW(w_file_path.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (h_file == INVALID_HANDLE_VALUE) {
break;
}
IMAGE_DOS_HEADER dos_hdr;
DWORD bytes_read;
if (!ReadFile(h_file, &dos_hdr, sizeof(IMAGE_DOS_HEADER), &bytes_read,
NULL) ||
bytes_read != sizeof(IMAGE_DOS_HEADER)) {
break;
}
if (dos_hdr.e_magic != IMAGE_DOS_SIGNATURE) {
break;
}
if (SetFilePointer(h_file, dos_hdr.e_lfanew, NULL, FILE_BEGIN) ==
INVALID_SET_FILE_POINTER) {
break;
}
DWORD signature;
if (!ReadFile(h_file, &signature, sizeof(DWORD), &bytes_read, NULL) ||
bytes_read != sizeof(DWORD)) {
break;
}
if (signature != IMAGE_NT_SIGNATURE) {
break;
}
IMAGE_FILE_HEADER file_hdr;
if (!ReadFile(h_file, &file_hdr, sizeof(IMAGE_FILE_HEADER), &bytes_read,
NULL) ||
bytes_read != sizeof(IMAGE_FILE_HEADER)) {
break;
}
BOOL is_64_bit =
(file_hdr.SizeOfOptionalHeader == sizeof(IMAGE_OPTIONAL_HEADER64));
DWORD opt_hdr_size = file_hdr.SizeOfOptionalHeader;
p_opt_hdr_buffer = (LPBYTE)malloc(opt_hdr_size);
if (!p_opt_hdr_buffer) {
break;
}
if (!ReadFile(h_file, p_opt_hdr_buffer, opt_hdr_size, &bytes_read,
NULL) ||
bytes_read != opt_hdr_size) {
break;
}
DWORD rsrc_rva = 0;
if (is_64_bit) {
PIMAGE_OPTIONAL_HEADER64 optHeader64 =
(PIMAGE_OPTIONAL_HEADER64)p_opt_hdr_buffer;
rsrc_rva =
optHeader64->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE]
.VirtualAddress;
} else {
PIMAGE_OPTIONAL_HEADER32 optHeader32 =
(PIMAGE_OPTIONAL_HEADER32)p_opt_hdr_buffer;
rsrc_rva =
optHeader32->DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE]
.VirtualAddress;
}
if (rsrc_rva == 0) {
break;
}
WORD num_secs = file_hdr.NumberOfSections;
DWORD sec_tbl_offset = dos_hdr.e_lfanew + sizeof(DWORD) +
sizeof(IMAGE_FILE_HEADER) + opt_hdr_size;
if (num_secs == 0 || num_secs > 100) {
break;
}
DWORD sec_tbl_size = num_secs * sizeof(IMAGE_SECTION_HEADER);
p_sections = (PIMAGE_SECTION_HEADER)malloc(sec_tbl_size);
if (!p_sections) {
break;
}
if (SetFilePointer(h_file, sec_tbl_offset, NULL, FILE_BEGIN) ==
INVALID_SET_FILE_POINTER) {
break;
}
if (!ReadFile(h_file, p_sections, sec_tbl_size, &bytes_read, NULL) ||
bytes_read != sec_tbl_size) {
break;
}
DWORD rsrc_base_offset =
RvaToFileOffset(p_sections, num_secs, rsrc_rva);
if (rsrc_base_offset == 0) {
break;
}
IMAGE_RESOURCE_DIRECTORY type_dir;
std::vector<IMAGE_RESOURCE_DIRECTORY_ENTRY> type_entries;
if (!ReadResourceDirectory(h_file, rsrc_base_offset, 0, &type_dir,
type_entries)) {
break;
}
// Find RT_MANIFEST resource and extract assembly name
bool found_mf = false;
// 分配固定大小的 manifest 缓冲区(最大 10K) //
const DWORD MAX_MANIFEST_SIZE = 10 * 1024 + 1;
manifest_data = static_cast<char*>(malloc(MAX_MANIFEST_SIZE));
if (!manifest_data) {
break;
}
ZeroMemory(manifest_data, MAX_MANIFEST_SIZE);
// 解析type_entries //
for (size_t i = 0; !found_mf && i < type_entries.size(); i++) {
// Only process RT_MANIFEST (ID = 24)
if (type_entries[i].NameIsString || type_entries[i].Id != 24 ||
!type_entries[i].DataIsDirectory) {
continue;
}
IMAGE_RESOURCE_DIRECTORY name_dir;
std::vector<IMAGE_RESOURCE_DIRECTORY_ENTRY> name_entries;
if (!ReadResourceDirectory(h_file, rsrc_base_offset,
type_entries[i].OffsetToDirectory,
&name_dir, name_entries)) {
continue;
}
// 解析name_entries //
for (size_t j = 0; !found_mf && j < name_entries.size(); j++) {
if (!name_entries[j].DataIsDirectory) {
continue;
}
IMAGE_RESOURCE_DIRECTORY lang_dir;
std::vector<IMAGE_RESOURCE_DIRECTORY_ENTRY> lang_entries;
if (!ReadResourceDirectory(h_file, rsrc_base_offset,
name_entries[j].OffsetToDirectory,
&lang_dir, lang_entries)) {
continue;
}
// 解析lang_entries //
for (size_t k = 0; !found_mf && k < lang_entries.size(); k++) {
if (lang_entries[k].DataIsDirectory ||
SetFilePointer(
h_file,
rsrc_base_offset +
(lang_entries[k].OffsetToData & 0x7FFFFFFF),
NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER) {
continue;
}
IMAGE_RESOURCE_DATA_ENTRY data_entry;
if (!ReadFile(h_file, &data_entry,
sizeof(IMAGE_RESOURCE_DATA_ENTRY),
&bytes_read, NULL) ||
bytes_read != sizeof(IMAGE_RESOURCE_DATA_ENTRY) ||
data_entry.Size == 0 || data_entry.Size > 0x100000) {
continue;
}
DWORD dataFileOffset = RvaToFileOffset(
p_sections, num_secs, data_entry.OffsetToData);
if (dataFileOffset == 0) {
continue;
}
if (SetFilePointer(h_file, dataFileOffset, NULL,
FILE_BEGIN) ==
INVALID_SET_FILE_POINTER) {
continue;
}
DWORD read_size = data_entry.Size;
if (data_entry.Size > MAX_MANIFEST_SIZE) {
// manifest文件大小最多读10k //
read_size = MAX_MANIFEST_SIZE;
}
ZeroMemory(manifest_data, MAX_MANIFEST_SIZE);
// 读取manifest文件内容 //
if (ReadFile(h_file, manifest_data, read_size, &bytes_read,
NULL) &&
bytes_read == read_size) {
manifest_data[read_size - 1] = '\0';
msg = ExtractAssemblyName(manifest_data, read_size);
if (!msg.empty()) {
found_mf = true;
ret = 0;
}
}
}
}
}
} while (false);
if (p_opt_hdr_buffer) {
free(p_opt_hdr_buffer);
}
if (p_sections) {
free(p_sections);
}
if (manifest_data) {
free(manifest_data);
}
if (h_file != INVALID_HANDLE_VALUE) {
CloseHandle(h_file);
}
return ret;
}
int main() {
const wchar_t* file_path = L"F:\\Download\\Clash.Verge_2.0.3_x64-setup.exe";
std::string msg = "";
// 解析Manifest资源
if (!ParseManifestResource(file_path, msg)) {
printf("解析Manifest资源成功 msg:%s\n", msg.c_str());
}
system("pause");
return 0;
}
总结
从PE资源中提取信息是一个典型的底层解析任务,它要求开发者既要有扎实的PE知识,又要具备良好的工程化思维。本文通过一个具体的提取程序集名称的例子,展示了:
- PE文件结构的深度解析方法
- 资源目录的遍历算法
- 内存与性能的平衡策略
- 防御性编程实践
希望这篇文章能帮助你在处理类似问题时少走弯路。