PE文件结构详解
1、PE文件的结构
1、什么是可执行文件?
可执行文件(executablefile)指的是可以由操作系统进行加载执行的文件。
可执行文件的格式:
- Windows平台:PE(PortableExecutable)文件结构
- Linux平台:ELF(ExecutableandLinkingFormat)文件结构
PE和ELF非常相似,它们都是源于同一种可执行文件格式COFF
- COFF是由UnixSystemVRelease3首先提出并且使用的格式规范,
- 微软基于COFF格式,制定了PE格式标准,并将其用于当时的WindowsNT系统
- SystemVRelease4在COFF的基础上引入了ELF格式。
事实上,在Windows平台,VISUALC++编译器产生的目标文件仍然使用COFF格式,而可执行文件为PE格式
微软对64位Windows平台上的PE文件结构叫做PE32+,就是把那些原来32位的字段变成了64位。
2、PE文件的特征
识别一个文件是不是PE文件不应该只看文件后缀名,还应该通过PE指纹
使用UE打开一个exe文件,发现文件的头两个字节都是MZ,0x3C位置保存着一个地址,查该地址处发现保存着“PE”,这样基本可以认定改文件是一个PE文件
通过这些重要的信息(“MZ”和“PE”)验证文件是否为PE文件,这些信息即PE指纹。
3、PE文件的整体结构
这里将一个PE文件的主要部分列为4部分,这里可以先有模糊概念,后面会详细解释
“节”或“块”或”区块“都是一个意思,后文会穿插使用
下面从二进制层面整体把握其结构,看看一个PE文件的组成
4、PE文件到内存的映射
PE文件存储在磁盘时的结构和加载到内存后的结构有所不同。
当PE文件通过Windows加载器载入内存后,内存中的版本称为模块(Module)。
映射文件的起始地址称为模块句柄(hModule),也称为基地址(ImageBase)。
(模块句柄是不是和其他句柄不太一样呢?)
文件数据一般512字节(1扇区)对齐(现也多4k),32位内存一般4k(1页)对齐,512D=200H,4096D=1000H
文件中块的大小为200H的整数倍,内存中块的大小为1000H的整数倍,映射后实际数据的大小不变,多余部分可用0填充
PE文件头部(DOS头+PE头)到块表之间没有间隙,然而他们却和块之间有间隙,大小取决于对齐参数
VC编译器默认编译时,exe文件基地址是0x400000,DLL文件基地址是0x10000000
VA:虚拟内存地址
RVA:相对虚拟地址即相对于基地址的偏移地址
FOA:文件偏移地址
5、DOS部分
DOSMZ文件头实际是一个结构体(IMAGE_DOS_HEADER),占64字节
typedefstruct_IMAGE_DOS_HEADER{//DOS.EXEheader WORDe_magic;//Magicnumber WORDe_cblp;//Bytesonlastpageoffile WORDe_cp;//Pagesinfile WORDe_crlc;//Relocations WORDe_cparhdr;//Sizeofheaderinparagraphs WORDe_minalloc;//Minimumextraparagraphsneeded WORDe_maxalloc;//Maximumextraparagraphsneeded WORDe_ss;//Initial(relative)SSvalue WORDe_sp;//InitialSPvalue WORDe_csum;//Checksum WORDe_ip;//InitialIPvalue WORDe_cs;//Initial(relative)CSvalue WORDe_lfarlc;//Fileaddressofrelocationtable WORDe_ovno;//Overlaynumber WORDe_res[4];//Reservedwords WORDe_oemid;//OEMidentifier(fore_oeminfo) WORDe_oeminfo;//OEMinformation;e_oemidspecific WORDe_res2[10];//Reservedwords LONGe_lfanew;//Fileaddressofnewexeheader }IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;
DOS头用于16位系统中,在32位系统中DOS头成为冗余数据,但还存在两个重要成员e_magic字段(偏移0x0)和 e_lfanew字段(偏移0x3C)
e_magic保存“MZ”字符,e_lfanew保存PE文件头地址,通过这个地址找到PE文件头,得到PE文件标识“PE”。
e_magic和e_lfanew是验证PE指纹的重要字段,其他字段现基本不使用(可填充任意数据)
“DOSStub”区域的数据由链接器填充(可自己填充如意数据),是一段可以在DOS下运行的一小段代码,
这段代码的唯一作用是向终端输出一行字:“ThisprogramcannotberuninDOS”(“e_cs”和“e_ip”指向)
然后退出程序,表示该程序不能在DOS下运行。
6、PE文件头(PEHeader)
PE文件头是一个结构体(IMAGE_NT_HEADERS32),里面还包含两个其它结构体,占用4B+20B+224B
typedefstruct_IMAGE_NT_HEADERS{ DWORDSignature;//PE文件标识4Bytes IMAGE_FILE_HEADERFileHeader;//40Bytes IMAGE_OPTIONAL_HEADER32OptionalHeader;//224BytesPE32可执行文件,不讨论PE32+的情况 }IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;
Signature字段设置为0x00004550,ANCII码字符是“PE00”,标识PE文件头的开始,PE标识不能破坏。
1、IMAGE_FILE_HEADER结构体
IMAGE_FILE_HEADER(映像文件头或标准PE头)结构包含PE文件的一些基本信息,该结构在微软的官方文档中被称为标准通用对象文件格式(CommonObjectFileFormat,COFF)头
typedefstruct_IMAGE_FILE_HEADER{ WORDMachine;//可运行在什么样的CPU上。0代表任意,Intel386及后续:0x014C,x64:0x8664 WORDNumberOfSections;//文件的区块(节)数 DWORDTimeDateStamp;//文件的创建时间。1970年1月1日以GMT计算的秒数,编译器填充的,不重要的值 DWORDPointerToSymbolTable;//指向符号表(用于调试) DWORDNumberOfSymbols;//符号表中符号的个数(用于调试) WORDSizeOfOptionalHeader;//IMAGE_OPTIONAL_HEADER32结构的大小,可改变,32位为E0,64位为F0 WORDCharacteristics;//文件属性 }IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;
重要字段:NumberOfSections,SizeOfOptionalHeader
对应结构为下图紫线部分
0x014C说明运行于x86CPU;0x0007说明当前exe有7个节;
0x00E0说明IMAGE_OPTIONAL_HEADER32为224字节;
0x030F(0000001100001111)代表文件属性,由下列对应位为1的组合
2、IMAGE_OPTIONAL_HEADER结构体
IMAGE_OPTIONAL_HEADER(可选映像头或扩展PE头)是一个可选的结构,是IMAGE_FILE_HEADER结构的扩展
大小由IMAGE_FILE_HEADER结构的SizeOfOptionalHeader字段记录(可能不准确)
typedefstruct_IMAGE_OPTIONAL_HEADER{ // //Standardfields. // WORDMagic;//说明文件的类型PE32:10BHPE32+:20BHRom映像文件:107H BYTEMajorLinkerVersion;//链接器主版本号 BYTEMinorLinkerVersion;//链接器次版本号 DWORDSizeOfCode;//所有代码节的总和(基于文件对齐)编译器填的没用 DWORDSizeOfInitializedData;//包含所有已经初始化数据的节的总大小编译器填的没用 DWORDSizeOfUninitializedData;//包含未初始化数据的节的总大小编译器填的没用 DWORDAddressOfEntryPoint;//程序入口RVA在大多数可执行文件中,这个地址不直接指向Main、WinMain或DIMain函数,而指向运行时的库代码并由它来调用上述函数 DWORDBaseOfCode;//代码起始RVA,编译器填的没用 DWORDBaseOfData;//数据段起始RVA,编译器填的没用 // //NTadditionalfields. // DWORDImageBase;//内存镜像基址,可链接时自己设置 DWORDSectionAlignment;//内存对齐一般一页大小4k DWORDFileAlignment;//文件对齐一般一扇区大小512字节,现在也多4k WORDMajorOperatingSystemVersion;//标识操作系统版本号主版本号 WORDMinorOperatingSystemVersion;//标识操作系统版本号次版本号 WORDMajorImageVersion;//PE文件自身的主版本号 WORDMinorImageVersion;//PE文件自身的次版本号 WORDMajorSubsystemVersion;//运行所需子系统主版本号 WORDMinorSubsystemVersion;//运行所需子系统次版本号 DWORDWin32VersionValue;//子系统版本的值,必须为0 DWORDSizeOfImage;//内存中整个PE文件的映射的尺寸,可比实际的值大,必须是SectionAlignment的整数倍 DWORDSizeOfHeaders;//所有头+节表按照文件对齐后的大小,否则加载会出错 DWORDCheckSum;//校验和,一些系统文件有要求.用来判断文件是否被修改 WORDSubsystem;//子系统 驱动程序(1)图形界面(2)控制台、DLL(3) WORDDllCharacteristics;//文件特性不是针对DLL文件的 DWORDSizeOfStackReserve;//初始化时保留的栈大小 DWORDSizeOfStackCommit;//初始化时实际提交的大小 DWORDSizeOfHeapReserve;//初始化时保留的堆大小 DWORDSizeOfHeapCommit;//初始化时保留的堆大小 DWORDLoaderFlags; DWORDNumberOfRvaAndSizes;//数据目录项数目 IMAGE_DATA_DIRECTORYDataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//数据目录表 }IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32;
重要字段:
AddressOfEntryPoint:程序入口地址(RVA),下图为32C40H
ImageBase:内存镜像基地址,下图为400000H
FileAlignment:文件对齐,下图为200H
SectionAlignment:内存对齐,下图为1000H
DataDirectory[16]:数据目录表,由数个相同的IMAGE_DATA_DIRECTORY结构组成,
指向输出表、输入表、资源块,重定位表等(后面详解这里先跳过)
typedefstruct_IMAGE_DATA_DIRECTORY{ DWORDVirtualAddress;//对应表的起始RVA DWORDSize;//对应表长度 }IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
ImageBase + AddressOfEntryPoint=程序实际运行入口地址(实际加载地址等于ImageBase)
0x400000+ 0x32C40=0x432C40(使用OD运行程序发现就是从这个地址开始运行)
应用:在PE文件空白区添加代码,让程序执行先执行添加的代码再跳转程序入口
思路:
①在PE的空白区构造一段代码(call ->E8)
②修改入口地址为新增代码(IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint)
③新增代码执行后,跳回入口地址(jmp ->E9)
7、块表
块表是一个IMAGE_SECTION_HEADER的结构数组,每个IMAGE_SECTION_HEADER结构40字节。
每个IMAGE_SECTION_HEADER结构包含了它所关联的区块的信息,例如位置、长度、属性。
#defineIMAGE_SIZEOF_SHORT_NAME8 typedefstruct_IMAGE_SECTION_HEADER{ BYTEName[IMAGE_SIZEOF_SHORT_NAME];//块名。多数块名以一个“.”开始(例如.text),这个“.”不是必需的 union{ DWORDPhysicalAddress;//常用第二个字段 DWORDVirtualSize;//加载到内存实际区块的大小(对齐前),为什么会变呢?可能是有时未初始化的全局变量不放bss段而是通过扩展这里 }Misc; DWORDVirtualAddress;//该块装载到内存中的RVA(内存对齐后,数值总是SectionAlignment的整数倍) DWORDSizeOfRawData;//该块在文件中所占的空间(文件对齐后),VirtualSize的值可能会比SizeOfRawData大例如bss节(SizeOfRawData为0),data节(关键看未初始化的变量放哪) DWORDPointerToRawData;//该块在文件中的偏移(FOA) /*调试相关,忽略*/ DWORDPointerToRelocations;//在“.obj”文件中使用,指向重定位表的指针 DWORDPointerToLinenumbers; WORDNumberOfRelocations;//重定位表的个数(在OBJ文件中使用)。 WORDNumberOfLinenumbers; DWORDCharacteristics;//块的属性该字段是一组指出块属性(例如代码/数据、可读/可写等)的标志 }IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;
重要字段:Name[8],VirtualSize,VirtualAddress,SizeOfRawData,PointerToRawData,Characteristics
IMAGE_FILE_HEADER的NumberOfSections字段是不是记录着当前文件的节数呢?
31C80H代表载入内存代码块对齐前大小;1000H代表代码块装载到内存RVA1000H;
31E00H代表文件对齐后代码块大小;400H代表代码块在文件中的偏移
60000020H代表代码块属性(01100000000000000000000000100000)查下表得到属性为可读可执行的代码
更多属性参考:https://docs.microsoft.com/zh-cn/windows/win32/api/winnt/ns-winnt-image_section_header
8、RVA与FOA的转换
RVA:相对虚拟地址,FOA:文件偏移地址。
计算步骤:
① 计算RVA =虚拟内存地址-ImageBase
②若RVA是否位于PE头:FOA==RVA
③判断RVA位于哪个节:
RVA>=节.VirtualAddress(节在内存对齐后RVA )
RVA<=节.VirtualAddress+当前节内存对齐后的大小
偏移量 =RVA-节.VirtualAddress;
④FOA=节.PointerToRawData+偏移量;
应用举例:
有初始值的全局变量初始值会存储在PE文件中,想要修改文件中全局变量的数据值即
需要找到文件中存储全局变量值的地方,然后修改即可
2、输出表和输入表
可选PE头(扩展PE头)的最后一个字段DataDirectory[16]代表数据目录表,由16个相同的IMAGE_DATA_DIRECTORY结构组成,成员分别指向输出表、输入表、资源块等
typedefstruct_IMAGE_DATA_DIRECTORY{ DWORDVirtualAddress;//对应表的起始RVA DWORDSize;//对应表大小(包含子表) }IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
1、输出表(导出表)
创建一个DLL时,实际上创建了一组能让EXE或其他DLL调用的函数
DLL文件通过输出表(ExportTable)向系统提供输出函数名、序号和入口地址等信息。
数据目录表的第1个成员指向输出表。
找到文件中的输出表(以DllDemo.dll为例,看图就行)
成功找到输出表在文件偏移0C00H处,如下:
特别说明:①如果文件对齐与内存对齐都是4k则不需要地址转换②输出表大小是指输出表大小与其子表大小和
输出表实际是一个40字节的结构体(IMAGE_EXPORT_DIRECTORY),输出表的结构如下
typedefstruct_IMAGE_EXPORT_DIRECTORY{ DWORDCharacteristics;//未定义,总是为0。 DWORDTimeDateStamp;//输出表创建的时间(GMT时间) WORDMajorVersion;//输出表的主版本号。未使用,设置为0。 WORDMinorVersion;//输出表的次版本号。未使用,设置为0。 DWORDName;//指向一个ASCII字符串的RVA。这个字符串是与这些输出函数相关联的DLL的名字(例如"KERNEL32.DLL") DWORDBase;//导出函数起始序号(基数)。当通过序数来查询一个输出函数时,这个值从序数里被减去,其结果将作为进入输出地址表(EAT)的索引 DWORDNumberOfFunctions;//输出函数地址表(ExportAddressTable,EAT)中的条目数量(最大序号-最小序号) DWORDNumberOfNames;//输出函数名称表(ExportNamesTable,ENT)里的条目数量 DWORDAddressOfFunctions;//EAT的RVA(输出函数地址表RVA) DWORDAddressOfNames;//ENT的RVA(输出函数名称表RVA),每一个表成员指向ANCII字符串表成员的排列顺序取决于字符串的排序 DWORDAddressOfNameOrdinals;//输出函数序号表RVA,每个表成员2字节 }IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;
重要字段:Name,Base,NumberOfNames,AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals
过程分析:
//功能:加载动态链接库到内存
HMODULEWINAPILoadLibrary(
LPCTSTRlpFileName//模块的文件名
);
/*功能:检索指定的动态链接库(DLL)中的输出库函数地址*/
FARPROCGetProcAddress(
HMODULEhModule,//DLL模块句柄(模块基地址)
LPCSTRlpProcName//函数名或者指定函数的序数值
);
PE装载器调用GetProcAddress来查找DlIDemo.DLL里的API函数MsgBox,
系统通过定位DlIDemo.DLL的输出表(IMAGE_EXPORT_DIRECTORY)结构获得输出函数名称表(ENT)的起始地址,
对名字进行二进制查找,直到发现字符串“MsgBox”为止,PE装载器发现MsgBox是数组的第1个条目后,加载器从输出序数表
中读取相应的第1个值,这个值是MsgBox的在函数地址表(EAT)的索引。使用索引在EAT取值得到MsgBox的RVA1008h。
用1008h加DllDemo.DLL的载入地址,得到MsgBox的实际地址。
特别说明:如果lpProcName是序号,则需要通过字段Base确定起始序号,序号-Base的差值作为索引得到函数RVA地址(注意这里的序号和索引)
注意:输出序号表存放的是索引值而不是序号,真正的序号是Base+索引值
例如:写一个简单加法函数(intadd(inta,intb)),创建一个A.dll
//def文件
EXPORTS
add@12
分析A.dll的导出表
当时用序号(12)获得函数地址时会拿12-Base=0作为输出函数地址表的索引值
使用A.dll
#include#include usingnamespacestd; typedefint(*lpAdd)(int,int); lpAddmyAdd; intmain() { //动态加载dll到内存中 HINSTANCEhModule=LoadLibrary("A.dll"); cout<<"ImageBase:"<
2、输入表(导入表)
PE文件映射到内存后,Windows将相应的DLL文件装入,EXE文件通过“输入表”找到相应的DLL中的导入函数,从而完成程序的正常运行
数据目录表的第2个成员指向输入表。当前文件依赖几个模块就会有几张输入表且是连续排放的。
如何找到输入表?
上图看出当前文件只依赖一个模块,只有一张导入表,如果有多个会连续存放直到连续出现20个0说明结束。
输入表实际是个20字节的结构体IMAGE_IMPORT_DESCRIPTOR
typedefstruct_IMAGE_IMPORT_DESCRIPTOR{ union{ DWORDCharacteristics;//0forterminatingnullimportdescriptor DWORDOriginalFirstThunk;//RVAtooriginalunboundIAT(PIMAGE_THUNK_DATA) }DUMMYUNIONNAME; DWORDTimeDateStamp;//0ifnotbound, //-1ifbound,andrealdate\timestamp //inIMAGE_DIRECTORY_ENTRY_BOUND_IMPORT(newBIND) //O.W.date/timestampofDLLboundto(OldBIND) DWORDForwarderChain;//-1ifnoforwarders DWORDName; DWORDFirstThunk;//RVAtoIAT(ifboundthisIAThasactualaddresses) }IMAGE_IMPORT_DESCRIPTOR;重要字段:
Name:DLL(依赖模块)名字的指针。是一个以“00”结尾的ASCII字符的RVA地址。
OriginalFirstThunk:包含指向输入名称表(INT)的RVA。
INT是一个IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构都指向
IMAGE_IMPORT_BY_NAME结构,数组以一个内容为0的IMAGE_THUNK_DATA结构结束。
FirstThunk:包含指向输入地址表(IAT)的RVA。IAT是一个IMAGE_THUNK_DATA结构的数组。
IMAGE_THUNK_DATA结构实际只占4字节
typedefstruct_IMAGE_THUNK_DATA32{ union{ DWORDForwarderString;//指向一个转向者字符串的RVA DWORDFunction;//被输入的函数的内存地址 DWORDOrdinal;//被输入的API的序数 DWORDAddressOfData;//指向IMAGE_IMPORTBYNAME }u1; }IMAGE_THUNK_DATA32;如果IMAGE_THUNK_DATA32的最高位为1,则低31位代表函数的导出序号,
否则4个字节是一个RVA,指向IMAGE_IMPORT_BY_NAME结构
IMAGE_IMPORT_BY_NAME结构字面仅有4个字节,存储了一个输入函数的相关信息
typedefstruct_IMAGE_IMPORT_BY_NAME{ WORDHint;//输出函数地址表的索引(不是导出序号),(究竟是啥没试验,因为看的很多资料说是序号),不必须,链接器可能将其置0 CHARName[1];//函数名字字符串,以“\0”作为字符串结束标志,大小不确定 }IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
由上图,我们是不是通过导入表能够很轻松获得当前文件依赖模块的名字和函数名?
这里INT和IAT完全内容一致,为什么呢?稍后解释
INT和IAT内容一致其实是PE文件未加载时的状态,
PE加载器将文件载入内存后会向IAT填入真正的函数地址(GetProcAddress)
例如:
3、重定位表
如果PE文件不在首选的地址(ImageBase)载入,那么文件中的每一个绝对地址都需要被修正。
需要修正的地址有很多,可以在文件中使用重定位表记录这些绝对地址的位置,在载入内存后若载入基地址与ImageBase不同再进行修正,若相同就不需要修正这些地址。
数据目录项的第6个结构,指向重定位表(RelocationTable)
重定位表由一个个的重定位块组成,每个块记录了4KB(一页)的内存中需要重定位的地址。
每个重定位数据块的大小必须以DWORD(4字节)对齐。它们以一个IMAGE_BASE_RELOCATION结构开始,格式如下
typedefstruct_IMAGE_BASE_RELOCATION{ DWORDVirtualAddress;//记录内存页的基址RVA DWORDSizeOfBlock;//当前重定位块结构的大小。这个值减8就是TypeOffset数组的大小 /*下面字段可加与不加*/ /*数组每项大小为2字节。代表页内偏移,16位分为高4位和低12位。高4位代表重定位类型; 低12位是重定位地址(12位就可以寻址4k),与VitualAddress相加就是一个完整RVA */ //WORDTypeOffset[1]; }IMAGE_BASE_RELOCATION; typedefIMAGE_BASE_RELOCATIONUNALIGNED*PIMAGE_BASE_RELOCATION;这些字段可能直接不好理解在后面会看一个实例一切就彻底明白了
虽然有多种重定位类型,但对x86可执行文件来说,所有的基址重定位类型都是IMAGE_REL_BASED_HIGHLOW。
在一组重定位结束的地方会出现一个类型IMAGE_REL_BASED_ABSOLUTE的重定位,这些重定位什么都不做,只用于填充,以便下一个MAGE_BASE_RELOCATION按4字节分界线对齐。
对于IA-64可执行文件,重定位类型似乎总是IMAGE_REL_BASED_DIR64。
有趣的是,尽管IA-64的EXE页大小是8KB,但基址重定位仍是4KB的块
所有重定位块以一个VitualAddress字段为0的MAGE_BASE_RELOCATION结构结束。
// //Basedrelocationtypes. // #defineIMAGE_REL_BASED_ABSOLUTE0//没有具体含义,只是为了让每个段4字节对齐 #defineIMAGE_REL_BASED_HIGH1 #defineIMAGE_REL_BASED_LOW2 #defineIMAGE_REL_BASED_HIGHLOW3//重定位指向的整个地址都需要修正,实际上大部分情况下都是这样的 #defineIMAGE_REL_BASED_HIGHADJ4 #defineIMAGE_REL_BASED_MACHINE_SPECIFIC_55 #defineIMAGE_REL_BASED_RESERVED6 #defineIMAGE_REL_BASED_MACHINE_SPECIFIC_77 #defineIMAGE_REL_BASED_MACHINE_SPECIFIC_88 #defineIMAGE_REL_BASED_MACHINE_SPECIFIC_99 #defineIMAGE_REL_BASED_DIR6410//出现在64位PE文件中,对指向的整个地址进行修正示例分析:
继续以DllDemo.dll为例
先用工具定位重定位表在文件的位置如下
查看重定位表信息如下
->RelocationDirectory 1.RelocationBlock: VirtualAddress:0x00001000("CODE") SizeOfBlock:0x00000010(0x0004blockentries) RVAType --------------------------- 0x0000100FHIGHLOW 0x00001023HIGHLOW n/aABSOLUTE n/aABSOLUTE ————————————————下面实际分析
根据下面判断出当前RVA在CODE节
所以
100Fh(RVA)→60Fh(FOA)
1023h(RVA)→623h(FOA)
60Fh和623h分别指向00402000h和00403030h处,即为所需要重定位的数据
执行PE文件前,加载程序在进行重定位的时候,会用PE文件在内存中的实际映像地址减PE文件所要求的映像地址,根据重定位类型的不同将差值添加到相应的地址数据中。
可以看到重定位表扮演的角色:文件加载到内存后,通过重定位表记录的RVA找到需要重定位的数据
重定位表通过页基址RVA+页内偏移地址方式得到一个完整RVA大大缩小了表大小。
4、资源
Windows程序的各种界面称为资源,包括加速键(Accelerator)、位图(Bitmap)、光标(Cursor)、对话框(DialogBox)、图标(Icon)、菜单(Menu)、串表(StringTable)、工具栏(Toolbar)和版本信息(VersionInformation)等。
定义资源时,既可以使用字符串作为名称来标识一个资源,也可以通过ID号来标识资源
资源分类
-标准资源类型
-非标准资源类型
若资源类型的高位如果为1,说明对应的资源类别是一个非标准的新类型
数据目录项的第3个结构,指向资源表,不直接指向资源数据,而是以磁盘目录形式定位资源数据
资源表是一个四层的二叉排序树结构。
每一个节点都是由资源目录结构和紧随其后的数个资源目录项结构组成的,
两种结构组成了一个资源目录结构单元(目录块)
资源目录结构(IMAGE_RESOURCE_DIRECTORY)占16字节,其定义如下
typedefstruct_IMAGE_RESOURCE_DIRECTORY{ DWORDCharacteristics;//理论上是资源的属性标志,但是通常为0 DWORDTimeDateStamp;//资源建立的时间 WORDMajorVersion;//理论上是放置资源的版本,但是通常为0 WORDMinorVersion; //定义资源时,既可以使用字符串作为名称来标识一个资源,也可以通过ID号来标识资源。资源目录项的数量等于两者之和。 WORDNumberOfNamedEntries;//以字符串命名的资源数量 WORDNumberOfIdEntries;//以整型数字(ID)命名的资源数量 //IMAGE_RESOURCE_DIRECTORY_ENTRYDirectoryEntries[]; }IMAGE_RESOURCE_DIRECTORY,*PIMAGE_RESOURCE_DIRECTORY;资源目录项结构(IMAGE_RESOURCE_DIRECTORY_ENTRY),占8字节,包含2个字段,结构定义如下。
//如果看不懂下面的结构建议复习一下C中的union,struct,位域 typedefstruct_IMAGE_RESOURCE_DIRECTORY_ENTRY{ union{ struct{ DWORDNameOffset:31; DWORDNameIsString:1; }DUMMYSTRUCTNAME; DWORDName; WORDId; }DUMMYUNIONNAME; union{ DWORDOffsetToData; struct{ DWORDOffsetToDirectory:31; DWORDDataIsDirectory:1; }DUMMYSTRUCTNAME2; }DUMMYUNIONNAME2; }IMAGE_RESOURCE_DIRECTORY_ENTRY,*PIMAGE_RESOURCE_DIRECTORY_ENTRY;重要字段:
Name字段:定义目录项的名称或ID。
-当结构用于第1层目录时,定义的是资源类型;
-当结构用于第2层目录时,定义的是资源的名称;
-当结构用于第3层目录时,定义的是代码页编号。
-当最高位为0时,表示字段的值作为ID使用;由该字段的低16位组成整数标识符ID
-当最高位为1时,表示字段的低位作为指针使用,资源名称字符串使用Unicode编码,
这个指针不直接指向字符串,而指向一个IMAGE_RESOURCE_DIR_STRING_U结构。
typedefstruct_IMAGE_RESOURCE_DIR_STRING_U{ WORDLength;//字符串的长度 WCHARNameString[1];//Unicode字符串,按字对齐,长度可变;由Length指明Unicode字符串的长度 }IMAGE_RESOURCE_DIR_STRING_U,*PIMAGE_RESOURCE_DIR_STRING_U;OffsetToData字段:是一个指针。
-当最高位(位31)为1时,低位数据指向下一层目录块的起始地址;
-当最高位为0时,指针指向IMAGE_RESOURCE_DATA_ENTRY结构。
第3层目录结构中的OffsetToData将指向IMAGE_RESOURCE_DATA_ENTRY结构。
该结构描述了资源数据的位置和大小,其定义如下。
typedefstruct_IMAGE_RESOURCE_DATA_ENTRY{ DWORDOffsetToData;//资源数据的RVA DWORDSize;//资源数据的长度 DWORDCodePage;//代码页,一般为0 DWORDReserved;//保留字段 }IMAGE_RESOURCE_DATA_ENTRY,*PIMAGE_RESOURCE_DATA_ENTRY;重要字段:
OffsetToData:指向资源数据的指针(RVA)
Size:资源数据的长度
实例分析:
定位资源在文件中的位置
由于当前exe文件对齐与内存对齐都是4k,RVA不需要转FOA
所以:
图标的真正资源数据RVA为4100h,大小为2E8h。
菜单的真正资源数据RVA为4400h,大小为5Ah。
图标组的真正资源数据RVA为43E8h,大小为14h。
使用工具验证
' 可以清晰看到根目录有3个资源目录项(Icon,Menu,IconGroup)
第二层为资源ID或资源名称
第三层为代码页ID为2052表简体中文,1033表美国英语
右下角图标为真正资源数据
好了这篇文章暂时就先介绍到这了,希望大家以后多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。