本篇文章均为新手小白学习过程中,突然有的个人感悟,如果有错误欢迎大师傅们指出
PE的基本结构
这里是我自己突然启发的思路而已
计算机构造?
在买电脑的时候我们经常能看到,什么i5,i7处理器,什么16+512,32+1T,这都是什么呢?
在我浅显的理解里,电脑可以大致分为:CPU,运行内存,内存和显示屏
内存
首先说的是内存,很多人都知道,C、D盘是内存,那么为什么C、D盘叫磁盘,而且内存和运行内存又有什么区别呢?
看到PE文件,我突然想到了这个C,D盘为什么叫磁盘。其实外面买的那种u盘,和你电脑里看到的C,D磁盘没什么区别。简单来说就是说磁盘和电脑之间并不是紧密相连的,而只是相当于有一个u盘在你的电脑里,只不过它会自始至终的插在USB口中,而电脑真正运行的东西是CPU与运行内存,当你打开一个文件的时候,其实相当与是从U盘(磁盘),映射(拷贝)一份给运行内存,再将每一步要用到的东西传给CPU处理
那么既然知道了文件并不是在磁盘中打开运行的,是在运行内存中打开的,那么“打开”是什么呢?
打开便是映射,一个文件,他有一部分并不是可用的,我们知道,机器它是死的,编写程序的人知道,先走这个部分,再走那个部分,但机器怎么知道呢
于是便出现了PE文件格式,在文件中会有一部分信息来告诉机器,他应该怎么做,所以一个文件里并不是所有的内容都是需要用到的,所以在映射的时候,只会映射相对有用的部分(代码块、数据块等)
运行内存
说到这里大家就明白了,运行内存的大小决定了什么呢,就是决定了你一次性可以打开多少个文件,运行内存足够大,内存中可以映射到运行内存的文件就足够多(打开的文件就足够多)
CPU
那么处理器(CPU)的作用,便是将运行内存中的文件按批次处理,相当于计算机的大脑,这里要注意,就像内存与运行内存一样,CPU与运行内存是分开的,是需要运行内存先通过线路传输数据到CPU中,再从CPU传输数据到运行内存中去的,至于一次可以传输多少处理多少,这就和多线程,或者CPU几代有关了,所以CPU的代数与线程数,可以相对反应的是这个电脑,它的“大脑“转的有多快,以及一次可以处理多少事情
显示屏
这个就没啥了,我就不过多说了,就是高刷和清晰度啥的区别了,唯一注意的就是显卡,也就是显示屏中的CPU,独立显卡,就是给你的显示屏也装个大脑,处理图像的时候更灵活,更不卡顿
基地址ImageBase
当PE文件传入到内存中,内存中的版本叫模块,映射文件的起始地址叫模块句柄,这个初始内存地址叫及地址
这里可以进阶的去理解GetModuleHandle函数了,调用该函数时,传参传入一个文件或者一个名字,函数会返回对应模块的句柄(也就是那个文件被映射的基地址),如果传入的时NULL参数的话,则返回调用的函数的可执行文件
虚拟地址VA
每个PE文件被映射到内存中,都有自己的虚拟空间,这个空间里的地址是虚拟地址
相对虚拟地址RVA
虚拟地址(VA)=基地址+相对虚拟地址(RVA)
文件偏移地址
PE文件在磁盘中的时候,数据相对于文件头地址的地址是文件偏移地址
MS-DOS头部 00 00-00 B0
每个PE文件都是以一个DOS程序开始的,有了它,DOS就可以识别它为有效的执行体,然后运行紧随的DOSstub。DOSstub也相当于一个.exe文件了,在不支持PE文件格式的操作系统中,它会简单的提示一个不可执行的错误提示,DOS一般由编译软件编译成
PE文件的第一个字节位于一个传统的MS-DOS头部,称为IMAGE_DOS_HEADER,其结构体中比较重要的字段分别是
e_magic和e_lfanew。e_magic字段的值一般需要被设置为5A4Dh,意味着这是一个可执行的文件。这个值有一个#define,名为IMAGE_DOS_SIGNTURE(0x5A4D),在ascii表示法里它的值为“MZ”,他在PE文件中起到定义宏的作用,而有这个宏定义,操作系统才会把这个文件认定为可执行的,而e_lfanew字段是真正的PE文件头的相对偏移,指出真正的PE头的文件偏移位置,占用4字节,位于从文件开始偏移3Ch字节处
第一个划红线的地方是e_magic,值为MZ,第二个是e_flanew,值经过大小端序之后是00 00 00 B0,所以B0处才是真正的文件偏移位置,并且还可以看到右边有一串字符串,!This program cannot be run in DOS mode.
这个就是前面提到的不可执行错误
PE文件头(IMAGE_NT_HEADER) 00 B0-01 20
紧跟着DOSstub的是PE文件头,其中包含很多PE转载器会用到的重要字段,当执行体在支持PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER结构的e_lfanew字段里找到PE_Header的起始偏移量,用其加上基址,得到PE文件头的指针
PNTHeader=ImageBase+dosHeader->e_lfanew
Signature字段
在一个有效的PE文件里,Signature字段被设置为0x 00 00 45 50,ACSII码字符是”PE\0\0“,定义宏为#define IMAGE_NT_SIGNATURE
"PE\0\0"是一个PE文件的开始,而e_flanew字段正是指向"PE\0\0"
IMAGE_FILE_HEADER结构
IMAGE_FILE_HEADER(映射文件头)结构包含PE文件的一些基本信息,最重要的是,其中一个域指出了IMAGE_OPTIONAL_HEADER的大小,这里按照红色框框划分,依次介绍
Machine:可执行文件的目标CPU类型。PE文件可以在多种机器上使用,不同平台上指令的机器码不同
NumberOfSestions:区块数目,块表紧跟在IMAGE_NT_HEADER后面
TimeDateStamp:表示文件的创建时间。这个值是自1970年1月1日以来用格林威治时间计算的秒数,是一个比文件系统的日期/时间更精确的文件创建时间指示器,将这个值翻译成易读的字符串需要_ctime函数。另一个对此字段有用的函数是gmtime
PointerToSymbolTable:COFF符号表的偏移地址位置,如果没有就是0
NumberOfSymbols:如果有COFF符号表,这个代表符号表的数目
SizeOfOptionalHeader:紧跟IMAGE_FILE_HEADER,表示数据的大小。在PE文件中,这个数据结构叫做IMAGE_OPTIONAL_HEADER,其大小依赖于当前文件是32位还是64位文件。对32位文件这个域通常是00E0h;对64位PE32+文件,这个域是00F0h。不管怎样,这些事要求的最小值,较大的值也可能出现
Characteristics:文件属性,有选择地通过几个值的运算得到。这些标志的有效只是定义于winnt.h内的IMAGE_FILE_XXX值。普通EXE文件的这个字段的值一般是010h,DLL文件里这个字段一般是2102h
IMAGE_OPTIONAL_HEADER结构-可选映像头
IMAGE_FILE_HEADER与IMAGE_OPTIONAL_HEADER看起来组成了完整的“PE文件头结构”。IMAGE_OPTINAL_HEADER结构也分为32与32+,不过他们大差不差,如下表示
typedef struct _IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
+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
//
// NT additional fields. 以下是属于NT结构增加的领域。
//
+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[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
// 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b // 32位PE可选头
#defineIMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b // 64位PE可选头
#defineIMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
Magic:标识可选头位数。
AddressOfEntryPoint:持有EP的RVA(地址),指出程序最先执行的代码起始地址。
ImageBase:优先装填区域,有点像段地址的感觉。
SectionAlignment、FileAlignment:File——节区在文件中的最小单位,Section——节区在内存中的最小单位。
SizeOfimage:指定了PE Image在虚拟内存中所占空间大小。
SizeOfHeaders:指出整个PE头的大小。
Subststem:区分文件类型(如:sys,exe,dll)。
NumberOfRvaAndSizes:指定DataDirectory数组的个数。
DataDirectory:由IMAGE_DATA_DIRECTORY结构体组成的数组,指向输出表,输入表等等数据
typedefstruct_IMAGE_DATA_DIRECTORY{
DWORDVirtualAddress;//数据起始的RVA
DWORDSize;//数据块的长度
}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;VirtualAddress
#defineIMAGE_DIRECTORY_ENTRY_EXPORT 0 //0 导出表
#defineIMAGE_DIRECTORY_ENTRY_IMPORT 1 //1 导入表
#defineIMAGE_DIRECTORY_ENTRY_RESOURCE 2 //2 资源目录
#defineIMAGE_DIRECTORY_ENTRY_EXCEPTION 3 //3 异常目录
#defineIMAGE_DIRECTORY_ENTRY_SECURITY 4 //4 安全目录
#defineIMAGE_DIRECTORY_ENTRY_BASERELOC 5 /5 重定位基本表
#defineIMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#defineIMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#defineIMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#defineIMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#defineIMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#defineIMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#defineIMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#defineIMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#defineIMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
区块表
在pe文件头后面便是区块表,是一个IMAGE_SECTION_HEADER结构数组。每个结构都包含了他所关联的区块信息,该数组个数由IMAGE_SECTION_HEADER.FileHeader.NumberOfSections提出([想起来了,都想起来了].jpg
typedef struct _IMAGE_SECTION_HEADER
{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text”
//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;
(1)Name:区块名。这是一个由8个ASCII码组成,用来定义区块的名称的数组。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.”实际上是不是必须的。值得我们注意的是,如果区块名达到8 个字节,后面就没有0字符了。前边带有一个$ 的区块名字会从连接器那里得到特殊的待遇,前边带有$ 的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$”后边的字符的字母顺序进行合并的。每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data”或者说将包含数据的区块命名为“.Code”都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。
(2) VirtualSize:对表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。如果VirtualSize > SizeOfRawData,那么SizeOfRawData是可执行文件初始化数据的大小(SizeOfRawData – VirtualSize)的字节用0来填充。这个字段在OBJ文件中被设为0。
(3)VirtualAddress:该块时装载到内存中的RVA,注意这个地址是按内存页对齐的,她总是SectionAlignment的整数倍,在工具中第一个块默认RVA为1000,在OBJ中为0。
(4)SizeofRawData:该块在磁盘中所占的大小,在可执行文件中,该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节。
(5) PointerToRawData:该块是在磁盘文件中的偏移,程序编译或汇编后生成原始数据,这个字段用于给出原始数据块在文件的偏移,如果程序自装载PE或COFF文件(而不是由OS装载),这种情况,必须完全使用线性映像方法装入文件,需要在该块处找到块的数据。
上述四个数据很重要,在分析的时候经常用到,因此需要熟悉其代表的具体意义。
PointerToRawDate+SizeofRawData=下一块偏移地址
(6) PointerToRelocations 在PE中无意义
(7) PointerToLinenumbers 行号表在文件中的偏移值,文件调试的信息
(8) NumberOfRelocations 在PE中无意义
(9) NumberOfLinenumbers 该块在行号表中的行号数目
(10) Characteristics 块属性,(如代码/数据/可读/可写)的标志,这个值可通过链接器的/SECTION选项设置.下面是比较重要的标志:
常见区块与区块合并
区块在映像中是按照RVA划分的,常见区块如下
链接器的一个有趣特征就是能够合并区块。如果两个区块有相似、一致性的属性,那么它们在链接的时候能被合并成一个单一的区块。这取决于是否开启编译器的 /merge 开关。事实上合并区块有一个好处就是可以节省磁盘的内存空间……注意:我们不应该将.rsrc、.reloc、.pdata 合并到··的区块里。
区块的对齐值
区块的大小是需要对齐的,有两种对齐值,一种用于磁盘文件,一种用于内存中,PE文件头中FileAlignment与SectionAlignment分别声明了磁盘区块与内存区块的对齐值
如果区块没这么多,就用0填充
文件偏移与虚拟地址的转换
RVA 和文件偏移的转换
RVA 是相对虚拟地址(RelativeVirtual Address)的缩写,顾名思义,它是一个“相对地址”是相对于基地址的相对地址
举个例子,如果 Windows 装载器将一个PE 文件装入到 00400000h 处的内存中,而某个区块中的某个数据被装入 0040··xh 处,那么这个数据的 RVA 就是(0040··xh - 00400000h )= ··xh,反过来说,将 RVA 的值加上文件被装载的基地址,就可以找到数据在内存中的实际地址。
换算 RVA 和文件偏移
因为对齐值的不同,所以文件映射在内存中相对偏移地址会不同,也就出现了文件偏移地址-相对虚拟地址的转换
从图中可以看到 File Offset=RVA+k
File Offset=VA-k-ImageBase
后面不同块的转换计算又要涉及到,区块间隙的不同,可以按照表自己去算,也可以用工具
本篇文章均为新手小白学习过程中,突然有的个人感悟,如果有错误欢迎大师傅们指出
PE的基本结构
这里是我自己突然启发的思路而已
计算机构造?
在买电脑的时候我们经常能看到,什么i5,i7处理器,什么16+512,32+1T,这都是什么呢?
在我浅显的理解里,电脑可以大致分为:CPU,运行内存,内存和显示屏
内存
首先说的是内存,很多人都知道,C、D盘是内存,那么为什么C、D盘叫磁盘,而且内存和运行内存又有什么区别呢?
看到PE文件,我突然想到了这个C,D盘为什么叫磁盘。其实外面买的那种u盘,和你电脑里看到的C,D磁盘没什么区别。简单来说就是说磁盘和电脑之间并不是紧密相连的,而只是相当于有一个u盘在你的电脑里,只不过它会自始至终的插在USB口中,而电脑真正运行的东西是CPU与运行内存,当你打开一个文件的时候,其实相当与是从U盘(磁盘),映射(拷贝)一份给运行内存,再将每一步要用到的东西传给CPU处理
那么既然知道了文件并不是在磁盘中打开运行的,是在运行内存中打开的,那么“打开”是什么呢?
打开便是映射,一个文件,他有一部分并不是可用的,我们知道,机器它是死的,编写程序的人知道,先走这个部分,再走那个部分,但机器怎么知道呢
于是便出现了PE文件格式,在文件中会有一部分信息来告诉机器,他应该怎么做,所以一个文件里并不是所有的内容都是需要用到的,所以在映射的时候,只会映射相对有用的部分(代码块、数据块等)
运行内存
说到这里大家就明白了,运行内存的大小决定了什么呢,就是决定了你一次性可以打开多少个文件,运行内存足够大,内存中可以映射到运行内存的文件就足够多(打开的文件就足够多)
CPU
那么处理器(CPU)的作用,便是将运行内存中的文件按批次处理,相当于计算机的大脑,这里要注意,就像内存与运行内存一样,CPU与运行内存是分开的,是需要运行内存先通过线路传输数据到CPU中,再从CPU传输数据到运行内存中去的,至于一次可以传输多少处理多少,这就和多线程,或者CPU几代有关了,所以CPU的代数与线程数,可以相对反应的是这个电脑,它的“大脑“转的有多快,以及一次可以处理多少事情
显示屏
这个就没啥了,我就不过多说了,就是高刷和清晰度啥的区别了,唯一注意的就是显卡,也就是显示屏中的CPU,独立显卡,就是给你的显示屏也装个大脑,处理图像的时候更灵活,更不卡顿
基地址ImageBase
当PE文件传入到内存中,内存中的版本叫模块,映射文件的起始地址叫模块句柄,这个初始内存地址叫及地址
这里可以进阶的去理解GetModuleHandle函数了,调用该函数时,传参传入一个文件或者一个名字,函数会返回对应模块的句柄(也就是那个文件被映射的基地址),如果传入的时NULL参数的话,则返回调用的函数的可执行文件
虚拟地址VA
每个PE文件被映射到内存中,都有自己的虚拟空间,这个空间里的地址是虚拟地址
相对虚拟地址RVA
虚拟地址(VA)=基地址+相对虚拟地址(RVA)
文件偏移地址
PE文件在磁盘中的时候,数据相对于文件头地址的地址是文件偏移地址
MS-DOS头部 00 00-00 B0
每个PE文件都是以一个DOS程序开始的,有了它,DOS就可以识别它为有效的执行体,然后运行紧随的DOSstub。DOSstub也相当于一个.exe文件了,在不支持PE文件格式的操作系统中,它会简单的提示一个不可执行的错误提示,DOS一般由编译软件编译成
PE文件的第一个字节位于一个传统的MS-DOS头部,称为IMAGE_DOS_HEADER,其结构体中比较重要的字段分别是
e_magic和e_lfanew。e_magic字段的值一般需要被设置为5A4Dh,意味着这是一个可执行的文件。这个值有一个#define,名为IMAGE_DOS_SIGNTURE(0x5A4D),在ascii表示法里它的值为“MZ”,他在PE文件中起到定义宏的作用,而有这个宏定义,操作系统才会把这个文件认定为可执行的,而e_lfanew字段是真正的PE文件头的相对偏移,指出真正的PE头的文件偏移位置,占用4字节,位于从文件开始偏移3Ch字节处
第一个划红线的地方是e_magic,值为MZ,第二个是e_flanew,值经过大小端序之后是00 00 00 B0,所以B0处才是真正的文件偏移位置,并且还可以看到右边有一串字符串,!This program cannot be run in DOS mode.
这个就是前面提到的不可执行错误
PE文件头(IMAGE_NT_HEADER) 00 B0-01 20
紧跟着DOSstub的是PE文件头,其中包含很多PE转载器会用到的重要字段,当执行体在支持PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER结构的e_lfanew字段里找到PE_Header的起始偏移量,用其加上基址,得到PE文件头的指针
PNTHeader=ImageBase+dosHeader->e_lfanew
Signature字段
在一个有效的PE文件里,Signature字段被设置为0x 00 00 45 50,ACSII码字符是”PE\0\0“,定义宏为#define IMAGE_NT_SIGNATURE
"PE\0\0"是一个PE文件的开始,而e_flanew字段正是指向"PE\0\0"
IMAGE_FILE_HEADER结构
IMAGE_FILE_HEADER(映射文件头)结构包含PE文件的一些基本信息,最重要的是,其中一个域指出了IMAGE_OPTIONAL_HEADER的大小,这里按照红色框框划分,依次介绍
Machine:可执行文件的目标CPU类型。PE文件可以在多种机器上使用,不同平台上指令的机器码不同
NumberOfSestions:区块数目,块表紧跟在IMAGE_NT_HEADER后面
TimeDateStamp:表示文件的创建时间。这个值是自1970年1月1日以来用格林威治时间计算的秒数,是一个比文件系统的日期/时间更精确的文件创建时间指示器,将这个值翻译成易读的字符串需要_ctime函数。另一个对此字段有用的函数是gmtime
PointerToSymbolTable:COFF符号表的偏移地址位置,如果没有就是0
NumberOfSymbols:如果有COFF符号表,这个代表符号表的数目
SizeOfOptionalHeader:紧跟IMAGE_FILE_HEADER,表示数据的大小。在PE文件中,这个数据结构叫做IMAGE_OPTIONAL_HEADER,其大小依赖于当前文件是32位还是64位文件。对32位文件这个域通常是00E0h;对64位PE32+文件,这个域是00F0h。不管怎样,这些事要求的最小值,较大的值也可能出现
Characteristics:文件属性,有选择地通过几个值的运算得到。这些标志的有效只是定义于winnt.h内的IMAGE_FILE_XXX值。普通EXE文件的这个字段的值一般是010h,DLL文件里这个字段一般是2102h
IMAGE_OPTIONAL_HEADER结构-可选映像头
IMAGE_FILE_HEADER与IMAGE_OPTIONAL_HEADER看起来组成了完整的“PE文件头结构”。IMAGE_OPTINAL_HEADER结构也分为32与32+,不过他们大差不差,如下表示
typedef struct _IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
+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
//
// NT additional fields. 以下是属于NT结构增加的领域。
//
+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[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
// 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b // 32位PE可选头
#defineIMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b // 64位PE可选头
#defineIMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
Magic:标识可选头位数。
AddressOfEntryPoint:持有EP的RVA(地址),指出程序最先执行的代码起始地址。
ImageBase:优先装填区域,有点像段地址的感觉。
SectionAlignment、FileAlignment:File——节区在文件中的最小单位,Section——节区在内存中的最小单位。
SizeOfimage:指定了PE Image在虚拟内存中所占空间大小。
SizeOfHeaders:指出整个PE头的大小。
Subststem:区分文件类型(如:sys,exe,dll)。
NumberOfRvaAndSizes:指定DataDirectory数组的个数。
DataDirectory:由IMAGE_DATA_DIRECTORY结构体组成的数组,指向输出表,输入表等等数据
typedefstruct_IMAGE_DATA_DIRECTORY{
DWORDVirtualAddress;//数据起始的RVA
DWORDSize;//数据块的长度
}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;VirtualAddress
#defineIMAGE_DIRECTORY_ENTRY_EXPORT 0 //0 导出表
#defineIMAGE_DIRECTORY_ENTRY_IMPORT 1 //1 导入表
#defineIMAGE_DIRECTORY_ENTRY_RESOURCE 2 //2 资源目录
#defineIMAGE_DIRECTORY_ENTRY_EXCEPTION 3 //3 异常目录
#defineIMAGE_DIRECTORY_ENTRY_SECURITY 4 //4 安全目录
#defineIMAGE_DIRECTORY_ENTRY_BASERELOC 5 /5 重定位基本表
#defineIMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#defineIMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#defineIMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#defineIMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#defineIMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#defineIMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#defineIMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#defineIMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#defineIMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
区块表
在pe文件头后面便是区块表,是一个IMAGE_SECTION_HEADER结构数组。每个结构都包含了他所关联的区块信息,该数组个数由IMAGE_SECTION_HEADER.FileHeader.NumberOfSections提出([想起来了,都想起来了].jpg
typedef struct _IMAGE_SECTION_HEADER
{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text”
//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;
(1)Name:区块名。这是一个由8个ASCII码组成,用来定义区块的名称的数组。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.”实际上是不是必须的。值得我们注意的是,如果区块名达到8 个字节,后面就没有0字符了。前边带有一个$ 的区块名字会从连接器那里得到特殊的待遇,前边带有$ 的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$”后边的字符的字母顺序进行合并的。每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data”或者说将包含数据的区块命名为“.Code”都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。
(2) VirtualSize:对表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。如果VirtualSize > SizeOfRawData,那么SizeOfRawData是可执行文件初始化数据的大小(SizeOfRawData – VirtualSize)的字节用0来填充。这个字段在OBJ文件中被设为0。
(3)VirtualAddress:该块时装载到内存中的RVA,注意这个地址是按内存页对齐的,她总是SectionAlignment的整数倍,在工具中第一个块默认RVA为1000,在OBJ中为0。
(4)SizeofRawData:该块在磁盘中所占的大小,在可执行文件中,该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节。
(5) PointerToRawData:该块是在磁盘文件中的偏移,程序编译或汇编后生成原始数据,这个字段用于给出原始数据块在文件的偏移,如果程序自装载PE或COFF文件(而不是由OS装载),这种情况,必须完全使用线性映像方法装入文件,需要在该块处找到块的数据。
上述四个数据很重要,在分析的时候经常用到,因此需要熟悉其代表的具体意义。
PointerToRawDate+SizeofRawData=下一块偏移地址
(6) PointerToRelocations 在PE中无意义
(7) PointerToLinenumbers 行号表在文件中的偏移值,文件调试的信息
(8) NumberOfRelocations 在PE中无意义
(9) NumberOfLinenumbers 该块在行号表中的行号数目
(10) Characteristics 块属性,(如代码/数据/可读/可写)的标志,这个值可通过链接器的/SECTION选项设置.下面是比较重要的标志:
常见区块与区块合并
区块在映像中是按照RVA划分的,常见区块如下
链接器的一个有趣特征就是能够合并区块。如果两个区块有相似、一致性的属性,那么它们在链接的时候能被合并成一个单一的区块。这取决于是否开启编译器的 /merge 开关。事实上合并区块有一个好处就是可以节省磁盘的内存空间……注意:我们不应该将.rsrc、.reloc、.pdata 合并到··的区块里。
区块的对齐值
区块的大小是需要对齐的,有两种对齐值,一种用于磁盘文件,一种用于内存中,PE文件头中FileAlignment与SectionAlignment分别声明了磁盘区块与内存区块的对齐值
如果区块没这么多,就用0填充
文件偏移与虚拟地址的转换
RVA 和文件偏移的转换
RVA 是相对虚拟地址(RelativeVirtual Address)的缩写,顾名思义,它是一个“相对地址”是相对于基地址的相对地址
举个例子,如果 Windows 装载器将一个PE 文件装入到 00400000h 处的内存中,而某个区块中的某个数据被装入 0040··xh 处,那么这个数据的 RVA 就是(0040··xh - 00400000h )= ··xh,反过来说,将 RVA 的值加上文件被装载的基地址,就可以找到数据在内存中的实际地址。
换算 RVA 和文件偏移
因为对齐值的不同,所以文件映射在内存中相对偏移地址会不同,也就出现了文件偏移地址-相对虚拟地址的转换
从图中可以看到 File Offset=RVA+k
File Offset=VA-k-ImageBase
后面不同块的转换计算又要涉及到,区块间隙的不同,可以按照表自己去算,也可以用工具