跳到主要内容
AZ Tools

PE 检查器:Windows EXE 与 DLL 查看

像 dumpbin 或 pefile 那样直接从字节读取 Windows 的 PE 映像,全部在浏览器内完成,不上传任何东西。开头的 MS-DOS 头是历史遗留,真正还有意义的字段只有偏移 0x3C 的 e_lfanew,它指出真正的 "PE\0\0" 签名和 COFF 头从哪里开始。之后你能看到机器类型(x86、x86-64、ARM64、ARM64EC、Itanium、RISC-V 等)、可选头是 PE32 还是 PE32+、文件头是否带 DLL 属性,以及加载器会以哪个子系统启动它:Windows 控制台、Windows GUI、原生,或某种 EFI 变体。加固那一行相当于 Windows 版的 checksec,全部从 DllCharacteristics 读出:ASLR(DYNAMIC_BASE)、高熵 ASLR(把 64 位映像的重定位随机位从 8 位提高到 17 位以上)、DEP/NX(NX_COMPAT)、Control Flow Guard(GUARD_CF)、是否禁用了结构化异常处理(NO_SEH)、强制完整性校验,以及是否存在证书表。节表列出虚拟地址、虚拟大小与文件内大小以及解码后的属性;同时可写又可执行的节会被单独标出,因为除了加壳工具、安装程序和自修改代码,几乎没有理由这样做。导入表按 DLL 分组并列出每个函数名,按序号导入、文件里根本没有记录名字的条目则显示为 #序号——这张表是“这个程序到底需要什么”最诚实的回答。导出表给出库自己声明的名称、序号基址(常常不是 1)和全部导出名。有两个限制要说清楚:证书目录不为空只说明文件带了签名,并不说明签名有效、未过期或来自你信任的人;校验 Authenticode 需要证书链和时间戳机构,不在本工具范围内。链接时间也只是链接器写入的一个字段,可复现构建会把它固定成常量或哈希。畸形文件会被拒绝而不是读一半:MZ 标记不对、DOS 头没有指向 PE 签名、可选头 magic 无法识别、节声明的数据超出文件长度,都会给出各自命名的错误。

使用方法

  1. 把文件拖到方框里。.exe、.dll、.sys 驱动、.ocx 控件、.efi 二进制的解析方式完全一样,起决定作用的不是扩展名。
  2. 看文件头的卡片:架构、PE32 还是 PE32+、EXE 还是 DLL、子系统。同一个程序的控制台版和 GUI 版只在这里不同。
  3. 看加固标签。现在的 Windows 工具链默认开启 ASLR、DEP 和 CFG,所以较新的二进制上出现黄色标签,发布前应当能解释原因。
  4. 在导入表里找你关心的能力:WS2_32 是套接字,WININET 或 WINHTTP 表示会访问网络,ADVAPI32 表示操作注册表或服务,CRYPT32 表示证书。
  5. 在节表里看有没有同时可写可执行的节,在数据目录里看有没有证书、调试项或 CLR 运行时头,最后这一项会改变其余内容的读法。

常见问题

文件显示有签名标签,是不是说明它安全?
不是,这个区别很重要。标签只说明证书数据目录不为空,也就是最后一个节之后附加了一段 Authenticode 数据。它并没有说这段数据与前面的字节一致,没有说签名证书能链接到 Windows 信任的根,没有说签名时证书仍然有效,也没有说签名者就是文件声称的那一方。这些都可能不成立而目录照样是满的:可以用自签发证书重新签名,也可以在签名之后修改内容让哈希对不上。真正校验需要完整的证书链、吊销信息,通常还需要可信的时间戳副署,而这是网络操作,本就不属于一个承诺绝不上传你文件的浏览器工具的职责。
高熵 ASLR 比普通 ASLR 多了什么?
普通 ASLR 会在加载时把映像放到随机基址,但在 32 位 Windows 上真正随机的只有大约 8 位地址,少到可以循环暴力试出来。HIGH_ENTROPY_VA 告诉加载器这个映像放到 64 位地址空间的任何位置都安全,于是随机位数升到 17 位以上,猜测变得不切实际。它只有在同时设置了 DYNAMIC_BASE 的 PE32+ 映像上才有意义,而且要求程序不会把指针截断成 32 位——这正是链接器把它放在 /HIGHENTROPYVA 后面的原因,也是某些从 32 位时代代码演化而来的 64 位程序故意不开它的原因。
为什么有些导入函数显示成 #12 而不是名字?
因为文件是按序号导入它们的。thunk 数组的每一项都是一个机器字,最高位是标志位:置位时低 16 位就是序号,要到导出方 DLL 的地址表里去查,导入方文件里任何地方都没有存名字。清零时字的其余部分是一个 RVA,指向一对 hint/name,名字就在那里。按序号导入解析稍快,在旧代码里很常见;想隐藏调用了哪个 API 时也会这么做。本工具直接显示原始序号而不去猜,因为序号到名字的对应关系存在导出方 DLL 里,还取决于系统上装的是哪个版本。
节表里有一个既可写又可执行的节,问题有多大?
少见到值得看一眼,但单凭它不能证明什么。正常的编译器和链接器把 .text 设成可读可执行、把 .data 设成可读可写,绝不会两者兼有,因为 DEP 存在的目的就是让这种组合触发异常。同时带 IMAGE_SCN_MEM_WRITE 和 IMAGE_SCN_MEM_EXECUTE 的节通常来自运行时加壳工具:它把代码解压到自己的节里再跳进去,UPX 就把那个节命名为 UPX1。老式安装程序、自修改的保护方案,或在映像内分配的 JIT 也会这样。如果这些你都没预期到,那么运行之前值得先弄清楚这个程序在做什么。
链接时间显示 1970 年或者未来的日期,文件坏了吗?
基本不会。TimeDateStamp 是链接器填写的 32 位、以 1970 年为起点的秒数字段,现代工具链早已不把它当日期看。可复现构建会把它置零或固定成常量,好让同样的源码总是产生逐字节相同的输出;MSVC 加上 /Brepro 会在那里写输入的哈希,于是落在日历上一个任意的、往往很遥远的未来时刻。要推断工具链,Rich 头和调试目录通常比这个字段更可靠,而且它们都不能证明文件是什么时候真正发布的。把它当成恰好长得像日期的元数据就好。
这是一个 .NET 程序集,为什么导入表和入口点几乎是空的?
因为对托管程序集来说,PE 这一层只是外壳。当 CLR 运行时数据目录(第 14 项,COM 描述符)不为空时,真正的程序是存放在 .NET 元数据里的 IL,本机头主要是为了让 Windows 加载器满意。典型的 C# 可执行文件只有一个导入,即 mscoree.dll 的 _CorExeMain,入口点是跳向它的两条指令,.text 节里元数据比机器码还多。所以架构字段可能写着 Intel 386,程序却能在 64 位下正常运行,而节的大小也说明不了程序有多大。要真正读懂它,需要 ildasm 或 ILSpy 这类 IL 反汇编器。

相关工具