PE文件结构剖析---基本结构

PE简介

PE文件衍生于早期建立的COFF文件格式,EXE和DLL文件实际上用的是同一种文件格式,唯一的区别就是用一个字段标识出这个文件是EXE还是DLL。64位Windows格式为PE32+。只是简单的将以前的32位字段扩展到64位。

重要要点

要认识到PE文件不是作为单一内存映射文件被装入内存的。
PE加载器通过遍历PE文件决定哪一部分被映射
磁盘上的数据结构布局和内存中的数据结构布局是一致的。
PE文件被装入内存后,内存中的版本称为模块。起始地址(也被称为基地址ImageBase)被当做模块句柄
按照默认设置,用VC++建立起来的EXE文件基地址是00400000h,DLL文件基地址是10000000h
PE尽管有一个首选的载入基址,但是他们可以载入到进程空间的任何地方,所以不能依赖PE的载入点,所以出现了相对虚拟地址RVA(Relative Virtual Address).PE用语里,实际的内存基址称为VA(Virtual Address)
VA = ImageBase + RVA

PE文件结构:

DOS头

由DOS MZ头和DOS stub组成,其中最重要的是e_magic 和 e_lfanew(file address of new header)
**e_magic **为固定的5A4Dh,其ascii值为“MZ”
e_lfanew为真正的PE文件头(PE Header)的相对偏移(很重要!!!) 位于开始的3C处,占用四个字节。

DOS头部

PE文件头

PE文件头是NT映像头的简称,即IMAGE_NT_HEADER
由三个字段组成
IMAGE_NT_HEADERS STRUCT
Signatrue DWORD ;其ASCII值为”PE00”
FileHeader IMAGE_FILE_HEADER
OptionalHeader IMAGE_OPTIONAL_HEADER32

IMAGE_FILE_HEADER STRUCT
Machine				WORD		运行平台
NumberOfSections	WORD		区块数目
TimeDateStamp		DWORD		时间戳(自从GMT开始的秒数)
PointerToSymbolTable	DWORD		符号表指针(用于调试)
NumberOfSymbols	DWORD		符号表符号个数
SizeOfOptionalHeader	WORD		可选头大小(IMAGE_OPTIONAL_HEADER32)
Characteristics		WORD		文件属性(通过相关的位运算得到的)
IMAGE_FILE_HEADER ENDS

可选映像头(IMAGE_OPTIONAL_HEADER32):

IMAGE_OPTIONAL_HEADER32 STRUCT
+18h    WORD    Magic;         // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
+1Ah    BYTE      MajorLinkerVersion;     // 链接程序的主版本号
+1Bh    BYTE      MinorLinkerVersion;     // 链接程序的次版本号
+1Ch    DWORD   SizeOfCode;     // 所有含代码的节的总大小
+20h    DWORD   SizeOfInitializedData;    // 所有含已初始化数据的节的总大小
+24h    DWORD   SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28h    DWORD   AddressOfEntryPoint;    // 程序执行入口RVA
+2Ch    DWORD   BaseOfCode;      // 代码的区块的起始RVA
+30h    DWORD   BaseOfData;      // 数据的区块的起始RVA
+34h    DWORD   ImageBase;      // 程序的首选装载地址
+38h    DWORD   SectionAlignment;      // 内存中的区块的对齐大小
+3Ch    DWORD   FileAlignment;      // 文件中的区块的对齐大小
+40h    WORD    MajorOperatingSystemVersion;  // 要求操作系统最低版本号的主版本号
+42h    WORD    MinorOperatingSystemVersion;  // 要求操作系统最低版本号的副版本号
+44h    WORD    MajorImageVersion;       // 可运行于操作系统的主版本号
+46h    WORD    MinorImageVersion;       // 可运行于操作系统的次版本号
+48h    WORD    MajorSubsystemVersion;  // 要求最低子系统版本的主版本号
+4Ah    WORD    MinorSubsystemVersion;  // 要求最低子系统版本的次版本号
+4Ch    DWORD   Win32VersionValue;       // 莫须有字段,不被病毒利用的话一般为0
+50h    DWORD   SizeOfImage;       // 映像装入内存后的总尺寸
+54h    DWORD   SizeOfHeaders;       // 所有头 + 区块表的尺寸大小
+58h    DWORD   CheckSum;       // 映像的校检和
+5Ch    WORD    Subsystem;       // 可执行文件期望的子系统
+5Eh    WORD    DllCharacteristics;       // DllMain()函数何时被调用,默认为 0
+60h    DWORD   SizeOfStackReserve;       // 初始化时的栈大小
+64h    DWORD   SizeOfStackCommit;       // 初始化时实际提交的栈大小
+68h    DWORD   SizeOfHeapReserve;        // 初始化时保留的堆大小
+6Ch    DWORD   SizeOfHeapCommit;        // 初始化时提交的堆大小
+70h    DWORD   LoaderFlags;        // 与调试有关,默认为 0
+74h   DWORD   NumberOfRvaAndSizes;  // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16
+78h    IMAGE_DATA_DIRECTORY DataDirectory[16];  // 数据目录表(数目固定为16,很重要!!!)
IMAGE_OPTIONAL_HEADER32 ENDS

