首页
论坛
课程
招聘
[原创]PE头、区段解析
2021-11-30 11:12 19399

[原创]PE头、区段解析

2021-11-30 11:12
19399

前言

感谢互联网的知识分享。

什么是PE文件

PE文件的全称是Portable Executable ,意为可移植的可执行文件,常见的有EXE,DLL,SYS,COM,OCX,PE文件是微软Windows操作系统上的程序文件。

 

简单来理解,就是一种数据结构。我们知道知道CPU只认识二进制数,所以可执行文件保存在磁盘中都是以二进制数保存的,不过为了查看,所以市面上的编辑器都是用16进制显示的,比如下面这样,使用010Editer打开一个PE文件:

 

image-20211129111842467

 

PE文件结构如下:

 

有的数据结构是用来执行的代码,有的数据结构是用来保存数据。

 

img

MS—DOS头解析

​ 首先是DOS头和DOS存根,它们的存在主要是用来兼容DOS系统。当我们的程序运行在DOS系统的时候,就会运行DOS存根中的代码,代码内容就是输出一段字符串告诉用户,这个程序不能在16位系统运行。

 

​ 而DOS头保存了程序的信息,DOS头的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;

这里面有用的信息就两个:

  1. 第一个成员e_magic,作为判断是否为PE文件的一个表示,如果不是"MZ"(16进制0x5A4D),那就不是PE文件,如果是还要看PE头标识。
  1. 最后一个成员e_lfanew,指定PE头的开始位置距离文件的偏移,它的数值就是NT头的开始地址。如下图,这里是E8,那么NT头开始地址就是E8的地方。

image-20211130091959186

PE头解析

PE头结构如下:

 

image-20211130095442582

 

注意:e_magic是word类型,占两个字节;Signature是dword类型,占四个字节。

 

image-20211130094858189

 

image-20211130094838165

 

Signature表示是否是PE,vs中有一个宏来定义它:

 

image-20211130095739805

 

PE头其实由两部分组成,一个是标准PE文件头,一个是可选头。

 

先说标准PE头:

标准PE头

在VS中继续跟进查看PE文件头结构体:

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; //PE文件的属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

下面做一些重点介绍:

 

Machine:

 

微软也给出了相应平台的宏定义:

 

image-20211130100226056

 

NumberOfSections:

 

用loadPE打开一个PE文件,可以看到其区段:

 

image-20211130100556885

 

常用区段:
.text: 代码段,里面的数据都是代码,有的编译器也叫做code段,其实都是宏定义的;
.data:数据段(可读写),存放全局变量和静态变量;
.rdata:数据段(只读);
.idata:导入数据区段,存放导入表数据信息;
.edata:导出数据区段,存放导出表数据信息;
.rsrc:资源段;
.bss:存放未初始化数据;
.crt:c++ 运行时库 runtime;
.reloc:重定位;
.tls:线程局部存储。

 

Characteristics

 

PE文件的属性,相应宏定义如下:

 

image-20211130102206861

扩展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
typedef struct _IMAGE_ROM_OPTIONAL_HEADER {
    WORD   Magic; //文件类型的标识 32位的PE还是64位PE
    BYTE   MajorLinkerVersion; //链接器主版本号
    BYTE   MinorLinkerVersion;//链接器子版本号
    DWORD  SizeOfCode; //区段的总大小
    DWORD  SizeOfInitializedData; //已初始化数据段的大小
    DWORD  SizeOfUninitializedData;//未初始化数据段的大小
    DWORD  AddressOfEntryPoint; // 程序入口RVA
    DWORD  BaseOfCode; //代码段机址RVA
    DWORD  BaseOfData;//数据段基址RVA
    DWORD  BaseOfBss;//入口点,文件在内存中的首选装入地址
    DWORD  GprMask;
    DWORD  CprMask[4];
    DWORD  GpValue;
} IMAGE_ROM_OPTIONAL_HEADER, *PIMAGE_ROM_OPTIONAL_HEADER;
 
typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   ImageBase;
    DWORD       SectionAlignment;//映像文件在装入内存中区段的对齐大小 ,通常是0x1000
    DWORD       FileAlignment;//磁盘中节区对齐大小
    WORD        MajorOperatingSystemVersion;//操作系统最低版本的主版本号
    WORD        MinorOperatingSystemVersion;//操作系统最低版本的子版本号
    WORD        MajorImageVersion;
    WORD        MinorImageVersion;
    WORD        MajorSubsystemVersion;
    WORD        MinorSubsystemVersion;
    DWORD       Win32VersionValue; //保留值 最低为00000000
    DWORD       SizeOfImage;
    DWORD       SizeOfHeaders;//ms—dos头,pe头,区段表总和
    DWORD       CheckSum;//映像文件静态和
    WORD        Subsystem;//可执行文件期望的子系统的值
    WORD        DllCharacteristics;//dllMain函数何时被调用
    ULONGLONG   SizeOfStackReserve;//exe中线程被保存的堆栈大小
    ULONGLONG   SizeOfStackCommit;//栈初始化的内存大小 默认大小4kb
    ULONGLONG   SizeOfHeapReserve;//堆初始化的内存大小默认大小1mb
    ULONGLONG   SizeOfHeapCommit;//每次指派给堆的大小,默认4kb
    DWORD       LoaderFlags;// 和调试有关 模式0
    DWORD       NumberOfRvaAndSizes;//数据目录成员的数量 一般16
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

Magic

 

宏定义PE是32位还是64位

1
2
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC   0x10b  //32位PE
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC   0x20b  //64位PE

