PE文件详解 - PE头
1、PE头
- PE文件头位于DOS Stub后面,是一个以PE/0/0为起始标记
- PE文件头是Windows NT内核下判断可执行文件的唯一有效结构,它由3个字段组成
- Signature字段 : 是PE文件头的标识,其值始终为0x50450000
- IMAGE_FILE_HEADER结构 : 映像文件头, 这个结构包含整个PE文件的概览信息
- IMAGE_OPTIONAL_HEADER32结构 :映像扩展头,是对PE文件进行更为详细的属性设定的扩展结构,它与IMAGE_FILE_HEADER结构合并起来统称为 “PE文件头结构”
- 用代码表示
#include<stdio.h>
#include<windows.h>
// PE头
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE标识
IMAGE_FILE_HEADER FileHeader; // 文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 扩展头
} IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;
1.1、IMAGE_FILE_HEADER
-
重要的两个字段为:区段数量与扩展头大小
-
查看 IMAGE_FILE_HEADER的定义 (winnt.h)
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;
- 字段解读
Machine
大小两字节,表示当前pe文件的目标CPU类型,0代表任何平台,14c代表i386及后续处理器。NumberOfSections
大小两字节,代表节表(区块)的数量。TimeDateStamp
大小四字节,一个时间戳,表示当前pe文件何时创建时间。PointerToSymbolTable
和NumberOfSymbols
这两个成员很少被使用SizeOfOptionalHeader
大小两字节,表示可选头(Option头)的大小。Characteristics
大小两字节,表示文件的类型,010f代表可执行文件。
1.2、IMAGE_OPTIONAL_HEADER32
- 重要字段:
- Magic : 文件类型标识
- AddressOfEntryPoint:程序执行入口RVA
- ImageBase:程序默认载入基地址
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
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;
- 字段解读
Magic
大小两字节,用于表示是32位pe文件还是64位pe文件,如果是10B就代表32位文件,20B代表64位文件SizeOfCode
大小四字节,代码总大小,需要文件对齐。SizeOfInitializedData
大小四字节,代表已经初始化数据的大小,需要按照文件对齐SizeOfUninitializedData
大小四字节,代表未初始化数据大小,也是要按照文件对齐。AddressOfEntryPoint
大小四字节,是程序入口地址,也就是OEP(需要加上ImageBase才是真正的程序入口)。BaseOfCode
大小四字节,代码节开始的地方。BaseOfData
大小四字节,数据开始的地方。ImageBase
大小四字节,内存镜像基址也就是程序加载进内存中时的基址(一般是0x400000)。FileAlignment
大小四字节,文件对齐,如果文件对齐是200那么不足200的会在后面补0,如过一个数是188那么它按照文件对齐后就是200,主要作用是提高cpu工作效率。SectionAlignment
大小四字节,内存对齐,和文件对齐一样,也是为了提高cpu工作效率。SizeOfImage
大小四字节,PE文件在内存中的总大小,按照内存对齐SizeOfHeaders
大小四字节,所有头的大小,Dos头+Nt头成员Signature+File头+Option头+节表的总大小,需要按照文件对齐。
1.3、常用的区段
- .text:代码段
- .data : 数据段
- .rdata:只读数据段
- .idata:导入表数据段
- .edata:导出表数据段
- .rsrc:资源段
- .bss:未初始化数据
- .crt
- .tls
- .reloc
1.4、VA|RVA|FOA
-
VA : 虚拟内存地址
- 虚拟内存地址 = 进程的基地址 + 相对虚拟内存地址
-
RVA : 虚拟内存地址
-
FOA :文件偏移地址
-
在免杀时,经常要找文件偏移地址与寻找内存地址
-
文件偏移地址:PE文件数据在硬盘中存放的地址就称为文件偏移地址,例如用WinHex查看文件时Offset就是文件偏移地址。所谓文件偏移地址就是指文件在磁盘上存放时相对于文件开头的偏移,当PE文件存储在磁盘中时,某个数据的位置相对于文件头的偏移量称为文件偏移地址(File Offset)。文件偏移地址从PE文件的第一个字节开始计数,起始值为0。
-
基地址(ImageBase):是指PE文件装入内存时的基地址,当PE文件通过Windows加载器载入内存后,内存中的版本称为模块,映射文件的起始地址称为模块句柄,可通过模块句柄访问内存中其他数据结构,这个内存起始地址就称为基地址。
-
虚拟地址(VA):是指PE文件被装入内存之后的地址,在Windows系统中,PE文件被系统加载到内存后,每个程序都有自己的虚拟空间,这个虚拟空间的内存地址称为虚拟地址。
-
相对虚拟地址(RVA):指的是在没有被计算机装载基址的情况下的内存地址,可执行文件中,有许多地方需要指定内存中的地址。例如,应用全局变量时需要指定它的地址。为了避免在PE文件中出现绝对内存地址引入了相对虚拟地址,它就是在内存中相对于PE文件载入地址的偏移量。
它们之间的关系:虚拟地址(VA) = 基地址(Image Base)+相对虚拟地址(RVA)
1.5、通过代码读取PE头
- C++
#include<stdio.h>
#include<Windows.h>
int main()
{
FILE* pFile = NULL;
char* buffer;
int nFileLength = 0;
errno_t err = fopen_s(&pFile, "E:\\C_code\\MyDemo\\Debug\\MyDemo.exe", "rb");
if (err != 0)
{
// 处理文件打开错误
printf("Failed to open file.\n");
return 1;
}
// fseek()设置文件指针的位置。这里将文件指针移动到文件末尾
// SEEK_END 指定偏移量为0
fseek(pFile, 0, SEEK_END);
// ftell() 获取文件指针的当前位置,也就是文件的长度
nFileLength = ftell(pFile);
// rewind() 将文件指针重置到文件开头
rewind(pFile);
// 存储缓冲区大小
// 计算方式:文件长度乘以sizeof(char)(sizeof(char)表示char类型的大小,通常是1字节),再加1字节用于存储字符串结束符\0
int imageLength = nFileLength * sizeof(char) + 1;
// 动态分配内存 : malloc函数动态分配了一块内存,大小为imageLength字节
buffer = (char*)malloc(imageLength);
// memset() :将分配的内存初始化为0。
memset(buffer, 0, nFileLength * sizeof(char) + 1);
// fread函数用于从文件中读取数据。这里将文件中的内容读取到之前分配的缓冲区buffer中,每次读取1字节,总共读取imageLength个字节。
fread(buffer, 1, imageLength, pFile);
// 读取DOS头
// 定义指向 PIMAGE_DOS_HEADER的指针 ReadDosHeader
PIMAGE_DOS_HEADER ReadDosHeader;
// 因为buffer是char类型,所以要转成指针类型
// 目的是将缓冲区中的数据解释为IMAGE_DOS_HEADER结构
ReadDosHeader = (PIMAGE_DOS_HEADER)buffer;
// 打印指针中的数据
printf("MS-DOS Info:\n");
printf("MZ标志位:%x\n", ReadDosHeader->e_magic);
printf("PE头位置:%x\n", ReadDosHeader->e_lfanew);
// PE头
// 要注意你读取的EXE是32位还是64位的,64位要使用 PIMAGE_NT_HEADERS64
printf("PE Header Info:\n");
PIMAGE_NT_HEADERS ReadNTHeaders;
// 因为要跳过DOS头,读取到PE开头,所以要加e_lfanew
ReadNTHeaders = (PIMAGE_NT_HEADERS)(buffer + ReadDosHeader->e_lfanew);
printf("PE标志位:%x\n", ReadNTHeaders->Signature);
// 打印标准PE头里的东西
printf("运行平台:%x\n", ReadNTHeaders->FileHeader.Machine);
// 打印拓展PE头里的东西
printf("ImageBase: %x\n",ReadNTHeaders->OptionalHeader.ImageBase);
// 释放动态内存
free(buffer);
// 关闭文件
fclose(pFile);
return 0;
}
推荐阅读: