PE解析器fopen/fgetc文件指针随机跳转问题排查
PE解析器文件指针异常跳转问题排查
我正在编写一个基础PE(可移植可执行文件)解析器,使用fopen、fread、fgetc和ftell等文件操作函数时遇到异常:在部分正常可解析的PE文件中,文件指针会毫无缘由地出现跳转(例如从0x118跳至0x308),跳转后读取的字节与文件原始字节不符,导致解析失败。已排除文件损坏、换行符解析错误等问题,测试文件可被PE-Bear正常加载。
最小复现代码
#include <stdio.h> #include <stdlib.h> #include <urlmon.h> #include <assert.h> #include <winnt.h> #define ___IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 typedef struct __IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10]; LONG e_lfanew; //offset in the file of the NT header structure } ___IMAGE_DOS_HEADER, * ___PIMAGE_DOS_HEADER; typedef struct __IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } ___IMAGE_DATA_DIRECTORY, * ___PIMAGE_DATA_DIRECTORY; typedef struct __IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; ___IMAGE_DATA_DIRECTORY DataDirectory[___IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } ___IMAGE_OPTIONAL_HEADER64, * ___PIMAGE_OPTIONAL_HEADER64; ___IMAGE_DOS_HEADER *parse_dos_header(FILE *fp){ ___IMAGE_DOS_HEADER *dos_hdr = malloc(sizeof(___IMAGE_DOS_HEADER)); fread(dos_hdr,sizeof(___IMAGE_DOS_HEADER),1,fp); // printf("offset optional header is %lu\n",dos_hdr->e_lfanew); return dos_hdr; } ___IMAGE_OPTIONAL_HEADER64* parse_optional_header(FILE *fp, ___IMAGE_DOS_HEADER* dos_hdr){ ___IMAGE_OPTIONAL_HEADER64 *opt_hdr = malloc(sizeof(___IMAGE_OPTIONAL_HEADER64)); long offset = dos_hdr->e_lfanew + sizeof(long) + sizeof(IMAGE_FILE_HEADER); fseek(fp,offset,SEEK_SET); fread(opt_hdr,sizeof(___IMAGE_OPTIONAL_HEADER64),1,fp); fseek(fp,offset,SEEK_SET); for(int i=0;i<512;i++){ //debug loop to test char by char char byte = fgetc(fp); if (byte == EOF) break; printf("ftell position %lx\n",ftell(fp)); printf("%02x\n",byte); } assert(opt_hdr->Magic == 0x20b); long end = ftell(fp); fseek(fp,0,SEEK_END); long end_file = ftell(fp); assert(end_file>end); printf("Passed the check\n"); rewind(fp); return opt_hdr; } int main(int argc, char* argv[]){ if(argc!=2){ printf("Specify the PE you want to parse through argv[1]\n usage : ./binary <path_to_pe>\n"); exit(-1); } FILE *fp; fopen_s(&fp,argv[1],"rb"); fseek(fp, 0L, SEEK_END); long long sz = ftell(fp); rewind(fp); if(sz<sizeof(___IMAGE_DOS_HEADER)){ printf("Not enough byte to parse the DOS header, exiting\n"); exit(-1); } ___IMAGE_DOS_HEADER *dos_hdr = parse_dos_header(fp); ___IMAGE_OPTIONAL_HEADER64* opt_hdr = parse_optional_header(fp,dos_hdr); }
异常输出片段
ftell position 109 0b ftell position 10a 02 ftell position 10b 0e ftell position 10c 1d ftell position 10d 00 ftell position 10e ffffff88 ftell position 10f 01 ftell position 110 00 ftell position 111 00 ftell position 112 ffffffec ftell position 113 00 ftell position 114 00 ftell position 115 00 ftell position 116 00 ftell position 117 00 ftell position 118 00 ftell position 308 <<< return value of ftell jumping from 118 to 308 ffffffc0 ftell position 309 00 ...
核心问题分析
结构体对齐不匹配
PE文件中的所有结构都是按1字节紧凑排列的,没有填充字节。但C编译器默认会按结构体中最大成员的大小(如ULONGLONG对应8字节)进行对齐,导致自定义结构体的sizeof结果远大于实际PE结构的大小。这会让fread读取过多字节,或计算偏移时出错,直接导致文件指针位置异常。自定义结构体冗余且错误
WinNT.h中已经提供了标准的IMAGE_DOS_HEADER、IMAGE_OPTIONAL_HEADER64等PE结构定义,重复自定义不仅冗余,还容易出现成员类型、顺序错误,进一步加剧偏移计算的问题。偏移计算错误
NT头的签名(Signature)是DWORD类型(4字节),代码中误用sizeof(long)计算偏移:在64位系统中long是8字节,会导致偏移多算4字节,后续的文件操作都基于错误的起始位置。fgetc返回值处理错误
fgetc返回int类型(范围-1到255),用signed char接收会将0x80以上的字节解析为负数,与EOF(-1)混淆,导致循环提前终止或错误判断,间接引发指针位置异常。
修复方案
- 强制结构体1字节对齐
在自定义结构体定义前后添加对齐指令,确保结构体大小与PE文件中的结构一致:
#pragma pack(push, 1) // 结构体定义 #pragma pack(pop)
- 使用系统标准PE结构体
直接使用WinNT.h中定义的标准结构,避免自定义错误:
- 替换
___IMAGE_DOS_HEADER为IMAGE_DOS_HEADER - 替换
___IMAGE_OPTIONAL_HEADER64为IMAGE_OPTIONAL_HEADER64 - 替换
___IMAGE_DATA_DIRECTORY为IMAGE_DATA_DIRECTORY
- 修正偏移计算
将偏移计算中的sizeof(long)改为sizeof(DWORD):
long offset = dos_hdr->e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER);
- 正确处理fgetc返回值
用int类型接收fgetc的返回值,避免与EOF混淆:
int byte = fgetc(fp); if (byte == EOF) break; printf("ftell position %lx\n", ftell(fp)); printf("%02x\n", (unsigned char)byte);
- 检查文件操作返回值
添加fread、fseek等函数的返回值检查,确保操作成功,避免后续基于错误状态的操作:
if (fread(dos_hdr, sizeof(IMAGE_DOS_HEADER), 1, fp) != 1) { // 处理读取错误 }
内容的提问来源于stack exchange,提问作者Jdxp
相关产品推荐
相关产品推荐

