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
由DOS MZ头和DOS stub组成,其中最重要的是e_magic 和 e_lfanew(file address of new header)
**e_magic **为固定的5A4Dh,其ascii值为“MZ”
e_lfanew为真正的PE文件头(PE Header)的相对偏移(很重要!!!) 位于开始的3C处,占用四个字节。
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头的位置
颜色标注的都是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,下面是几种典型的平台
2.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位的,文件可执行,即代码可执行)
这是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界面的子系统
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
再看下第二块
节名是.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结构时偶然从网上获得的,但找不到出处了,不过很谢谢他的图。