PE文件的全称是Portable Executable,意为可移植的可执行文件,是微软Windows操作系统上广泛使用的程序文件格式(包括间接被执行的文件,如DLL)。PE文件被称为“可移植的”是因为在所有平台(如x86、Alpha、MIPS等)上实现的Windows NT及其后续版本(如Windows 95、Windows 2000、Windows XP、Windows Vista、Windows 7、Windows 8、Windows 10等)都使用相同的可执行文件格式。
ELF(Executable and Linking Format可执行可链接格式)文件结构是Linux平台的可执行文件,相比于pe文件,在不同内核编译的ELF文件在不同内核的环境下是无法使用的。
PE文件的识别
判断一个文件是不是PE文件,不能只看他能不能运行,而是要看他符不符合"PE指纹"。
PE指纹是指用于识别PE文件的一种特征标识。
具体来说,PE指纹的识别过程通常包括以下几个步骤:
-
观察前两个字节:打开PE文件,以二进制方式(010editor)查看文件的前两个字节。在PE文件中,这两个字节通常是“MZ”,这是DOS可执行文件的标志,用于兼容旧版DOS系统。虽然“MZ”并不是PE文件特有的,但它通常作为PE文件头部的开始。
-
查找PE标识:在文件偏移量为0x3C(即第60个字节)的位置,有一个DWORD值,它指向DOS块末尾的PE文件头的偏移量。根据这个偏移量,可以找到PE文件头的开始位置。在PE文件头的开始位置,紧接着“MZ”之后,通常会有“PE”或“PE\0\0”(即50 45 00 00)这样的字节序列,这是PE文件的真正标识。
-
验证PE头结构:一旦找到PE标识,就可以进一步验证PE文件头的结构是否符合PE格式的标准。PE文件头包含了关于文件的重要信息,如目标机器类型、节的数量、入口点地址等。
如果以上步骤都满足,那么这就是一个PE文件,如下例:
而作为反例,下面这个apk文件就不满足
PE文件的整体结构
1.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
DWORD e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
DOS头是一个结构体,占2*16+4+14*2=64个字节。里面的e_magic和e_lfanew是鉴定PE指纹重要的依据,非常重要。
DOS块是PE文件格式中用于保持向下兼容性的一个部分,它位于DOS文件头和PE文件头之间。虽然在现代Windows系统中,DOS块的内容通常不会被执行,只会向终端输出一行字:“This program cannot be run in DOS mode”,但它仍然是PE文件格式的一个重要组成部分。程序员可以根据自己的需求来设计和实现DOS块的内容,以提高程序的兼容性和用户体验。
2.PE文件头
PE文件头包括PE标识符(4字节)、IMAGE_FILE_HEADER结构体(20字节)和IMAGE_OPTIONAL_HEADER结构体。IMAGE_FILE_HEADER结构体是标准文件头,IMAGE_OPTIONAL_HEADER结构体是可选文件头。
1.IMAGE_FILE_HEADER结构体
- 内容:包含了关于PE文件的基本信息,如机器类型(如x86或x64)、节的数量、时间戳、文件属性等。
- 关键字段:
- Machine:指定了PE文件的目标机器类型,如
0x014C
表示x86,0x8664
表示x64。 - NumberOfSections:文件中节的数量。
- TimeDateStamp:文件创建的时间。
- SizeOfOptionalHeader:IMAGE_OPTIONAL_HEADER32结构的大小
- Machine:指定了PE文件的目标机器类型,如
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 可运行在什么样的CPU上。
WORD NumberOfSections; // 文件的区块(节)数
DWORD TimeDateStamp; // 文件的创建时间。
DWORD PointerToSymbolTable; // 指向符号表(用于调试)
DWORD NumberOfSymbols; // 符号表中符号的个数(用于调试)
WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32结构的大小,可改变,32位为E0,64位为F0
WORD Characteristics; // 文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
2.IMAGE_OPTIONAL_HEADER结构
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
* WORD Magic; //说明文件的类型 PE32:10BH PE32+:20BH Rom映像文件:107H
BYTE MajorLinkerVersion; //链接器主版本号
BYTE MinorLinkerVersion; //链接器次版本号
* DWORD SizeOfCode; //所有代码节的总和(基于文件对齐) 编译器填的
* DWORD SizeOfInitializedData; //包含所有已经初始化数据的节的总大小 编译器填的
* DWORD SizeOfUninitializedData;//包含未初始化数据的节的总大小 编译器填的
* DWORD AddressOfEntryPoint; //程序入口RVA 在大多数可执行文件中,这个地址不直接指向Main、
WinMain或DIMain函数,而指向运行时的库代码并由它来调用上述函数
* DWORD BaseOfCode; //代码起始RVA,编译器填的
* DWORD BaseOfData; //数据段起始RVA,编译器填的
//
// NT additional fields.
//
* DWORD ImageBase; //内存镜像基址 ,可链接时自己设置
* DWORD SectionAlignment; //内存对齐 一般一页大小4k
* DWORD FileAlignment; //文件对齐 一般一扇区大小512字节,现在也多4k
WORD MajorOperatingSystemVersion; //标识操作系统版本号 主版本号
WORD MinorOperatingSystemVersion; //标识操作系统版本号 次版本号
WORD MajorImageVersion; //PE文件自身的主版本号
WORD MinorImageVersion; //PE文件自身的次版本号
WORD MajorSubsystemVersion; //运行所需子系统主版本号
WORD MinorSubsystemVersion; //运行所需子系统次版本号
DWORD Win32VersionValue; //子系统版本的值,必须为0
* DWORD SizeOfImage; //内存中整个PE文件的映射的尺寸,可比实际的值大,必须是SectionAlignment
的整数倍
* DWORD SizeOfHeaders; //所有头+节表按照文件对齐后的大小,否则加载会出错
* DWORD CheckSum; //校验和,一些系统文件有要求.用来判断文件是否被修改
WORD Subsystem; //子系统 驱动程序(1) 图形界面(2) 控制台、DLL(3)
WORD DllCharacteristics; //文件特性 不是针对DLL文件的
* DWORD SizeOfStackReserve; //初始化时保留的栈大小
* DWORD SizeOfStackCommit; //初始化时实际提交的大小
* DWORD SizeOfHeapReserve; //初始化时保留的堆大小
* DWORD SizeOfHeapCommit; //初始化时保留的堆大小
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; //数据目录项数目
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
默认情况下它在32位下是224字节,在64位下是240字节,也可以通过IMAGE_FILE_HEADER结构的成员去获取/修改扩展PE头的宽度。