Windows PE 文件头解析
0x01 PE文件基本介绍
0x02 DOS头
struct _IMAGE_DOS_HEADER {
0x00 WORD e_magic; * //5A 4D MZ标记,用来判断是否为可执行文件
0x02 WORD e_cblp; //00 90
0x04 WORD e_cp; //00 03
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
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 00 E8 PE头相对于文件的偏移,用于定位PE文件
};
e_magic
:在DOS头开始的位置,大小为2字节,存储的内容一般为5A 4D
,也就是'MZ'的十六进制,'MZ'是MS-DOS的创建者之一Mark Zbikowski名字的缩写,通常用来判断文件是否为可执行文件。e_lfanew
:在DOS头结束的位置,大小为4字节,是PE头相对于文件的偏移,用于定位PE文件头的位置。e_lfanew
中存储的数据找到PE文件头的位置,图中e_lfanew
存储的内容为00 00 00 F0
(小端存储),所以可以定位到PE文件头的位置在F0H。e_lfanew
到PE文件头的这段空间,是由编译器生成,用来存储一些程序运行的信息,可以随意更改,对程序没有影响。0x03 PE文件头
struct _IMAGE_NT_HEADERS {
0x00 DWORD Signature; //00 00 45 50 PE文件标识,PE文件头的开始
0x04 _IMAGE_FILE_HEADER FileHeader; //标准PE头
0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader; //可选PE头
};
PE\0\0
,DOS头中的e_lfanew
指向该位置1、标准PE头
struct _IMAGE_FILE_HEADER {
0x00 WORD Machine; * //01 4C 程序运行的cpu型号:0x0任何处理器 386即后续处理器
0x02 WORD NumberOfSections; * //00 04 文件中存在的节的总数,如果要新增节或者合并节,就需要修改这个值
0x04 DWORD TimeDateStamp; * //4B 67 34 C3 时间戳:文件创建的时间(编译器填写)
0x08 DWORD PointerToSymbolTable; //00 00 00 00 指向符号表(用于调试)
0x0c DWORD NumberOfSymbols; //00 00 00 00 符号表中符号的个数(用于调试)
0x10 WORD SizeOfOptionalHeader; * //00 E0 可选PE头的大小,32位PE文件默认0xE0,64位PE文件默认0xF0
0x12 WORD Characteristics; * //01 22 文件属性 每一位的含义不同
};
Machine
:存储了程序运行的cpu型号,2字节大小,一般0x14C遇见的比较多,这是在网上找到的winnt.h中各个值对应的信息。NumberOfSections
:存储了文件中节的总数,2字节大小,不同的可执行文件节的总数是不一样的,可以通过NumberOfSections得知当前文件节的总数,对节的内容进行遍历,如果要增加或者删除节,也是修改此参数。TimeDateStamp
:存储了文件创建的时间,4字节大小,是由编译器写入的。SizeOfOptionalHeader
:存储了可选PE头的大小,2字节大小,可选PE头的大小一般是不固定的,通常情况下32位程序为0xE0,64位程序为0xF0,此值可以自定义。Characteristics
:存放了文件属性,2字节大小,它的每一位的含义都不同,当前程序的值为0x0122
,所以换算成二进制就是0000 0001 0010 0010
,对应的属性如下图所示2、可选PE头
struct _IMAGE_OPTIONAL_HEADER32 {
0x00 WORD Magic; * //01 0B 说明文件是ROM镜像(0107h),还是普通的可执行的镜像(010Bh)
0x02 BYTE MajorLinkerVersion; //0A 链接程序的主版本号
0x03 BYTE MinorLinkerVersion; //00 链接程序的次版本号
0x04 DWORD SizeOfCode; * //00 06 22 00 所有代码节的和,必须为FileAlignment的整数倍,编译器填写
0x08 DWORD SizeOfInitializedData; * //00 03 16 00 已初始化数据大小的和,必须为FileAlignment的整数倍
0x0c DWORD SizeOfUninitializedData; * //00 00 00 00 未初始化数据大小的和,必须为FileAlignment的整数倍
0x10 DWORD AddressOfEntryPoint; * //00 05 8C 77 程序入口
0x14 DWORD BaseOfCode; * //00 00 10 00 代码开始的基址,编译器填写
0x18 DWORD BaseOfData; * //00 06 40 00 数据开始的基址,编译器填写
0x1c DWORD ImageBase; * //01 00 00 00 内存镜像基址
0x20 DWORD SectionAlignment; * //00 00 10 00 内存对齐
0x24 DWORD FileAlignment; * //00 00 02 00 文件对齐
0x28 WORD MajorOperatingSystemVersion; //00 06 要求操作系统的最低版本号的主版本号
0x2a WORD MinorOperatingSystemVersion; //00 01 要求操作系统的最低版本号的次版本号
0x2c WORD MajorImageVersion; //00 06 该可执行文件的主版本号
0x2e WORD MinorImageVersion; //00 01 该可执行文件的次版本号
0x30 WORD MajorSubsystemVersion; //00 05 要求最低子系统版本的主版本号
0x32 WORD MinorSubsystemVersion; //00 00 要求最低子系统版本的次版本号
0x34 DWORD Win32VersionValue; //00 00 00 00 从来不用的字段,通常设置为0
0x38 DWORD SizeOfImage; * //00 09 70 00 内存中整个PE文件的映射的尺寸
0x3c DWORD SizeOfHeaders; * //00 00 04 00 所有头+节表按照文件对齐后的大小,必须是正确的,否则加载会出错
0x40 DWORD CheckSum; * //00 08 F0 A9 校验和,用来判断文件是否被修改
0x44 WORD Subsystem; //00 02 表明可执行文件所期望的子系统的枚举值
0x46 WORD DllCharacteristics; //81 40 DllMain()函数何时被调用,默认为0
0x48 DWORD SizeOfStackReserve; * //00 04 00 00 初始化时保留的堆栈大小
0x4c DWORD SizeOfStackCommit; * //00 00 20 00 初始化时实际提交的大小
0x50 DWORD SizeOfHeapReserve; * //00 10 00 00 初始化时保留的堆的大小
0x54 DWORD SizeOfHeapCommit; * //00 00 10 00 初始化时实际提交的大小
0x58 DWORD LoaderFlags; //00 00 00 00 与调试有关,默认为0
0x5c DWORD NumberOfRvaAndSizes; * //00 00 00 10 目录项数目
0x60 _IMAGE_DATA_DIRECTORY DataDirectory[16];
};
IMAGE_OPTIONAL_HEADER64
和IMAGE_OPTIONAL_HEADER32
不同的是IMAGE_OPTIONAL_HEADER64
没有BaseOfData
参数,并且ImageBase、SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit
的大小为ULONGLONG,也就是64位8个字节。struct _IMAGE_OPTIONAL_HEADER {
0x00 WORD Magic; * //02 0B 说明文件是ROM镜像(0107h),还是普通的可执行的镜像(010Bh)
0x02 BYTE MajorLinkerVersion; //0B 链接程序的主版本号
0x03 BYTE MinorLinkerVersion; //00 链接程序的次版本号
0x04 DWORD SizeOfCode; * //00 01 82 00 所有代码节的和,必须为FileAlignment的整数倍,编译器填写
0x08 DWORD SizeOfInitializedData; * //00 01 EE 00 已初始化数据大小的和,必须为FileAlignment的整数倍
0x0c DWORD SizeOfUninitializedData; * //00 00 00 00 未初始化数据大小的和,必须为FileAlignment的整数倍
0x10 DWORD AddressOfEntryPoint; * //00 00 2C 80 程序入口
0x14 DWORD BaseOfCode; * //00 00 10 00 代码开始的基址,编译器填写
0x1c ULONGLONG ImageBase; * //00 00 00 01 40 00 00 00 内存镜像基址
0x20 DWORD SectionAlignment; * //00 00 10 00 内存对齐
0x24 DWORD FileAlignment; * //00 00 02 00 文件对齐
0x28 WORD MajorOperatingSystemVersion; //00 06 要求操作系统的最低版本号的主版本号
0x2a WORD MinorOperatingSystemVersion; //00 03 要求操作系统的最低版本号的次版本号
0x2c WORD MajorImageVersion; //00 06 该可执行文件的主版本号
0x2e WORD MinorImageVersion; //00 03 该可执行文件的次版本号
0x30 WORD MajorSubsystemVersion; //00 06 要求最低子系统版本的主版本号
0x32 WORD MinorSubsystemVersion; //00 03 要求最低子系统版本的次版本号
0x34 DWORD Win32VersionValue; //00 00 00 00 从来不用的字段,通常设置为0
0x38 DWORD SizeOfImage; * //00 03 B0 00 内存中整个PE文件的映射的尺寸
0x3c DWORD SizeOfHeaders; * //00 00 04 00 所有头+节表按照文件对齐后的大小,必须是正确的,否则加载会出错
0x40 DWORD CheckSum; * //00 03 CE 33 校验和,用来判断文件是否被修改
0x44 WORD Subsystem; //00 02 表明可执行文件所期望的子系统的枚举值
0x46 WORD DllCharacteristics; //C1 60 DllMain()函数何时被调用,默认为0
0x48 ULONGLONG SizeOfStackReserve; * //00 00 00 00 00 08 00 00 初始化时保留的堆栈大小
0x50 ULONGLONG SizeOfStackCommit; * //00 00 00 00 00 01 10 00 初始化时实际提交的大小
0x58 ULONGLONG SizeOfHeapReserve; * //00 00 00 00 00 10 00 00 初始化时保留的堆的大小
0x60 ULONGLONG SizeOfHeapCommit; * //00 00 00 00 00 00 10 00 初始化时实际提交的大小
0x68 DWORD LoaderFlags; //00 00 00 00 与调试有关,默认为0
0x6C DWORD NumberOfRvaAndSizes; * //00 00 00 10 目录项数目
0x70 _IMAGE_DATA_DIRECTORY DataDirectory[16];
};
Magic
:说明文件是ROM镜像(0107h),还是普通的可执行的镜像(010Bh),一般来说32位程序是010Bh,64位程序是020Bh。SizeOfCode
:存储了所有代码节的和,他必须是FileAlignment文件对齐的整数倍,此程序文件对齐的大小是200H,所以SizeOfCode的大小为1000H,为200H的整数倍,由编译器填写。SizeOfInitializedData
:存储已经初始化数据块的大小,即在编译的时候所构成的块的大小(不包括代码段),此值也为文件对齐的整数倍,由编译器填写。SizeOfUninitializedData
:未初始化数据块的大小,装载程序要在虚拟地址空间中为这些数据约定空间,一般存在.bss节中,为文件对齐的整数倍,由编译器填写。AddressOfEntryPoint
:可选PE头中最重要的一个参数,也就是我们通常说的OEP,是当前程序的入口位置,该地址是一个相对虚拟地址,指向了程序执行的第一条代码,如果程序被加壳,那么这个地址就会被修改,通常在使用OD进行动态调试的时候,OD首次停留的位置就是AddressOfEntryPoint。BaseOfCode
:代码开始的基址,在内存中,代码段通常在PE文件头之后,数据段开始之前,此值通常由编译器填写。BaseOfData
:数据开始的基址,数据段通常在内存的末尾,在64位程序中没有该参数,此值通常由编译器编写。ImageBase
:内存的镜像地址,也称基地址,是文件在内存中的首选装入地址,如果文件需要在内存中执行的话,会首先使用ImageBase中存放的地址,如果地址被占用,文件会被装入到其他地址中,因为直接装入这个地址不需要进行重定位,所以速度会很快,如果当前地址被占用就需要重定位后装入其他的地址,相对来说速度就会慢一些。SectionAlignment
:程序被装入内存后的对齐大小,通常为1000H。FileAlignment
:程序在没有被装入内存前文件对齐的大小,通常为200H或者1000H,在为1000H的时候文件对齐和内存对齐相同,会加快程序的运行速度。为200H时程序装载到内存中需要进行拉伸操作,把对齐大小拉伸到1000H,这样做相对来说速度会慢一些,但是在磁盘中存储会节省空间。SizeOfImage
:是程序在装入内存后的整个PE文件在内存中的映射尺寸,指的就是装入文件从ImageBase到最后一个块的大小,可以比实际的值大,但必须是SectionAlignment内存对齐的整数倍。SizeOfHeaders
:是DOS头,PE文件头和节表的总大小,该值必须是正确的,否则程序无法运行。CheckSum
:映像的校验和,可以用来检查文件是否被更改。SizeOfStackReserve
:初始化时保留的堆栈大小。SizeOfStackCommit
:初始化时实际提交的大小。SizeOfHeapReserve
:初始化时保留的堆的大小。SizeOfHeapCommit
:初始化时实际提交的大小。NumberOfRvaAndSizes
:数据目录的项数。DataDirectory
:数据目录表,由数个IMAGE_DATA_DIRECTORY结构组成,指向输出表、输入表、资源块等数据,如下图所示0x04 节表
typedef struct _IMAGE_SECTION_HEADER {
0x00 BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //00 00 00 74 78 65 74 2E 8字节,节表的名字,一般情况下"\0"来结束,内容可以自己定义
union {
0x08 DWORD PhysicalAddress;
0x08 DWORD VirtualSize;
} Misc; //00 01 80 6C 双字,是该节在没有对齐前的真是尺寸,内容可以不准确
0x0c DWORD VirtualAddress; //00 00 10 00 节区在内存中的偏移地址
0x10 DWORD SizeOfRawData; //00 01 82 00 节在文件中对齐后的尺寸
0x14 DWORD PointerToRawData; //00 00 04 00 节区在文件中的偏移
0x18 DWORD PointerToRelocations; //00 00 00 00 在exe文件中无意义
0x1c DWORD PointerToLinenumbers; //00 00 00 00 在exe文件中无意义
0x20 WORD NumberOfRelocations; //00 00 在exe文件中无意义
0x22 WORD NumberOfLinenumbers; //00 00 该节在行号表中的行号数
0x24 DWORD Characteristics; //60 00 00 20 节的属性
};
Name
:存储节表的名字,一般情况下‘.’开始,"\0"来结束,内容可以自定义,所以并不能完全靠Name
参数来判断节表中的内容。VirtualSize
:实际使用的节的大小,也就是对齐前的节的大小。如果VirtualSize的值大SizeOfRawData,那么SizeOfRawData表示来自可执行文件初始化数据的大小,与VirtualSize相差的字节用0填充。VirtualAddress
:存储节区在内存中的偏移地址,需要加上ImageBase才是真实的地址。SizeOfRawData
:节在文件中的尺寸,也就是该节在磁盘中所占用的空间。PointerToRawData
:节区在文件中的偏移。Characteristics
:节的属性,通过属性表里的值相加得到。0x05 简单的32位PE头解析器编写
//程序打印DOS头,PE头和所有的节表,代码比较简单,主要是便于理解DOS头,PE头和所有的节表
LPVOID ReadPEFile(LPSTR lpszFile){
FILE* pFile = NULL;
DWORD FileSize = 0;
LPVOID pFileBuffer = NULL;
pFile = fopen(lpszFile,"rb");
if(!pFile){
printf("无法打开exe文件");
return NULL;
}
fseek(pFile,0,2);
FileSize = ftell(pFile);
fseek(pFile,0,0);
pFileBuffer = malloc(FileSize);
if(!pFileBuffer){
printf("初始化空间失败");
fclose(pFile);
return NULL;
}
size_t n = fread(pFileBuffer,FileSize,1,pFile);
if(!n){
printf("读取数据失败");
free(pFileBuffer);
fclose(pFile);
return NULL;
}
fclose(pFile);
return pFileBuffer;
}
VOID PrintSectionHeader(){
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;
PIMAGE_SECTION_HEADER pSeationHeader = NULL;
int SectionsCounts = 0;
pFileBuffer = ReadPEFile("xxxxx/notepad.exe");
if(!pFileBuffer){
printf("读取文件失败");
return ;
}
if(*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE){
printf("不是有效的MZ标志\n");
free(pFileBuffer);
return ;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
if(*((PWORD)((DWORD)pFileBuffer + pDosHeader -> e_lfanew)) != IMAGE_NT_SIGNATURE){
printf("不是有效的PE标志\n");
free(pFileBuffer);
return ;
}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader -> e_lfanew);
printf("****************NT头***************\n");
printf("NT:%x\n\n",pNTHeader -> Signature);
pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);
printf("****************PE头***************\n");
printf("PE:%x\n",pPEHeader -> Machine);
printf("节的数量:%x\n",pPEHeader -> NumberOfSections);
printf("可选PE头的大小:%x\n",pPEHeader -> SizeOfOptionalHeader);
SectionsCounts = pPEHeader -> NumberOfSections;
printf("节表数:%x\n\n",SectionsCounts);
pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
printf("****************OPTION_PE头***************\n");
printf("OPTION_PE: %x\n\n",pOptionHeader -> Magic);
pSeationHeader = (PIMAGE_SECTION_HEADER)(((DWORD)pOptionHeader) + pPEHeader -> SizeOfOptionalHeader);
printf("****************节表***************\n");
for (int i = 0; i < SectionsCounts; i++,pSeationHeader++){
printf("节表%d名字:%s\n",(i + 1),pSeationHeader);
printf("Misc:%x\n",pSeationHeader -> Misc);
printf("VirtualAddress:%x\n",pSeationHeader -> VirtualAddress);
printf("SizeOfRawData:%x\n",pSeationHeader -> SizeOfRawData);
printf("PointerToRawData:%x\n",pSeationHeader -> PointerToRawData);
printf("Characteristics:%x\n\n",pSeationHeader -> Characteristics);
}
free(pFileBuffer);
}
int main(int argc, char* argv[])
{
PrintSectionHeader();
getchar();
return 0;
}
E
N
D
关
于
我
们
Tide安全团队正式成立于2019年1月,是新潮信息旗下以互联网攻防技术研究为目标的安全团队,团队致力于分享高质量原创文章、开源安全工具、交流安全技术,研究方向覆盖网络攻防、系统安全、Web安全、移动终端、安全开发、物联网/工控安全/AI安全等多个领域。
团队作为“省级等保关键技术实验室”先后与哈工大、齐鲁银行、聊城大学、交通学院等多个高校名企建立联合技术实验室。团队公众号自创建以来,共发布原创文章400余篇,自研平台达到31个,目有18个平台已开源。此外积极参加各类线上、线下CTF比赛并取得了优异的成绩。如有对安全行业感兴趣的小伙伴可以踊跃加入或关注我们。
微信扫码关注该文公众号作者
戳这里提交新闻线索和高质量文章给我们。
来源: qq
点击查看作者最近其他文章