说了这么多,我们来真实的实验下,随便找个PE文件,我这里这个是加密与解密第四版的那个文件。用winhex打开它,
offset为0的地方是一个IMAGE_DOS_HEADER,两个字节4D 5A,表明它的magic number
在这里插入图片描述

另一个重要的地方是0x3C处的一个四字节数据e_lfnew,表示IMAGE_NT_HEADERS头的位置

在这里插入图片描述这里看出它是在offset为0x40的地方

颜色标注的都是IMAGE_NT_HEADER的部分,我们来详细分析一下
在这里插入图片描述IMAGE_NT_HEADERS由三部分组成
三部分
signature部分:
开头是IMAGE_NT_HEADERS的signature,50 45 00 00(UNICODE 编码需要两个00结束符),这是signature部分。

Image_File_Header部分:
1.Machinie : 0x014C,这是机器运行的平台,这里为i386,下面是几种典型的平台
Machine2.NumberOfSection : 区块数目,这里为3
3.TimeDateStamp : 时间戳,这里是0x3db8972c,是GMT时间,换算一下是(GMT: Fri Oct 25 00:58:20 2002)
4.PointerToSymbolTable : COFF符号表的文件偏移位置,这里是0x726f4c5b用于调试用的,不过这个格式的PE文件很少用,多见于OBJ文件。
5.NumberOfSymbol:符号数目,这里为0x5d455064
6.SizeOfOptionalHeader : 表示可选头的大小,虽然说是可选,但其实是必须的,32位下通常为0xe0,64位下多为0xf0,这里为0x00E0
7.Characteristics : PE文件属性,这里为0x010F,通过位运算,多个属性结合的结果。这库的0x010F表示不存在重定位信息,可执行,行号和符号信息都没有,是32位程序。dll字段一般为2102(表示是DLL文件,32位的,文件可执行,即代码可执行)
Characteristics域这是WinNT.h中的定义 :

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM                    0x1000  // System File.
#define IMAGE_FILE_DLL                       0x2000  // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.

