具备基础: (1)掌握C语言
学习环境: VS2017 或 VC++6.0,Windows操作系统,LordPE(PE解释器)
地址空间:这个地址空间指的是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文件是由许许多多的结构体组成的,程序在运行时就会通过这些结构快速定位到PE文件的各种资源,其结构大致如图所示,从上到下依次是Dos头、Nt头、节表、节区和调试信息(可选)。其中Dos头、Nt头和节表在本文中统称为PE文件头(因为SizeOfHeaders就是这三个头的总大小)、节区则称为节,所以也可以说PE文件是由PE文件头和节组成。 PE文件头保存着整个PE文件的索引信息,可以帮助PE装载器定位资源,而节则保存着整个PE文件的所有资源。正因为如此,所以存在着这样的说法:头是节的描述,节是头的具体化。
IMAGE_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
结构体的起始位置)。
咱们看图说话
在IMAGE_DOS_HEADER
结构体后面紧跟着就是IMAGE_DOS_STUB
程序,它是运行在MS-DOS
下的可执行程序,当可执行文件运行于MS-DOS
下时,这个程序会打印This program cannot be run in DOS mode
这条消息。用户可以自己更改该程序,MS-DOS程序当前是可有可无的,如果你想使文件大小尽可能的小可以省掉MS-DOS程序,同时把前面的参数都清0。
IMAGE_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签名后无法识别后续内容的关系吧)。
修改PE签名后的运行结果:
如果把MZ标志和PE签名同时改变的话,其效果和只修改MZ是一样的,可见程序在载入时是先检测MZ标志然后才检测PE签名的:
IMAGE_FILE_HEADER的结构体定义如下:
Machine
所表示的是计算机的体系结构类型,也就是说这个成员可以指定该PE文件能够在32位还是在64位CPU上执行。如果强行更改该数值程序就会报错。该成员可以是以下的数值:
NumberOfSections
它的含义就是当前PE文件的节区数量,虽然它是大小是两个字节,但是在windows加载程序时会将节区的最大数量限制为96个。
从010 Editor模版上验证节区数量:
TimeDateStamp
它的含义是时间戳,用于表示该PE文件创建的时间,时间是从国际协调时间也就是1970年1月1日00:00
起开始计数的,计数单位是秒。例如0x5CFBB225的计算方法如下:
上图验证计算结果
SizeOfOptionalHeader
它存储该PE文件的可选PE头的大小,在32位PE文件中可选头大小为0xE0,64位可选头大小为0xF0。正因为如此,所以就必须通过该成员来确定可选PE头的大小。 Characteristics
它描述了PE文件的一些属性信息,比如是否可执行,是否是一个动态连接库等。该值可以是一个也可以是多个值的和,具体定义如下:
IMAGE_OPTIONAL_HEADER32的结构体定义如下(大部分成员不重要):
下方只对一些相对重要的成员进行讲解: Magic
这个无符号整数指出了镜像文件的状态,此成员可以是以下的值:
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
的整数倍。 SizeOfHeaders
该成员指定了PE文件头的大小,并且向上舍入为FileAlignment的倍数,值的计算方式为:
NumberOfRvaAndSizes
该成员指定了可选头中目录项的具体数目,由于以前发行的Windows NT的原因,它只能为10h。
该结构体中剩下的那一个成员比较特殊,会在下一章进行详细讲解。
IMAGE_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
该成员指出了该节区的属性特征。其中的不同数据位代表了不同的属性,这些数据位组合起来就是这个节的属性特征,具体数值定义如下:
节表各个成员意义图解:
1.通过编写控制台程序,将一个EXE文件读取到内存,打印出它所有的文件信息。(与LordPE的结果进行对照) 2.通过编写控制台程序,将一个EXE文件读取到内存(FileBuffer),在内存中将它进行拉伸(ImageBuffer),再压缩(NewFileBuffer),然后将压缩后的NewFileBuffer存盘并可以正常运行,实现PE加载过程。 3.通过编写控制台程序,将一个EXE文件读取到内存,在它的节表中新增一个节表和节区,存盘后让他可以正常运行。 4.通过编写控制台程序,将一个EXE文件读取到内存,把该文件的最后一个节扩大1000h,并保证程序的正常运行。 5.通过编写控制台程序,将一个EXE文件读取到内存,把该文件的所有节进行合并,并保证程序的正常运行。 6.通过编写控制台程序,将一个EXE文件读取到内存,在它的可执行节(代码节)中加一个弹出对话框(MessgeBox)的ShellCode,通过修改程序执行入口实现文件感染,可以正常运行。
练习小提示:
IMAGE_DATA_DIRECTORY的结构体定义如下:
在这个数据目录结构体中只有两个成员VirtualAddress
和Size
,这两个成员的含义比较简单,VirtualAddress指定了数据块的相对虚拟地址(RVA)。Size则指定了该数据块的大小,有时并不是该类型数据的总大小,可能只是该类型数据一个数据项的大小。这两个成员(主要是VirtualAddress)成为了定位各种表的关键,所以一定要知道每个数组元素所指向的数据块类型,以下表格就是它的对应关系:
IMAGE_EXPORT_DIRECTORY的结构体定义如下:
导出表简介:在导出表中前四个成员基本没有用,我们就不用去管他,但是剩下的成员都是非常重要的,我们会通过讲解导出表的结构时顺带介绍。现在我们来说说导出表的作用,简单来说导出表就是用来描述模块中的导出函数的结构,导出函数就是将功能的提供给外部使用的函数,如果一个PE文件导出了函数,那么这个函数的信息就会记录PE文件的导出表中,方便外部程序加载该文件进行动态调用。可能有时函数在导出表中只有一个序号而没有名字,也就造成了导出表中有了三个子表的存在,分别是:函数地址表、函数名称表和函数序号表。使得外部程序可以通过函数名称和函数序号两种方式获取该函数的地址。
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个。
导出表结构图:
通过导出表查找函数地址的两种方法:
1、通过函数名查找函数地址: (1)、首先定位函数名表,然后通过函数名表中的RVA地址定位函数名,通过比对函数名获取目标函数名的在函数名表中的索引。 (2)、通过获取函数名表的索引获取函数序号表中对应索引中的函数序号。 (3)、通过把该序号当作函数地址表的下标,就可以得到该下标中的函数地址。
2、通过函数序号查找函数地址: (1)、首先计算函数地址表的索引:index = 目标函数的函数序号 - 导出表的Base。 (2)、通过计算出的索引就可以在函数地址表中获取到目标序号的函数地址。 注:通过序号获取函数地址不需要使用函数名称表和函数序号表就可以直接获取函数地址,实现上相对来说比较方便。
1.通过编写控制台程序,打印导出表信息,并打印出函数地址表、函数名表、序号表。 2.写出按名字查找函数地址、按序号查找函数地址相关函数。 3.在PE文件中创建一个新节,然后将导出表的所有信息移动到新节中。最后将文件写入硬盘,并可以正确解析导出表。
练习小提示:
IMAGE_EXPORT_DIRECTORY的结构体定义如下:
重定位表简介:正如我们所知,在程序运行时系统首先会给程序分配一个4GB的虚拟内存空间,低2G空间用于放置EXE文件和DLL文件,高2G空间则是用于取得程序使用(这个空间所有程序共享)。系统随后就会将EXE文件第一个贴入低2G空间占据文件指定的ImageBase,所以EXE文件有时会没有重定位表,因为ImageBase区域大多数情况是可以使用的,也就不需要重定位。贴完EXE文件后接下来就会将大量程序使用的DLL文件贴入虚拟空间,然而这些DLL文件的ImageBase可能会发生冲突,所以有些DLL文件就不会被贴入指定的地址,但是为了让程序正常运行就只能将这些DLL贴入其他的地址。但是在PE文件中很多地址都是被编译器写死固定的(例子在下方代码块),如果基址改变这些地址就会无法使用,为了避免这样的事情发生就需要修正这些固定的地址,所以就有了重定位表。重定位表就是记录了这些需要修正的地址,在ImageBase发生改变时就会进行修正重定位表。 修正方法:需要重定位的地址 - 以前的基址 + 当前的基址。
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是否一致,如果一致就不会自动修复重定位表,双击运行时就会报错。
重定位表结构:
通过重定位表找到需要修正的数据:
1.通过编写控制台程序,打印出重定位表所有信息以及重定位项,同时找到需要修正的数据。 2.在PE文件中创建一个新节,然后将重定位表的所有信息移动到新节中。最后将文件写入硬盘,并可以正确解析重定位表。 3.改变EXE文件中的ImageBase,然后手动修复重定位表,使其能够正常运行。(EXE文件必须包含重定位表,否则会失败)
练习小提示:
1、代码比较简单,参考图片可以轻松完成,提供下参考打印格式: 2、移动重定位表的步骤: 1)在PE文件中创建一个新节 2)将重定位表的数据块循环拷贝到新的节区 3)修复目录项对应的虚拟地址 3、修复重定位表的方式: 修复结果 = 需要重定位的数据 - 以前的ImageBase + 现在的ImageBase;
IMAGE_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的结构体定义如下:
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文件载入前对应的导入表结构图:
1.通过编写控制台程序,打印导入表的导入文件名、INT表和IAT表。 2.在PE文件中创建一个新节,然后将导入表、INT表以及函数名、文件名移动到新节中。最后将文件写入硬盘,并可以执行。(有点难度)
练习小提示:
IMAGE_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填充的绑定表结构。
IMAGE_BOUND_IMPORT_DESCRIPTOR的结构体定义如下:
绑定导入表结构图:
1.通过编写控制台程序,打印绑定导入表的信息。(很多文件没有绑定导入表,需要判断)
练习小提示:
1、代码比较简单,参考图片可以轻松完成,提供下参考打印格式:
IMAGE_RESOURCE_DIRECTORY的结构体定义如下:(由四种结构体组成)
资源表简介:在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:
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字段的大小。
资源表的结构图:
资源表的结构图(简图):
手动寻找资源数据(为了方便程序的两个对齐方式的值都是0x1000):
1、通过可选PE头定位资源表,解析第一个资源目录项: (1)、可以得到资源目录头中一共有9个目录项。 (2)、在第一个目录项中以ID号命名,资源类型位2也就是位图资源。 (3)、在目录项的二个字段可以得知该目录的子节点也是目录,偏移是0x58,RVA = 0x2B000 + 0x58 = 0x2B058。
2、定位二级资源目录并解析: (1)、可以得到资源目录头中一共有1个目录项。 (2)、目录项以ID号命名,ID号为0x80。 (3)、在目录项的二个字段可以得知该目录的子节点也是目录,偏移是0x268,RVA = 0x2B000 + 0x268 = 0x2B268。
3、定位三级资源目录并解析: (1)、可以得到资源目录头中一共有1个目录项。 (2)、目录项以ID号命名,ID号为0x804,表示使用简体中文的代码页。 (3)、在目录项的二个字段可以得知该目录的子节点是数据项,偏移是0x6E8,RVA = 0x2B000 + 0x6E8 = 0x2B6E8。
4、定位数据项: (1)、资源数据的RVA地址:0x2C260. (2)、资源数据的大小:0x680.
1.通过编写控制台程序,打印资源表的信息。(可以考虑递归实现) 2.在PE文件中创建一个新节,然后将资源表移动到新节中。最后将文件写入硬盘,并可以正确解析资源表。
练习小提示:
文章写到这里,我们打造PE解析器的基础理论已经全部介绍完毕了,相信大家也都跟着文章完成了其中的练习题,那么接下来的事情就非常简单了,我们只要把所有的代码整合起来就可以实现一个简单PE解析器了,当然只是一个控制台程序。如果想要打造窗口程序就要具备一定的Windows编程经验了,然后将对应的内容输出出来就大功告成了。 希望同学们完成这最后一个任务!
写了一个月的文章完成,第一次发帖语言上有点生涩,如果文章有错误的地方希望能够指出,谢谢!
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-8-12 10:10
被QiuJYu编辑
,原因:
上传的附件: