什么是PE结构:PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,就是只要在Windows下的可执行程序的内部结构都是PE结构,就是一个程序代码的分布结构,什么位置放什么数据代码,是有一定格式的,不然就乱了,系统就找不到了。
为什么学习PE结构:逆向分析一个Windows程序,必须了解它的代码分布情况,知己知彼百战不殆。
常用基本概念:
名称 | 描述 |
地址 | 是“虚拟地址”而不是“物理地址”。为什么不是“物理地址”呢?因为数据在内存的位置经常在变,这样可以节省内存开支、避开错误的内存位置等的优势。同时用户并不需要知道具体的“真实地址”,因为系统自己会为程序准备好内存空间的(只要内存足够大) |
镜像文件 | 包含以EXE文件为代表的“可执行文件”、以DLL文件为代表的“动态链接库”。为什么用“镜像”?这是因为他们常常被直接“复制”到内存,有“镜像”的某种意思。看来西方人挺有想象力的哦^0^ |
RVA | 英文全称Relatively Virtual Address。偏移(又称“相对虚拟地址”)。相对镜像基址的偏移。 |
节 | 节是PE文件中代码或数据的基本单元。原则上讲,节只分为“代码节”和“数据节”。 |
VA | 英文全称Virtual Address。基址 |
一个PE文件的结构。
DOS头部结构体:
typedef struct _IMAGE_DOS_HEADER { WORD e_magic; // [0x00] Magic number(重要,是否为PE文件的第一个标志) WORD e_cblp; // [0x02]Bytes on last page of file WORD e_cp; // [0x04]Pages in file WORD e_crlc; // [0x06]Relocations WORD e_cparhdr; // [0x08]Size of header in paragraphs WORD e_minalloc; //[0x0A] Minimum extra paragraphs needed WORD e_maxalloc; //[0x0C] Maximum extra paragraphs needed WORD e_ss; // [0x0E]Initial (relative) SS value WORD e_sp; // [0x10]Initial SP value WORD e_csum; // [0x12]Checksum WORD e_ip; //[0x14] Initial IP value WORD e_cs; //[0x16] Initial (relative) CS value WORD e_lfarlc; // [0x18]File address of relocation table WORD e_ovno; // [0x1A]Overlay number WORD e_res[4]; // [0x1C]Reserved words WORD e_oemid; // [0x24]OEM identifier (for e_oeminfo) WORD e_oeminfo; //[0x26] OEM information; e_oemid specific WORD e_res2[10]; // [0x28]Reserved words LONG e_lfanew; // [0x3C]File address of new exe header(有用,PE解析时用它找到PE头的位置) } IMAGE_DOS_HEADER, * PIMAGE_DOS_HEADER; 一共有64个字节,一个WORD两个字节,一个LONG四个字节。
上图随便用16进制文本编辑器打开一个可执行程序,前64字节就是DOS头部。
重要数据的是第一个和最后一个,也就是e_magic与e_lfanew这两个数据成员,也就是4D 5A 和 F8 00 00 00 00,e_magic,翻译为魔数,其实它就是一个标记,DOS头标志位,其值恒为4D5A,在系统中用宏定义为:IMAGE_DOS_SIGNATURE,e_lfanew,它表示NT头部在文件中的偏移。
NT头部结构体:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; //标记(重要,判断是否为PE文件的第二个标志) IMAGE_FILE_HEADER FileHeader; //文件头(重要,存储着PE文件的基本信息) IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展头(重要,存储着关于PE文件时加载的信息) } IMAGE_NT_HEADERS32, * PIMAGE_NT_HEADERS32;
IMAGE_FILE_HEADER fileheader:
typedef struct _IMAGE_FILE_HEADER { WORD Machine; 1 (文件的运行平台) WORD NumberOfSections; 2 (区段的数量) DWORD TimeDateStamp; 3 (文件创建时间) DWORD PointerToSymbolTable; 4 (符号表偏移,用于调试) DWORD NumberOfSymbols; 5 (符号个数,用于调试) WORD SizeOfOptionalHeader; 6 (扩展头的大小) WORD Characteristics; 7 (PE文件的一些属性) } IMAGE_FILE_HEADER, * PIMAGE_FILE_HEADER;
一共有2 + 2 + 4 + 4 + 4 + 2 + 2 = 20个字节。
Machine :01 4C(注意是小端)
值 描述 0x0 适用于任何类型处理器 0x1d3 Matsushita AM33处理器 0x8664 x64处理器 0x1c0 ARM小尾处理器 0xebc EFI字节码处理器 0x14c Intel 386或后继处理器及其兼容处理器 0x200 Intel Itanium处理器 0x9041 Mitsubishi M32R小尾处理器 0x266 MIPS16处理器 0x366 带FPU的MIPS处理器 0x466 带FPU的MIPS16处理器 0x1f0 PowerPC小尾处理器 0x1f1 带符点运算支持的PowerPC处理器 0x166 MIPS小尾处理器 0x1a2 Hitachi SH3处理器 0x1a3 Hitachi SH3 DSP处理器 0x1a6 Hitachi SH4处理器 0x1a6 Hitachi SH5处理器 0x1c2 Thumb处理器 0x169 MIPS小尾WCE v2处理器
- NumberOfSections(区段的数量):00 07
- TimeDateStamp (文件创建时间): 58 A1 43 31
- PointerToSymbolTable(符号表偏移): 00 00 00 00
- NumberOfSymbols (符号表个数):00 00 00 00
- SizeOfOptionalHeader(扩展头的大小):00 E0
- Characteristics:01 02
位置 | 描述 |
0 |
|
1 |
|
2 |
|
3 | |
4 | |
5 |
|
6 |
|
7 | |
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
IMAGE_OPTIONAL_HEADER32 OptionalHeader
typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; 1 文件类型标识,32位一般是0x010B,64位的PE文件一般是0x020B,还有0x0170,代表ROM镜像。 BYTE MajorLinkerVersion; 2 连接器主版本 BYTE MinorLinkerVersion; 3 连接器次版本 DWORD SizeOfCode; 4 (重要)指所有代码区段(节)的总大小 DWORD SizeOfInitializedData; 5 已初始化数据的总大小 DWORD SizeOfUninitializedData; 6 未初始化数据的总大小,在磁盘中不占用空间,在加载进内存之后,会预留这么大的空间。一般存储在.bss区段中。 DWORD AddressOfEntryPoint; 7 (重要)程序开始执行的相对虚拟地址(RVA),也叫OEP,Orginal Entry Point ,源入口点。 DWORD BaseOfCode; 8 (重要)起始代码的相对虚拟地址(RVA),一般这个值为0x00001000. DWORD BaseOfData; 9 起始数据的相对虚拟地址(RVA) // // NT additional fields. // DWORD ImageBase; 10 (重要)默认加载基址(如果没有加载到这个地址,会发生重定位.) DWORD SectionAlignment; 11 (重要)块对齐数,就是在映射到内存中的区段(节)对齐,这个数必须大于文件对齐数,一般是0x1000 DWORD FileAlignment; 12 (重要)文件对齐数,就是在硬盘中的文件的区段(节)对齐,一般是0x200 WORD MajorOperatingSystemVersion; 13 主操作系统版本号 WORD MinorOperatingSystemVersion; 14 次操作系统版本号 WORD MajorImageVersion; 15 主映像版本 WORD MinorImageVersion; 16 次映像版本 WORD MajorSubsystemVersion; 17 主子系统版本 WORD MinorSubsystemVersion; 18 次子系统版本 DWORD Win32VersionValue; 19 保留值,一般是0 DWORD SizeOfImage; 20 (重要)要把文件加载进内存,所需要的内存大小,注意是进行了块对齐之后 DWORD SizeOfHeaders; 21 所有头部大小,Dos头、PE头、区段表的尺寸之和 DWORD CheckSum; 22 校验和(一般无用)对于驱动和一些系统dll来说需要校验(使用IMAGEHLP.DLL中的CheckSumMappedFile API) WORD Subsystem; 23 (重要)子系统值 WORD DllCharacteristics ; 24 (重要)指示Dll特征的标志,DllMain()函数何时被调用,默认为0. DWORD SizeOfStackReserve; 25 初始化时栈的大小 DWORD SizeOfStackCommit; 26 初始化时实际提交的栈的大小 DWORD SizeOfHeapReserve; 27 初始化时保留的堆的大小 DWORD SizeOfHeapCommit; 28 初始化时实际提交的堆的大小 DWORD LoaderFlags; 29 与调试相关 DWORD NumberOfRvaAndSizes; 30 数据目录的个数,也就是下面那个数组中元素的个数。 IMAGE_DATA_DIRECTORY DataDirectory[ IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; 31 (非常重要)数据目录表 } IMAGE_OPTIONAL_HEADER32, * PIMAGE_OPTIONAL_HEADER32;
一共有96个字节再加上IMAGE_DATA_DIRECTORY DataDirectory[ IMAGE_NUMBEROF_DIRECTORY_ENTRIES]的大小,32位系统中扩展头大小一般是224(0x00E0),在64位系统中一般是240(0x00F0)。
偏移 | 大小 | 英文名 | 中文名 | 描述 |
0 | 2 | Magic | 魔数 | 这个无符号整数指出了镜像文件的状态。 0x10B表明这是一个32位镜像文件。 0x107表明这是一个ROM镜像。 0x20B表明这是一个64位镜像文件。 |
2 | 1 | MajorLinkerVersion | 链接器的主版本号 | 链接器的主版本号。 |
3 | 1 | MinorLinkerVersion | 链接器的次版本号 | 链接器的次版本号。 |
4 | 4 | SizeOfCode | 代码节大小 | 一般放在“.text”节里。如果有多个代码节的话,它是所有代码节的和。必须是FileAlignment的整数倍,是在文件里的大小。 |
8 | 4 | SizeOfInitializedData | 已初始化数大小 | 一般放在“.data”节里。如果有多个这样的节话,它是所有这些节的和。必须是FileAlignment的整数倍,是在文件里的大小。 |
12 | 4 | SizeOfUninitializedData | 未初始化数大小 | 一般放在“.bss”节里。如果有多个这样的节话,它是所有这些节的和。必须是FileAlignment的整数倍,是在文件里的大小。 |
16 | 4 | AddressOfEntryPoint | 入口点 | 当可执行文件被加载进内存时其入口点RVA。对于一般程序镜像来说,它就是启动地址。为0则从ImageBase开始执行。对于dll文件是可选的。 |
20 | 4 | BaseOfCode | 代码基址 | 当镜像被加载进内存时代码节的开头RVA。必须是SectionAlignment的整数倍。 |
24 | 4 | BaseOfData | 数据基址 | 当镜像被加载进内存时数据节的开头RVA。(在64位文件中此处被并入紧随其后的ImageBase中。)必须是SectionAlignment的整数倍。 |
28/24 | 4/8 | ImageBase | 镜像基址 | 当加载进内存时镜像的第1个字节的首选地址。它必须是64K的倍数。DLL默认是10000000H。Windows CE 的EXE默认是00010000H。Windows 系列的EXE默认是00400000H。 |
32 | 4 | SectionAlignment | 内存对齐 | 当加载进内存时节的对齐值(以字节计)。它必须≥FileAlignment。默认是相应系统的页面大小。 |
36 | 4 | FileAlignment | 文件对齐 | 用来对齐镜像文件的节中的原始数据的对齐因子(以字节计)。它应该是界于512和64K之间的2的幂(包括这两个边界值)。默认是512。如果SectionAlignment小于相应系统的页面大小,那么FileAlignment必须与SectionAlignment相等。 |
40 | 2 | MajorOperatingSystemVersion | 主系统的主版本号 | 操作系统的版本号可以从“我的电脑”→“帮助”里面看到,Windows XP是5.1。5是主版本号,1是次版本号 |
42 | 2 | MinorOperatingSystemVersion | 主系统的次版本号 | |
44 | 2 | MajorImageVersion | 镜像的主版本号 | |
46 | 2 | MinorImageVersion | 镜像的次版本号 | |
48 | 2 | MajorSubsystemVersion | 子系统的主版本号 | |
50 | 2 | MinorSubsystemVersion | 子系统的次版本号 | |
52 | 2 | Win32VersionValue | 保留,必须为0 | |
56 | 4 | SizeOfImage | 镜像大小 | 当镜像被加载进内存时的大小,包括所有的文件头。向上舍入为SectionAlignment的倍数。 |
60 | 4 | SizeOfHeaders | 头大小 | 所有头的总大小,向上舍入为FileAlignment的倍数。可以以此值作为PE文件第一节的文件偏移量。 |
64 | 4 | CheckSum | 校验和 | 镜像文件的校验和。计算校验和的算法被合并到了Imagehlp.DLL 中。以下程序在加载时被校验以确定其是否合法:所有的驱动程序、任何在引导时被加载的DLL以及加载进关键Windows进程中的DLL。 |
68 | 2 | Subsystem | 子系统类型 | 运行此镜像所需的子系统。参考后面的“Windows子系统”部分。 |
70 | 2 | DllCharacteristics | DLL标识 | 参考后面的“DLL特征”部分。 |
72 | 4/8 | SizeOfStackReserve | 堆栈保留大小 | 最大栈大小。CPU的堆栈。默认是1MB。 |
76/80 | 4/8 | SizeOfStackCommit | 堆栈提交大小 | 初始提交的堆栈大小。默认是4KB。 |
80/88 | 4/8 | SizeOfHeapReserve | 堆保留大小 | 最大堆大小。编译器分配的。默认是1MB。 |
84/96 | 4/8 | SizeOfHeapCommit | 堆栈交大小 | 初始提交的局部堆空间大小。默认是4KB。 |
88/104 | 4 | LoaderFlags | 保留,必须为0 | |
92/108 | 4 | NumberOfRvaAndSizes | 目录项数目 | 数据目录项的个数。由于以前发行的Windows NT的原因,它只能为16。 |
- Magic:01 0B
- ?AddressOfEntryPoint:3A 74
- ?ImageBase:00 40 00 00
- ?SectionAlignment:1000
- ?FileAlignment:200
- ?SizeOfImage:E7 28
- ?SizeOfHeaders:400
IMAGE_DATA_DIRECTORY
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; // 数据的相对虚拟地址(RVA) DWORD Size; // 数据的大小 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
IMAGE_SECTION_HEADER
typedef struct _IMAGE_SECTION_HEADER { +0 BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text” //IMAGE_SIZEOF_SHORT_NAME=8 +8 union { DWORD PhysicalAddress; // 物理地址 DWORD VirtualSize; // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一 // 般是取后一个 } Misc; +ch DWORD VirtualAddress; // 节区的 RVA 地址 +10h DWORD SizeOfRawData; // 在文件中对齐后的尺寸 +14h DWORD PointerToRawData; // 在文件中的偏移量 +18h DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移 +1ch DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地) +1eh WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目 +20h WORD NumberOfLinenumbers; // 行号表中行号的数目 +24h DWORD Characteristics; // 节属性如可读,可写,可执行等 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name: 区块名。这是一个由8位的ASCII 码名,用来定义区块的名称。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.” 实际上是不是必须的。值得我们注意的是,如果区块名超过 8 个字节,则没有最后的终止标志“NULL” 字节。并且前边带有一个“$” 的区块名字会从连接器那里得到特殊的待遇,前边带有“$” 的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$” 后边的字符的字母顺序进行合并的。 另外每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” 或者说将包含数据的区块命名为“.Code” 都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。 Virtual Size:该区块表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。 Virtual Address:该区块装载到内存中的RVA 地址。这个地址是按照内存页来对齐的,因此它的数值总是 SectionAlignment 的值的整数倍。在Microsoft 工具中,第一个快的默认 RVA 总为1000h。在OBJ 中,该字段没有意义地,并被设为0。 SizeOfRawData:该区块在磁盘中所占的大小。在可执行文件中,该字段是已经被FileAlignment 潜规则处理过的长度。 PointerToRawData:该区块在磁盘中的偏移。这个数值是从文件头开始算起的偏移量哦。 PointerToRelocations:这哥们在EXE文件中没有意义,在OBJ 文件中,表示本区块重定位信息的偏移值。(在OBJ 文件中如果不是零,它会指向一个IMAGE_RELOCATION 结构的数组) PointerToLinenumbers:行号表在文件中的偏移值,文件的调试信息,于我们没用,鸡肋。 NumberOfRelocations:这哥们在EXE文件中也没有意义,在OBJ 文件中,是本区块在重定位表中的重定位数目来着。 NumberOfLinenumbers:该区块在行号表中的行号数目,鸡肋。 Characteristics:该区块的属性。该字段是按位来指出区块的属性(如代码/数据/可读/可写等)的标志。 IMAGE_SCN_CNT_CODE 0x00000020 The section contains executable code. 包含代码,常与 0x10000000一起设置。 IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 The section contains initialized data. 该区块包含以初始化的数据。 IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 The section contains uninitialized data. 该区块包含未初始化的数据。 IMAGE_SCN_MEM_DISCARDABLE 0x02000000 The section can be discarded as needed. 该区块可被丢弃,因为当它一旦被装入后, 进程就不在需要它了,典型的如重定位区块。 IMAGE_SCN_MEM_SHARED 0x10000000 The section can be shared in memory. 该区块为共享区块。 IMAGE_SCN_MEM_EXECUTE 0x20000000 The section can be executed as code. 该区块可以执行。通常当0x00000020被设置 时候,该标志也被设置。 IMAGE_SCN_MEM_READ 0x40000000 The section can be read. 该区块可读,可执行文件中的区块总是设置该 标志。 IMAGE_SCN_MEM_WRITE 0x80000000 The section can be written to. 该区块可写。
每个节一共有40个字节,这个程序有7个节,所以一共有280个字节。
用程序解析代码(C/C++):
// PE.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <windows.h> #include <iostream> using namespace std; int main(int argc, char *argv[]) { //定义变量 IMAGE_DOS_HEADER DosHeader; IMAGE_NT_HEADERS NtHeader; IMAGE_SECTION_HEADER SecHeader; HANDLE hFile; LPWSTR FileName; DWORD Dwsize; FileName = L"test.exe"; int OffsetSection = 0, NumSection = 0; int i = 0, j = 0; int Offset = 0; int Num = 0; //以系统自带的CMD程序为例进行说明 if ((hFile = CreateFile(FileName, GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE) { cout << "INVALID_HANDLE_VALUE"; return 0; } //SetFilePointer() SetFilePointer(hFile, 0, 0, FILE_BEGIN); //ReadFile() ReadFile(hFile, &DosHeader, sizeof(DosHeader), &Dwsize, NULL); if (DosHeader.e_magic != IMAGE_DOS_SIGNATURE) { cout << "没有DOS头" << endl; CloseHandle(hFile); return 0; } else { cout << "有DOS头" << endl; } SetFilePointer(hFile, DosHeader.e_lfanew, 0, FILE_BEGIN); ReadFile(hFile, &NtHeader, sizeof(NtHeader), &Dwsize, NULL); if (NtHeader.Signature != IMAGE_NT_SIGNATURE) { cout << "没有PE头" << endl; CloseHandle(hFile); } else { cout << "PE有效" << endl; cout << "IMAGE_FILE_HEADER" << endl; cout << "Machine:" << hex << NtHeader.FileHeader.Machine << endl; cout << "NumberOfSections:" << hex << NtHeader.FileHeader.NumberOfSections << endl; cout << "SizeOfOptionalHeader:" << hex << NtHeader.FileHeader.SizeOfOptionalHeader << endl; cout << endl; cout << "IMAGE_OPTIONAL_HEADER" << endl; cout << "AddresssOfEntryPoint:" << hex << NtHeader.OptionalHeader.AddressOfEntryPoint << endl; cout << "ImageBase:" << hex << NtHeader.OptionalHeader.ImageBase << endl; cout << "SectionAlignment:" << hex << NtHeader.OptionalHeader.SectionAlignment << endl; cout << "FileAlignment:" << hex << NtHeader.OptionalHeader.FileAlignment << endl; cout << "SizeOfImage:" << hex << NtHeader.OptionalHeader.SizeOfImage << endl; cout << "NumberOfHeaders:" << hex << NtHeader.OptionalHeader.SizeOfHeaders << endl; cout << endl; cout << "IMAGE_DESCRITOR结构数组的RVA地址" << endl; cout << "IMAGE_IMPORT_DESCRIPTOR的 RVA:" << hex << NtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress << endl; } ////采用IMAGE_DOS_HEADER.e_lfanew+sizeof(IMAGE_NT_SIGNATURE)+sizeof(IMAGE_FILE_HEADER)+sizeof(IMAGE_OPTIONAL_HEADER)的算法 //NumSection = NtHeader.FileHeader.NumberOfSections; //OffsetSection = DosHeader.e_lfanew + 0x18 + sizeof(IMAGE_OPTIONAL_HEADER); //for (i = 0; i < NumSection; i++) //{ // SetFilePointer(hFile, OffsetSection + sizeof(IMAGE_SECTION_HEADER)*i, 0, NULL); // ReadFile(hFile, &SecHeader, sizeof(IMAGE_SECTION_HEADER), &Dwsize, NULL); // for (j = 0; j < 8; j++) // { // //输出每一个节头 // cout << SecHeader.Name[j]; // } // cout << endl; // //输出每一个节的信息 // cout << "PointToRawOfData:" << hex << SecHeader.PointerToRawData << endl; // cout << "VirtualAddress:" << hex << SecHeader.VirtualAddress << endl; // //输出第一个节的节偏移并计算IMAGE_IMPORT_DESCRIPTOR结构数组的物理偏移 // if (i == 0) // { // //offset=va-ImageBase-节偏移 // cout << ".text段的节偏移:" << hex << SecHeader.VirtualAddress - SecHeader.PointerToRawData << endl; // cout << "IMAGE_IMPORT_DESCRIPTOR的物理偏 移:" << NtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress - (SecHeader.VirtualAddress - SecHeader.PointerToRawData) << endl; // } //} CloseHandle(hFile); system("pause"); return 0; }
转载请注明:即刻安全 » PE结构学习01-DOS头-NT头-节表头