PE文件格式(Portable Executable File)是Windows操作系统下可执行文件使用的一种文件格式,广泛用于存储可执行程序(EXE文件)、动态链接库(DLL文件)、驱动程序(SYS文件)以及其他类型的程序代码,它是Windows应用程序的基本运行单元。PE文件结构是经过精心设计的,以支持程序在不同版本的Windows操作系统上能够移植、执行,并能与操作系统的加载机制和动态链接功能兼容。
事实上,在Windows上,文件的拓展名并不直接决定文件是否是PE(Portable Executable)文件;PE文件的识别主要是通过其文件头(特别是DOS头和PE头)来确定,而不是文件的拓展名。拓展名只是用来帮助用户和操作系统识别文件类型的,并不能确定文件的内部格式。
PE 文件作为 Windows 操作系统下的标准可执行文件格式,它在逆向工程、安全研究、免杀技术和病毒分析中扮演着至关重要的角色,理解 PE 文件结构对于逆向分析、恶意软件分析、漏洞研究以及开发反病毒技术具有重要意义。
PE文件结构
PE文件格式包含多个部分,主要包括:
DOS头部 (DOS Header)、PE头部 (PE Header):文件头 (File Header)、可选头 (Optional Header)、节区表 (Section Table)、节区数据 (Section Data):各个节区的内容,如代码、数据、资源等、资源部分 (Resources)、重定位表 (Relocation Table)、导入表 (Import Table)、导出表 (Export Table)、TLS表 (Thread Local Storage)、调试信息 (Debug Information)、符号表 (Symbol Table)、异常处理表 (Exception Handling Table)
本篇文章主要针对PE
文件结构中的DOS
头部进行介绍。首先我们写一个简单程序,以供我们后续进行PE文件结构学习过程中使用。
#include <iostream>
#include <windows.h>
#pragma comment( linker, "/subsystem:"windows" /entry:"mainCRTStartup"" ) // 设置入口地址;隐藏程序黑窗口
int main() {
MessageBox(NULL, "Hello Wolven", "Hey", MB_HELP);
return 0;
}
代码解释
#pragma comment( linker, "/subsystem:"windows" /entry:"mainCRTStartup"" ) // 设置入口地址
这行代码是一个 编译器指令,用于在 Windows 平台上设置程序的 子系统类型 和 入口点。具体来说,这行代码使用 #pragma comment
来传递链接器指令,从而影响编译过程中的链接设置。
#pragma comment(linker, ...)
:这是一个编译器指令,它向链接器传递命令。在这里,linker
表示这条命令将传递给链接器,而不是编译器。链接器是负责将目标文件(.obj 文件)合并并生成最终可执行文件(.exe)的工具。/subsystem:"windows"
:这部分告诉链接器,生成的程序是一个 Windows GUI 应用程序,而不是一个 控制台应用程序。这样,程序不会显示控制台窗口(黑窗口)。/entry:"mainCRTStartup"
:该部分告诉链接器使用 mainCRTStartup
作为程序的入口点。mainCRTStartup
是 C 运行时(CRT
,C Runtime
)库的入口点,用于初始化程序(包括设置堆栈、初始化 C 库等)。
MessageBox
是一个用于在 Windows 操作系统中显示消息框的函数。其原型如下:
int MessageBox(
HWND hWnd, // 父窗口句柄
LPCSTR lpText, // 消息框中显示的文本内容
LPCSTR lpCaption,// 消息框的标题栏文本
UINT uType // 消息框的样式
);
接着右击项目,生成程序;
并在同一个菜单中,选择在文件资源管理器中打开文件夹;找到生成的程序,可以看到最后程序执行的结果如下:
DOS 头部
PE
文件中的DOS
头部(DOS Header)是PE
文件的最前端部分,虽然它在现代Windows
操作系统中已经基本没有实际用途,但它仍然保留在文件格式中,以保持向后兼容性;DOS
头部位于PE
文件的开头,通常为 64 字节。Windows 95及之前的版本中,该部分是DOS程序的入口点,为了兼容性,DOS头部仍然保留至今。
在Visual Studio中查看DOS头部结构
打开Visual Studio
,打开任意可以编写代码的文件,写入结构体类型:_IMAGE_DOS_HEADER
;
接着按下ctrl
的同时点击该结构体进行结构查看:即可看到DOS
头部的具体结构。
完整结构如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
DOS头部中两个比较重要的成员:e_magic(Magic number)
和e_lfanew(File address of new exe header)
;
e_magic(Magic Number)
DOS头部的Magic number 0x5A4D(ASCII字符M和Z)是PE文件格式的一部分,它作为历史遗留的标识符,表示该文件最初为一个可执行的DOS程序;这使得早期的操作系统能够识别文件类型并正确加载。但在现代的Windows系统中,它并不再代表DOS文件本身,而是作为PE文件格式的一部分来保持兼容性。
e_lfanew(File address of new exe header)
在DOS头中,e_lfanew字段的大小是4个字节,存储了一个文件偏移地址,指示PE头在文件中的位置。在PE文件中,DOS头的主要作用是提供一些基本的历史兼容性信息,尤其是对老旧的DOS程序的支持。在现代的PE文件中,DOS头的主要作用是作为一个指向PE头部的引导,e_lfanew字段就起到了这一作用。
在现代操作系统中,DOS
头部的作用主要是为了向后兼容,当Windows
试图加载一个PE文件时,如果该文件是一个有效的PE文件,它会首先检测DOS
头部;操作系统会检查 e_magic
字段是否为 MZ
,如果是,操作系统就会继续检查 e_lfanew
字段,它指向的是实际的PE头部。除了e_magic
和e_lfanew
之外 DOS
头部的其他字段大多数已经不再被使用,但它们依然保留在PE文件格式中,主要是为了向后兼容早期的DOS
操作系统。
那么这个时候如果我们修改这些字段会怎么样呢?这个时候我们可以将刚刚生成的程序拖入010 Editor
中进行查看。(关于010 Editor
的注册/破解请转到文章末尾)
010 Editor 是一款功能强大的十六进制编辑器和二进制文件分析工具,广泛应用于数据分析、逆向工程、调试、文件格式分析等领域、010 Editor 提供了高效的十六进制查看和编辑功能,可以直接编辑二进制文件的每个字节。支持多种文件类型的二进制内容显示,适用于分析各种文件格式。
接着我们就可以根据上述代码中各个字段的大小去识别010 Editor
中的PE
文件结构中DOS
头部的内容,如:e_magic
字段类型为WORD(两个字节)
,那么我们就可以知道4D 5A(小端序)
就是标识e_magic
字段:
e_magic
字段后的17个大多数已经不再被使用的字段,在17个不再使用的字段中包含了两个数组e_res[4]
和e_res2[10]
所以这个时候不用的字段总大小则为:
15*word + 4*word + 10*word = 29*word
这个时候我们实际上可以全部用CC
字符进行修改以及填充:
修改后我们可以再次尝试运行程序;发现程序运行成果,因而发现程序运行并不会被这些字段影响。
最后则是DOS头部的最后一个字段也就是指向PE头部的e_lfanew
字段,它的大小为4字节(LONG
类型):
LONG e_lfanew;
在010 Editor
中查看到其位置以及值如下图所示:
这边需要注意,在Windows中,字段的值是以小端序形式进行存储的,所以显示的值为00 01 00 00实际则是00 00 01 00
那么关于这个正在使用的字段,我们可以尝试修改它的值,并观察程序在该字段被修改后还能否正常运行:这边我们将该字段的值全部以CC
进行填充。
接着运行程序后发现,此时程序无法正常运行;由此可知这个字段是不可以随意修改的。
到此DOS
头部就基本上讲述完毕,那么这边存在一个问题,也就是在DOS
头部和PE头部之间存在着一段间隙:
在中文中,这段位于 DOS
头部 和 PE头部
之间的代码间隙通常被称为 DOS
存根或DOS Stub
,有时也称为 MZ存根。
DOS Stub
是一个简单的程序,在现代操作系统下通常没有实际作用,但它用于在 DOS 环境中尝试运行 PE 文件时提供提示信息。其目的是告诉用户该程序不能在 DOS 环境下运行。包含了一段小的代码,通常会显示像 "This program cannot be run in DOS mode"
这样的消息。
同样的,我们对DOS stub
中的值进行随意修改,此处还是以CC
进行填充:
接着运行程序,发现程序依旧能够正常运行。
这也直接说明了在现代操作系统中,这段 DOS Stub
通常没有实际作用,只是一个为了兼容早期DOS
系统而保留的部分。
【注】关注公众号,回复[010]
获取破解版010 Editor。
原文始发于微信公众号(风铃Sec):PE文件结构-DOS头部&DOS stub
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论