PE文件结构(一)

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; //※Magic DOS signature MZ(4Dh 5Ah):MZ标记:用于标记是否是可执行文件
//0X02 WORD e_cblp; //Bytes on last page of file
//0X04 WORD e_cp; //Pages in file
//0X06 WORD e_crlc; //Relocations
//0X08 WORD e_cparhdr; //Size of header in paragraphs
//0X0A WORD e_minalloc; //Minimun extra paragraphs needs
//0X0C WORD e_maxalloc; //Maximun extra paragraphs needs
//0X0E WORD e_ss; //intial(relative)SS value
//0X10 WORD e_sp; //intial SP value
//0X12 WORD e_csum; //Checksum
//0X14 WORD e_ip; //intial IP value
//0X16 WORD e_cs; //intial(relative)CS value
//0X18 WORD e_lfarlc; //File Address of relocation table
//0X1A WORD e_ovno; //Overlay number
//0x1C WORD e_res[4]; //Reserved words
//0x24 WORD e_oemid; //OEM identifier(for e_oeminfo)
//0x26 WORD e_oeminfo; //OEM information;e_oemid specific
//0x28 WORD e_res2[10]; //Reserved words
0x3C DWORD e_lfanew; //※Offset to start of PE header:定位PE文件,PE头相对于文件的偏移量
};

简单算算,即整个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; // 5A 4D
//0X02 WORD e_cblp; // 00 90
//0X04 WORD e_cp; // 03 00
//0X06 WORD e_crlc; // 00 00
//0X08 WORD e_cparhdr; // 00 04
//0X0A WORD e_minalloc; // 00 00
//0X0C WORD e_maxalloc; // FF FF
//0X0E WORD e_ss; // 00 00
//0X10 WORD e_sp; // 00 B8
//0X12 WORD e_csum; // 00 00
//0X14 WORD e_ip; // 00 00
//0X16 WORD e_cs; // 00 00
//0X18 WORD e_lfarlc; // 00 40
//0X1A WORD e_ovno; // 00 00
//0x1C WORD e_res[4]; // 00 00 00 00 00 00 00 00 //注意这是个数组
//0x24 WORD e_oemid; // 00 00
//0x26 WORD e_oeminfo; // 00 00
//0x28 WORD e_res2[10]; // 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x3C DWORD e_lfanew; // 00 00 01 50
};

这里只需要注意第一个成员以及最后一个成员,第一个成员 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; // 01 4c //*可以运行在什么样的CPU上 任意:0 Intel 386以及后续:14C x64:8864
WORD NumberOfSections; // 00 09 //*表示节的数量
DWORD TimeDateStamp; // 64 79 c7 10 //*编译器填写的时间戳,与文件属性里面的创建时间、修改时间无关
DWORD PointerToSymbolTable; // 00 00 00 00 //调试相关
DWORD NumberOfSymbols; // 00 00 00 00 //调试相关
WORD SizeOfOptionalHeader; // 00 e0 //*可选PE头的大小(32位PE文件:0xE0 64位PE文件:0xF0)
WORD Characteristics; // 01 02 //*文件属性
} 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; //* 10B 32位PE 20B 64位PE 107 ROM映像
BYTE MajorLinkerVersion; // 链接器版本号
BYTE MinorLinkerVersion; // 链接器副版本号
DWORD SizeOfCode; //* 所有代码节的总和 必须是FileAlignment大小整数倍 已无用
DWORD SizeOfInitializedData; //* 所有含已初始化数据的节的总大小 必须是FileAlignment大小整数倍 已无用
DWORD SizeOfUninitializedData; //* 所有含未初始化数据的节的大小 必须是FileAlignment大小整数倍 已无用
DWORD AddressOfEntryPoint; //* 程序执行入口RVA
DWORD BaseOfCode; //* 代码节的起始RVA 已无用
DWORD BaseOfData; //* 数据节的起始RVA 已无用
DWORD ImageBase; //* 程序的优先装载地址
DWORD SectionAlignment; //* 内存中节的对齐粒度
DWORD FileAlignment; //* 文件中节的对齐粒度
WORD MajorOperatingSystemVersion; // 操作系统主版本号
WORD MinorOperatingSystemVersion; // 操作系统副版本号
WORD MajorImageVersion; // PE文件映像的版本号
WORD MinorImageVersion;
WORD MajorSubsystemVersion; // 子系统的版本号
WORD MinorSubsystemVersion;
DWORD Win32VersionValue; // 未用 必须设置0
DWORD SizeOfImage; //* 内存中整个PE文件的映像尺寸 必须是SectionAlignment整数倍
DWORD SizeOfHeaders; //* 所有头包括节表按照文件对齐粒度后的大小
DWORD CheckSum; // 校验和
WORD Subsystem; // 指定使用界面的子系统
WORD DllCharacteristics; // DALL文件属性
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 目录项
    • (无用但保留的字段基本都是为了兼容)

PE文件结构(一)
http://example.com/2023/06/30/PE_0/
Author
yring
Posted on
June 30, 2023
Licensed under