接下来则是紧跟的IMGAE_OPTIONAL_HEADER(可选头)
1.Magic字段:这里是0x010b,一般32位的是0x010b,64位PE文件则是020b(类似于signature那样是固定的)
2.MajorLinkerVersion : 链接器主版本号,这里是05
3.MinorLinkerVersion : 链接器次版本号,这里是0c
4.SizeOfCode :代码的区块大小(一般是.text段),这里是0x200 对齐的是FileAlignment
5.SizeOfInitializedData : 初始化的数据区块大小(一般是.bss段),这里是0x400
6.SizeOfUninitializedData未初始化的数据区块(一般是.data段),这里是0x0
7.AddressOfEntryPoint : OEP的RVA,我们要求OEP的话,就是ImageBase + AddressOfEntryPoint,这里是0x1000
8.BaseOfCode : 代码区块的RVA,这里是0x1000
9.BaseOfData : 数据区块的RVA,这里是0x2000
10.ImageBase : 程序默认的载入地址,vc++编译的一般是0x40 0000,Dll默认0x1000 0000,这里是0x400000
11.SectionAlignment : 程序加载至内存的区块对齐值,这里是0x1000
12.FileAlignment : 程序文件的区块对齐值,这里是0x200
13.MajorOperatingSystemVersion : 操作系统主版本号,这里是04
14.MinorOperatingSystemVersion : 操作系统次版本号,这里是00

操作系统版本号
15.MajorImageVersion : 用户定义的主版本号,这里是04
16.MinorImageVersion : 用户定义的次版本号,这里是00
17.MajorSubSystemVersion : 所需子系统的主版本号,这里是04
18.MinorSubSystemVersion : 所需子系统的次版本号,这里是00

所谓的子系统的含义呢,也就是:按照Windows NT 最初的设计,它支持三个环境子系统:OS/2、POSIX 和Windows(或称为Win32)。然而,Windows 子系统是必须要运行的,没有它Windows 系统无法运行,而其他两个子系统则被配置成按需启动。而且,到了Windows XP以后,只有Windows子系统随Windows 系统,所以其实xp之后只有Win32子系统了,故有版本号的区别。

19.Win32VersionValue : 保留值 通常为0
20.SizeOfImage : 映像载入内存后的总尺寸,PE加载器根据此处值决定申请连续内存空间大小,这里是申请了0x3038空间的内存
21.SizeOfHeaders:MS_DOS头,PE文件头,区块表的总大小,这里是0x200
22.CheckSum : 校验和,这里是0x20A7
23.SubSystem:常见的子系统,这是一个枚举值,只对EXE最重要。一般就是CUI和GUI较多,这里是02,代表了是一个GUI界面的子系统
SubSystem

24.DllCharacteristic : 显示DLL特性的旗标,这里为0,这个是EXE文件 所以无DLL特性,这里为0
25.SizeOfStackReserve:为线程的栈初始保留的虚拟内存的默认值 ,这里为0x10 0000
msdn中对于SizeOfStackReserve是这么说的:

Generally, the reserve size is the default reserve size specified in the executable header. However, if the initially committed size specified by dwStackSize is larger than the default reserve size, the reserve size is this new commit size rounded up to the nearest multiple of 1 MB.
ps:如果在调用CreateThread函数时指定堆栈的大小为0,被创建的线程的堆栈的初始大小就与这个值相同.

26.SizeOfStackCommit : 为线程的栈初始提交的虚拟内存的大小,只提交了少量的页面(即大部分地址空间没有映射,作为保留页面),这里为0x1000。
27.SizeOfHeapReserve : 为堆的初始化保留的虚拟内存的大小,这里为0x10 0000,为1MB
28.SizeOfHeapCommit : 为堆的初始化提交的虚拟内存的大小,这里为0x1000
29.LoaderFlags : 与调试有关,默认为0
30.NumberOfRvaAndSizes : 数据目录表的项数 NT系统以来一直是16
31DataDirectory[16] : 由数个IMAGE_DATA_DIRECTORY组成,这个结构是非常重要的

下面来详细看看DataDirectory的成员,当然首先我们看下这个IMAGE_DATA_DIRECTORY结构体

NT头里的定义是这样的

//
// Directory format.
//

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;//成员RVA
    DWORD   Size;//成员大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
