-
-
[原创]从Windows文件执行流程中来解析PE头
-
发表于: 2025-9-24 11:02 546
-
文件验证
MZ签名检查,检查文件开头两字节是否为4D 5A,失败则报错“Not a valid Win32 application”。
定位NT头,读取e_lfanew值(IMAGE_DOS_HEADER -> e_lfanew)并检验地址有效性,失败则报错“Invalid PE header offset”。
检验NT头签名(IMAGE_NT_HEADERS -> Signature)是否为00 00 45 50,失败则报错“Invalid PE signature”。
检验CPU架构,验证Machine值(IMAGE_NT_HEADERS -> IMAGE_FILE_HEADER -> Machine)是否和CPU架构兼容,失败则报错“Unsupported machine type”。
检验可选头大小,验证SizeOfOptionalHeader(IMAGE_NT_HEADERS -> IMAGE_FILE_HEADER -> SizeOfOptionalHeader)是否与IMAGE_OPTIONAL_HEADER长度匹配,失败则报错“Invalid optional header size”。
检验文件头魔术字,验证Magic值(IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> Magic)有效性,失败则报错“Invalid PE format”。
检验入口点有效性,检查AddressOfEntryPoint(IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> AddressOfEntryPoint是否落在任一节的VirtualAddress 范围内,失败则报错“Invalid entry point”。
检验内存对齐粒度,确认 SectionAlignment ≥ FileAlignment(IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> SectionAlignment/FileAlignment),失败则报错“Invalid alignment values”。
校验映像大小,计算 SizeOfImage(IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> SizeOfImage)是否 ≥ 所有头 + 节的内存总和,失败则报错“Corrupt image size”。
内存映射
遍历节表,按NumberOfSections(IMAGE_NT_HEADERS -> IMAGE_FILE_HEADER -> NumberOfSections)循环处理每个IMAGE_SECTION_HEADER,处理中检验PointerToRawData(IMAGE_SECTION_HEADER -> PointerToRawData)是否超出文件大小和Characteristics(IMAGE_SECTION_HEADER -> Characteristics)权限是否合法(如可执行节不可写),失败则报错 “Invalid section table”。
分配虚拟内存,调用VirtualAlloc(Windows API中用于操作虚拟内存的核心函数)按ImageBase(IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> ImageBase)和SizeOfImage(IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> SizeOfImage)保留地址空间,失败则报错“Memory allocation failed”。
映射头部数据,复制 DOS头 + NT头 + 节表到 ImageBase。
加载节数据,按PointerToRawData和SizeOfRawData(IMAGE_SECTION_HEADER -> PointerToRawData/SizeOfRawData)将文件内容写入VirtualAddress(IMAGE_SECTION_HEADER -> VirtualAddress)对应内存。
设置内存保护,根据Characteristics(IMAGE_SECTION_HEADER -> Characteristics)调用VirtualProtect(Windows API 中用于动态修改内存页保护属性的关键函数)。
动态链接处理
解析导入表,从 DataDirectory[1]定位IMAGE_IMPORT_DESCRIPTOR数组关键数据(IMAGE_OPTIONAL_HEADER -> IMAGE_DATA_DIRECTORY[1] -> VirtualAddress -> IMAGE_IMPORT_DESCRIPTOR -> Name/OriginalFirstThunk/FirstThunk),对每个DLL:按 Name 字段加载依赖库(LoadLibrary),遍历 OriginalFirstThunk 解析函数名/序号,填充 FirstThunk 指向的 IAT。
应用重定位(ASLR),若实际加载地址 ≠ ImageBase则从 DataDirectory[5] 读取 IMAGE_BASE_RELOCATION(IMAGE_OPTIONAL_HEADER -> IMAGE_DATA_DIRECTORY[5] -> VirtualAddress -> IMAGE_BASE_RELOCATION),按重定位项修正代码/数据中的绝对地址。
执行准备
初始化线程局部存储(TLS),若存在 DataDirectory[9],调用 TLS 回调函数
调用入口点,跳转至 ImageBase + AddressOfEntryPoint,是EXE → 进入 mainCRTStartup,是DLL → 调用 DllMain(若存在)。
这个执行流程主要就是可以了解文件结构一些关键字段的使用。比如加壳,一些壳会对文件头部进行扭曲,也就是说,某一些值是调试器严重依赖而执行器并不依赖的。文件无法被调试,但是又能成功执行,这就是因为那些执行中需要被用到的值并没有被改变,这里列出的执行过程中遇到的一些值就是执行中需要依赖的值,理论上来说,除这些值以外修改后不影响文件执行,不过实际情况好像要复杂一些。

这个是我修改的一个Helloworld程序的头部(突然感觉能利用这个在程序里加一点彩蛋啥的),里面修改了一些执行中不需要依赖的值,所以尝试运行完全不会有问题,可以用工具扫描一下对比看看:


第一张是正常的程序,第二张是修改过的程序,虽然不足以干扰到调试器,但是其实可以看到有一些区别了。
一点个人学习经验,欢迎交流、指正。