什么是可执行文件
可执行文件(executable file)指的是可以由操作系统进行加载执行的文件
windows平台:PE(Portable Executable)文件结构 Linux平台:ELF(Executable and Linking Format)文件结构
一些用到的概念
地址空间:这个地址空间指的是PE文件被加载到内存的空间,是一个虚拟的地址空间,之所以不是物理空间是因为数据在内存中的位置经常在变,这样既可以节约内存开支又可以避开错误的内存位置。这个地址空间的大小为4G,但其中供程序装载的空间只有2G而且还是低2G空间,高2G空间则被用于装载内核DLL文件,所以也被称作内核空间。
文件映射:PE文件在磁盘上的状态和在内存中的状态是不一样的,我们把PE文件在磁盘上的状态称作FileBuffer
,在内存中的状态称为ImageBuffer
。当PE文件通过装载器装入内存是会经过“拉伸”的过程,所以它在FileBuffer
状态下和ImageBuffer
状态下的大小是不一样的。这个拉伸的具体过程会在讲完PE头结构后进行介绍。大致的图解如下:
VA:英文全称是Virual Address
,简称VA,中文意思是虚拟地址。指的是文件被载入虚拟空间后的地址。
ImageBase:中文意思是基址,指的是程序在虚拟空间中被装载的位置。
RVA:英文全称是Relative Virual Address
,简称RVA,中文意思是相对虚拟地址。可以理解为文件被装载到虚拟空间(拉伸)后先对于基址的偏移地址。计算方式:RVA = VA(虚拟地址) - ImageBase(基址)。它的对齐方式一般是以1000h为单位在虚拟空间中对齐的(传说中的4K对齐),具体对齐需要参照IMAGE_OPTIONAL_HEADER32
中的SectionAlignment
成员。
FOA:英文全称是File Offset Address
,简称FOA,中文意思是文件偏移地址。可以理解为文件在磁盘上存放时相对于文件开头的偏移地址。它的对齐方式一般是以200h为单位在硬盘中对齐的(512对齐),具体对齐需要参照IMAGE_OPTIONAL_HEADER32
中的FileAlignment
成员。
全局变量:如果全局变量没有初始化值,那么这个全局变量在PE文件中是没有保存它的位置的,如果有初始化那么就有它位置保存初始值。定位到全局变量的位置后即可修改。
FOA(文件偏移,基于PE文件开始的地址0)和RVA(虚拟地址偏移,基于ImageBase):
当SectionAlignment(内存中节区对齐大)和FileAlignment(文件中节区对齐大)相同时,这2个偏移相同,现在的大部分程序都相同.
如果不相同时:要判断是否在PE头中,如果在PE头这个节区那么他们还是相同,如果不在PE头节区,RVA先算出当前虚拟地址相对与当前节区的开始地址的偏移,然后在文件中也找到这个节区在PE文件中的偏移位置加上相同的偏移即可。
就算SectionAlignment(内存中节区对齐大)和FileAlignment(文件中节区对齐大)相同时,每个节区加载地址也可能不同这时还是要自己转换RVA和FOA
PE文件结构
PE文件是由许许多多的结构体组成的,程序在运行时就会通过这些结构快速定位到PE文件的各种资源,其结构大致如图所示,从上到下依次是Dos头、Nt头、节表、节区和调试信息(可选)。其中Dos头、Nt头和节表在本文中统称为PE文件头(因为SizeOfHeaders就是这三个头的总大小)、节区则称为节,所以也可以说PE文件是由PE文件头和节组成。
PE文件头保存着整个PE文件的索引信息,可以帮助PE装载器定位资源,而节则保存着整个PE文件的所有资源。正因为如此,所以存在着这样的说法:头是节的描述,节是头的具体化。PE文件格式中的所有结构体声明在WINNT.h
更详细一点的图
IMAGE_DOS_HEADER
typedef struct _IMAE_DOS_HEADER {
WORD e_magic; 相对文件开头的偏移0,也就是文件的前2个字节,固定值0x4D 0x5A (MZ)
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew; 相对文件开头的偏移0x3C,保存IMAGE_NT_HEADERS32这个结构体在PE文件中的偏移地址
} IMAGE_DOS-HEADER, *PIMAGE_DOS_HEADER;
当我们用16进制编辑器打开一个PE文件时,就会发现所有PE文件的前两个字节都是MZ
,用十六进制表示是4D 5A
,这两个字母就是Mark Zbikowski
的姓名缩写,他是最初的MS-DOS设计者之一。如果把PE文件的这两个字节修改成其他数据,运行该PE文件就会无法正常运行(跳出黑窗口打印Program too big to fit in memory
然后闪退,有兴趣的朋友可以尝试下)。这里可以证明当PE文件运行时,首先就会检测这两个字节,如果不是MZ
则会退出运行。
在该结构体中另一个重要成员就是最后一个成员e_lfanew
。该成员的大小是LONG
类型4个字节。之所以说它重要是因为它保存着IMAGE_NT_HEADERS32
这个结构体在PE文件中的偏移地址,PE文件运行时只有通过该成员才能定位到PE签名(也就是IMAGE_NT_HEADERS32
结构体的起始位置)。上图框出来的就是该成员的值0x00000100,IMAGE_NT_HEADERS32结构体从0x100开始
IMAGE_DOS_STUB (了解)
在IMAGE_DOS_HEADER
结构体后面紧跟着就是IMAGE_DOS_STUB
程序,它不是一个结构体,它是运行在MS-DOS
下的可执行程序,当可执行文件运行于MS-DOS
下时,这个程序会打印This program cannot be run in DOS mode
这条消息。用户可以自己更改该程序,MS-DOS程序当前是可有可无的,如果你想使文件大小尽可能的小可以省掉MS-DOS程序,同时不要这部分。40H到PE文件头标志为IMAGE_DOS_STUB的空间不固定
IMAGE_NT_HEADERS32
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; **重要成员 PE签名 相对该结构的偏移0x00**
IMAGE_FILE_HEADER FileHeader; **重要成员 结构体 相对该结构的偏移0x04**
IMAGE_OPTIONAL_HEADER32 OptionalHeader; **重要成员 结构体 相对该结构的偏移0x18**
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
这个结构体是整个PE文件的核心,它是由一个Signature
、一个IMAGE_FILE_HEADER结构体
、一个IMAGE_OPTIONAL_HEADER32结构体
组成的。所以从整体看来这个结构比较简单,但实际上其内部结构较为复杂。
Signature
也称作PE签名,这个成员和DOS头的MZ标记一样都是一个PE文件的标准特征,只不过这个成员是DWORD
类型大小为4字节,如果把这个PE签名修改后,程序也是不会正常运行的(跳出黑窗口打印This program cannot be run in DOS mode
然后闪退,可能是因为修改PE签名后无法识别后续内容的关系吧)。
IMAGE_FILE_HEADER 标准PE头
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; ** 机器号 相对该结构的偏移0x00**
WORD NumberOfSections; **重要成员 节区数量 相对该结构的偏移0x02**
DWORD TimeDateStamp; ** 时间戳 相对该结构的偏移0x04**
DWORD PointerToSymbolTable; ** 符号表偏移 相对该结构的偏移0x08**
DWORD NumberOfSymbols; ** 符号表数量 相对该结构的偏移0x0C**
WORD SizeOfOptionalHeader; **重要成员 可选头大小 相对该结构的偏移0x10**
WORD Characteristics; **重要成员 PE文件属性 相对该结构的偏移0x12**
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine
所表示的是计算机的体系结构类型,也就是说这个成员可以指定该PE文件能够在32位还是在64位CPU上执行。如果强行更改该数值程序就会报错。该成员可以是以下的数值:
数值 | 0x014C | 0x8664 | 0x0200 |
---|---|---|---|
描述 | x86 | x64 | Intel Itanium |
NumberOfSections
它的含义就是当前PE文件的节区数量,虽然它是大小是两个字节,但是在windows加载程序时会将节区的最大数量限制为96个。
TimeDateStamp
它的含义是时间戳,用于表示该PE文件创建的时间,时间是从国际协调时间也就是1970年1月1日00:00
起开始计数的,计数单位是秒。该时间并不是右键属性里的时间,例如0x5CFBB225的计算方法如下:
通过计算,从1970年到2019年一共有12个闰年(通过闰年计算器获得),到6月有151天。
秒:0x5CFBB225 % 60 = 33
分:0x5CFBB225 / 60 % 60 = 3
时:0x5CFBB225 / 3600 % 24 = 13
日:0x5CFBB225 / 3600 / 24 - (365 * 49 + 12) - 151 + 1 = 8
月:(0x5CFBB225 / 3600 / 24 - (365 * 49 + 12)) / 30 + 1 = 6
年:0x5CFBB225 / 3600 / 24 / 365 + 1970 = 2019
结果为:2019年6月8日 13:03:33
SizeOfOptionalHeader
它存储该PE文件的可选PE头的大小,在32位PE文件中可选头大小为0xE0,64位可选头大小为0xF0。正因为如此,所以就必须通过该成员来确定可选PE头的大小。
Characteristics
它描述了PE文件的一些属性信息,比如是否可执行,是否是一个动态连接库等。该值可以是一个也可以是多个值的和,具体定义如下:
宏定义 | 数值 | 描述 |
---|---|---|
IMAGE_FILE_RELOCS_STRIPPED | 0x0001 | 从文件中删除了重定位信息 |
IMAGE_FILE_EXECUTABLE_IMAGE | 0x0002 | 该文件是可执行的 |
IMAGE_FILE_LINE_NUMS_STRIPPED | 0x0004 | COFF行号从文件中删除 |
IMAGE_FILE_LOCAL_SYMS_STRIPPED | 0x0008 | COFF符号表条目从文件中删除 |
IMAGE_FILE_AGGRESIVE_WS_TRIM | 0x0010 | 废弃 |
IMAGE_FILE_LARGE_ADDRESS_AWARE | 0x0020 | 该应用程序可以处理大于2GB的地址 |
IMAGE_FILE_BYTES_REVERSED_LO | 0x0080 | 废弃 |
IMAGE_FILE_32BIT_MACHINE | 0x0100 | 32位机器 |
IMAGE_FILE_DEBUG_STRIPPED | 0x0200 | 调试信息已删除并单独存储在另一个文件中 |
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP | 0x0400 | 如果在移动介质中,拷到交换文件中运行 |
IMAGE_FILE_NET_RUN_FROM_SWAP | 0x0800 | 如果在网络中,拷到交换文件中运行 |
IMAGE_FILE_SYSTEM | 0x1000 | 该文件是一个系统文件 |
IMAGE_FILE_DLL | 0x2000 | 该文件是一个动态链接库 |
IMAGE_FILE_UP_SYSTEM_ONLY | 0x4000 | 该文件应仅在单处理器计算机上运行 |
IMAGE_FILE_BYTES_REVERSED_HI | 0x8000 | 废弃 |
IMAGE_OPTIONAL_HEADER32 扩展PE头
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; **魔术字 偏移0x00
BYTE MajorLinkerVersion; **链接器主版本 偏移0x02
BYTE MinorLinkerVersion; **链接器副版本 偏移0x03
DWORD SizeOfCode; **所有含代码的节的总大小 偏移0x04
DWORD SizeOfInitializedData; **所有含初始数据的节的总大小 偏移0x08
DWORD SizeOfUninitializedData; **所有含未初始数据的节的总大小 偏移0x0C
DWORD AddressOfEntryPoint; **程序执行入口地址 偏移0x10 重要
DWORD BaseOfCode; **代码节的起始地址 偏移0x14
DWORD BaseOfData; **数据节的起始地址 偏移0x18
DWORD ImageBase; **程序首选装载地址 偏移0x1C 重要
DWORD SectionAlignment; **内存中节区对齐大小 偏移0x20 重要
DWORD FileAlignment; **文件中节区对齐大小 偏移0x24 重要
WORD MajorOperatingSystemVersion; **操作系统的主版本号 偏移0x28
WORD MinorOperatingSystemVersion; **操作系统的副版本号 偏移0x2A
WORD MajorImageVersion; **镜像的主版本号 偏移0x2C
WORD MinorImageVersion; **镜像的副版本号 偏移0x2E
WORD MajorSubsystemVersion; **子系统的主版本号 偏移0x30
WORD MinorSubsystemVersion; **子系统的副版本号 偏移0x32
DWORD Win32VersionValue; **保留,必须为0 偏移0x34
DWORD SizeOfImage; **镜像大小 偏移0x38 重要
DWORD SizeOfHeaders; **PE头大小 偏移0x3C 重要
DWORD CheckSum; **校验和 偏移0x40
WORD Subsystem; **子系统类型 偏移0x44
WORD DllCharacteristics; **DLL文件特征 偏移0x46
DWORD SizeOfStackReserve; **栈的保留大小 偏移0x48
DWORD SizeOfStackCommit; **栈的提交大小 偏移0x4C
DWORD SizeOfHeapReserve; **堆的保留大小 偏移0x50
DWORD SizeOfHeapCommit; **堆的提交大小 偏移0x54
DWORD LoaderFlags; **保留,必须为0 偏移0x58
DWORD NumberOfRvaAndSizes; **数据目录的项数 偏移0x5C
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Magic
这个无符号整数指出了镜像文件的状态,这个用来识别程序是32位还是64位,此成员可以是以下的值:
宏定义 | 数值 | 描述 |
---|---|---|
IMAGE_NT_OPTIONAL_HDR32_MAGIC | 0x010B | 表明这是一个32位镜像文件。 |
IMAGE_NT_OPTIONAL_HDR64_MAGIC | 0x020B | 表明这是一个64位镜像文件。 |
IMAGE_ROM_OPTIONAL_HDR_MAGIC | 0x0107 | 表明这是一个ROM镜像。 |
AddressOfEntryPoint
该成员保存着文件被执行时的入口地址,它是一个RVA。如果想要在一个可执行文件中附加了一段代码并且要让这段代码首先被执行,就可以通过更改入口地址到目标代码上,然后再跳转回原有的入口地址。
ImageBase
该成员指定了文件被执行时优先被装入的地址,如果这个地址已经被占用,那么程序装载器就会将它载入其他地址。当文件被载入其他地址后,就必须通过重定位表进行资源的重定位,这就会变慢文件的载入速度。而装载到ImageBase指定的地址就不会进行资源重定位。
对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被其他的DLL使用,所以DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 成员中,DLL 文件对应的IMAGE_FILE_RELOCS_STRIPPED
位总是为0,而EXE文件的这个标志位总是为1。
SectionAlignment
该成员指定了文件被装入内存时,节区的对齐单位。节区被装入内存的虚拟地址必须是该成员的整数倍,以字节为单位,并且该成员的值必须大于等于FileAlignment的值。该成员的默认大小为系统的页面大小。
FileAlignment
该成员指定了文件在硬盘上时,节区的对齐单位。节区在硬盘上的地址必须是该成员的整数倍,以字节为单位,并且该成员的值必须大于等于FileAlignment的值。该值应为200h到10000h(含)之间的2的幂。默认为200h。如果SectionAlignment的值小于系统页面大小,则FileAlignment的值必须等于SectionAlignment的值。
SizeOfImage
该成员指定了文件载入内存后的总体大小,包含所有的头部信息。并且它的值必须是SectionAlignment
的整数倍。整个PE文件在内存中展开后的大小
SizeOfHeaders
该成员指定了PE文件头的大小,并且向上舍入为FileAlignment的倍数,值的计算方式为:
SizeOfHeaders = (e_lfanew/*DOS头部*/ + 4/*PE签名*/ +
sizeof(IMAGE_FILE_HEADER) +
SizeOfOptionalHeader + /*NT头*/
sizeof(IMAGE_SECTION_HEADER) * NumberOfSections) / /*节表*/
FileAlignment *
FileAlignment +
FileAlignment; /*向上舍入 一般该结果不可能是FileAlignment的整数倍,所以直接加上FileAlignment还是没问题的 */
NumberOfRvaAndSizes
该成员指定了可选头中目录项的具体数目,由于以前发行的Windows NT的原因,它只能为10h。
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]
最后再分析
IMAGE_SECTION_HEADER(IMAGE_SECTION_TABLE节表)
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; **节区名 偏移0x00
union {
DWORD PhysicalAddress;
DWORD VirtualSize; **节区的虚拟大小 偏移0x08 重要
} Misc;
DWORD VirtualAddress; **节区的虚拟地址 偏移0x0C 重要
DWORD SizeOfRawData; **节区在硬盘上的大小 偏移0x10 重要
DWORD PointerToRawData; **节区在硬盘上的地址 偏移0x14 重要
DWORD PointerToRelocations; **指向重定位项开头的地址 偏移0x18
DWORD PointerToLinenumbers; **指向行号项开头的地址 偏移0x1C
WORD NumberOfRelocations; **节区的重定位项数 偏移0x20
WORD NumberOfLinenumbers; **节区的行号数 偏移0x22
DWORD Characteristics; **节区的属性 偏移0x24 重要
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name
这是一个8字节的ASCII字符串,长度不足8字节时用0x00填充,该名称并不遵守必须以"\0"结尾的规律,如果不是以"\0"结尾,系统会截取8个字节的长度进行处理。可执行文件不支持长度超过8字节的节名。对于支持超过字节长度的文件来说,此成员会包含斜杠(/),并在后面跟随一个用ASCII表示的十进制数字,该数字是字符串表的偏移量。
Misc.VirtualSize
这个成员在一个共用体中,这个共用体中还有另外一个成员,由于用处不大我们就不讲解了,主要讲解VirtualSize的含义。这个成员指定了该节区装入内存后的总大小,以字节为单位,如果此值大于SizeOfRawData的值,那么大出的部分将用0x00填充。这个成员只对可执行文件有效,如果是obj文件此成员的值为0。
VirtualAddress
指定了该节区装入内存虚拟空间后的地址,这个地址是一个相对虚拟地址(RVA),它的值一般是SectionAlignment的整数倍。它加上ImageBase后才是真正的虚拟地址。
SizeOfRawData
指定了该节区在硬盘上初始化数据的大小,以字节为单位。它的值必须是FileAlignment的整数倍,如果小于Misc.VirtualSize,那么该部分的其余部分将用0x00填充。如果该部分仅包含未初始化的数据,那么这个值将会为零。
PointerToRawData
指出零该节区在硬盘文件中的地址,这个数值是从文件头开始算起的偏移量,也就是说这个地址是一个文件偏移地址(FOA)。它的值必须是FileAlignment的整数倍。如果这个部分仅包含未初始化的数据,则将此成员设置为零。
Characteristics
该成员指出了该节区的属性特征。其中的不同数据位代表了不同的属性,这些数据位组合起来就是这个节的属性特征,具体数值定义如下:
宏定义 | 数值 | 描述 |
---|---|---|
0x00000001 | 保留 | |
0x00000002 | 保留 | |
0x00000004 | 保留 | |
IMAGE_SCN_TYPE_NO_PAD | 0x00000008 | 废弃 替换为IMAGE_SCN_ALIGN_1BYTES |
0x00000010 | 保留 | |
IMAGE_SCN_CNT_CODE | 0x00000020 | 节中包含可执行代码。 |
IMAGE_SCN_CNT_INITIALIZED_DATA | 0x00000040 | 节中包含已初始化数据。 |
IMAGE_SCN_CNT_UNINITIALIZED_DATA | 0x00000080 | 节中包含未初始化数据。 |
IMAGE_SCN_LNK_OTHER | 0x00000100 | 保留 |
IMAGE_SCN_LNK_INFO | 0x00000200 | 节中包含注释或其他信息,对目标文件有效。 |
0x00000400 | 保留 | |
IMAGE_SCN_LNK_REMOVE | 0x00000800 | 该节不会成为镜像文件的一部分,对目标文件有效。 |
IMAGE_SCN_LNK_COMDAT | 0x00001000 | 该节包含COMDAT数据,对目标文件有效。 |
0x00002000 | 保留 | |
IMAGE_SCN_NO_DEFER_SPEC_EXC | 0x00004000 | 重新计算异常处理TLB项中的位 |
IMAGE_SCN_GPREL | 0x00008000 | 节中包含通过全局指针引用的数据。 |
0x00010000 | 保留 | |
IMAGE_SCN_MEM_PURGEABLE | 0x00020000 | 保留 |
IMAGE_SCN_MEM_LOCKED | 0x00040000 | 保留 |
IMAGE_SCN_MEM_PRELOAD | 0x00080000 | 保留 |
IMAGE_SCN_ALIGN_1BYTES | 0x00100000 | 在1字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_2BYTES | 0x00200000 | 在2字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_4BYTES | 0x00300000 | 在4字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_8BYTES | 0x00400000 | 在8字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_16BYTES | 0x00500000 | 在16字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_32BYTES | 0x00600000 | 在32字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_64BYTES | 0x00700000 | 在64字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_128BYTES | 0x00800000 | 在128字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_256BYTES | 0x00900000 | 在256字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_512BYTES | 0x00A00000 | 在512字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_1024BYTES | 0x00B00000 | 在1024字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_2048BYTES | 0x00C00000 | 在2048字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_4096BYTES | 0x00D00000 | 在4096字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_ALIGN_8192BYTES | 0x00E00000 | 在8192字节边界上对齐数据,对目标文件有效。 |
IMAGE_SCN_LNK_NRELOC_OVFL | 0x01000000 | 此节包含扩展的重定位信息。 |
IMAGE_SCN_MEM_DISCARDABLE | 0x02000000 | 此节可以在需要时被丢弃。 |
IMAGE_SCN_MEM_NOT_CACHED | 0x04000000 | 此节无法缓存。 |
IMAGE_SCN_MEM_NOT_PAGED | 0x08000000 | 此节无法分页。 |
IMAGE_SCN_MEM_SHARED | 0x10000000 | 此节可以在内存中共享。 |
IMAGE_SCN_MEM_EXECUTE | 0x20000000 | 此节可以作为代码执行。 |
IMAGE_SCN_MEM_READ | 0x40000000 | 此节可读。 |
IMAGE_SCN_MEM_WRITE | 0x80000000 | 此节可写。 |
节表在PE文件头中的排列位置比较特殊,节表是紧跟在NT头(也可以说是可选PE头后)后的,它实际上是一个IMAGE_SECTION_HEADER类型的数组,数组的成员个数被定义在IMAGE_FILE_HEADER中的NumberOfSections成员上,需要注意的是在最后一个节表后最好应该有一个与节表同样大小的用0x00填充的空白数据。
空白区添加代码
思路:pe文件有节区对齐,每个节区的最后都有填充0,找到充填0的地方写入硬编码,最后用jmp跳到原入口,修改原入口为刚写入的代码
扩大最后一个节
1. 在文件的最后新增4096个字节(0x1000)
2. 修改最后一个节的SizeOfRawData和VirtualSize为N
N = max(SizeOfRawData, VirtualSize) + 0X1000
3.修改SizeOfImage的大小
4 为最后一个节添加可执行属性
在文件的最后新增4096个字节(0x1000)
注意是否插入到了最后一个字节的前面
修改最后一个节的SizeOfRawData和VirtualSize为N
max(5f10 , 6000 ) + 0X1000 = 7000
修改SizeOfImage的大小
100+4(PE指纹)+14(PE标准头)+38(镜像大小的偏移) = 150
c0000 + 1000 = c1000
新增节
1. 在节表后面新增一个节,拷贝.text这个节的信息,因为我们的节和它一样也是可执行的
Name改为.abcd
VirtualSize内存中块大小:0x1000H
VirtaualAddress内存中块RVA值:最后一个节rsrc的VirtaualAddress + VirtualSize = BA000 + 7000 = C1000
SizeOfRawData文件中块大小:0x1000H
PointerToRawData文件中块偏移:最后一个节rsrc的PointerToRawData + SizeOfRawData = A9000 + 7000 = B0000
后面的和.text一样即可
2. 修改标准PE头中的块数目(NumberOfSections) 4改成5
3. 修改SizeOfImage的大小 原值+0x1000
4. 在文件的最后插入0x1000字节
IMAGE_DATA_DIRECTORY
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; /**指向某个数据的相对虚拟地址 RAV 偏移0x00**/
DWORD Size; /**某个数据块的大小 偏移0x04**/
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
在这个数据目录结构体中只有两个成员VirtualAddress
和Size
,这两个成员的含义比较简单,VirtualAddress指定了数据块的相对虚拟地址(RVA)。虚拟地址转文件偏移要经过计算,Size则指定了该数据块的大小,有时并不是该类型数据的总大小,可能只是该类型数据一个数据项的大小。这两个成员(主要是VirtualAddress)成为了定位各种表的关键,所以一定要知道每个数组元素所指向的数据块类型,以下表格就是它的对应关系:
英文描述 | 数组下标 | 中文描述 |
---|---|---|
Export table address and size | 0x00 | 导出表的地址和大小 |
Import table address and size | 0x01 | 导入表的地址和大小 |
Resource table address and size | 0x02 | 资源表的地址和大小 |
Exception table address and size | 0x03 | 异常表的地址和大小 |
Certificate table address and size | 0x04 | 证书表的地址和大小 |
Base relocation table address and size | 0x05 | 基址重定位表的地址和大小 |
Debugging information starting address and size | 0x06 | 调试信息的起始地址和大小 |
Architecture-specific data address and size | 0x07 | 特定于体系结构数据的地址和大小 |
Global pointer register relative virtual address | 0x08 | 全局指针寄存器相对虚拟地址 |
Thread local storage (TLS) table address and size | 0x09 | (线程本地存储)TLS表的地址和大小 |
Load configuration table address and size | 0x0A | 加载配置表地址和大小 |
Bound import table address and size | 0x0B | 绑定导入表的地址和大小 |
Import address table address and size | 0x0C | 导入地址表的地址和大小 |
Delay import descriptor address and size | 0x0D | 延迟导入表的地址和大小 |
The CLR header address and size | 0x0E | CLR运行时头部数据地址和大小 |
Reserved | 0x0F | 保留 |
IMAGE_EXPORT_DIRECTORY——导出表
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 未使用,总为0
DWORD TimeDateStamp; // 文件创建时间戳
WORD MajorVersion; // 未使用,总为0
WORD MinorVersion; // 未使用,总为0
DWORD Name; // **重要 指向一个代表此 DLL名字的 ASCII字符串的 RVA
DWORD Base; // **重要 函数的起始序号
DWORD NumberOfFunctions; // **重要 导出函数地址表的个数
DWORD NumberOfNames; // **重要 以函数名字导出的函数个数
DWORD AddressOfFunctions; // **重要 导出函数地址表RVA
DWORD AddressOfNames; // **重要 导出函数名称表RVA
DWORD AddressOfNameOrdinals; // **重要 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
导出表简介:在导出表中前四个成员基本没有用,我们就不用去管他,但是剩下的成员都是非常重要的,我们会通过讲解导出表的结构时顺带介绍。现在我们来说说导出表的作用,简单来说导出表就是用来描述模块中的导出函数的结构,导出函数就是将功能的提供给外部使用的函数,如果一个PE文件导出了函数,那么这个函数的信息就会记录PE文件的导出表中,方便外部程序加载该文件进行动态调用。可能有时函数在导出表中只有一个序号而没有名字,也就造成了导出表中有了三个子表的存在,分别是:函数地址表、函数名称表和函数序号表。使得外部程序可以通过函数名称和函数序号两种方式获取该函数的地址。
//系统中获取函数地址的两种方法:
HMODULE hModule = LoadLibraryA("User32.dll");
//1、函数名获取
DWORD FuncAddress = GetProcAddress(hModule, "MessageBoxA");
//2、序号获取
DWORD FuncAddress = GetProcAddress(hModule, 12);
AddressOfFunctions
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的地址表,这个地址表可以当作一个成员宽度为4的数组进行处理,它的长度由NumberOfFunctions进行限定,地址表中的成员也是一个RVA地址,在内存中加上ImageBase后才是函数真正的地址。
AddressOfNames
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的名称表,这个名称表也可以当作一个成员宽度为4的数组进行处理,它的长度由NumberOfNames进行限定,名称表的成员也是一个RVA地址,在FIleBuffer状态下需要进行RVA到FOA的转换才能真正找到函数名称。
AddressOfNameOrdinals
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的序号表,这个序号表可以当作一个成员宽度为2的数组进行处理,它的长度由NumberOfNames进行限定,名称表的成员是一个函数序号,该序号用于通过名称获取函数地址。
NumberOfFunctions
注意,这个值并不是真的函数数量,他是通过函数序号表中最大的序号减去最小的序号再加上一得到的,例如:一共导出了3个函数,序号分别是:0、2、4,NumberOfFunctions = 4 - 0 + 1 = 5个。
定位导出表
先定位到IMAGE_DATA_DIRECTORY 数据目录项
PE指纹4字节+标准PE头0x14字节+(扩展PE头0xE0-0x80=0x60) = 0x78
PE指纹开始地址0x120 + 0x78 = 198
这个Dll的内存对齐和文件对齐是一样的所以内存中偏移和文件中的偏移是一样的 0x938A0
Name:0x938d2 run.dll
导出表结构图:
通过导出表查找函数地址的两种方法:
1、通过函数名查找函数地址:
(1)、首先定位函数名表,然后通过函数名表中的RVA地址定位函数名,通过比对函数名获取目标函数名的在函数名表中的索引。
(2)、通过获取函数名表的索引获取函数序号表中对应索引中的函数序号。
(3)、通过把该序号当作函数地址表的下标,就可以得到该下标中的函数地址。
2、通过函数序号查找函数地址:
(1)、首先计算函数地址表的索引:index = 目标函数的函数序号 - 导出表的Base。
(2)、通过计算出的索引就可以在函数地址表中获取到目标序号的函数地址。
注:通过序号获取函数地址不需要使用函数名称表和函数序号表就可以直接获取函数地址,实现上相对来说比较方便。
IMAGE_IMPORT_DESCRIPTOR——导入表
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //导入名称表(INT)的RVA地址
} DUMMYUNIONNAME;
DWORD TimeDateStamp; //时间戳多数情况可忽略 如果是0xFFFFFFFF表示IAT表被绑定为函数地址
DWORD ForwarderChain;
DWORD Name; //导入DLL文件名的RVA地址
DWORD FirstThunk; //导入地址表(IAT)的RVA地址
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
导入表简介:PE文件使用来自于其他DLL的代码或数据是,称作导入(或者输入)。当PE文件装入时,Windows装载器的工作之一就是定位所有被输入的函数和数据,并且让正在被装入的问渐渐可以使用这些地址。这个过程就是通过PE文件的导入表来完成的,导入表中保存的是函数名和其驻留的DLL名等动态链接所需的信息。
OriginalFirstThunk
这个值是一个4字节的RVA地址,这个地址指向了导入名称表(INT),INT是一个IMAGE_THUNK_DATA结构体数组,这个结构体的最后一个成员内容为0时数组结束。这个数组的每一个成员又指向了一个IMAGE_IMPORT_BY_NAME结构体,这个结构体包含了两个成员函数序号和函数名,不过这个序号一般没什么用,所以有的编译器会把函数序号置0。函数名可以当作一个以0结尾的字符串。(注:这个表不在目录项中。)
Name
DLL名字的指针,是一个RVA地址,指向了一个以0结尾的ASCII字符串。
FirstThunk
这个值是一个4字节的RVA地址,这个地址指向了导入地址表(IAT),这个IAT和INT一样,也是一个IMAGE_THUNK_DATA结构体数组,不过它在程序载入前和载入后由两种状态,在程序载入前它的结构和内容和INT表完全一样,但却是两个不同的表,指向了IMAGE_IMPORT_BY_NAME结构体。在程序载入后,他的结构和INT表一样,但内容就不一样了,里面存放的都是导入函数的地址。(注:这个表在目录项中,需要注意。)
IMAGE_THUNK_DATA——INT、IAT的结构体定义如下:
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME 的地址RVA
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
//注:这个结构体是联合类型的,每一个成员都是4字节,所以为了编程方便,完全可以用一个4字节的数组取代它。
IMAGE_IMPORT_BY_NAME 结构体定义如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //可能为空,编译器决定,如果不为空是函数在导出表中的索引
CHAR Name[1]; //函数名称,以0结尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
//注:这个结构体由两个成员组成,大致一看它的大小是3个字节,其实它的大小是不固定的,
// 因为无法判断函数名的长度,所以最后一个成员是一个以0结尾的字符串。
定位导入表
先定位到IMAGE_DATA_DIRECTORY 数据目录项
导入表结构体IMAGE_IMPORT_DESCRIPTOR数组,一个pe文件的导入表有多个,每个IMAGE_IMPORT_DESCRIPTOR的大小为20字节,最后20个字节为0表示结束
第一张导入表的Name:
第二张导入表的Name
第一张导入表的INT导入名称表数组
第一地址指向的IMAGE_IMPORT_BY_NAME,可以看出后面紧接着的就是第二个地址指向的IMAGE_IMPORT_BY_NAME
EXE文件载入后IAT表的状态:
注:我们随便用OD载入一个EXE文件,找到一个Kernel32.DLL的函数GetStartupInfoA,双击这条反汇编看看它的指令,发现call的是0x41D034中存放的内容,接着我们搜索这个地址发现里面存放了一个函数的地址,而这个函数正好就是GetStartupInfoA。于是我们得知在程序载入后,IAT表中存放的是函数的地址,而不是一个RVA地址。
EXE文件载入后对应的导入表结构图:
EXE文件载入前IAT表的状态:
注:为了查看0x41D034这个地址在程序载入前存放的内容,我们就要将这个地址减去ImageBase得到一个RAV地址:0x01D034,由于这个PE文件的FileAlignment和SectionAlignment是一样的(都是0x1000),用16进制编辑器打开这个文件直接跳转到0x01D034这个地址就可以获得里面的内容了。跳转到这个地址后发现里面存储的是一个RVA地址,并不是函数地址。我们就进行跳转到0x23256这个RVA地址,我们就可以发现它指向了IMAGE_IMPORT_BY_NAME结构体,这个结构体存储的函数名刚好就是GetStartupInfoA。所以我们就可以断定载入前和载入后的IAT表是不一样的。
EXE文件载入前对应的导入表结构图:
IMAGE_BOUND_IMPORT_DESCRIPTOR——绑定导入表
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp; //时间戳
WORD OffsetModuleName; //DLL名的地址偏移
WORD NumberOfModuleForwarderRefs; //该结构后IMAGE_BOUND_FORWARDER_REF数组的数量
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
绑定导入表简介:绑定导入是一个文件快速启动的技术,但是只能起到辅助的效果,它的存在只会影响到PE文件的加载过程,并不会影响PE文件的运行结果,这也就是说把绑定导入的信息从PE文件中清除后对这个PE文件的运行结果没有任何影响。从导入表部分我们可以知道,FirstThunk这个成员指向了IAT表,在程序加载时加载器会通过INT表来修复IAT表,使里面存放上对应函数的地址信息,但是如果导入的函数太多在加载过程中就会使程序启动变慢,绑定导入就是为了减少IAT表的修复时间。它会在程序加载前修复IAT表,然后在PE文件中声明绑定导入的数据信息,让操作系统知道这些事情已经提前完成。这就是绑定导入表的作用。
TimeDateStamp
这个时间戳相对来说还是比较重要的,因为这个值只有和导入DLL的IMAGE_FILE_HEADER中的TimeDateStamp值相同才能起到绑定导入的效果,如果不一致加载器就会重新计算IAT表中的函数地址。(由于DLL文件的版本不同或者DLL文件的ImageBase被重定位时,IAT绑定的函数的地址就会发生变化)
OffsetModuleName
这个偏移不是RVA页不是FOA,所以模块名的定位与之前的方法不同,它的定位方式是以第一个IMAGE_BOUND_IMPORT_DESCRIPTOR的地址为基址,加上OffsetModuleName的值就是模块名所在的地址了,这个模块名是以0结尾的ASCII字符串。
NumberOfModuleForwarderRefs
这个值是在IMAGE_BOUND_IMPORT_DESCRIPTOR结构后跟随的IMAGE_BOUND_FORWARDER_REF结构的数量。在每一个IMAGE_BOUND_IMPORT_DESCRIPTOR结构后都会跟随着大于等于0个IMAGE_BOUND_FORWARDER_REF结构,然后在其后面又会跟上绑定表结构体,直至全部用0填充的绑定表结构。
typedef struct _IMAGE_BOUND_FORWARDER_REF {
DWORD TimeDateStamp; //时间戳
WORD OffsetModuleName; //DLL名的地址偏移
WORD Reserved; //保留
} IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;
//注:
// 该结构中的成员和绑定导入表的成员含含义一致,所以不再过多叙述。
// 由于IMAGE_BOUND_IMPORT_DESCRIPTOR和IMAGE_BOUND_FORWARDER_REF的大小结构相同,所以可以相互转型,方便编程。
绑定导入表结构图:
IMAGE_BASE_RELOCATION——重定位表
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; 重定位数据所在页的RVA
DWORD SizeOfBlock; 当前页中重定位数据块的大小
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
重定位表简介:正如我们所知,在程序运行时系统首先会给程序分配一个4GB的虚拟内存空间,低2G空间用于放置EXE文件和DLL文件,高2G空间则是用于取得程序使用(这个空间所有程序共享)。系统随后就会将EXE文件第一个贴入低2G空间占据文件指定的ImageBase,所以EXE文件有时会没有重定位表,因为ImageBase区域大多数情况是可以使用的,也就不需要重定位。贴完EXE文件后接下来就会将大量程序使用的DLL文件贴入虚拟空间,然而这些DLL文件的ImageBase可能会发生冲突,所以有些DLL文件就不会被贴入指定的地址,但是为了让程序正常运行就只能将这些DLL贴入其他的地址。但是在PE文件中很多地址都是被编译器写死固定的(例子在下方代码块),如果基址改变这些地址就会无法使用,为了避免这样的事情发生就需要修正这些固定的地址,所以就有了重定位表。重定位表就是记录了这些需要修正的地址,在ImageBase发生改变时就会进行修正重定位表。
修正方法:需要重定位的地址 - 以前的基址 + 当前的基址。
//需要重定位的值013EBC98h和013ED49Ch
printf("Helloworld %s", "hahaha");
013E64B2 68 98 BC 3E 01 push offset string "hahaha" (013EBC98h)
013E64B7 68 9C D4 3E 01 push offset string "Helloworld %s" (013ED49Ch)
013E64BC E8 A3 AB FF FF call _printf (013E1064h)
013E64C1 83 C4 08 add esp,8
VirtualAddress
这个虚拟地址是一组重定位数据的开始RVA地址,只有重定位项的有效数据加上这个值才是重定位数据真正的RVA地址。
SizeOfBlock
它是当前重定位块的总大小,因为VirtualAddress和SizeOfBlock都是4字节的,所以(SizeOfBlock - 8)才是该块所有重定位项的大小,(SizeOfBlock - 8) / 2就是该块所有重定位项的数目。
重定位项
重定位项在该结构中没有体现出来,他的位置是紧挨着这个结构的,可以把他当作一个数组,宽度为2字节,每一个重定位项分为两个部分:高4位和低12位。高4位表示了重定位数据的类型(0x00没有任何作用仅仅用作数据填充,为了4字节对齐。0x03表示这个数据是重定位数据,需要修正。0x0A出现在64位程序中,也是需要修正的地址),低12位就是重定位数据相对于VirtualAddress的偏移,也就是上面所说的有效数据。之所以是12位,是因为12位的大小足够表示该块中的所有地址(每一个数据块表示一个页中的所有重定位数据,一个页的大小位0x1000)。
注:如果修改了EXE文件的ImageBase,就要手动修复它的重定位表,因为系统会判断程序载入地址和ImageBase是否一致,如果一致就不会自动修复重定位表,双击运行时就会报错。
重定位表结构:
通过重定位表找到需要修正的数据:
定位重定位表
先定位到IMAGE_DATA_DIRECTORY 数据目录项
先定位到IMAGE_DATA_DIRECTORY 数据目录项
数组的第六个:c9000 是虚拟地址 通过节表可以看出是在.reloc这个节计算偏移c900-c900(节开始地址)= 0,这个节对应的ROF为A9000+0=A9000
数组的第一个IMAGE_BASE_RELOCATION
IMAGE_RESOURCE_DIRECTORY——资源表
//资源目录头
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; //资源属性 一般为0
DWORD TimeDateStamp; //资源创建时间戳 一般为0
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries; //以名称命名的目录项数量 重要
WORD NumberOfIdEntries; //以ID命名的目录项数量 重要
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
//资源目录项
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31; //字符串的偏移(不是RVA、FOA,相对特殊)
DWORD NameIsString:1; //判断名字是否是字符串 1:是 0:不是
} DUMMYSTRUCTNAME;
DWORD Name;
WORD Id; //目录项的ID(在一级目录指资源类型,二级目录指资源编号,三级目录指代码的页号)
} DUMMYUNIONNAME;
union {
DWORD OffsetToData; //如果不是目录,这里指数据的偏移(不是RVA、FOA,相对特殊)
struct {
DWORD OffsetToDirectory:31;//目录的偏移(不是RVA、FOA,相对特殊)
DWORD DataIsDirectory:1; //判断子资源项是否是目录 1:是 0:不是
} DUMMYSTRUCTNAME2;
} DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
//数据项
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; //数据的偏移 重要
DWORD Size; //数据的大小 重要
DWORD CodePage; //代码页(一般为0)
DWORD Reserved; //保留
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
//名字字符串结构
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length; //Unicode字符串长度
WCHAR NameString[ 1 ]; //Unicode字符串
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
资源表简介:在Windows程序中其各种界面被称作为资源,其中被系统预先定义的资源类型包括:鼠标指针,位图, 图标,菜单,对话框, 字符串列表,字体目录, 字体,加速键,非格式化资源,消息列表,鼠标指针组,图标组,版本信息。当然还有用户自定义的资源类型,这些资源的就不举例了。这些资源都是以二进制的形式保存到PE文件中,而保存资源信息的结构就是资源表,它位于目录项的第三位。在PE文件的所有结构中,资源表的结构最为复杂,这是因为资源表用类似于文件目录结构的方式进行保存的,从根目录开始,下设一级目录、二级目录和三级目录,三级目录下才是资源文件的信息,而且资源表的结构定位也是最为特殊的,希望重点掌握。
一级目录是按照资源类型分类的,如位图资源、光标资源、图标资源。
二级目录是按照资源编号分类的,同样是菜单资源,其子目录通过资源ID编号分类,例如:IDM_OPEN的ID号是2001h,IDM_EXIT的ID号是2002h等多个菜单编号。
三级目录是按照资源的代码页分类的,即不同语言的代码页对应不同的代码页编号,例如:简体中文代码页编号是2052。
三级目录下是节点,也称为资源数据,这是一个IMAGE_RESOURCE_DATA_ENTRY的数据结构,里面保存了资源的RVA地址、资源的大小,对所有资源数据块的访问都是从这里开始的。
注:资源表的一级目录、二级目录、三级目录的目录结构是相同的都是由一个资源目录头加上一个资源目录项数组组成的,可以将这个结构称作资源目录结构单元。
IMAGE_RESOURCE_DIRECTORY.NumberOfNamedEntries
和IMAGE_RESOURCE_DIRECTORY.NumberOfIdEntries
在资源目录头结构中这两个字段是最为重要的,其他字段大部分为0。NumberOfNamedEntries表示在该资源目录头后跟随的资源目录项中以IMAGE_RESOURCE_DIR_STRING_U结构命名的资源目录项数量。NumberOfIdEntries表示在该资源目录头后跟随的资源目录项中以ID命名的资源目录项数量。两个字段加起来就是本资源目录头后的资源目录项的数量总和。也就是后面IMAGE_RESOURCE_DIRECTORY_ENTRY结构的总数量。
IMAGE_RESOURCE_DIRECTORY_ENTRY.DUMMYUNIONNAME
在资源目录项中该字段是一个联合体类型,大小为4个字节,它决定这个资源目录的名字是字符串还是ID号。如果这个字段的最高位是1,则表示该资源的名字是字符串类型,该字段的低31位是IMAGE_RESOURCE_DIR_STRING_U结构的偏移,但这个偏移既不是FOA也不是RVA,它是以首个资源表的地址为基址,加上低31位的值才是字符串结构的地址。如果最高位为0,则表示该资源的名字是一个ID号,整个字段的值就是该资源的ID。(如果是一级目录的资源项,该ID有14个号码被预先定义了)
一级目录中预定义的资源ID:
资源ID | 含义 | 资源ID | 含义 | |
---|---|---|---|---|
0x01 | 鼠标指针(Cursor) | 0x08 | 字体(Font) | |
0x02 | 位图(Bitmap) | 0x09 | 加速键(Accelerators) | |
0x03 | 图标(Icon) | 0x0A | 非格式化资源(Unformatted) | |
0x04 | 菜单(Menu) | 0x0B | 消息列表(Message Table) | |
0x05 | 对话框(Dialog) | 0x0C | 鼠标指针组(Group Cursor) | |
0x06 | 字符串列表(String) | 0x0E | 图标组(Group Icon) | |
0x07 | 字体目录(Font Directory) | 0x10 | 版本信息(Version Information) |
IMAGE_RESOURCE_DIRECTORY_ENTRY.DUMMYUNIONNAME2
在资源目录项中该字段是一个联合体类型,大小为4个字节,它决定这个资源目录的目录中子节点的类型(是目录还是节点)。如果这个字段的最高位是1,则表示该资源的子节点是一个目录类型,该字段的低31位是子目录的资源目录头结构的偏移,但这个偏移既不是FOA也不是RVA,它是以首个资源表的地址为基址,加上低31位的值才是资源目录头结构的地址。如果最高位为0,则表示该资源的子节点是一个节点,它也以首个资源表的地址为基址,整个字段的值就是该资源节点的偏移。这个节点是IMAGE_RESOURCE_DATA_ENTRY类型的结构体。(一般在三级目录中该字段的最高位位0,而在其他两个目录中该字段的最高位为1)
注:为了编程方便,IMAGE_RESOURCE_DIRECTORY_ENTRY的联合体中出现了一组特殊的struct结构体,其成员声明格式为:[类型] [变量名] : [位宽表达式], 这个格式就是C语言中位段的声明格式。NameOffset字段的值等于该联合体的低31位,NameIsString字段的值等于该联合体的最高位。将一个4字节的类型拆成这样两个字段就可以方便的避免了繁琐的位操作了,而且该结构的总大小不会发生变化。
IMAGE_RESOURCE_DATA_ENTRY
这个结构体就是目录资源的三级目录下的子目录,里面存储的就是资源文件的信息,如OffsetToData字段存储的就是资源文件的RVA地址,它指向了资源的二进制信息,Size字段存储的就是资源文件的大小,CodePage字段存储资源的代码页但大多数情况为0。
注:在其指向的资源数据中,字符串都是Unicode的编码方式,每个字符都是由一个16位(一个单字)的值表示,并且都是以UNICODE_NULL结束(其实就是两个0x00)。
IMAGE_RESOURCE_DIR_STRING_U
该结构体就是目录资源的名称结构,里面存在两个字段,都是2个字节,Length字段存储的是目录资源名称的长度,以2个字节为单位。NameString字段是一个Unicode字符串的第一个字符,并不以0结尾,其长度是由Length字段限制。该结构的总大小并不是表面上的4个字节,而是根据名字长度变化的,计算方式为:Size = SizeOf(WCHAR) * (Length + 1); 这里的1是Length字段的大小。
资源表的结构图(简图):