•  #define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory 					输出表
•  #define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory 					输入表
•  #define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory 					资源表
•  #define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory 				异常表
•  #define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory					安全表
•  #define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table				重定位表
•  #define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory						调试表
•  #define IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)				
•  #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data			版权
•  #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP							全局指针
•  #define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory						线程本地存储(TLS)表
•  #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory		载入配置信息目录
•  #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers	绑定输入表
•  #define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table				输入地址表
•  #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors		延迟绑定表
•  #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor   			COM信息

这里我们只是有个大概印象,具体的成员目录干什么的,我会记录在别的博客里。

到此IMAGE_NT_HEADERS就结束了,下面的一块表示SECTION TABLE(节区表),它的长度是不定的,是一个
IMAGE_SECTION_HEADER的结构数组,元素个数为IMAGE_NT_HEADERS里的Image_File_Header中的NumberOfSections

给出结构:

//
// Section header format.
//

#define IMAGE_SIZEOF_SHORT_NAME              8

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];	//一个最大长度为8的节名长度
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;									//virtualSize是程序加载到内存中之后的真实数据长度。
    DWORD   VirtualAddress;					//区块的RVA值(总是SectionAlignment对齐,未经过)
    DWORD   SizeOfRawData;					//在文件中的尺寸(对齐于FileAlignment后的size)
    DWORD   PointerToRawData;				//文件中的偏移 以0为偏移
    DWORD   PointerToRelocations;			//在OBJ文件中使用,重定位的偏移
    DWORD   PointerToLinenumbers;			//调试用 行号表偏移
    WORD    NumberOfRelocations;			//OBJ文件中使用 需要重定位项的数量
    WORD    NumberOfLinenumbers;			//行号表中 行号的数目
    DWORD   Characteristics;				//区块属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Winhex中实战下找到每一个表项,并分析(这里的winhex貌似出了点bug,颜色一样的表示是连一块的…)
第一块
1.段名为".text"占当段名小于8个字节的时候,结束符以一个NULL结尾,否则超过特定长度的时候,会以$符作为连接符,顺序连接段名,还在待实验中。
2.表明.text段的块尺寸为0x1000,是程序加载到内存中之后的真实数据长度,和SizeOfRawData不一样,是块对齐前的长度,例如若是0x1301,那么这就是真实内存长度,VirtualSize >= SizeOfRawData
3.表明该块的RVA是0x1000
4.在文件中的尺寸,这里是0x200,在可执行文件中,该字段包含了经过FileAlignment调整后的块的长度。例如,指定FileAlignment的大小为200h,如果VirtualSize中块的长度为35ah字节,这一块应保存的长度为400h字节。
5.在文件中的RVA,如果程序想自加载PE文件,那么这个字段非常重要,这里是0x400
6.用于OBJ文件,表示重定位信息的offset,指向一个IMAGE_RELOCATION结构数组,这里是0
7.行号表偏移,这里是0
8.应用于OBJ文件,重定位项数目,这里是0,注意这里是个WORD大小
9.行号表数目,这里是0,注意这里是个WORD大小
10.区块属性,这里是0x60000020
这里给出NT头的说明,我们重点关注最低字节的位设置和最高字节的位设置

#define IMAGE_SCN_CNT_CODE                   0x00000020  // Section contains code. 表示该节是代码节
#define IMAGE_SCN_CNT_INITIALIZED_DATA       0x00000040  // Section contains initialized data. 表示该节是初始化数据段
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA     0x00000080  // Section contains uninitialized data. 表示该节是未初始化的数据段

