简介这是一份面向软件开发者、逆向工程学习者与系统管理员的PE格式工具合集围绕Windows可执行文件与DLL的结构解析展开帮助读者查看、分析与修改PE文件的内部组成。压缩包共83个文件约301KB以dll动态库、exe可执行程序为主辅以dsp/dsw工程文件、cpp与h源码、lib静态库、asm汇编、bat脚本及txt说明文档覆盖主程序、插件与SDK示例等模块。其中PETools.exe承担查看与分析PE结构的主功能HEdit32、RebPE32、PESniffer等库分别对应编辑、增强与嗅探用途NDump、xDump偏向内存转储PSAPI与Procs32则涉及进程信息获取另有导入表、导出表、节区表等结构解析支持。目前已有808人学习下载适合希望理解DOS头、COFF头、可选头与节区组织方式并动手调试或修改PE文件的读者参考。1. PE TOOLS为什么每个逆向新手的第一个坎都是它很多人第一次接触 PE 工具不是因为想做逆向而是因为一个更朴素的需求手头有个 exe想看看它到底链接了哪些 DLL、导出了什么函数、是不是 32 位、有没有被加壳。你双击运行它可能报错、可能闪退、可能行为诡异而你手里唯一的线索就是那个二进制文件本身。这时候 PE TOOLS 这类工具就是你的第一双眼睛——它不负责破解也不负责脱壳它只做一件事把 Windows 可执行文件PE 格式的内部结构摊开给你看。PE 是 Portable Executable 的缩写是 Windows 上 exe、dll、sys、ocx 等文件的统一容器格式。它的结构并不复杂但字段极多手工用十六进制编辑器去数偏移量基本等于自虐。PE TOOLS 的价值就在于把 DOS 头、NT 头、节表、导入表、导出表、资源、重定位、TLS、调试目录这些结构用树形或表格的方式呈现出来。适合谁三类人做安全分析的需要快速判断样本性质做软件排障的需要确认依赖和导出做开发的想验证自己编译出来的产物是否符合预期。这一章先把「它是什么、能解决什么」讲清楚后面几章再落到具体怎么看、怎么用、怎么避坑。2. PE 文件结构拆解从 DOS 头到节表每一段在看什么2.1 为什么 PE 工具的第一个面板永远是 DOS 头打开任何一个 PE 文件前 64 字节是 DOS 头。它的存在是历史包袱但有两个字段你必须记住e_magic和e_lfanew。e_magic固定是0x5A4D也就是 ASCII 的 MZ这是判断一个文件是不是 PE 的第一道门槛。e_lfanew在偏移0x3C处是一个 4 字节的偏移量指向真正的 NT 头起始位置。很多新手用 PE 工具时只看节表忽略了e_lfanew结果遇到某些加壳或手工构造的样本时工具显示异常却不知道问题出在哪。常见做法是先用十六进制视图跳到0x3C读出e_lfanew的值再跳过去确认PE\0\0签名。如果e_lfanew指向的位置不是0x00004550那这个文件要么被破坏要么被刻意篡改过。import struct def parse_dos_header(data: bytes): # e_magic 在 0x00e_lfanew 在 0x3C e_magic struct.unpack_from(H, data, 0x00)[0] e_lfanew struct.unpack_from(I, data, 0x3C)[0] print(fe_magic {hex(e_magic)} (期望 0x5a4d)) print(fe_lfanew {hex(e_lfanew)}) # 跳到 e_lfanew 处读 PE 签名 pe_sig struct.unpack_from(I, data, e_lfanew)[0] print(fPE signature {hex(pe_sig)} (期望 0x4550)) return e_lfanew with open(sample.exe, rb) as f: raw f.read() parse_dos_header(raw)这段代码只做一件事验证 DOS 头和 PE 签名。参数上struct.unpack_from的第二个参数是偏移量H表示小端 2 字节I表示小端 4 字节。如果你读出来的e_lfanew大于文件长度说明文件被截断或头部被改。PE TOOLS 在界面上通常会直接显示这两个值但知道底层怎么读遇到工具报错时你才有排查方向。2.2 NT 头里的 FileHeader 和 OptionalHeader 怎么区分NT 头由三部分组成4 字节签名、20 字节 FileHeader、以及可变长度的 OptionalHeader。FileHeader 里最关键的是Machine、NumberOfSections、SizeOfOptionalHeader、Characteristics。Machine告诉你目标架构0x014c是 x860x8664是 x640x01c4是 ARM。NumberOfSections决定后面节表有多少项。OptionalHeader 虽然叫 Optional但对可执行文件来说是必须的。它里面有两个字段直接决定程序能不能跑起来AddressOfEntryPoint和ImageBase。入口点是一个 RVA相对虚拟地址不是文件偏移。很多新手在这里翻车拿着入口点的 RVA 直接去文件里找发现对不上。正确做法是先判断这个 RVA 落在哪个节里再用节的文件偏移和虚拟偏移做换算。字段偏移x86含义常见值Magic0x00可选头类型0x10b (PE32) / 0x20b (PE32)AddressOfEntryPoint0x10入口点 RVA随程序变化ImageBase0x1C首选加载基址0x400000 / 0x140000000SectionAlignment0x20内存对齐0x1000FileAlignment0x24文件对齐0x200SizeOfImage0x38内存中总大小随节表变化Subsystem0x44子系统2GUI, 3CUI这张表建议存下来。用 PE TOOLS 看 OptionalHeader 时对照着看比死记偏移强。SectionAlignment和FileAlignment的关系决定了节在文件和内存中的布局差异后面算 RVA 转文件偏移时要用到。2.3 节表RVA 转文件偏移的核心公式节表紧跟在 OptionalHeader 之后每个节 40 字节。关键字段有Name、VirtualAddress、VirtualSize、SizeOfRawData、PointerToRawData、Characteristics。RVA 转文件偏移的公式是文件偏移 RVA - 节的 VirtualAddress 节的 PointerToRawData前提是这个 RVA 落在该节的VirtualAddress到VirtualAddress VirtualSize范围内。如果落在节与节之间的空隙或者超出所有节的范围那这个 RVA 就是无效的。很多壳会把入口点设到节外或者把节名改成空、改成乱码PE TOOLS 里看到节名异常、VirtualSize远大于SizeOfRawData基本可以怀疑被处理过。def rva_to_offset(data: bytes, e_lfanew: int, rva: int): # 读 FileHeader 的 NumberOfSections 和 SizeOfOptionalHeader num_sections struct.unpack_from(H, data, e_lfanew 6)[0] size_opt struct.unpack_from(H, data, e_lfanew 20)[0] # 节表起始 NT头 4 20 SizeOfOptionalHeader sec_off e_lfanew 24 size_opt for i in range(num_sections): base sec_off i * 40 va struct.unpack_from(I, data, base 12)[0] vsize struct.unpack_from(I, data, base 8)[0] raw struct.unpack_from(I, data, base 20)[0] if va rva va vsize: return rva - va raw return None参数说明e_lfanew从 DOS 头拿rva是你要转换的地址。返回None表示 RVA 不在任何节内。这个函数是后面看导入表、导出表、资源的基础建议自己手写一遍不要只依赖工具。3. 用 PE TOOLS 看导入表、导出表和资源三个高频场景3.1 导入表判断一个程序依赖什么、可能做什么导入表Import Table列出程序运行时需要从其他 DLL 加载的函数。PE TOOLS 通常按 DLL 分组显示每个 DLL 下面列出函数名或序号。看导入表有两个目的一是确认依赖是否完整二是从函数名推测行为。比如看到kernel32.dll下的CreateFileW、ReadFile、WriteFile说明有文件操作看到ws2_32.dll下的socket、connect、send说明有网络通信看到advapi32.dll下的RegOpenKeyExW、RegSetValueExW说明碰注册表。这些不是证据但是线索。如果导入表里出现LoadLibrary和GetProcAddress说明程序可能在运行时动态解析 API静态看导入表就不完整了。用 PE TOOLS 看导入表时注意区分按名称导入和按序号导入。按序号导入的函数只显示一个数字没有名字这种常见于某些系统 DLL 或刻意隐藏意图的样本。遇到大量按序号导入且集中在少数几个 DLL值得多看一眼。3.2 导出表DLL 对外暴露了什么导出表Export Table主要出现在 DLL 里列出这个 DLL 对外提供的函数。PE TOOLS 会显示导出函数的名称、序号和 RVA。看导出表时关注三点导出数量、是否有名称、是否有转发。转发Forwarder是指一个 DLL 的导出函数实际指向另一个 DLL 的函数。PE TOOLS 里通常显示为OtherDLL.FunctionName的形式。系统 DLL 大量使用转发比如kernel32.dll里很多函数转发到kernelbase.dll。如果你在看一个非系统 DLL发现里面有转发说明它可能是个包装层。导出函数的 RVA 也可以用来定位代码。把 RVA 转成文件偏移跳到对应位置就能看到函数的机器码。这是做静态分析的基本功PE TOOLS 只负责给你 RVA换算和跳转得自己来。3.3 资源节图标、版本、清单文件都藏在这里资源Resource在 PE 里是一棵三级树类型、名称、语言。常见类型有图标RT_ICON、版本信息RT_VERSION、清单RT_MANIFEST、字符串表RT_STRING。PE TOOLS 一般提供资源浏览器可以按树展开并预览。版本信息里藏着CompanyName、FileDescription、FileVersion、OriginalFilename等字段。这些字段可以伪造但伪造往往不彻底。比如一个样本声称是某系统组件但版本信息里的语言是异常值或者OriginalFilename和实际文件名对不上这些都是排查时的切入点。清单文件Manifest决定程序请求的权限级别。requestedExecutionLevel如果是requireAdministrator说明程序启动就要管理员权限。PE TOOLS 里可以直接查看清单内容不需要额外工具。如果你在排查一个程序为什么总是弹 UAC先看清单。# 用 Python 的 pefile 库快速导出资源列表需先 pip install pefile python -c import pefile pe pefile.PE(sample.exe) if hasattr(pe, DIRECTORY_ENTRY_RESOURCE): for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries: print(entry.name, entry.id) 这段命令用pefile库遍历资源目录打印资源类型。entry.name是字符串名称entry.id是数字 ID。PE TOOLS 图形界面更直观但脚本适合批量处理。参数上pefile.PE默认会解析所有目录如果文件很大或结构异常可以加fast_loadTrue只加载基本结构。4. 避坑与排查PE 工具使用中的五个血泪教训4.1 工具显示“不是有效的 PE 文件”但文件明明能运行现象PE TOOLS 打开报错说签名无效或结构异常但双击 exe 能正常跑。原因通常有三种文件被加壳壳修改了头部导致标准解析失败文件是 .NET 程序虽然也是 PE但元数据布局特殊文件被追加了数据e_lfanew指向的 NT 头被移动或覆盖。解决先用十六进制视图确认MZ和PE签名是否还在如果签名在但工具不认换一个解析库或手工读关键字段。能运行不代表结构标准壳和自修改程序都会让工具翻车。4.2 RVA 转文件偏移算出来是错的现象按公式算出来的偏移跳过去看到的不是预期数据。原因忽略了节的对齐。VirtualSize是内存中的实际大小SizeOfRawData是文件中的对齐后大小。如果 RVA 落在VirtualSize之外但在SizeOfRawData之内公式仍然成立但数据可能是填充。更常见的原因是拿错了节——RVA 可能落在头部区域而头部不属于任何节。解决先判断 RVA 是否小于第一个节的VirtualAddress如果是直接按 RVA 当文件偏移用头部在文件和内存中布局一致。4.3 导入表里全是序号没有函数名现象PE TOOLS 显示导入函数只有数字没有名称。原因样本按序号导入或者导入表被破坏。按序号导入是合法行为但常见于刻意隐藏 API 调用的场景。解决查对应 DLL 的导出表用序号反查函数名。如果目标 DLL 是系统 DLL可以查公开的导出表数据如果是第三方 DLL需要拿到那个 DLL 本身。PE TOOLS 一般支持加载两个文件做对照但手工查更可靠。4.4 节名是乱码或空节数量异常多现象节表里节名不是.text、.data这类标准名而是空、乱码或者节数量达到十几个。原因加壳、混淆、或者手工构造。正常编译器产出的 PE 节数量通常在 5 到 8 个之间节名有规律。解决不要慌先看入口点落在哪个节再看那个节的Characteristics是否包含可执行标志。如果入口点在一个没有名字、权限为可读可写可执行的节里基本可以确定被处理过。这时候静态分析难度大需要考虑动态方法。4.5 32 位工具看 64 位文件字段全错位现象用只支持 PE32 的工具打开 64 位 exeOptionalHeader 的字段读出来全是离谱的值。原因PE32 和 PE32 的 OptionalHeader 布局不同ImageBase在 PE32 里是 4 字节在 PE32 里是 8 字节后续字段偏移全部顺延。解决先看Magic字段0x10b是 PE320x20b是 PE32。确认之后再用对应的结构去解析。PE TOOLS 一般会自动识别但自己写脚本时这是高频错误。5. 进阶用脚本批量提取 PE 特征做快速筛选5.1 批量提取入口点、节数量、导入 DLL 列表当你手头有几十上百个样本时一个个用图形界面打开不现实。常见做法是写一个脚本遍历目录对每个文件提取关键特征输出成表格。特征不用多入口点、节数量、导入 DLL 列表、是否有资源、是否签过名这几项就够做初步分类。import os import pefile def extract_features(path): try: pe pefile.PE(path, fast_loadTrue) pe.parse_data_directories() feat { file: os.path.basename(path), machine: hex(pe.FILE_HEADER.Machine), sections: pe.FILE_HEADER.NumberOfSections, entry: hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint), dlls: [], has_res: False, } if hasattr(pe, DIRECTORY_ENTRY_IMPORT): feat[dlls] [e.dll.decode(errorsignore) for e in pe.DIRECTORY_ENTRY_IMPORT] if hasattr(pe, DIRECTORY_ENTRY_RESOURCE): feat[has_res] True pe.close() return feat except Exception as e: return {file: os.path.basename(path), error: str(e)} for root, _, files in os.walk(./samples): for f in files: p os.path.join(root, f) print(extract_features(p))这段代码用fast_loadTrue加快加载再手动调用parse_data_directories解析导入和资源。pe.close()释放文件句柄批量处理时很重要否则可能遇到文件占用。输出可以直接重定向到文件再用表格工具筛选。参数上errorsignore防止 DLL 名解码失败中断流程。5.2 用特征做初步分类的判断逻辑拿到特征表之后怎么判断几个经验规则节数量少于 3 且导入 DLL 极少可能是壳或极小程序导入表里有LoadLibrary和GetProcAddress且 DLL 列表很短可能是动态解析入口点不在第一个可执行节可能被改过Machine和实际运行环境不匹配可能是跨平台样本。这些规则不绝对但能帮你把大量样本分成几堆优先看可疑的。5.3 验证方法用系统自带工具交叉确认脚本跑出来的结果建议用系统自带工具交叉验证。比如用dumpbin /headers看头部用dumpbin /imports看导入表。如果脚本和系统工具结果不一致优先信系统工具然后回头检查脚本的解析逻辑。常见差异来源是 PE32 的字段偏移和节对齐处理。交叉验证这一步不能省否则批量筛选的结论可能整体偏移。我自己的习惯是任何脚本产出的 PE 特征至少抽三个样本手工用 PE TOOLS 核对一遍。核对重点看入口点、节数量和导入 DLL 列表。这个习惯帮我挡掉过好几次因为结构体定义错误导致的批量误判。希望帮到你。本文还有配套的精品资源点击获取