AddressOfEntryPoint

  • VA:Viratual Address(虚拟地址),用户的PE文件被操作系统加载到内存之后,PE文件对应的进程会支配一个独立的4GB的内存空间,范围在00000000h-0fffffffh。VA就被解释为:进程的基地址+相对虚拟内存地址.
  • RVA:相对虚拟内存地址(Reversc Viratual Address)针对某一个模块存在的,距离模块基地址的偏移(距离)。
  • FOA:File Offset Address ,文件偏移地址 某个位置距离文件头的偏移

Subsystem

 

可执行文件期望的子系统的值,宏定义如下:

 

image-20211130104831114

 

通过C++打印相关成员,在监视中,我们也能看到相关成员的属性值:

 

image-20211130110103689

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
#include <Windows.h>
#include <stdio.h>
#include<iostream>
#include <iomanip>
#define FilePath "C:\\Users\\Administrator\\Desktop\\CreateProcess.exe"
int main(int argc,char *argv[]) {
 
    FILE* pFile = NULL;
    char* buffer;
    int nFileLength = 0;
    pFile = fopen(FilePath, "rb");
    fseek(pFile, 0, SEEK_END);
    nFileLength = ftell(pFile);
    rewind(pFile);
    int imageLength = nFileLength * sizeof(char) + 1;
    buffer = (char*)malloc(imageLength);
    memset(buffer, 0, nFileLength * sizeof(char) + 1);
    fread(buffer, 1, imageLength, pFile);
 
    PIMAGE_DOS_HEADER ReadDosHeader;
    ReadDosHeader = (PIMAGE_DOS_HEADER)buffer;
    std::cout << "MS-DOS info:" << std::endl;
    std::cout << "MZ标志位:" <<std::hex<< ReadDosHeader->e_magic << std::endl;
    std::cout << "PE偏移头:" << std::hex<< ReadDosHeader->e_lfanew << std::endl;
    std::cout << "PE info:" << std::endl;
    PIMAGE_NT_HEADERS ReadNTheaders;
 
    ReadNTheaders = (PIMAGE_NT_HEADERS)(buffer + ReadDosHeader->e_lfanew);
    std::cout << "PE标志位:" << std::hex << ReadNTheaders->Signature << std::endl;
    std::cout << "运行平台:" << std::hex << ReadNTheaders->FileHeader.Machine << std::endl;
    std::cout << "ImageBase入口点:" << std::hex << ReadNTheaders->OptionalHeader.ImageBase << std::endl;
    free(buffer);
    return 0;
}

区段解析遍历

区段也是一个结构:

 

image-20211130132800303

 

前面我们在loadPE中打开,已经能看到区段表相关信息:

 

image-20211130100556885

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME]; //区段名称,比如.data .code等
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize; //实际使用的区段大小(没做对齐前的大小)
    } Misc;
    DWORD   VirtualAddress; //区段载入内存之后的RVA,按照内存页对齐
    DWORD   SizeOfRawData;//在磁盘中的大小  
    DWORD   PointerToRawData;//区段在文件中偏移
    DWORD   PointerToRelocations;//区段在重定位表中的偏移
    DWORD   PointerToLinenumbers;//行号表在文件中的偏移
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;//区段属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

现在通过已经了解到的,模仿loadPE,用控制台程序写一个PE 区段表解析器。

 

Microsoft Visual Studio提供了一个宏(IMAGE_FIRST_SECTION )来定义定位区段表:

 

image-20211130135515691

 

以此来定义一个区段头:

1
PIMAGE_SECTION_HEADER ReadSectionHeader = IMAGE_FIRST_SECTION(ReadNTheaders);

然后去PE文件标准头的地址:

1
PIMAGE_FILE_HEADER pFileHeader = &ReadNTheaders->FileHeader;

IMAGE_FILE_HEADER 有个值表示了区段数量,可以根据这个数量来进行遍历:

 

image-20211130133859342

 

代码如下:

1
2
3
4
5
6
7
8
9
10
for (int i = 0; i < pFileHeader->NumberOfSections; i++)
{
    std::cout << "Name[区段名称]:" << std::hex << ReadSectionHeader[i].Name << std::endl;
    std::cout << "VOffset[起始相对虚拟地址]:" << std::hex << ReadSectionHeader[i].VirtualAddress << std::endl;
    std::cout << "VSzie[区段大小(内存中)]:" << std::hex << ReadSectionHeader[i].SizeOfRawData << std::endl;
    std::cout << "ROffset[文件偏移]:" << std::hex << ReadSectionHeader[i].PointerToRawData << std::endl;
    std::cout << "RSize[区段大小(文件中)]:" << std::hex << ReadSectionHeader[i].Misc.VirtualSize << std::endl;
    std::cout << "标记[区段属性]:" << std::hex << ReadSectionHeader[i].Characteristics << std::endl;
    std::cout << "----------------------------------"<< std::endl;
}

image-20211130135847068


恭喜ID[飞翔的猫咪]获看雪安卓应用安全能力认证高级安全工程师!!

最后于 2021-11-30 14:02 被soloz编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (4)
雪    币: 1763
活跃值: 活跃值 (1209)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
无心红叶 活跃值 2021-12-1 14:10
2
0

楼主写的不错,帮你补充一下官方文档 PE Format


另外,老王卖瓜,自卖自夸。推荐你使用我的编辑器查看PE文件的结构。

(具体内容可参考我发的帖子 EDX编辑器, 下载地址 www.ed-x.cc )

还可以支持coff,elf, archive等。


雪    币: 614
活跃值: 活跃值 (1325)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
soloz 活跃值 2021-12-1 16:53
3
1
谢谢师傅,学习了,我其实打算后面用mfc也自己写个解析器来着
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
0ke 活跃值 2022-1-16 11:58
4
0
师兄好强
雪    币: 668
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
0llydbg 活跃值 2022-1-16 12:58
5
0


ollydbg x64也支持pe文件等文件结构解析.

游客
登录 | 注册 方可回帖
返回