#define IMAGE_SCN_LNK_OTHER                  0x00000100  // Reserved.
#define IMAGE_SCN_LNK_INFO                   0x00000200  // Section contains comments or some other type of information.
//      IMAGE_SCN_TYPE_OVER                  0x00000400  // Reserved.
#define IMAGE_SCN_LNK_REMOVE                 0x00000800  // Section contents will not become part of image.
#define IMAGE_SCN_LNK_COMDAT                 0x00001000  // Section contents comdat.
//                                           0x00002000  // Reserved.
//      IMAGE_SCN_MEM_PROTECTED - Obsolete   0x00004000
#define IMAGE_SCN_NO_DEFER_SPEC_EXC          0x00004000  // Reset speculative exceptions handling bits in the TLB entries for this section.
#define IMAGE_SCN_GPREL                      0x00008000  // Section content can be accessed relative to GP
#define IMAGE_SCN_MEM_FARDATA                0x00008000
//      IMAGE_SCN_MEM_SYSHEAP  - Obsolete    0x00010000
#define IMAGE_SCN_MEM_PURGEABLE              0x00020000
#define IMAGE_SCN_MEM_16BIT                  0x00020000
#define IMAGE_SCN_MEM_LOCKED                 0x00040000
#define IMAGE_SCN_MEM_PRELOAD                0x00080000

#define IMAGE_SCN_ALIGN_1BYTES               0x00100000  //
#define IMAGE_SCN_ALIGN_2BYTES               0x00200000  //
#define IMAGE_SCN_ALIGN_4BYTES               0x00300000  //
#define IMAGE_SCN_ALIGN_8BYTES               0x00400000  //
#define IMAGE_SCN_ALIGN_16BYTES              0x00500000  // Default alignment if no others are specified.
#define IMAGE_SCN_ALIGN_32BYTES              0x00600000  //
#define IMAGE_SCN_ALIGN_64BYTES              0x00700000  //
#define IMAGE_SCN_ALIGN_128BYTES             0x00800000  //
#define IMAGE_SCN_ALIGN_256BYTES             0x00900000  //
#define IMAGE_SCN_ALIGN_512BYTES             0x00A00000  //
#define IMAGE_SCN_ALIGN_1024BYTES            0x00B00000  //
#define IMAGE_SCN_ALIGN_2048BYTES            0x00C00000  //
#define IMAGE_SCN_ALIGN_4096BYTES            0x00D00000  //
#define IMAGE_SCN_ALIGN_8192BYTES            0x00E00000  //
// Unused                                    0x00F00000
#define IMAGE_SCN_ALIGN_MASK                 0x00F00000

#define IMAGE_SCN_LNK_NRELOC_OVFL            0x01000000  // Section contains extended relocations.
#define IMAGE_SCN_MEM_DISCARDABLE            0x02000000  // Section can be discarded.
#define IMAGE_SCN_MEM_NOT_CACHED             0x04000000  // Section is not cachable.
#define IMAGE_SCN_MEM_NOT_PAGED              0x08000000  // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED                 0x10000000  // Section is shareable.	该节是处于共享内存
#define IMAGE_SCN_MEM_EXECUTE                0x20000000  // Section is executable.	该节可执行 当低位字节设置20的时候 这个也应该被设置
#define IMAGE_SCN_MEM_READ                   0x40000000  // Section is readable.	该节是可读的
#define IMAGE_SCN_MEM_WRITE                  0x80000000  // Section is writeable.	该节是可写的

这里可以看出该节是代码段,并且是可读可执行
用peid查看下,可见对于一个EXE文件,节区表只有六个成员是重要的:Name,VirtualSize,VirtualAddress,SizeOfRawData,PointerToRawData,Characteristic
peid下的节区表再看下第二块
第二块
节名是.rdata,VirtualSize是0x1000,VirtualAddress是0x2000,SizeOfRawData是0x01C4,PointerToRawData是0x400
Characteristic是0x40000040,即表示是一个可读的初始化数据段

最后一个第三块
第三块
段名是.data,VirtualSize是0x38,VirtualAddress 是0x3000,SizeOfRawData是0x31,PointerToRawData是0x600
重定位信息和行号表信息都为空,Characteristic是0xC0000040,代表是一个可读可写的数据段。

最后放两张通俗易懂的PE结构图,第一张出自看雪自带的加密与解密(第四版)的PE那一章的图,我觉得很好,第二张是我曾经学习PE结构时偶然从网上获得的,但找不到出处了,不过很谢谢他的图。

PE文件结构
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值