Last updated on October 13, 2023 am
PE文件结构(一)DOS头 NT头
总起 首先我们需要知道PE是什么。PE,即Portable Executable,也就是Windows下的可执行文件,它包括exe文件,也包括dll、sys文件等,当我们使用十六进制文本编辑器打开一些exe文件时,会得到如下样子
如图是百度网盘的前面一小撮的部分
当我们打开其他的exe文件时,这一小撮内容应当是差不多的。这其实就说明这些PE文件应当具有某种格式。我们不妨先看看整个PE文件结构图
当我们只看最上面一行,可以发现PE文件大致分为如下几部分
IMGAE_DOS_HEADR - DOS头
DOS STUB - DOS填充
IMAGE_NT_HEADR - NT头
SECTION_TABLE - 节表
SECTIONS - 节/节区
OTHERS
在上面给出的百度网盘的文件格式中,所展现的就是DOS头+DOS填充部分。可能你会有个问题,即
这些部分有什么含义?
哪个部分在哪里开始哪里结束呢?
不急 ,这里先给出整个PE文件结构的图
可以看到在上面那个图中,又引申出许多小表,其实这些小表就是一个一个结构体,存储在PE文件结构中。可以这么说:PE文件结构就是由许许多多的结构体组成,每个结构体里的每个成员都有相应的含义,比如指示哪个部分的开始,哪个部分的结束,在指来指去的过程中,整个文件运行所需要的信息也就明确了。
DOS头 我们先来看看DOS头部分的结构体吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct _IMAGE_DOS_HEADER { 0X00 WORD e_magic; 0x3C DWORD e_lfanew; };
简单算算,即整个DOS头只有0x40个字节,那就对比这个结构体来分析一下百度网盘的DOS头吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct _IMAGE_DOS_HEADER { 0X00 WORD e_magic; 0x3C DWORD e_lfanew; };
这里只需要注意第一个成员以及最后一个成员,第一个成员 e_magic
是可执行文件标志,在一个程序运行时,会首先检查这两位来判断这是不是一个可执行文件,最后一个成员e_lfanew
就是存储的就是NT头的地址
可以看到e_lfanew
有一个箭头指向了IMAGE_NT_HEADERS
,也就是NT头。
NT头 先根据DOS头中e_lfanew
所指示的地址0x150,在16进制文本编辑器看看这个NT头是个啥吧
根据上面的PE文件结构图,NT头又可以分成三个部分
Signature
Image_File_Header - 标准PE头
Image_Optional_Header - 可选PE头
其中Signature
占4个字节,即 50 45 00 00
在右边也可以看到就是PE两个字母的ascii码(50 45)
标准PE头 标准PE头占0x14个字节,它也是个结构体,如下是他的结构体成员及相关含义,带星号为重要成员
1 2 3 4 5 6 7 8 9 typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
可选PE头 标准PE头过后就是可选PE头,这里就仅给出结构体成员,带星号为重要成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; 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;
RVA 这里提到了几个新的名词RVA,即Relative Virual Address,虚拟相对地址。
在解释此名词之前,需要明白一件事情,即PE文件实际上有两种格式,一种是存储在硬盘中,也就是16进制编辑器打开的样子,还有一种则是运行时,操作系统会分配一个独立的虚拟空间给它,通常这个虚拟空间大小是4GB,然后把这个文件装载到这个4GB中,装载到哪,则是先由ImageBase字段决定,一旦确定好从哪个地方装载,就相当于确定了一个基址,而这个RVA就是相对这个基址的偏移。
比如这个文件从内存中0x10000000开始装载,如果RVA是0x32,那么在内存中它的地址就是0x10000032
FOA 可能会想起,那我们刚刚提到的e_lfanew
是什么偏移?,既然PE文件有两种格式,那么偏移肯定也有两种,刚刚的提及的RVA是在虚拟内存中的,那么还有一种就是在文件中的偏移,即FOA - File Offset Address,也就是在16进制文本编辑器里的偏移
SectionAlignment and FileAlignment SectionAlignment :在内存里面节的对齐大小,必须是这个的整数倍
FileAlignment:在文件里面节的对齐大小,必须是这个的整数倍
使用代码读取 得益于微软的内置库,可以方便的使用以及定义好的指针类型来读取,而不需要自己手动计算
唯一需要注意的是,由于是文件读入内存中,因此文件在内存中相当于是有一个基址的,所以需要buffer+FOA。注意这里是把文件原封不动的读入程序内存,而不是运行时拉伸进内存。
可以使用网上的一些PE查看器来验证自己是否读出正确数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <stdio.h> #include <windows.h> void PE_info (FILE* fp,char * buffer) { PIMAGE_DOS_HEADER DosHeader = NULL ; PIMAGE_NT_HEADERS NTHeader = NULL ; PIMAGE_FILE_HEADER FILEHeader = NULL ; PIMAGE_OPTIONAL_HEADER OptionalHeader = NULL ; DosHeader = (PIMAGE_DOS_HEADER)(buffer); printf ("=================DosHeader PE_INFO=================\n" ); printf ("DOS_HEADER e_magic:%X\n" ,DosHeader->e_magic); printf ("DOS_HEADER e_lfanew:%X\n" ,DosHeader->e_lfanew); NTHeader = (PIMAGE_NT_HEADERS)(buffer + DosHeader->e_lfanew); printf ("=================NTHeader PE_INFO=================\n" ); printf ("IMAGE_DOS_SIGNATURE:%X\n" ,*(PDWORD)NTHeader); FILEHeader = (PIMAGE_FILE_HEADER)(buffer + DosHeader->e_lfanew + 4 ); printf ("=================FileHeader PE_INFO=================\n" ); printf ("Machine:%X\n" , FILEHeader->Machine); printf ("Number of Sections:%X\n" , FILEHeader->NumberOfSections); printf ("Size of OptionalHeader:%X\n" , FILEHeader->SizeOfOptionalHeader); printf ("Time Stamp:%X\n" , FILEHeader->TimeDateStamp); OptionalHeader = (PIMAGE_OPTIONAL_HEADER)(buffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER); printf ("=================Optional PE_INFO=================\n" ); printf ("Magic:%X\n" , OptionalHeader->Magic); printf ("Address of EntryPoint:%X\n" , OptionalHeader->AddressOfEntryPoint); printf ("ImageBase:%X\n" , OptionalHeader->ImageBase); printf ("SizeOfImage:%X\n" , OptionalHeader->SizeOfImage); printf ("SizeOfHeaders:%X\n" , OptionalHeader->SizeOfHeaders); }int Size (FILE* fp) { int size = 0 ; fseek(fp,0 ,SEEK_END); size = ftell(fp); fseek(fp,0 ,SEEK_SET); return size; }int main () { char * buffer; FILE* fp = NULL ; errno_t err_1 = fopen_s(&fp, "YOU PATH TO EXE HERE" , "rb" ); int size = Size(fp); buffer = (char *)malloc (size); fread(buffer,size,1 ,fp); PE_info(fp,buffer); return 0 ; }
总结 DOS头中第一个成员e_magic
和最后一个成员e_lfanew
最重要,前者是可执行文件的标志MZ,后者是NT头的文件偏移
NT头分三个部分,分别是
Signature - PE
标准PE头,其重要成员
Machine 可以运行在什么样的CPU
NumberofSections 节的数量
TimeDateStamp 时间戳
SizeOfOptionalHeader 可选PE头大小
Characteristics 文件属性
可选PE头,其重要成员且仍有用成分
Magic 该PE文件是什么类型的 10B 32位PE // 20B 64位PE // 107 ROM映像
AddressOfEntryPoint 程序入口,即所谓的OEP
ImageBase 程序的优先装载地址
SectionAlignment 内存中节的对齐粒度
FileAlignment 文件中节的对齐粒度
SizeOfImage 映像大小
SizeOfHeaders 所有节表对齐后的大小
SizeOfStackCommit 初始化时实际提交的栈的大小
SizeOfHeapReserve 初始化时实际提交的堆的大小
NumberOfRvaAndSizes 目录项
(无用但保留的字段基本都是为了兼容)