简介:这份PE文件结构详解PDF对照《加密与破解》第十章,系统梳理Windows下exe、dll、sys等可执行文件的格式规范,适合逆向工程、软件安全、病毒分析初学者,也适合备考事业单位计算机岗位的读者夯实底层基础,还可作为高校相关课程的课外补充资料。资源共1个文件,为PDF电子书,压缩包大小仅261KB,轻量便携,便于随时查阅或打印学习。目前已有175人下载学习。文档重点拆解DOS头、NT头、节区表三大组成,详细标注e_magic、e_lfanew、Signature、Machine、NumberOfSections、SizeOfOptionalHeader、AddressOfEntryPoint、ImageBase等关键字段含义,例如e_lfanew字段就用于定位真正的NT头位置;同时结合十六进制偏移图,直观展示IMAGE_OPTIONAL_HEADER32、IMAGE_SECTION_HEADER、IMAGE_IMPORT_DESCRIPTOR等结构的实际排布,并延伸到导入表、数据目录、区块对齐等核心概念。通过这份整理,读者能快速建立PE结构全景认知,为后续调试、脱壳、加壳、导入表分析等实操打下扎实基础。
1. 为什么逆向绕不开PE文件结构
做软件安全、病毒分析、加壳脱壳,或者单纯想搞明白一个exe为什么能跑起来的人,迟早都会撞上PE文件结构这座山。我在看《加密与破解》第十章的时候,最大的感受就是:很多人在破解或逆向时卡住,不是算法看不懂,而是连文件在内存里长什么样都没搞清楚。PE结构不是单纯的理论知识,它直接决定了你能否定位关键代码、能否在dump之后修好一个能跑的进程、能否理解加壳工具到底动了什么手脚。
这一章覆盖的内容很典型:从DOS头到节表,从导入表到内存映射,几乎就是一部微型Windows可执行文件百科全书。PDF版的章节内容平时翻阅比较费劲,我的建议是把它拆成一张可以随手查的结构图,然后对照实际文件一个个字段去验证,这样比抱着PDF死记硬背要有效得多。
这篇文章我就按自己的实操经验,把《加密与破解》第十章里关于PE结构的关键知识点重新捋一遍,重点放在“逆向时你真正需要关心哪些字段”以及“这些字段在加壳、脱壳、dump修复中是怎么被用到的”这两个维度。适合刚接触逆向、被各种术语绕晕的初学者,也适合有基础但想系统整理一遍PE知识的老手。
2. PE文件的总体布局与核心概念
2.1 磁盘中的PE文件和内存中的PE文件
PE文件在磁盘上和在内存中,虽然整体结构相似,但不是完全一致的。最典型的就是节区的对齐粒度:磁盘上通常按0x200字节对齐,内存中通常按0x1000字节对齐。这个差异直接导致了RVA(相对虚拟地址)和FOA(文件偏移地址)之间的换算,是所有手工分析绕不开的基本功。
《加密与破解》第十章一开始就强调了这一点:分析PE文件时,头脑里要有两张图。一张是磁盘上的原始字节排列,另一张是Windows加载器把文件映射进内存之后的虚拟地址布局。我们在调试器里看到的地址是虚拟地址VA,而用十六进制编辑器打开的偏移是文件偏移FOA。分析工具(比如CFF Explorer)能自动换算,但手工调试时你得知道换算方法,不然连下断点都可能找错位置。
百分百的精力放在文件头字段上是没必要的,但有几个结构必须熟练到闭眼都能默写:DOS头里的e_magic和e_lfanew,NT头里的Signature、FileHeader、OptionalHeader,以及节表里的Name、VirtualSize、VirtualAddress、SizeOfRawData、PointerToRawData、Characteristics。这六个节表字段,尤其是后四个,几乎参与了所有和dump、加壳、定位相关的操作。
2.2 从DOS头到NT头的跳跃
每个PE文件的最开头是DOS头,它保留着MS-DOS时代的兼容性,以及一个极其关键的字段——e_lfanew。这个字段是一个文件偏移值,指向真正的PE文件头(NT头)所在的位置。用十六进制编辑器打开exe,你会看到文件开头是4D 5A(MZ),然后在偏移0x3C处读到的4字节值,就是NT头的起始偏移,通常是0x000000E0之类的值。
很多加壳工具会修改DOS头甚至伪造它,但e_lfanew几乎不会被破坏,因为Windows加载器要靠它找到NT头。手工分析时,我一般直接用CFF Explorer点开“DOS Header”一目了然,但在写脚本或手工patch时,这个字段的价值就体现出来了——它是最稳定的PE定位锚点。
NT头由三部分组成:4字节的Signature(通常是0x00004550,即"PE\0\0")、20字节的FileHeader、以及OptionalHeader。FileHeader里有Machine字段(x86是0x14C,x64是0x8664)、节区数量NumberOfSections、时间戳、符号表信息等。对逆向来说,NumberOfSections和Characteristics比较常用,前者决定遍历节表时循环多少次,后者用于判断文件是否是可执行文件、是否为DLL等。
2.3 OptionalHeader里的关键字段
OptionalHeader名字叫“可选”,实际上必选,它是对齐字段、入口点、镜像基址、节区对齐、子系统等信息的集合。在32位和64位下,它有不同变体(PE32和PE32+),但关键字段一致。
这里最需要背下来的几个字段包括:
- AddressOfEntryPoint(入口点RVA):程序执行的起始位置,加壳工具几乎都会修改它指向壳的入口,而脱壳时你要找的OEP(原始入口点)就是壳解密完成后跳回去的那个地址。
- ImageBase(镜像基址):exe默认是0x00400000(32位)或0x0000000140000000(64位),DLL通常也是这个范围。加了ASLR的系统里,实际加载基址会变化,但文件里写的这个值是PE头的一部分。
- SectionAlignment和FileAlignment:分别对应内存和文件的对齐值,就是前面说过的0x1000和0x200。
- SizeOfImage:整个镜像在内存中的大小,dump修复时如果这个值不对,加载器直接拒绝运行。
- Subsystem:2表示GUI程序,3表示控制台程序,有些壳会修改这里来干扰分析。
- DataDirectory:一个数组,每个元素是一个IMAGE_DATA_DIRECTORY结构(包含VirtualAddress和Size),其中第0项是导出表,第1项是导入表,第14项是延迟导入表。这些表在加壳时经常被抹掉或重定向,是分析的重点对象。
直白点说,OptionalHeader就是PE的“运行配置单”,看到它你就知道这个程序想以什么方式跑起来。而DataDirectory的各项,则是逆向中定位导入函数、导出函数的地图。
3. 节区表、RVA与FOA换算,绕不过的基本功
3.1 节区表的结构与常见节区
在OptionalHeader之后,紧跟着的是节区表。每个节区表项占用40字节,核心字段就是前面提到的名称、虚拟大小、虚拟地址、原始数据大小、原始数据指针和属性。节区名称只是约定俗称,没有强制标准,加壳工具经常用一些迷惑性的名字比如".upx"、".themida"、"B92"等,不靠名字判断,要看字段值。
常见的节区有这么几类:
- .text:代码段,包含程序的可执行指令,属性通常是0x60000020(可执行、可读)。
- .rdata:只读数据段,通常存放字符串、导入表、导出表、只读全局变量,属性是0x40000040。
- .data:可读写数据段,存放全局变量、静态变量。
- .reloc:重定位表段,DLL和开启了ASLR的exe都有,脱壳时如果没处理这个段,可能运行崩溃。
- .rsrc:资源段,图标、对话框、版本信息、清单等都在这里。
每个段在磁盘上的位置和内存中的位置都由节表项决定。你如果做一个非常小的测试程序,会发现磁盘上整个文件都对齐成0x200的倍数;但在内存里,每节都占用0x1000的倍数。dumped下来的进程镜像可能在SizeOfRawData和指针上对不上,这时候就得手动修正。
3.2 RVA与FOA的换算公式
RVA是虚拟地址VA减去ImageBase得到的相对偏移,FOA是文件内的实际字节偏移。当一个节区在内存中的对齐和文件中不一致时,不能直接用RVA当FOA。换算公式大概是这样的:
- 如果RVA落在某个节区的VirtualAddress到VirtualAddress+Max(VirtualSize, SizeOfRawData)范围内,那么对应的FOA = PointerToRawData + (RVA - VirtualAddress)。
- 如果RVA不在任何节区范围内(比如位于头部数据区域),那FOA一般直接等于RVA(因为头部通常按文件对齐直接映射)。
我举个具体例子:假设某个节的VirtualAddress是0x1000,PointerToRawData是0x400,SizeOfRawData是0x600,你要访问RVA 0x12A4的位置,那么FOA = 0x400 + (0x12A4 - 0x1000) = 0x6A4。这个换算在手动修改导入表、patch字节时几乎是每天都要做的操作。
刚开始接触时,为了熟悉这套换算,我建议拿一个简单的exe,用十六进制编辑器逐字节对照CFF Explorer显示的内容,把几个关键节区全部换算一遍。不要觉得自己用工具就能永远不用手算,等你要写一个Loader、要修一个损坏的dump,或者要解析一个内存镜像时,这套基本功就是你的护身符。
4. 导入表、导出表与加密/破解的天然关联
4.1 导入表机制与IAT
导入表描述的是这个PE文件依赖了哪些外部DLL,以及调用了哪些函数。具体实现分两层:导入描述符(IMAGE_IMPORT_DESCRIPTOR)数组,和它指向的DLL名称字符串、函数名称/序号数组(INT)、以及真正运行时要被修改的IAT数组。
IAT的全称是Import Address Table,原本存放的是函数名称或序号,当程序加载时,Windows加载器会把这些条目替换为函数在内存中的真实地址。这也是导入表在逆向里特别有价值的原因——你在反汇编代码里看到call dword ptr [IAT地址],实际上就是调用一个外部函数。而在动态分析时,通过dump内存中的IAT就能直接看到所有被调用的系统API的真实地址,这对追踪程序行为、分析恶意代码很有帮助。
但加壳技术的核心思路之一,就是隐藏或“模拟”导入表。传统壳会把IAT加密,运行时动态解密、动态获取API地址;更强的壳(比如VMProtect)干脆在虚拟机里模拟API调用,让静态分析看到的导入表几乎为空。所以有没有完整的导入表,经常被用来判断一个程序是否加了壳、壳的强度大概是什么级别。
《加密与破解》第十章里对导入表的剖析,我觉得价值很高的是它把“导入表是数据也是代码”这个点讲透了。导入表里的数据在被加载器填充真实地址之前,它只是普通数据,可以被任意修改;被填充之后,它就是整个进程地址空间的一部分。很多dump修复工具的本质工作,就是根据内存中IAT的真实值,反推出文件里应该怎么写导入表。
4.2 导出表与DLL劫持
导出表则是一个PE文件对外暴露的函数列表,主要用于DLL。它包含三个核心数组:函数地址表(EAT)、函数名称表、名称序数表。调试你自己的DLL、做插件系统、或者分析恶意DLL时,导出表就是首要查看对象。
很多破解分析里的“DLL劫持”思路就基于导出表机制:程序运行时会按一定搜索顺序加载DLL,如果攻击者在一个高优先级的目录下放置同名DLL,并且这个DLL导出了目标程序需要的函数(可以转发给原DLL),就能在目标程序加载DLL时执行自己的代码。
书上这一章虽然没直接给DLL劫持的完整案例,但导出表结构讲明白之后,你完全可以自己构造一个劫持DLL。模版很简单:用你熟悉的语言写一个DLL,用def文件导出和原DLL相同的函数名,并在DllMain里写你的初始化逻辑。关键是理解导出表的名称和序数查找逻辑。
5. 实战演练:手把手分析一个PE文件的关键信息
5.1 环境准备与工具清单
分析PE文件,我推荐的工具有这么几个:
- CFF Explorer:功能全面,能直接修改PE头字段,能修复导入表,是首选图形工具。
- DIE(Detect It Easy):快速识别编译器、加壳类型、熵值,强烈推荐配一个。
- x64dbg / OllyDbg:动态调试,查看内存加载后的PE结构,x64dbg现在基本是标配。
- 十六进制编辑器:010 Editor或HxD都行,用于手工对照字节。
- pefile(Python库):写脚本批量分析时的利器,解析PE结构很方便。
如果你跟着这篇文章做一次分析,建议挑一个你常用的轻量级exe,或者自己编译一个简单的C程序(比如只有一个printf的),这样结构清晰,字段不容易被复杂程序干扰。
5.2 六步定位法,快速建立文件全貌
我自己分析一个陌生PE文件时,有一个固定的“六步定位法”,效率很高,分享给你:
第一步,用DIE看整体类型和是否加壳。如果显示“UPX”、“ASPack”,先按壳的处理思路走;如果显示“Microsoft Visual C++”,说明大概率是未加壳文件。
第二步,用CFF Explorer打开,先看DOS头的e_lfanew值,再跳转到NT头,确认Signature、Machine、NumberOfSections这些基本信息。
第三步,看OptionalHeader里的AddressOfEntryPoint和ImageBase,用ImageBase + AddressOfEntryPoint算出入口点的VA,然后在反汇编窗口里定位到入口。
第四步,查看节区表。列出每个节的名称、虚拟地址、原始大小、属性,先判断哪些节是可执行的。正常情况下.text的原始大小和虚拟大小不会差异太大;如果某个节虚拟大小远大于原始大小(比如.upx节),说明是壳在内存中解压后扩充的。
第五步,看导入表。如果一个exe声称功能复杂,但导入表只有几个函数,基本可以断定它被加壳了或者使用了动态API解析。
第六步,切换到十六进制视图,把节区表里的VirtualAddress和PointerToRawData摘出来,手动找一个RVA换算成FOA,然后在十六进制编辑器里跳转到对应位置,对比文件内容是否一致。这一步是验证你对PE结构理解是否到位的最好练习。
5.3 手工解析一个小型PE文件的关键字节
我们来做一个非常小的实操。用十六进制编辑器打开一个最简单的exe,前两个字节是4D 5A(MZ),确认是DOS头。然后在偏移0x3C读取四字节,比如是E0 00 00 00,即0xE0。跳转到0xE0位置,你会看到50 45 00 00,也就是PE\0\0签名,说明NT头确实在这里。
继续往下读,偏移0xE4和0xE5是Machine字段,值如果是4C 01,说明是x86架构;如果是64 86,说明是x64。接着在NT头后面偏移0xE8+0x10的位置读取NumberOfSections,值就是节区数量。接下来是OptionalHeader,入口点RVA通常在FileHeader之后16字节处(也就是OptionalHeader的偏移0x10位置),这样一段一段解析下去,你就能完全脱离工具,纯手工描述一个PE文件的结构。
这个练习虽然费力,但对建立“PE不过就是有规律的一堆字节”这种感觉非常有效。之后再看那些花里胡哨的壳,你会发现它们本质也只是在这些字段上做文章。
6. 与加密/破解相关的实用技巧
6.1 加壳程序的特征识别
前面提到加壳会修改入口点、压缩节区、隐藏导入表。实际分析时,有几个特征几乎一找一个准:
- 入口点落在最后一节:正常编译的exe入口通常在.text节中,壳为了在解密完成后跳回原程序,会把入口点放进壳自己的节区(通常是最后一个节)。
- 节区数量突然增多:比如UPX壳通常有3~4个节,比如UPX0、UPX1、UPX2;而正常程序可能只有2~3个节。
- 熵值异常高或过低:加壳压缩后节区数据熵值接近随机,DIE会以颜色标识出来。
- 导入表异常稀疏:如前所述,大部分API被隐藏。
6.2 脱壳后的三个修复点
如果只是做简单的解压型壳(UPX、ASPack),脱壳后大概率会遇到三个问题:
第一个是入口点不对。需要手动找到OEP,修改AddressOfEntryPoint。许多情况下,壳的末尾有跳转到OEP的指令,你可以在壳最后一个节区末尾搜索一个跨节跳转,比如E9开头的相对跳转,目标地址就是OEP。
第二个是导入表损坏或稀疏。可以用CFF Explorer的Import Adder从内存进程dump里重建IAT,或者用Scylla工具自动修复。Scylla也会自动处理IAT偏移和长度的问题,运行一次就能生成修复好的二进制。
第三个是重定位表缺失或节区对齐错误。DLL脱壳后尤其容易出这个问题,Scylla会把重定位信息也补一份,不过要确保你Dump的是进程在内存中完整映射的镜像,包含所有节的真实数据,然后再修复PE头。
我在实际脱壳练习中遇到过这样一个情况:UPX脱壳后程序能启动但立即崩溃,原因就是我在dump时丢了最后一个节,因为调试器里该节在内存里是全零,看起来“什么都不像”,实际上那节是.bss类节区,运行时要用的。修复方式就是在dump时把VirtualSize扩展适当加大,然后再手动修正SizeOfImage。这种问题不是你逆向技术不行,而是对PE结构理解不到位。
6.3 补丁制作与patch保存的位置
静态patch一个程序(也就是俗称的“爆破”)时,你要修改的字节往往都在代码节区。判断能否直接修改的关键是Characteristics字段是否包含IMAGE_SCN_MEM_EXECUTE(0x20000000),如果包含,你的修改可以被执行,否则修改无效或者导致异常。
很多破解教程让你把某个JE改成JMP,或者把JNZ改成NOP,本质就是修改.text节内的一个字节。你需要在文件里找到该VA对应的FOA,然后修改后再计算校验和(许多PE有CheckSum字段,但部分程序不校验)。熟练掌握RVA到FOA的换算,这个操作十秒就能完成;如果换算不熟,很可能会把二进制改坏。
6.4 关于“加密”表层之外的三点提醒
《加密与破解》的书名容易让人误解,以为重点是加密算法,但我的体会是PE结构才是它的暗线。加密算法再多,壳再强,分析的落脚点还是“定位代码、修改数据”。所以无论你是想学防破解还是学破解,先把PE结构吃透,精力就不会白费。
有一点得说清楚:这本书里的破解方法只适用于你有权分析、调试、修改的程序,比如你自己写的软件、开源项目或已授权的CTF题目。用它来对付商业软件是不尊重他人劳动的表现,也可能惹上法律麻烦。技术本身没有立场,但用技术的人得有底线。
我见过太多人一上来就找壳、找注册机,连PE头都不会看,最后连调试器里模块列表都读不懂。反过来,如果你把PE结构这一章啃透,再回头看那些加壳、脱壳、dump修复、DLL劫持的知识,就像地图拿在手里一样,剩下的只是时间问题。
7. 我的学习路径建议
如果让我给刚接触PE文件结构的人一个建议,那就是不要只看书,要以“一个月内完全脱离自动工具手工解析一个exe”为目标。拿到一个最简单的exe文件之后,按下面这个顺序来练:
- 第一步,用十六进制编辑器打开,手工找到DOS头和NT头,读取出Machine、NumberOfSections、EntryPoint、ImageBase、SectionAlignment等关键字段。
- 第二步,根据节表信息,手工算出.text节的FOA范围,跳转过去,看看能不能看到类似汇编指令的字节。
- 第三步,找导入表描述符,自己算出DLL名称和IAT的RVA,再换算成FOA验证一下。
- 第四步,用CFF Explorer对照你的手工结果,查漏补缺。
- 第五步,编译一个带UPX壳的同款程序,重复上面的过程,看哪些字段发生了变化。
整个流程做完,你对PE文件的理解深度会远超那些只看教程不实践的人。有时候不是知识不够,而是你还没有让手跟上脑子。
《加密与破解》第十章PDF可以当作你的案头工具书,但最终要内化成自己的“肌肉记忆”。纸上得来终觉浅,PE结构这种东西,不动手拆几个文件,永远觉得隔了一层。等你能一眼看出一个exe大概是什么编译器出来的、是不是加壳了、入口点在哪,那时候你才算真正跨过了逆向的第一道门槛。
本文还有配套的精品资源,点击获取