学习Win PE结构笔记
windows PE 文件是指windows系统中的一种文件格式,它包括 .exe, .dll, .sys, .ocx, .cpl, .scr, .drv, .efi, .fon ....
在了解PE文件结构以前,大家应该先明白下面几个名词的概念:
1 虚拟地址(VA)
windows中每一个进程加载到内存中,都会有一个的4GB大小的虚拟内存地址空间,在这片虚拟的空间中,任何一个位置的地址,都称之为虚拟地址(VA)。
2 相对虚拟地址(RVA)
每一个PE文件加载到内存中,都会有一个加载基址,相对虚拟地址(RVA)就是相对于加载基址的偏移量。
公式:虚拟地址(VA) = 加载基址(ImageBase)+ 相对虚拟地址(RVA);
PE文件会有一个默认的加载基址,是扩展头的第十个数据成员ImageBase(映像基址)。当这个位置被其他PE文件占用,就会加载到别地方。
3 文件偏移(Offset)
文件偏移是用于指定文件中的某个位置(未加载到内存中,在磁盘文件中的位置),是指在磁盘中相对于文件起始的偏移量。
4 对齐的概念
PE中的SectionAligment 字段与FileAligment字段描述了此映像文件的内存对齐大小与文件对齐大小。
根据上面两个字段,系统再给某一个区段上的数据块分配空间时,都会根据其对齐大小的整数倍来分配空间。
文件对齐和内存对齐的概念,举例来说一个体积为0x1150的区段,如果它的内存对齐大小为0x1000(4KB),文件对齐大小为0x200(512B),
那么这个区段在内存中实际占的大小就是0x2000,其中0x1150是有意义的,剩余的用零填充。在文件中实际占用的大小是0x1200,其中0x1150,是有意义的,剩下的用零填充。
下面是比较常见的节表的名称和作用,在分析的过程中如果遇到不在下表中的一些节表名称,这种现象说明这个文件是十分可疑的。
节名 |
描述 |
.text |
代码段,里面的数据全都是代码 |
.data |
可读写的数据段,存放全局变量或静态变量 |
.rdata |
只读数据区 |
.idata |
导入数据区,存放导入表信息 |
.edata |
导出数据区,导出表信息 |
.rsrc |
资源区段,存放程序用到的所有资源,如图表,菜单等 |
.bss |
未初始化数据区 |
.crt |
用于支持C++运行时库所添加的数据 |
.tls |
存储线程局部变量 |
.reloc |
包含重定位信息 |
.sdata |
包含相对于可被全局指针定位的可读写数据 |
.srdata |
包含相对于可被全局指针定位的只读数据 |
.pdata |
包含异常表 |
.debug$S |
包含OBJ文件中的Codeview格式符号 |
.debug$T |
包含OBJ文件中的Codeview格式类型的符号 |
.debug$P |
包含使用预编译头时的一些信息 |
.drectve |
包含编译时的一些链接命令 |
.didat |
包含延迟装入的数据 |
PE文件由以下几个结构顺序构成:
DOS头部 //大小为64(0x40)
DOS Stub //为兼容DOS程序而设立.
NT头部 //存储PE文件的全部属性,初始化信息
区段头表(也叫节表,因为区段也可以叫做节) //对于PE主体文件属性的分段描述,个数不定.
各个区段(节)(代码,数据,重定位等等) //PE文件的主体,分段存储着可执行的代码,各种数据,资源等( .rdata .reloc .text .rsrc ...)
接下来,分别介绍各个部分,每一个部分,都有一些常用到的重要成员,把这些常用的成员位置记住,基本就算熟悉了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;
补充:
1 PE 文件的第一个结构便是这个结构体.
2 重要的是第一个和最后一个,也就是e_magic与e_lfanew这两个数据成员。
3 e_magic,翻译为魔数,其实它就是一个标记,DOS头标志位,其值恒为4D5A,在系统中用宏定义为:IMAGE_DOS_SIGNATURE
4 e_lfanew,它表示NT头部在文件中的偏移.
5 其余大多已经没有什么用了,其内容通常为0居多.
DOS Stub:
在DOS头部与NT头部之间有一部分区域,存储着一些被dos头使用的数据.包括一些提示字符串等等,这部分的大小不太确定,所以,NT头的具体位置要由DOS头的最后一个成员e_lfanwe确定.
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;
补充:
1 NT头由一个简单的标记,一个不太复杂的文件头和一个比较复杂的扩展头组成.
2 如果是PE文件,标记的值恒为0x00004550, ASCII为PE00,,在系统中用宏定义为:IMAGE_NT_SIGNATURE
3 另外两个成员是两个结构体,里面存储的信息非常有用,可以说解析这两个结构是PE解析真正的开始。
文件头:
NT头的第二个成员,是一个结构体,可以称为文件头,存储着一些关于这个PE文件的信息.
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;
补充:
1 关于Machine: 这个文件可以运行在哪一个CPU平台.常见的 0x014c 代表 CPU 型号为Intel 386, 0x0200 代表 CPU 型号为Intel 64.
2 NumberOfSections:区段的个数,也就是PE文件的主体被分成了多少个部分,一般有代码,只读数据,数据,重定位等区段(节)。
3 TimeDateStamp:表明文件是何时被创建的,但是这个数据是一个非常大的32位的数值,具体解析这个数据可以使用函数
struct tm *gmtime(
const time_t *timer
);
将TimeDateStamp的地址强制转换后作为参数,之后使用一个tm结构体接收就可以得到具体时间了.
4 PointerToSymbolTable:指向符号表,用于调试.
5 SizeOfOptionalHeader:扩展头的大小,32位系统中一般是224(0x00E0),在64位系统中一般是240(0x00F0),扩展头大小其实是不太确定的,因为不排除人为的修改.
6 Characteristics:PE文件属性值,可以用来判断文件类型。有几个需要知道:Dll一般是0x0210,EXE一般是0x010F.win32 SDK 中的winNT.h定义一组宏,来表示不同的文件属性.
扩展头:
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;
说明:
1 一共有31个成员,重要的大概有10个。另外有一个极度重要的数据目录。
2 扩展头是属于NT头部的第三部分,紧随文件头结构之后,存储着加载文件时的一些初始化信息,32位系统中扩展头大小一般是224(0x00E0),在64位系统中一般是240(0x00F0)。
3 IMAGE_NUMBEROF_DIRECTORY_ENTRIES 这是个宏定义,值是0x10,表示一般情况下有16个数据目录.
这个是数据目录的定义
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 数据的相对虚拟地址(RVA)
DWORD Size; // 数据的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
它包含了这段数据存储的相对虚拟地址(RVA)以及这段数据的大小,所以数据目录表只是一个引导,只是帮助我们找到这段数据,这里列出数据目录的宏与名称。
IMAGE_DIRECTORY_ENTRY_EXPORT 0
导出表(IMAGE_EXPORT_DIRECTORY结构)。
IMAGE_DIRECTORY_ENTRY_IMPORT 1
导入表(IMAGE_IMPORT_DESCRIPTOR结构数组)。
IMAGE_DIRECTORY_ENTRY_RESOURCE 2
资源表(IMAGE_RESOURCE_DIRECTORY结构)。
IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
异常处理程序表(IMAGE_RUNTIME_FUNCTION_ENTRY结构数组)
IMAGE_DIRECTORY_ENTRY_SECURITY 4
安全目录表,一般情况用于保存数字签名或安全证书。
IMAGE_DIRECTORY_ENTRY_BASERELOC 5
基址重定位信息。
IMAGE_DIRECTORY_ENTRY_DEBUG 6
索引调试信息
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7
版权
IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
全局指针目录。用在64位平台
IMAGE_DIRECTORY_ENTRY_TLS 9
指向线程局部存储(Thread Local Storage)初始化节。
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
载入配置
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11
绑定输入目录
IMAGE_DIRECTORY_ENTRY_IAT 12
导入地址表
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13
延迟载入描述
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14
COM信息
最后还有一个全零的保留目录,也就是数组长度是16.这是一般的情况之下,文件是可以拥有更多数据目录的。这个时候,扩展头的大小也不是E0了。
至此,NT头的内容就结束了,之后是区段头表.
区段头表:
再回忆PE文件结构:1 DOS头 2 DOS头用的数据 3 NT头(包括文件头与扩展头) 4 区段(节)头表 5 各个区段(节).
区段是不需要我们直接解析的地方,也就说,区段头表是我们直接探索的最后位置了。
区段头表存储着PE文件主体的一些属性,区段头表是由若干个结构体依次排列组成,每一个结构体代表着PE文件主体中一段数据的属性,也就是每一个区段头都对应着PE文件主体的一段数据,这段数据叫做区段或者节,区段头规定了区段(节)的属性。
区段头结构
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[ IMAGE_SIZEOF_SHORT_NAME]; 1 //区段的名字,如.text .reloc .rdata
union {
DWORD PhysicalAddress;
DWORD VirtualSize; 2 (重要)//这个区段在虚拟内存中会使用的总大小,没有经过对齐。
} Misc;
DWORD VirtualAddress; 3 (重要)//这个区段起始的相对虚拟地址(RVA)
DWORD SizeOfRawData; 4 (重要)//区段在文件中的大小,这个值进行了文件对齐。
DWORD PointerToRawData; 5 (重要)//区段的文件偏移 RVA 转 Offset 时用到
DWORD PointerToRelocations; 6 //区段的重定位信息的文件偏移。在OBJ文件中才有用
DWORD PointerToLinenumbers; 7 //COFF行号信息的文件偏移.
WORD NumberOfRelocations; 8 //PointerToRelocations域指向的重定位信息的数目。在OBJ文件中才有用
WORD NumberOers; 9 //PointerToLinenumbers域指向的行号信息的数目。只有当生成COFF行号信息时才使用。
DWORD Characteristics; 10 (重要)//区段的属性,这个在文件被载入的时候意义比较重大.
} IMAGE_SECTION_HEADER, * PIMAGE_SECTION_HEADER;
补充:
1 区段头表是由多个这个结构体构成,以一个全是0的结构体结尾
2 关于区段名字的规矩是这样的:
.text 一般是代码段,这个是非常重要的
.data段 一般是数据段
.bss段 表示未初始化的数据,比如static变量,可能是在进入一个函数的时候才被初始化的
.rdata段 表示只读的数据,比如字符串,
.textbss段 和代码有关,不是很清楚做什么用的
.idata和edata 存储导入表和导出表的信息。
.rsrc段 存储资源的区段(节)
.relcoc段 存储重定位信息的区段(节)
3 Characteristics: 区段(节)的属性。属性的具体值,参考MSDN.
4 这个结构的大小是40(0x28)。
5 虽然区段头表就在NT头的后面,系统提供了一个宏来方便的找到它的位置:IMAGE_FIRST_SECTION( pNTHeader ),参数是NT头的指针。
6 相对虚拟地址(RVA)与文件偏移(Offectset)的转换:
这是解析数据目录表的基础。
这里提供转换的方法:我们把需要转换的RVA,在下面的公式中定义成 RVA(转)。
RVA(转),一定是在某个区段中,通过循环遍历上面的结构体中成员变量,如果RVA(转)符合大于 VirtualAddress(区段起始的相对虚拟地址RVA),
小于 (VirtualAddress + Misc.VirtualSize).就是在这个区段中,这样得到了这个区段的起始RVA和区段起始Offset.通过下面的公式就可以得到转换后的文件偏移了。
公式:Offect = RVA(转) - RVA(区段起始)+Offect(区段起始)
导出表:
概述:
1)导出是指这个PE文件所导出的供其他PE文件使用的函数,变量,或者类的行为。
2)每一个导出的函数(变量,类),都有一个唯一的序号与之对应,有的情况下,会没有函数名(变量,类),但是会有函数(变量,类)地址和序号。可以通过序号调用这样的函数。
3)由上一条可知,导出表包括 1 函数(变量,类)地址 2 序号 3 函数(变量,类)名
4)导出表,是根据数据目录表的第一个数组元素中的相对虚拟地址,再通过刚刚说过的相对虚拟地址转换文件偏移的方式,可以方便的找到它。
下面是导出表的数据结构
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; 1 保留值,恒为0
DWORD TimeDateStamp; 2 时间,和文件头中的时间一样的
WORD MajorVersion; 3 主版本号
WORD MinorVersion; 4 次版本号
DWORD Name; 5(重要)本PE文件的名字,也就是谁导出的这些函数(变量,类)
DWORD Base; 6(重要)序号基数
DWORD NumberOfFunctions; 7(重要)函数数量
DWORD NumberOfNames; 8(重要)函数名称数量
DWORD AddressOfFunctions; 9(重要)函数地址表的相对虚拟地址RVA
DWORD AddressOfNames; 10(重要)函数名称表的相对虚拟地址RVA
DWORD AddressOfNameOrdinals; 11(重要)序号表的相对虚拟地址RVA
} IMAGE_EXPORT_DIRECTORY, * PIMAGE_EXPORT_DIRECTORY;
补充:
1) 导出表应该被安排在.edata中,不过这个段一般都会合并到.rdata中。
2) 有一个序号基数,通过序号表得到的序号再加上这个序号基数才是真正的函数序号。
3) 最后三个成员,通过这三个相对虚拟地址,转换为文件偏移后,可以方便的找到函数地址表,函数名称表,序号表。
4) 函数名称表,存储的是函数名称的相对虚拟地址,再一次的转换为文件偏移后才能使用,这一点在解析的时候要注意。
5) 导出的函数地址表,序号表,函数名表的关系:
5.1序号不是按顺序排列的
5.2序号与函数名是一一对应的,一一对应的意思是,两个表中相同位置的元素相对应。这也说明了结构体中为什么没有序号的数量,因为序号的数量和函数名数量是一样的。
5.3序号可能不是连续的,比如可能会没有1 ,有2,没有5,有6,但这并不代表缺失的这个序号没有所对应的函数地址
5.4函数个数会比函数名个数多,多出来的这些函数,可能是用序号导出的函数,也可能是一个无效函数,地址填充0
5.5序号表元素的值,对应着函数地表的位置,那一个位置中的函数地址,是这个序号所对应的函数名的函数地址。由此,三个表联系起来。这条很重要。
5.6函数地址表中元素,有地址值,但是序号表中没有序号与之对应,说明这是一个序号导出函数,没有函数名,他的序号就是它自己在函数地址表中的位置。(当然这个位置加上序号基数才是它真正的序号)
5.7函数地址表中元素,填充为0,说明这是一个无效的函数,也不会有序号和函数名与之对应。
下面这个图指出了一个比较混乱的导出表结构,通过这个混乱的结构能更好的理解导出表。
可以看出,序号的基数为X,导出表中的函数地址表中的第一个函数,函数名表和序号表中没有相应的值和它对应的,它是由序号导出的。
导入表:
概述
1)导入表是根据数据目录表的第二个元素找到的。找出的方法与找导出表相同。
2)导入是这个PE文件在运行时,需要别的PE文件给予的支持。导入表存储的是从其他PE文件导入过来的函数名,序号。在加载到内存之后,会存储这些函数的地址。
3)由于一个PE文件可能会需要多个PE文件的支持,所以导入表结构一般有多个,就是说导入表其实是一个结构体数组,以一个全零元素为结尾,每一个数组的元素,代表一个PE文件的导入信息。
下面是导入表的数据结构:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; 1 (重要)指向一个结构体数组的相对虚拟地址(RVA),结构体数组叫输入名称表(INT:Import Name Table)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; 2 时间标志
DWORD ForwarderChain; 3 转发机制用到
DWORD Name; 4(重要)导入的PE文件的名字的相对虚拟地址RVA
DWORD FirstThunk; 5 (重要) 指向一个结构体数组的相对虚拟地址(RVA),结构体数组叫做输入地址表(IAT:Import Address Table)
} IMAGE_IMPORT_DESCRIPTOR, * PIMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; //转发用到
DWORD Function; //导入函数的地址,在加载到内存后,这里才起作用
DWORD Ordinal; //假如是序号导入的,用到这里
DWORD AddressOfData; //假如是函数名导入的,用到这里,它指向一个PIMAGE_IMPORT_BY_NAME结构体
} u1;
} IMAGE_THUNK_DATA32;
1 在磁盘文件中,起作用的只有后两个成员
2 这个结构占据4个字节,假如最高位为1,那么序号导入起作用,只需输出一个序号,假如最高位为0,那么是最后一个成员其作用,指向一个PIMAGE_IMPORT_BY_NAME,判断最高位是否为1可以使用系统提供的宏IMAGE_SNAP_BY_ORDINAL32(),参数就是这个结构体。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, * PIMAGE_IMPORT_BY_NAME;
这个结构包含了序号和函数名。
1 OriginalFirstThunk 与 FirstThunk 指向的是相同类型的结构体IMAGE_THUNK_DATA32
2 在磁盘文件中OriginalFirstThunk与FirstThunk中的数据是相同的,可以将输入名称表(INT)看成是输入地址表(IAT)的一个备份。在加载到内存中之后,输入地址表会由PE加载器把相应PE文件的函数地址覆盖到这里来。这时,输入地址表才是真正的输入地址表。
重定位:
1)什么是重定位?
重定位就是你本来这个程序理论上要占据这个地址,但是由于某种原因,这个地址现在不能让你霸占,你必须转移到别的地址,这就需要基址重定位。由于每个进程都有自己独立的虚拟地址空间,既然都是自己的,怎么会被占据呢?其实对于EXE应用程序来说,是这样的。但是动态链接库就不一样了,动态链接库都是寄居在别的应用程序的空间的,所以出现要载入的基地址被应用程序占据了也是很正常的,这时它就不得不进行重定位了。
2)重定位表是根据数据目录表的第6个元素。
3)重定位表也是一个结构体数组,以全零元素结尾,每一个数组元素描述了4KB大小的区域的重定位信息。
以下是重定位表的数据结构
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; 1(重要)需要重定位内存页的起始位置(RVA)
DWORD SizeOfBlock; 2 (重要)这个结构体(算上TypeOffset)的大小
// WORD TypeOffset[1]; 3 (重要)一个特殊的数据,存放的是相对于第一个元素描述的位置的偏移
} IMAGE_BASE_RELOCATION;
说明:
1 这是一个特殊的结构,第三个成员并不真正是这个结构体的成员,他紧随在结构体之后,是一个不定多长的数组,第一个成员描述的是某个区段中第一个需要重定位开始。
2 那我们如何知道这个区域中有多少个需要重定位的位置呢?就要根据第二个成员,它的大小是这个结构体大小与后面的TypeOffset数组的总大小。可以推出,重定位个数等于总大小减去结构体大小,再除以2。
公式描述:需重定位个数 n = (SizeOfBlock-8)/ 2;
3 第三个成员的高4位,描述的是一个属性。低12位描述的才是一个偏移。如下图所示:
4 当基址重定位发生的时候,用第一个成员是虚拟基址(VA),依次加上偏移(第三个成员的后12位),就能得到存储全局变量地址的相对虚拟地址,也就找到了这个地方,再根据第三个成员高四位描述的属性,对其进行重定位操作。
资源表:
概述:
1 Windows 将程序的各种界面定义为资源,包括加速键(Accelerator)、位图(Bitmap)、光标(Cursor)、对话框(Dialog Box)、图标(Icon)、菜单(Menu)、串表(String Table)、工具栏(Toolbar)和版本信息(Version Information)等。
2 资源表有三层结构。每一层都以一个IMAGE_RESOURCE_DIRECTORY开头,之后跟数个IMAGE_RESOURCE_DIRECTORY_ENTRY结构,可以说每一层由一个IMAGE_RESOURCE_DIRECTORY结构与一个IMAGE_RESOURCE_DIRECTORY_ENTRY结构体数组组成,这个结构体数组元素的个数由之前的结构给出,所以要注意:这个结构体数组不是以一个全零元素为结尾了,在解析的时候要注意。
3 资源表是根据数据目录表的第3个元素找到的,它找到的是资源结构的第一层。
4 理解三层目录结构,第一层告诉你有几种资源。每种资源叫什么,第二层告诉你这一种资源有多少个,每个资源叫什么,第三层告诉你一个具体资源在文件的什么位置,注意区分多少种资源和多少个资源。
如图所示:
下面分别查看两个结构体
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; 1属性,一般填0
DWORD TimeDateStamp; 2时间,一般填0
WORD MajorVersion; 3 主版本号
WORD MinorVersion; 4 次版本号
WORD NumberOfNamedEntries; 5 (重要)用字符串作为资源标识的条目个数
WORD NumberOfIdEntries; 6 (重要) 用数字ID作为资源标识的条目个数
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, * PIMAGE_RESOURCE_DIRECTORY;
说明
其实在这里边我们唯一要注意的就是 NameberOfNamedEntries 和 NumberOfIdEntries,它们说明了本目录中目录项的数量。两者加起来就是本目录中的目录项总和。
也就是后边跟着的IMAGE_RESOURCE_DIRECTORY_ENTRY 数目。
下面是IMAGE_RESOURCE_DIRECTORY_ENTRY结构。
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY STRUCT
{
DWORD Name; 1//目录项的名称字符串指针或ID
DWORD OffsetOfData; 2//目录项指针
}IMAGE_RESOURCE_DIRECTORY_ENTRY
1 .Name 字段是联合体,当最高位为 0 的时候,表示字段的值作为 ID 使用;而最高位为 1 的时候,字段的低位作为指针使用(资源名称字符串是使用 UNICODE编码),但是这个指针不是直接指向字符串哦,而是指向一个 IMAGE_RESOURCE_DIR_STRING_U 结构的。当结构用于第一层目录时,定义的是资源类型;当结构定义于第二层目录时,定义的是资源的名称;当结构用于第三层目录时,定义的是代码页编号。
该结构定义如下:
typedef struct _IMAGE_RESOURCE_DIR_STRING_U STRUCT
{
DWORD Length ; 字符串的长度
DWORD NameString ; UNICODE字符串,由于字符串是不定长的。由Length 制定长度
}IMAGE_RESOURCE_DIR_STRING_U
2. OffsetOfData 字段是一个指针,当最高位为 1 时,低位数据指向下一层目录块的其实地址;当最高位为 0 时,指针指向 IMAGE_RESOURCE_DATA_ENTRY 结构。
注意:将 Name 和 OffsetOfData 用做指针时需要注意,该指针是从资源区块开始的地方算起的偏移量(即根目录的起始位置的偏移量),不是我们习惯的 RVA 哦。
3下面讲一下常规情况下的三层结构
3.1第一层:
通过数据目录表的第3个元素找到这里,首先遇到的是那个IMAGE_RESOURCE_DIRECTORY结构,最后两个成员的和会告诉你后面有多少种需要解析的资源
如果这种资源是已知的,那么这种资源属于按序号作为资源标识,Name元素最高位为0,这个时候整个四个字节代表着已知资源的类型, 这个表能说明不同的数字,代表的资源类型。
如果这种资源是未知的,那么这种资源属于字符串作为资源标识,这时联合体的最高位为1, OffseToData指明了一个结构体IMAGE_RESOURCE_DIR_STRING_U的位置。结构 体中保存着标识字符串。
typedef struct _IMAGE_RESOURCE_DIR_STRING_U
{
WORD Length; 字符串的长度
WCHAR NameString[ 1 ] ; UNICODE字符串,由于字符串是不定长的。由Length 制定长度
} IMAGE_RESOURCE_DIR_STRING_U, * PIMAGE_RESOURCE_DIR_STRING_U;
第二个成员是这种资源类型的名字。第一个成员是这个名字的长度。需要注意的是这个名字不是以0结尾的。有个长度给你,解析的时候不要越界。
当OffseToData最高位为 1 时,指针指向 IMAGE_RESOURCE_DATA_ENTRY 结构。这个地方就是第二层。
3.2第二层
通过第一层找到这里,首先遇到的是那个IMAGE_RESOURCE_DIRECTORY结构,最后两个成员的和会告诉你后面有多少个需要解析的资源
第一个成员,通过上一层的理解,其实这一层也很好理解了。当Name成员最高位是0的时候,说明这个资源的标识是一个数字,其实一些对话框,控件的ID值就是这个数字。如果Name成员最高位是1,说明这个资源的标识是字符串,
第二个成员的OffsetToData成员最高位为1,说明这个联合体表示的地方是一个目录,会带你去寻找这个资源的具体在地方,这个地方就是第三层。
3.3第三层
第一个联合体已经不是标识的意思了。整个四个字节代表的是这个资源是什么语言的。
第二个联合体的OffsetToData成员会为0,说明这个联合体表示的地方是一个数据,它指出了资源具体的位置。由OffsetToData会得到一个结构体IMAGE_RESOURCE_DATA_ENTRY
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; //(重要)资源的偏移,注意这是一个相对虚拟地址(RVA)
DWORD Size; // ( 重要)资源的大小
DWORD CodePage; // ( 重要)资源页属性
DWORD Reserved; // 保留,一般是0
} IMAGE_RESOURCE_DATA_ENTRY, * PIMAGE_RESOURCE_DATA_ENTRY;
比较重要第一个成员和第二个成员,一个是资源的偏移,另一个是资源的大小。
3.4 以上为常规情况下,不常规的情况下,可能第二层的时候,OffsetToData成员就为0了,直接指出了资源的位置。可能是因为不需要知道这个资源是什么语言类型。
3.5 但是最后得到的结构体中数据的偏移OffsetToData是一个相对虚拟地址(RVA),找到它需要转换文件偏移。
声明:以上资料图片参考(黑客免杀攻防 任晓珲 Windows PE 权威指南 戚利 鱼C工作室 小甲鱼)