本文最后更新于:星期二, 六月 16日 2020, 3:16 下午
以test.exe文件为例子,分析PE文件的各部分格式
1. MZ文件头
用winhex打开test.exe,找到MZ文件头,为偏移0x00~0x3F
的部分
其中每个字段的意义如下所示:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number设置ox5A4D ascii码值为'MZ'(标志,不会变的标志)
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; //PE头文件的偏移地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
比较重要的有以下几个字段:
offset 0x00-0x01
:e_magic字段,一般为4D5A,表示这是一个PE文件offset 0x3C-0x3D
:e_lfanew字段,表示PE文件头的偏移地址,根据这个可以找到PE文件头,在这里为0x000000B0
2. DOS stub
接下来的偏移0x40-0xAF
部分,处于MZ头部和PE文件头之间,称为DOS stub,它与MZ头部一起组成DOS小程序
可以看到其中一部分的ASCII码显示为“This is program connot be run in DOS mode.”,这一部分的长度不确定。
3. PE文件头
从MZ文件头中指示的偏移0x000000B0
开始为PE文件头,其中包含三个部分:
typedef struct _IMAGE_NT_HEADER
{
DWORD Signature; //PE Signature : 50450000("PE"00)
IMAGE_FILE_HEADER FileHeader; //文件头结构体
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //可选头结构体
} IMAGE_NT_HEADER, *PIMAGE_NT_HEADER32;
① PE Signature(签名)
这一字段占0x004
字节,为固定子字串”PE \ 0 \ 0”,即50450000
,标志着PE文件头的开始
② FileHeader(映像文件头)
紧跟在PE签名后的是映像文件头,占0x014
字节,各字段的意义如下:
typedef struct _IMAGE_FILE_HEADER
{
WORD Machine; //每个CPU都拥有唯一的machine码
WORD NumberOfSections; //节区数量,当定义节区数与实际不同时会发生错误
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; //IMAGE_OPTIONAL_HEADER32结构体的大小,固定的
WORD Characteristics; //文件属性,0x0002h为可执行文件,0x2000h为DLL文件
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEAD
test.exe中的实际情况如下:
其中比较重要的几个字段:
offset 0xB6-0xB7
:该字段为NumberOfSections,定义了节区的数量,在这里的值为0x0003
,说明该程序含有三个节区offset 0xC4-0xC5
:该字段为SizeOfOptionalHeader,定义了可选映像头(OptionalHeader)的大小,在这里的值为0x00E0
③ OptionalHeader(可选映像头)
根据映像文件头中的信息,可选映像头占0xE0
字节,其中各字段意义如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; //标志字(32位时0x10Bh)
BYTE MajorLinkerVersion; //连接器主版本号
BYTE MinorLinkerVersion; //连接器次版本号
DWORD SizeOfCode; //代码段大小
DWORD SizeOfInitializedData; //已初始化数据块大小
DWORD SizeOfUninitializedData; //未初始化数据块大小
DWORD AddressOfEntryPoint; //EP的RVA值,程序最先执行代码的地址
DWORD BaseOfCode; //代码段起始RVA
DWORD BaseOfData; //数据段起始RVA
DWORD ImageBase; //PE文件的装载地址
DWORD SectionAlignment; //块对齐,节区在内存中最小单位
DWORD FileAlignment; //文件块对齐,节区在文件中的最小单位
WORD MajorOperatingSystemVersion;//所需操作系统版本号
WORD MinorOperatingSystemVersion;//
WORD MajorImageVersion; //用户自定义主版本号
WORD MinorImageVersion; //用户自定义次版本号
WORD MajorSubsystemVersion; //win32子系统版本。若PE文件是专门为Win32设计的
WORD MinorSubsystemVersion; //该子系统版本必定是4.0否则对话框不会有3维立体感
DWORD Win32VersionValue; //保留
DWORD SizeOfImage; //内存中整个PE映像体的尺寸
DWORD SizeOfHeaders; //所有头+节表的大小,即整个PE头的大小
DWORD CheckSum; //校验和
WORD Subsystem; //NT用来识别PE文件属于哪个子系统(系统驱动、GUI、CUI)
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; //指定DataDirectory数组的个数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA,比如引入地址表等
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
test.exe中的实际情况如下:
其中比较重要的几个字段:
offset 0xD8-0xDB
:该字段为AddressOfEntryPoint,定义了文件开始执行位置的相对虚拟地址(RVA),在这里的值为0x00001000
offset 0xE4-0xE7
:该字段为ImageBase,定义了可执行文件默认装入的内存地址,在这里的值为0x00400000
根据以上两个值,我们可以计算出程序入口的虚拟地址(VA)为0x00401000
除此之外,还有两个特别重要的值:
offset 0x124-0x127
:该字段为NumberOfRvaAndSizes,指示了数据目录项(DataDirectory)的项数,在这里的值为0x00000010
,即16项offset 0x128-0x1A7
:该字段为DataDirectory,是一个IMAGE_DATA_DIRECTORY数组,里面存放的是可执行文件的一些重要部分的起始RVA和尺寸,目的是使可执行文件更快地进行装载
IMAGE_DATA_DIRECTORY的结构如下:
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 8 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 9 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 10 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 11 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 12 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 13 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 14 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 15 // COM Runtime descriptor
分析数据,可以发现数据目录中只有两项:
- IMAGE_DIRECTORY_ENTRY_IMPORT(Import Directory)RVA=
0x00002014
Size=0x0000003C
- IMAGE_DIRECTORY_ENTRY_IAT(Import Address Table)RVA=
0x00002000
Size=0x00000014
4. 节表(Section Table)
紧接着PE文件头的是节表。节表实际上是一个结构数组,每个结构包含了一个节的具体信息(每个结构占用0x28
字节),该结构的内容如下:
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 8个字节的节区名称
union {
DWORD PhysicalAddress;
DWORD VirtualSize; //内存中节区的大小
} Misc;
DWORD VirtualAddress; // 内存中节区的起始地址(RVA)
DWORD SizeOfRawData; // 磁盘中文件中节区所占大小
DWORD PointerToRawData; // 磁盘中文件的起始位置
DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移
DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地)
WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目
WORD NumberOfLinenumbers; // 行号表中行号的数目
DWORD Characteristics; // 节属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
test.exe 节表中结构的数目由之前映像文件头中的 NumberOfSections 字段确定,一共包含3个该结构,分别是 .text , .rdata 和 .data ,具体如下:
① .text节表
其中比较重要的几个字段:
offset 0x1A8-0x1AF
:该字段为Name,可以从ASCII码值看出该节表对应的节为.text节offset 0x1B0-0x1B3
:该字段在exe文件中为Virtual-Size,表示节的实际字节数,这里的值为0x00000046
字节offset 0x1B4-0x1B7
:该字段为VirtualAddress,表示本节起始的相对虚拟地址(RVA),这里的值为0x00001000
offset 0x1BC-0x1BF
:该字段为PointerToRawData,表示本节在磁盘中对齐后的位置,这里的值为0x00000400
offset 0x1CC-0x1CF
:该字段为Characteristic,表示该节的属性,这里的值为0x60000020 = 0x40000000 + 0x20000000 + 0x20
,表示的意义是该节包含代码,并且可读可执行
② .rdata节表
其中比较重要的几个字段:
offset 0x1D0-0x1D7
:该字段为Name,可以从ASCII码值看出该节表对应的节为.rdata节offset 0x1D8-0x1DB
:该字段在exe文件中为Virtual-Size,表示节的实际字节数,这里的值为0x000000A6
字节offset 0x1DC-0x1DF
:该字段为VirtualAddress,表示本节起始的相对虚拟地址(RVA),这里的值为0x00002000
offset 0x1E4-0x1E7
:该字段为PointerToRawData,表示本节在磁盘中对齐后的位置,这里的值为0x00000600
offset 0x1F0-0x1F7
:该字段为Characteristic,表示该节的属性,这里的值为0x40000040 = 0x40000000 + 0x40
,表示的意义是该节包含已初始化的数据,并且可读
③ .data节表
offset 0x1F8-0x1FF
:该字段为Name,可以从ASCII码值看出该节表对应的节为.data节offset 0x200-0x203
:该字段在exe文件中为Virtual-Size,表示节的实际字节数,这里的值为0x0000008E
字节offset 0x204-0x207
:该字段为VirtualAddress,表示本节起始的相对虚拟地址(RVA),这里的值为0x00003000
offset 0x20C-0x20F
:该字段为PointerToRawData,表示本节在磁盘中对齐后的位置,这里的值为0x00000800
offset 0x21C-0x21F
:该字段为Characteristic,表示该节的属性,这里的值为0xC0000040 = 0x80000000 + 0x40000000 + 0x30
,表示的意义是该节包含已初始化的数据,并且可读可写
5. 节(Section)
① .text节
这一节含有程序的可执行代码,根据节表中的值,可以确定.text节在文件中的地址为0x00000400
,实际长度为0x46
字节,具体代码如下:
② .rdata节
这一节称为引入函数节,包含有从其他DLL中引入的函数,根据IMAGE_DATA_DIRECTORY中引入函数表地址为0x00002014
,且根据节表,该表在内存中的偏移为0x00002000
,即RVA为0x0
。根据节表,该节从文件偏移0x600
处开始,那么.rdata表在文件中的偏移就为0x614
。大小为0xA6
字节。
该节的开始是一个成员为IMAGE_IMPORT_DESCRIPTOR结构的数组,该结构的定义如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; // RVA,指向字符串,是这个可执行文件的名字。例如"ACE.dll"
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
该节的实际数据如下:
第一个IDD的分析如下:
offset 0x614-0x617
:该字段为OriginalFirstThunk,是一个IMAGE_THUNK_DATA的指针,值为0x00002050
,因为该节在内存中的偏移为0x00002000
所以RVA为0x50
,在文件中即为0x00000650
,这个地址对应的即是第一个IID的IMAGE_THUNK_DATA,其结构如下
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // 一个RVA地址,指向forwarder string
DWORD Function; // PDWORD,被导入的函数的入口地址
DWORD Ordinal; // 该函数的序数
DWORD AddressOfData; // 一个RVA地址,指向IMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
(IMAGE_THUNK_DATA64与IMAGE_THUNK_DATA32的区别,仅仅是把DWORD换成了64位整数。)
所以0x650
的值在这里对应的字段为AddressOfData,即一个指向IMAGE_IMPORT_BY_NAME的指针,为0x00002064
,即RVA为0x64
,在文件中的偏移为0x664
,该结构的定义如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // 该函数的导出序数
BYTE Name[1]; // 该函数的名字
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
前两个字节为引入函数的序号,其值为0x0080
,后面紧跟的为函数名字符串”ExitProcess”,以0x00
结尾
offset 0x620-0x623
:该字段为Name,是指向字符串的指针,值为0x00002072
,所以文件中偏移为0x00000672
,对应的就是”kernel32.dll”,以0x00
结尾offset 0x624-0x627
:该字段为FirstThunk,是指向字符串的指针,值为0x00002000
,所以文件中偏移为0x00000600
,该地址在内存中将会设置成API函数ExitProcess的真实地址
第二个IDD的分析如下:
offset 0x628-0x62B
:该字段是OriginalFirstThunk,与第一个IDD的相同,值为0x00002058
,RVA为0x58
,文件中的偏移为0x658
,指向的地址的值为0x0000208C
,该地址是指向IMAGE_IMPORT_BY_NAME结构的指针,该值的RVA为0x8C
,在文件中的偏移为0x68C
,对应的前两个字节为引入函数序号0x019D
,后面紧跟的为函数名字符串”MessageBox”,以0x00
结尾offset 0x634-0x637
:该字段为Name,是指向字符串的指针,值为0x0000209A
,所以文件中偏移为0x0000069A
,对应的就是”user32.dll”,以0x00
结尾offset 0x638-0x63B
:该字段为FirstThunk,是指向字符串的指针,值为0x00002008
,所以文件中偏移为0x00000608
,该地址在内存中将会设置成API函数MessageBox的真实地址
③ .data节
.data节称为已初始化的数据节,其中存放的是在编译时刻已经确定的数据。可以从节表中知道,该节从文件偏移0x800
处开始,实际大小为0x8E
字节,实际数据如下:
可以看到其中存放着一些程序预设好的字符串,这一部分在程序运行时也会被装入内存之中
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!