PE文件ParseHeaders函数无法读取NT/可选头的问题求助
PE文件解析问题排查与修复
核心报错原因分析
从输出看,两个关键错误:Unknown optional header size和Failed to read NT headers,分别来自ParseSections和ParseIAT函数,核心问题是PE结构的偏移计算错误、NT头读取逻辑错误。
问题1:ParseSections函数的可选头大小判断与节区偏移计算错误
错误点
- 直接用
sizeof(IMAGE_OPTIONAL_HEADER32/64)和FileHeader.SizeOfOptionalHeader比较,不符合PE规范:文件中SizeOfOptionalHeader是实际可选头大小,和结构体大小可能因编译器对齐有差异,且应该通过可选头的Magic值判断位数,而非大小比较。 - 节区头的偏移计算完全错误:正确的节区头起始位置是NT头的起始地址(e_lfanew) + NT签名(4字节) + 文件头大小(20字节) + 可选头大小,而非
sizeof(IMAGE_DOS_HEADER) + sizeof(IMAGE_NT_HEADERS) + sizeOfHeaders。
修复后的ParseSections函数
PIMAGE_SECTION_HEADER ParseSections(FILE* pefile, IN IMAGE_NT_HEADERS* NtHeaders) { // 通过Magic值判断是32位还是64位可选头 size_t sizeOfHeaders = 0; if (NtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { sizeOfHeaders = NtHeaders->OptionalHeader.OptionalHeader32.SizeOfHeaders; } else if (NtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { sizeOfHeaders = NtHeaders->OptionalHeader.OptionalHeader64.SizeOfHeaders; } else { printf("Unknown optional header magic value\n"); return NULL; } // 计算节区头的正确偏移:NT头起始位置 + NT头总大小 DWORD ntHeaderTotalSize = sizeof(DWORD) // NT签名 + sizeof(IMAGE_FILE_HEADER) + NtHeaders->FileHeader.SizeOfOptionalHeader; DWORD sectionHeaderOffset = NtHeaders->FileHeader.e_lfanew + ntHeaderTotalSize; if (fseek(pefile, sectionHeaderOffset, SEEK_SET) != 0) { printf("Failed to seek to section headers\n"); return NULL; } // 分配节区头内存 PIMAGE_SECTION_HEADER SectionHeaders = (PIMAGE_SECTION_HEADER)malloc( NtHeaders->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER) ); if (SectionHeaders == NULL) { printf("Allocation failed (section headers)\n"); return NULL; } // 读取节区头 size_t readCount = fread(SectionHeaders, sizeof(IMAGE_SECTION_HEADER), NtHeaders->FileHeader.NumberOfSections, pefile); if (readCount != NtHeaders->FileHeader.NumberOfSections) { printf("Failed to read section headers\n"); free(SectionHeaders); return NULL; } return SectionHeaders; }
问题2:ParseIAT函数中NT头读取逻辑错误
错误点
ParseIAT中直接从文件开头读取sizeof(IMAGE_NT_HEADERS),但NT头实际位于DOS头的e_lfanew偏移处,且IMAGE_NT_HEADERS结构体大小对32/64位PE不同,直接读取会导致数据错误。此外,主函数已经解析过NT头,无需重复读取,可直接传入已有的NtHeaders和SectionHeaders,避免重复解析。
修复方案
- 修改
ParseIAT函数参数,直接传入已解析的IMAGE_NT_HEADERS和IMAGE_SECTION_HEADER,避免重复读取文件。 - 移除错误的NT头读取逻辑。
- 修正x64 PE的IAT条目类型为64位,避免数据截断。
修复后的ParseIAT函数定义
// 修改函数参数,直接传入已解析的头信息 void ParseIAT(FILE* pefile, IMAGE_NT_HEADERS* NtHeaders, PIMAGE_SECTION_HEADER SectionHeaders) { if (!pefile || !NtHeaders || !SectionHeaders) { printf("Invalid input parameters\n"); return; } // 确认是x64 PE if (NtHeaders->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { printf("Error: Not x64 PE\n"); return; } IMAGE_OPTIONAL_HEADER64 OptionalHeader64 = NtHeaders->OptionalHeader.OptionalHeader64; IMAGE_DATA_DIRECTORY ImportDirectory = OptionalHeader64.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; uint32_t ImportDirectoryRVA = ImportDirectory.VirtualAddress; uint32_t ImportDirectorySize = ImportDirectory.Size; if (ImportDirectoryRVA == 0) { printf("No import directory\n"); return; } printf(" [>] Import Directory Address: 0x%08X - Size: 0x%08X\n\n", ImportDirectoryRVA, ImportDirectorySize); // 转换导入目录RVA到文件偏移 uint32_t ImportDirectoryOffset = rva_to_offset(ImportDirectoryRVA, SectionHeaders, NtHeaders->FileHeader.NumberOfSections); if (ImportDirectoryOffset == 0xFFFFFFFF) { printf("Invalid import directory offset\n"); return; } if (fseek(pefile, ImportDirectoryOffset, SEEK_SET) != 0) { printf("Failed to seek to import directory\n"); return; } IMAGE_IMPORT_DESCRIPTOR ImportDescriptor; while (fread(&ImportDescriptor, sizeof(ImportDescriptor), 1, pefile) == 1) { if (ImportDescriptor.Name == 0) { break; } // 读取模块名 uint32_t moduleNameRVA = ImportDescriptor.Name; uint32_t moduleNameOffset = rva_to_offset(moduleNameRVA, SectionHeaders, NtHeaders->FileHeader.NumberOfSections); if (moduleNameOffset == 0xFFFFFFFF) { printf("Invalid module name offset\n"); return; } char moduleName[256]; fseek(pefile, moduleNameOffset, SEEK_SET); if (fread(moduleName, 1, sizeof(moduleName) - 1, pefile) <= 0) { printf("Failed to read module name\n"); return; } moduleName[sizeof(moduleName) - 1] = '\0'; printf("[+] Module: %s\n", moduleName); // 读取IAT uint32_t iatRVA = ImportDescriptor.FirstThunk; uint32_t iatOffset = rva_to_offset(iatRVA, SectionHeaders, NtHeaders->FileHeader.NumberOfSections); if (iatOffset == 0xFFFFFFFF) { printf("Invalid IAT offset\n"); return; } if (fseek(pefile, iatOffset, SEEK_SET) != 0) { printf("Failed to seek IAT offset\n"); return; } printf("[+] Import Address Table Offset: 0x%08X\n", iatOffset); // x64的IAT条目是64位,修正类型为uint64_t uint64_t iatEntry; while (fread(&iatEntry, sizeof(iatEntry), 1, pefile) == 1) { if (iatEntry == 0) { break; } printf(" [>] Function Address: 0x%016llX ", iatEntry); if (iatEntry & 0x8000000000000000) { uint16_t ordinal = (uint16_t)(iatEntry & 0xFFFF); printf(" [>] Function imported by ordinal: %u\n", ordinal); } else { uint32_t functionNameRVA = (uint32_t)iatEntry; uint32_t functionNameOffset = rva_to_offset(functionNameRVA, SectionHeaders, NtHeaders->FileHeader.NumberOfSections); if (functionNameOffset == 0xFFFFFFFF) { printf("Invalid function name offset\n"); return; } fseek(pefile, functionNameOffset, SEEK_SET); IMAGE_IMPORT_BY_NAME functionName; if (fread(&functionName, sizeof(functionName), 1, pefile) != 1) { printf("Failed to read function name\n"); return; } printf(" [>] Name: %s\n", functionName.Name); } } } }
主函数调用修改
// 主函数中调用ParseIAT时,传入已解析的NtHeaders和SectionHeaders if (architecture == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { ParseEAT(pefile, NtHeaders, SectionHeaders); // 同理修改ParseEAT的参数逻辑 ParseIAT(pefile, NtHeaders, SectionHeaders); // 释放内存,避免泄漏 free(SectionHeaders); free(NtHeaders); }
问题3:rva_to_offset函数的节区范围判断优化
错误点
用sectionEndRVA = sectionStartRVA + sections[i].Misc.VirtualSize判断RVA是否在节区范围内,部分情况下(如节区的VirtualSize大于SizeOfRawData),可能导致RVA在内存中存在但文件中无对应数据,需补充判断文件偏移的有效性。
修复后的rva_to_offset函数
uint32_t rva_to_offset(uint32_t rva, IMAGE_SECTION_HEADER* sections, int numberOfSections) { for (int i = 0; i < numberOfSections; i++) { uint32_t sectionStartRVA = sections[i].VirtualAddress; // 节区的RVA范围是[sectionStartRVA, sectionStartRVA + VirtualSize) uint32_t sectionEndRVA = sectionStartRVA + sections[i].Misc.VirtualSize; if (rva >= sectionStartRVA && rva < sectionEndRVA) { uint32_t offsetInSection = rva - sectionStartRVA; // 检查偏移是否在文件中的节区数据范围内 if (offsetInSection <= sections[i].SizeOfRawData) { uint32_t offset = sections[i].PointerToRawData + offsetInSection; printf("RVA 0x%08X maps to offset 0x%08X in section %d\n", rva, offset, i); return offset; } else { printf("RVA 0x%08X is in section %d but beyond file data\n", rva, i); return 0xFFFFFFFF; } } } printf("RVA 0x%08X not found in any section\n", rva); return 0xFFFFFFFF; }
额外注意事项
- 内存泄漏:主函数中
NtHeaders和SectionHeaders是malloc分配的,使用完后必须调用free释放。 - 文件指针管理:每次读取操作后注意文件指针位置,必要时用
fseek重置,避免后续读取错位。 - PE结构兼容性:处理PE文件时需严格遵循PE规范,避免依赖结构体的默认大小,优先使用文件中存储的字段值。
内容的提问来源于stack exchange,提问作者kot123
相关产品推荐
相关产品推荐

