PE文件结构详解
作者:洋葱汪
1、PE文件的结构
1、什么是可执行文件?
可执行文件 (executable file) 指的是可以由操作系统进行加载执行的文件。
可执行文件的格式:
- Windows平台:PE(Portable Executable)文件结构
- Linux平台:ELF(Executable and Linking Format)文件结构
PE和ELF非常相似,它们都是源于同一种可执行文件格式 COFF
- COFF 是由Unix System V Release 3首先提出并且使用的格式规范,
- 微软基于COFF格式,制定了PE格式标准,并将其用于当时的Windows NT系统
- System V Release 4在COFF的基础上引入了ELF格式。
事实上,在Windows平台,VISUAL C++编译器产生的目标文件仍然使用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部分
DOS MZ文件头实际是一个结构体(IMAGE_DOS_HEADER),占64字节
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number 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; // File address of new exe header } 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指纹的重要字段,其他字段现基本不使用(可填充任意数据)
“DOS Stub”区域的数据由链接器填充(可自己填充如意数据),是一段可以在DOS下运行的一小段代码,
这段代码的唯一作用是向终端输出一行字:“This program cannot be run in DOS”(“e_cs”和“e_ip”指向)
然后退出程序,表示该程序不能在DOS下运行。
6、PE文件头(PE Header)
PE文件头是一个结构体(IMAGE_NT_HEADERS32),里面还包含两个其它结构体,占用4B + 20B + 224B
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; // PE文件标识 4Bytes IMAGE_FILE_HEADER FileHeader; // 40 Bytes IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 224 Bytes PE32可执行文件,不讨论PE32+的情况 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature字段设置为0x00004550,ANCII码字符是“PE00”,标识PE文件头的开始,PE标识不能破坏。
1、IMAGE_FILE_HEADER结构体
IMAGE_FILE_HEADER(映像文件头或标准PE头)结构包含PE文件的一些基本信息,该结构在微软的官方文档中被称为标准通用对象文件格式(Common Object File Format,COFF)头
typedef struct _IMAGE_FILE_HEADER { WORD Machine; // 可运行在什么样的CPU上。0代表任意,Intel 386及后续:0x014C, x64: 0x8664 WORD NumberOfSections; // 文件的区块(节)数 DWORD TimeDateStamp; // 文件的创建时间。1970年1月1日以GMT计算的秒数,编译器填充的,不重要的值 DWORD PointerToSymbolTable; // 指向符号表(用于调试) DWORD NumberOfSymbols; // 符号表中符号的个数(用于调试) WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32结构的大小,可改变,32位为E0,64位为F0 WORD Characteristics; // 文件属性 } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
重要字段:NumberOfSections,SizeOfOptionalHeader
对应结构为下图紫线部分
0x014C说明运行于x86 CPU;0x0007说明当前exe有7个节;
0x00E0说明IMAGE_OPTIONAL_HEADER32为224字节;
0x030F(0000 0011 0000 1111)代表文件属性 ,由下列对应位为1的组合
2、IMAGE_OPTIONAL_HEADER结构体
IMAGE_OPTIONAL_HEADER(可选映像头或扩展PE头)是一个可选的结构,是IMAGE_FILE_HEADER结构的扩展
大小由IMAGE_FILE_HEADER结构的SizeOfOptionalHeader字段记录(可能不准确)
typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; //说明文件的类型 PE32:10BH PE32+:20BH Rom映像文件:107H BYTE MajorLinkerVersion; //链接器主版本号 BYTE MinorLinkerVersion; //链接器次版本号 DWORD SizeOfCode; //所有代码节的总和(基于文件对齐) 编译器填的 没用 DWORD SizeOfInitializedData; //包含所有已经初始化数据的节的总大小 编译器填的 没用 DWORD SizeOfUninitializedData;//包含未初始化数据的节的总大小 编译器填的 没用 DWORD AddressOfEntryPoint; //程序入口RVA 在大多数可执行文件中,这个地址不直接指向Main、WinMain或DIMain函数,而指向运行时的库代码并由它来调用上述函数 DWORD BaseOfCode; //代码起始RVA,编译器填的 没用 DWORD BaseOfData; //数据段起始RVA,编译器填的 没用 // // NT additional fields. // DWORD ImageBase; //内存镜像基址 ,可链接时自己设置 DWORD SectionAlignment; //内存对齐 一般一页大小4k DWORD FileAlignment; //文件对齐 一般一扇区大小512字节,现在也多4k WORD MajorOperatingSystemVersion; //标识操作系统版本号 主版本号 WORD MinorOperatingSystemVersion; //标识操作系统版本号 次版本号 WORD MajorImageVersion; //PE文件自身的主版本号 WORD MinorImageVersion; //PE文件自身的次版本号 WORD MajorSubsystemVersion; //运行所需子系统主版本号 WORD MinorSubsystemVersion; //运行所需子系统次版本号 DWORD Win32VersionValue; //子系统版本的值,必须为0 DWORD SizeOfImage; //内存中整个PE文件的映射的尺寸,可比实际的值大,必须是SectionAlignment的整数倍 DWORD SizeOfHeaders; //所有头+节表按照文件对齐后的大小,否则加载会出错 DWORD CheckSum; //校验和,一些系统文件有要求.用来判断文件是否被修改 WORD Subsystem; //子系统 驱动程序(1) 图形界面(2) 控制台、DLL(3) WORD DllCharacteristics; //文件特性 不是针对DLL文件的 DWORD SizeOfStackReserve; //初始化时保留的栈大小 DWORD SizeOfStackCommit; //初始化时实际提交的大小 DWORD SizeOfHeapReserve; //初始化时保留的堆大小 DWORD SizeOfHeapCommit; //初始化时保留的堆大小 DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; //数据目录项数目 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录表 } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
重要字段:
AddressOfEntryPoint:程序入口地址(RVA),下图为32C40H
ImageBase:内存镜像基地址,下图为400000H
FileAlignment:文件对齐,下图为200H
SectionAlignment:内存对齐,下图为1000H
DataDirectory[16]:数据目录表,由数个相同的IMAGE_DATA_DIRECTORY结构组成,
指向输出表、输入表、资源块,重定位表等(后面详解这里先跳过)
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //对应表的起始RVA DWORD Size; //对应表长度 } 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结构包含了它所关联的区块的信息,例如位置、长度、属性。
#define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //块名。多数块名以一个“.”开始(例如.text),这个“.”不是必需的 union { DWORD PhysicalAddress; //常用第二个字段 DWORD VirtualSize; //加载到内存实际区块的大小(对齐前),为什么会变呢?可能是有时未初始化的全局变量不放bss段而是通过扩展这里 } Misc; DWORD VirtualAddress; //该块装载到内存中的RVA(内存对齐后,数值总是SectionAlignment的整数倍) DWORD SizeOfRawData; //该块在文件中所占的空间(文件对齐后),VirtualSize的值可能会比SizeOfRawData大 例如bss节(SizeOfRawData为0),data节(关键看未初始化的变量放哪) DWORD PointerToRawData; //该块在文件中的偏移(FOA) /*调试相关,忽略*/ DWORD PointerToRelocations; //在“.obj”文件中使用,指向重定位表的指针 DWORD PointerToLinenumbers; WORD NumberOfRelocations; //重定位表的个数(在OBJ文件中使用)。 WORD NumberOfLinenumbers; DWORD Characteristics; //块的属性 该字段是一组指出块属性(例如代码/数据、可读/可写等)的标志 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
重要字段:Name[8],VirtualSize,VirtualAddress,SizeOfRawData,PointerToRawData,Characteristics
IMAGE_FILE_HEADER的NumberOfSections字段是不是记录着当前文件的节数呢?
31C80H代表载入内存代码块对齐前大小;1000H代表代码块装载到内存RVA1000H;
31E00H代表文件对齐后代码块大小;400H代表代码块在文件中的偏移
60000020H代表代码块属性(0110 0000 0000 0000 0000 0000 0010 0000)查下表得到属性为可读可执行的代码
更多属性参考: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结构组成,成员分别指向输出表、输入表、资源块等
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //对应表的起始RVA DWORD Size; //对应表大小(包含子表) } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
1、输出表(导出表)
创建一个DLL时,实际上创建了一组能让EXE或其他DLL调用的函数
DLL文件通过输出表(Export Table)向系统提供输出函数名、序号和入口地址等信息。
数据目录表的第1个成员指向输出表。
找到文件中的输出表(以DllDemo.dll为例,看图就行)
成功找到输出表在文件偏移0C00H处,如下:
特别说明:① 如果文件对齐与内存对齐都是4k则不需要地址转换 ② 输出表大小是指输出表大小与其子表大小和
输出表实际是一个40字节的结构体(IMAGE_EXPORT_DIRECTORY),输出表的结构如下
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; //未定义,总是为0。 DWORD TimeDateStamp; //输出表创建的时间(GMT时间) WORD MajorVersion; //输出表的主版本号。未使用,设置为0。 WORD MinorVersion; //输出表的次版本号。未使用,设置为0。 DWORD Name; //指向一个ASCII字符串的RVA。这个字符串是与这些输出函数相关联的DLL的名字(例如"KERNEL32.DLL") DWORD Base; //导出函数起始序号(基数)。当通过序数来查询一个输出函数时,这个值从序数里被减去,其结果将作为进入输出地址表(EAT)的索引 DWORD NumberOfFunctions; //输出函数地址表(Export Address Table,EAT)中的条目数量(最大序号 - 最小序号) DWORD NumberOfNames; //输出函数名称表(Export Names Table,ENT)里的条目数量 DWORD AddressOfFunctions; // EAT的RVA(输出函数地址表RVA) DWORD AddressOfNames; // ENT的RVA(输出函数名称表RVA),每一个表成员指向ANCII字符串 表成员的排列顺序取决于字符串的排序 DWORD AddressOfNameOrdinals; // 输出函数序号表RVA,每个表成员2字节 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
重要字段: Name,Base,NumberOfNames,AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals
过程分析:
//功能:加载动态链接库到内存
HMODULE WINAPI LoadLibrary(
LPCTSTR lpFileName //模块的文件名
);
/*功能:检索指定的动态链接库(DLL)中的输出库函数地址*/
FARPROC GetProcAddress(
HMODULE hModule, // DLL模块句柄 (模块基地址)
LPCSTR lpProcName // 函数名 或者 指定函数的序数值
);
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+索引值
例如:写一个简单加法函数(int add(int a, int b)),创建一个A.dll
//def文件
EXPORTS
add @12
分析A.dll的导出表
当时用序号(12)获得函数地址时会拿12-Base = 0作为输出函数地址表的索引值
使用A.dll
#include <iostream> #include <windows.h> using namespace std; typedef int(*lpAdd)(int, int); lpAdd myAdd; int main() { //动态加载dll到内存中 HINSTANCE hModule = LoadLibrary("A.dll"); cout << "ImageBase: " << hModule << endl; //通过函数名获取函数地址 myAdd = (lpAdd)GetProcAddress(hModule, "add"); cout << "myAdd(10, 20) = " << myAdd(10, 20) << endl; //通过序号获取函数地址 myAdd = (lpAdd)GetProcAddress(hModule, (char*)0x0C); cout << "myAdd(10, 20) = " << myAdd(10, 20) << endl; FreeLibrary(hModule); return 0; }
2、输入表(导入表)
PE 文件映射到内存后,Windows 将相应的 DLL文件装入,EXE 文件通过“输入表”找到相应的 DLL 中的导入函数,从而完成程序的正常运行
数据目录表的第2个成员指向输入表。当前文件依赖几个模块就会有几张输入表且是连续排放的。
如何找到输入表?
上图看出当前文件只依赖一个模块,只有一张导入表,如果有多个会连续存放直到连续出现20个0说明结束。
输入表实际是个20字节的结构体 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) } DUMMYUNIONNAME; 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; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } 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字节
typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // 指向一个转向者字符串的RVA DWORD Function; // 被输入的函数的内存地址 DWORD Ordinal; // 被输入的API的序数 DWORD AddressOfData; // 指向IMAGE_IMPORT BY NAME } u1; } IMAGE_THUNK_DATA32;
如果IMAGE_THUNK_DATA32的最高位为1,则低31位代表函数的导出序号,
否则4个字节是一个RVA,指向IMAGE_IMPORT_BY_NAME结构
IMAGE_IMPORT_BY_NAME结构字面仅有4个字节,存储了一个输入函数的相关信息
typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; // 输出函数地址表的索引(不是导出序号),(究竟是啥没试验,因为看的很多资料说是序号),不必须,链接器可能将其置0 CHAR Name[1]; // 函数名字字符串,以“\0”作为字符串结束标志,大小不确定 } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
由上图,我们是不是通过导入表能够很轻松获得当前文件依赖模块的名字和函数名?
这里INT和IAT完全内容一致,为什么呢?稍后解释
INT和IAT内容一致其实是PE文件未加载时的状态,
PE加载器将文件载入内存后会向IAT填入真正的函数地址(GetProcAddress)
例如:
3、重定位表
如果PE文件不在首选的地址(ImageBase)载入,那么文件中的每一个绝对地址都需要被修正。
需要修正的地址有很多,可以在文件中使用重定位表记录这些绝对地址的位置,在载入内存后若载入基地址与ImageBase不同再进行修正,若相同就不需要修正这些地址。
数据目录项的第6个结构,指向重定位表(Relocation Table)
重定位表由一个个的重定位块组成,每个块记录了4KB(一页)的内存中需要重定位的地址。
每个重定位数据块的大小必须以DWORD(4字节)对齐。它们以一个IMAGE_BASE_RELOCATION结构开始,格式如下
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; //记录内存页的基址RVA DWORD SizeOfBlock; //当前重定位块结构的大小。这个值减8就是TypeOffset数组的大小 /*下面字段可加与不加*/ /*数组每项大小为2字节。代表页内偏移,16位分为高4位和低12位。高4位代表重定位类型; 低12位是重定位地址(12位就可以寻址4k),与VitualAddress相加就是一个完整RVA */ //WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED * 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结构结束。
// // Based relocation types. // #define IMAGE_REL_BASED_ABSOLUTE 0 // 没有具体含义,只是为了让每个段4字节对齐 #define IMAGE_REL_BASED_HIGH 1 #define IMAGE_REL_BASED_LOW 2 #define IMAGE_REL_BASED_HIGHLOW 3 // 重定位指向的整个地址都需要修正,实际上大部分情况下都是这样的 #define IMAGE_REL_BASED_HIGHADJ 4 #define IMAGE_REL_BASED_MACHINE_SPECIFIC_5 5 #define IMAGE_REL_BASED_RESERVED 6 #define IMAGE_REL_BASED_MACHINE_SPECIFIC_7 7 #define IMAGE_REL_BASED_MACHINE_SPECIFIC_8 8 #define IMAGE_REL_BASED_MACHINE_SPECIFIC_9 9 #define IMAGE_REL_BASED_DIR64 10 // 出现在64位PE文件中,对指向的整个地址进行修正
示例分析:
继续以DllDemo.dll为例
先用工具定位重定位表在文件的位置如下
查看重定位表信息如下
->Relocation Directory 1. Relocation Block: VirtualAddress: 0x00001000 ("CODE") SizeOfBlock: 0x00000010 (0x0004 block entries) RVA Type ---------- ----------------- 0x0000100F HIGHLOW 0x00001023 HIGHLOW n/a ABSOLUTE n/a ABSOLUTE ————————————————
下面实际分析
根据下面判断出当前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)、对话框(Dialog Box)、图标(Icon)、菜单(Menu)、串表(String Table)、工具栏(Toolbar)和版本信息(Version Information)等。
定义资源时,既可以使用字符串作为名称来标识一个资源,也可以通过ID号来标识资源
资源分类
- 标准资源类型
- 非标准资源类型
若资源类型的高位如果为1,说明对应的资源类别是一个非标准的新类型
数据目录项的第3个结构,指向资源表,不直接指向资源数据,而是以磁盘目录形式定位资源数据
资源表是一个四层的二叉排序树结构。
每一个节点都是由资源目录结构和紧随其后的数个资源目录项结构组成的,
两种结构组成了一个资源目录结构单元(目录块)
资源目录结构(IMAGE_RESOURCE_DIRECTORY)占16字节,其定义如下
typedef struct _IMAGE_RESOURCE_DIRECTORY { DWORD Characteristics; //理论上是资源的属性标志,但是通常为0 DWORD TimeDateStamp; //资源建立的时间 WORD MajorVersion; //理论上是放置资源的版本,但是通常为0 WORD MinorVersion; //定义资源时,既可以使用字符串作为名称来标识一个资源,也可以通过ID号来标识资源。资源目录项的数量等于两者之和。 WORD NumberOfNamedEntries; //以字符串命名的资源数量 WORD NumberOfIdEntries; //以整型数字(ID)命名的资源数量 // IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[]; } IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
资源目录项结构(IMAGE_RESOURCE_DIRECTORY_ENTRY),占8字节,包含2个字段,结构定义如下。
//如果看不懂下面的结构建议复习一下C中的union,struct,位域 typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY { union { struct { DWORD NameOffset:31; DWORD NameIsString:1; } DUMMYSTRUCTNAME; DWORD Name; WORD Id; } DUMMYUNIONNAME; union { DWORD OffsetToData; struct { DWORD OffsetToDirectory:31; DWORD DataIsDirectory: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结构。
typedef struct _IMAGE_RESOURCE_DIR_STRING_U { WORD Length; //字符串的长度 WCHAR NameString[ 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结构。
该结构描述了资源数据的位置和大小,其定义如下。
typedef struct _IMAGE_RESOURCE_DATA_ENTRY { DWORD OffsetToData; //资源数据的RVA DWORD Size; //资源数据的长度 DWORD CodePage; //代码页,一般为0 DWORD Reserved; //保留字段 } 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,Icon Group)
第二层为资源ID或资源名称
第三层为代码页ID为2052表简体中文,1033表美国英语
右下角图标为真正资源数据
好了这篇文章暂时就先介绍到这了,希望大家以后多多支持脚本之家。