这个是pe文件个格式,先有了一个形象的概念。
两个基本概念
section
就是段,不同的应用程序都有基本的段。我们也可以通过程序修改段,一般.text段放着程序的源代码,.data 存放初始化信息的地方。包括全局变量、字符串常量和静态变量,这些变量在编译时期就给定初值。连接器把obj和lib中的所有.data组合起来放到exe的.data中。而变量放在执行过程中的堆栈中。等等了。可以通过域numbersofsection来获得一个应用程序的段
相对偏移量(rav)
RVA是虚拟空间中到参考点的一段距离。举例说明,如果PE文件装入虚拟地址(VA)空间的400000h处,且进程从虚址401000h开始执行,我们可以说进程执行起始地址在RVA 1000h。 为什么PE文件格式要用到RVA呢? 这是为了减少PE装载器的负担。因为每个模块多有可能被重载到任何虚拟地址空间
第一部分:MS_DOS头部,它的定义为IMAGE_DOS_HEADER结构体,具体定义如下:
typedef struct _IMAGE_DOS_HEADER
{
USHORT e_magic; // 魔术数字
USHORT e_cblp; // 文件最后页的字节数
USHORT e_cp; // 文件页数
USHORT e_crlc; // 重定义元素个数
USHORT e_cparhdr; // 头部尺寸,以段落为单位
USHORT e_minalloc; // 所需的最小附加段
USHORT e_maxalloc; // 所需的最大附加段
USHORT e_ss; // 初始的SS值(相对偏移量)
USHORT e_sp; // 初始的SP值
USHORT e_csum; // 校验和
USHORT e_ip; // 初始的IP值
USHORT e_cs; // 初始的CS值(相对偏移量)
USHORT e_lfarlc; // 重分配表文件地址
USHORT e_ovno; // 覆盖号
USHORT e_res[4]; // 保留字
USHORT e_oemid; // OEM标识符(相对e_oeminfo)
USHORT e_oeminfo; // OEM信息
USHORT e_res2[10]; // 保留字
LONG e_lfanew; // 新exe头部的文件地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
该结构体一共有64个字节大小。对于windows应用程序这个头部有用的域只有e_lfanew它标识着windows应用程序的真正的文件地址,还有就是e_lfarlc,定义为重新分配表文件地址,表示实模式残留程序的起始地址。这两个域定位都只是给出了文件的偏移量,这点要记住。一个具体的例子:
00000000 5A4D 签名: MZ
00000002 0090 额外字节
00000004 0003 页
00000006 0000 重定位项目
00000008 0004 标头大小
0000000A 0000 最小允许
0000000C FFFF 最大允许
0000000E 0000 初始 SS
00000010 00B8 初始 SP
00000012 0000 校验
00000014 0000 初始 IP
00000016 0000 初始 CS
00000018 0040 重定位表
0000001A 0000 覆盖
00000001C 0000 保留字(8个字节)
000000024 0000 OEM标识符(相对e_oeminfo)
000000026 0000 OEM信息
00000028 0000 保留字(20个字节)
0000003C 000000C8 新exe头部的文件地址(8个字节)
第二部分:实模式残留程序
实模式残余程序是一个在装载时能够被MS-DOS运行的实际程序。对于一个MS-DOS的可执行映像文件,应用程序就是从这里执行的。对于Windows、OS/2、Windows NT这些操作系统来说,MS-DOS残余程序就代替了主程序的位置被放在这里。这种残余程序通常什么也不做,而只是输出一行文本。还是举个实模式残留程序的代码例子吧,如下:
00000040 0E 1F BA 0E 00 B4 09 CD - 21 B8 01 4C CD 21 54 68 ...........L..Th
00000050 69 73 20 70 72 6F 67 72 - 61 6D 20 63 61 6E 6E 6F is.program.canno
00000060 74 20 62 65 20 72 75 6E - 20 69 6E 20 44 4F 53 20 t.be.run.in.DOS.
00000070 6D 6F 64 65 2E 0D 0D 0A - 24 00 00 00 00 00 00 00 mode............
00000080 C8 A4 79 1C 8C C5 17 4F - 8C C5 17 4F 8C C5 17 4F ..y....O...O...O
00000090 E6 D9 15 4F 9B C5 17 4F - D5 E6 04 4F 83 C5 17 4F ...O...O...O...O
000000A0 8C C5 16 4F 28 C5 17 4F - A6 CD 11 4F 8D C5 17 4F ...O...O...O...O
000000B0 8C C5 17 4F 99 C5 17 4F - 52 69 63 68 8C C5 17 4F ...O...ORich...O
000000C0 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................
000000D0 00 00 00 00 00 00 00 00 - 50 45 00 00 4C 01 03 00 ........PE..L...
第三部分:pe头部
这个头部的结构如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader; } IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
它包括三个域,第一个域它是一个固定的格式,"PE/0/0",用来标识pe文件。
第二个域,pe文件头部,一个结构体,IMAGE_FILE_HEADER。该结构体一个20个字节。它具体的定义如下:
typedef struct _IMAGE_FILE_HEADER
{
USHORT Machine; //指定运行平台
USHORT NumberOfSections; //文件的节表(Section)数目
ULONG TimeDateStamp; //文件创建日期和时间
ULONG PointerToSymbolTable; //用于调试
ULONG NumberOfSymbols; //用于调试
USHORT SizeOfOptionalHeader; //指示紧随本结构之后的 OptionalHeader 结构大小,必须为有效值
USHORT Characteristics; //关于文件信息的标记,比如文件是 exe还是 dll
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
还是给一个具体的例子吧:
000000D8 4550 PE标识
000000DA 0000 PE标识
000000DC 014C 运行平台: 014C=I386
000000DE 0003 区段数
000000E0 37F6657C 时期/日期戳
000000E4 00000000 指示到符号表
000000E8 00000000 符号数
000000EC 00E0 可选标头大小
000000EE 030F 特性
第三个域,pe可选头部,一个结构体,IMAGE_OPTIONAL_HEADER。虽说是可选,但还是必不可少的,它包含了很多的信息,如可执行映像的重要信息,例如初始的堆栈大小、程序入口点的位置、首选基地址、操作系统版本、段对齐的信息等等。这个结构体分为两块,第一块是标准域,是和UNIX可执行文件的COFF格式所公共的部分。第二块就是Windows NT特定的进程行为提供了装载器的支持。这个结构体的具体定义如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode; //可执行代码尺寸
DWORD SizeOfInitializedData; //已初始化的数据尺寸
DWORD SizeOfUninitializedData; //未初始化的数据尺寸
DWORD AddressOfEntryPoint; //这个域表示应用程序入口点的位置。并且,对于系统黑客来说,这个位置就是导入地址表(IAT)的末尾
DWORD BaseOfCode; //已载入映像的代码(“.text”段)的相对偏移量
DWORD BaseOfData; //已载入映像的未初始化数据(“.bss”段)的相对偏移量
DWORD ImageBase; //进程映像地址空间中的首选基地址
DWORD SectionAlignment; //SectionAlignment则规定了装载时段能够占据的最小空间数量
DWORD FileAlignment; //映像文件首先装载的最小的信息块间隔。
WORD MajorOperatingSystemVersion; //表示Windows NT操作系统的主版本号
WORD MinorOperatingSystemVersion; //表示Windows NT Win32子系统的次版本号
WORD MajorImageVersion; //用来表示应用程序的主版本号
WORD MinorImageVersion; //用来表示应用程序的次版本号
WORD MajorSubsystemVersion; //表示Windows NT Win32子系统的主版本号
WORD MinorSubsystemVersion; //表示Windows NT Win32子系统的次版本号
DWORD Win32VersionValue; //通常不被系统使用,并被链接器设为0
DWORD SizeOfImage; //表示载入的可执行映像的地址空间中要保留的地址空间大小,这个数字很大程度上受SectionAlignment的影响
DWORD SizeOfHeaders; //这个域表示文件中有多少空间用来保存所有的文件头部,包括MS-DOS头部、PE文件头部、PE可选头部以及PE段头部
DWORD CheckSum; //校验和是用来在装载时验证可执行文件的,它是由链接器设置并检验的
WORD Subsystem; //用于标识该可执行文件目标子系统的域
WORD DllCharacteristics; //用来表示一个DLL映像是否为进程和线程的初始化及终止包含入口点的标记
DWORD SizeOfStackReserve; 这些域控制要保留的地址空间数量,并且负责栈和默认堆的申请
DWORD SizeOfStackCommit; 这些域控制要保留的地址空间数量,并且负责栈和默认堆的申请
DWORD SizeOfHeapReserve; 这些域控制要保留的地址空间数量,并且负责栈和默认堆的申请
DWORD SizeOfHeapCommit; 这些域控制要保留的地址空间数量,并且负责栈和默认堆的申请
DWORD LoaderFlags; //告知装载器是否在装载时中止和调试
DWORD NumberOfRvaAndSizes; 这个域标识了接下来的DataDirectory数组
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
AddressOfEntryPoint
程序的入口点, PE装载器准备运行的PE文件的第一个指令的RVA。若您要改变整个执行的流程,
可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。
ImageBase
PE文件的优先装载地址。比如,如果该值是400000h,PE装载器将尝试把文件装到虚拟地址空间的400000h处。
字眼"优先"表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。
SectionAlignment
内存中节对齐的粒度。例如,如果该值是4096 (1000h),那么每节的起始地址必须是4096的倍数。
若第一节从401000h开始且大小是10个字节,则下一节必定从402000h开始,即使401000h和402000h
之间还有很多空间没被使用。
FileAlignment
文件中节对齐的粒度。例如,如果该值是(200h),,那么每节的起始地址必须是512的倍数。若第一节从文件偏移量
200h开始且大小是10个字节,则下一节必定位于偏移量400h: 即使偏移量512和1024之间还有很多空间没被使用/定义。
SizeOfImage
内存中整个PE映像体的尺寸。它是所有头和节经过节对齐处理后的大小。
SizeOfHeaders
所有头+节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。
可以以此值作为PE文件第一节的文件偏移量。
Subsystem
NT用来识别PE文件属于哪个子系统。 对于大多数Win32程序,只有两类值: Windows GUI 和 Windows CUI (控制台)。
DataDirectory
这是一个结构体,定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
每个结构给出一个重要数据结构的RVA,windows总共定义了16个,但是常用的为11个,如下:
//WINNT.H
// 目录入口
// 导出目录
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
// 导入目录
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
// 资源目录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2
// 异常目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
// 安全目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4
// 重定位基本表
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
// 调试目录
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
// 描述字串
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7
// 机器值(MIPS GP)
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
// TLS目录
#define IMAGE_DIRECTORY_ENTRY_TLS 9
// 载入配置目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
基本上,每个数据目录都是一个被定义为IMAGE_DATA_DIRECTORY的结构。虽然数据目录入口本身是相同的,
但是每个特定的目录种类却是完全唯一的。每个数据目录的定义在本文的以后部分被描述为“预定义段”。
如果你要定义一个特定的目录的话,就需要从可选头部中的数据目录数组中决定相对的地址,然后使用虚拟
地址来决定该目录位于哪个段中。一旦你决定了哪个段包含了该目录,该段的段头部就会被用于查找数据目录的精确文
件偏移量位置。
如果PE文件各段的根目录的话,也可以认为 data directory 是存储在这些节里的逻辑元素的根目录。
还是继续给一个例子吧,便于理解:
// 标准域----------------------------
000000F0 010B Magic: 010B=普通可执行,0107=ROM 映像
000000F2 05 主版本号连接
000000F3 0C 副版本号连接
000000F4 00006600 代码段大小
000000F8 00005A00 已初始化数据大小
000000FC 00000000 未初始化数据大小
00000100 00006420 登录指示 RVA
00000104 00001000 代码基部
00000108 00008000 数据基部
// NT附加域--------------------------
0000010C 01000000 映像基数
00000110 00001000 区段队列
00000114 00000200 文件队列
00000118 0005 操作系统主版本
0000011A 0000 操作系统副版本
0000011C 0005 用户主版本
0000011E 0000 用户副版本
00000120 0004 子系统主版本
00000122 0000 子系统副版本
00000124 00000000 已保留
00000128 00010000 映像大小
0000012C 00000600 标头大小
00000130 0001102A 文件校验
00000134 0002 子系统: 1=本地,2=Windows GUI,3=Windows CUI,4=POSIX CUI
00000136 8000 DLL 标记 (废弃)
00000138 00040000 堆栈已保留大小
0000013C 00001000 堆栈提交大小
00000140 00100000 堆积保留大小
00000144 00001000 堆积提交大小
00000148 00000000 加载器标记 (废弃)
0000014C 00000010 数据目录数
00000150 00000000 输出表地址
00000154 00000000 & 大小
00000158 00006650 输入表地址
0000015C 000000B4 & 大小
00000160 0000A000 资源表地址
00000164 000045E4 & 大小
00000168 00000000 异常表地址
0000016C 00000000 & 大小
00000170 00000000 安全表地址
00000174 00000000 & 大小
00000178 00000000 基部重定位表地址
0000017C 00000000 & 大小
00000180 00001300 调试数据地址
00000184 0000001C & 大小
00000188 00000000 版权数据地址
0000018C 00000000 & 大小
00000190 00000000 全局 Ptr
00000194 00000000 & 大小
00000198 00000000 TLS 表地址
0000019C 00000000 & 大小
000001A0 00000000 载入配置表地址
000001A4 00000000 & 大小
最后一部分,各个段
段包含了文件的内容,包括代码、数据、资源以及其它可执行信息,每个段都有一个头部和一个实体(原始数据)。我将在下面描述段头部的有关信息,但是段实体则缺少一个严格的文件结构。因此,它们几乎可以被链接器按任何的方法组织,只要它的头部填充了足够能够解释数据的信息。每个段头部为40个字节长,并且没有任何的填充信息。它的定义如下:
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER
{
UCHAR Name[IMAGE_SIZEOF_SHORT_NAME]; //每个段都有一个8字符长的名称域,并且第一个字符必须是一个句点
union //nion域,现在已不使用了
{
ULONG PhysicalAddress;
ULONG VirtualSize;
}Misc;
ULONG VirtualAddress; //标识了进程地址空间中要装载这个段的虚拟地址
ULONG SizeOfRawData; //标识了相对FileAlignment的段实体尺寸
ULONG PointerToRawData; //标识了文件中段实体位置的偏移量
ULONG PointerToRelocations; //在PE格式中不使用
ULONG PointerToLinenumbers; //在PE格式中不使用
USHORT NumberOfRelocations; //在PE格式中不使用
USHORT NumberOfLinenumbers; //在PE格式中不使用
ULONG Characteristics; //定义了段的特征
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
整个格式的组成:一个MS-DOS的MZ头部,之后是一个实模式的残余程序、PE文件标志、PE文件头部、PE可选头部、
所有的段头部,最后是所有的段实体。 可选头部的末尾是一个数据目录入口的数组,这些相对虚拟地址指向段实体
之中的数据目录。每个数据目录都表示了一个特定的段实体数据是如何组织的。PE文件格式有11个预定义段,这是
对Windows NT应用程序所通用的,但是每个应用程序可以为它自己的代码以及数据定义它自己独特的段。