深入PE资源解析:从RT_MANIFEST提取程序集名称的技术实践

前言

在Windows应用程序开发与逆向工程中,PE(Portable Executable)文件格式是最基础也是最复杂的领域之一。本文将深入探讨一个具体的实践场景:如何从PE文件的RT_MANIFEST资源中提取程序集名称(Assembly Name)

这个问题源于我在分析某知名开源项目时遇到的需求——需要快速判断一个可执行文件所属的程序集,而无需完整加载或执行它。虽然看似简单,但在实现过程中却涉及了PE文件解析、资源目录遍历、数据偏移计算等多个技术难点。

本文将带你一步步实现这个功能,并分享在代码优化与错误处理方面的一些思考。

技术背景

PE文件结构简述

PE文件格式是Windows可执行文件的标准格式,主要由以下部分组成:

  1. DOS头部 - 包含DOS存根和指向PE头部的偏移
  2. PE头部 - 包括文件头和可选头部
  3. 节表 - 描述各个节的属性与位置
  4. 节数据 - 实际的代码、数据、资源等

资源目录结构

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

扩展与应用

这个工具可以进一步扩展:

  1. 批量扫描:集成到病毒分析流程,快速识别已知程序集
  2. 版本提取:同时提取version="..."属性
  3. 完整性校验:验证数字签名与清单的匹配性

代码

源代码如下:

#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文件结构的深度解析方法
  • 资源目录的遍历算法
  • 内存与性能的平衡策略
  • 防御性编程实践

希望这篇文章能帮助你在处理类似问题时少走弯路。

点赞

本文标签:

版权声明:本博客所有文章除特别声明外,本文皆为《shiver blog》原创,转载请保留文章出处。

本文链接:深入PE资源解析:从RT_MANIFEST提取程序集名称的技术实践 - https://www.binary-monster.top/article/83

1

发表评论

电子邮件地址不会被公开。 必填项已用*标注