PE文件结构
[TOC]
(图片未加载请使用科学上网)
PE文件结构
PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等
一般来说 PE文件结构依次为DOS头,NT头,节表,以及各种节
PE 结构是由若干个复杂的结构体组合而成的,不是单单的一个结构体那么简单,它的结构就像文件系统的结构是由多个结构体组成的。
1.1 DOS头
DOS头由两部分组成:DOS MZ文件头和DOS块。
DOS头是一个结构体 16位的程序标准情况如下:
1 |
|
e_magic: DOS文件的魔术数字,通常为0x5A4D
(’MZ’)。这是用来标识该文件为一个DOS可执行文件。
e_cblp: 文件最后一页的字节数。
e_cp: 文件中总共有多少个页面。
e_crlc: 重定位项的数量。
e_cparhdr: 文件头的大小,以段为单位。
e_minalloc: 该程序需要的最小附加段数。
e_maxalloc: 该程序允许的最大附加段数。
e_ss: 初始堆栈段寄存器(SS)的值。
e_sp: 初始堆栈指针寄存器(SP)的值。
e_csum: 用于校验文件的校验和。
e_ip: 初始指令指针寄存器(IP)的值。
e_cs: 初始代码段寄存器(CS)的值。
e_lfarlc: 文件中重定位表的位置。
e_ovno: 文件的覆盖号,指示是否支持覆盖。
e_res: 一组保留字段,通常用于未来扩展。
e_oemid 和 e_oeminfo: 用于OEM特定的信息,通常在某些特定的设备或应用中使用。
e_res2: 另一组保留字段。
e_lfanew: 新的EXE头部的文件地址,指向PE头部的起始位置,通常是文件的后面部分,标志着从DOS格式过渡到PE格式。
但是平时我们分析的都是32位和64位程序 仅仅使用了其中两个成员部分:
e_magic 和e_lfanew,它们是标识和Windows寻找PE头的位置的
其他的成员可以使用0x00填充,程序不受影响可以正常运行,但是它们两个不可以
使用010打开Hello_re查看
前两字节就是e_magic(0x5A4D–MZ)
这两个字母就是Mark Zbikowski的姓名缩写,他是最初的MS-DOS设计者之一。如果把PE文件的这两个字节修改成其他数据,运行该PE文件就会无法正常运行,跳出黑窗口打印Program too big to fit in memory然后闪退
010很贴心的标记了各个部分 第一段红色部分就是DOS MZ文件头(0x40h)64字节
由于这不是16位程序 我们可以跳过中间部分
最后的e_lfanew是DWORD类型 4个字节 也就是图中标红部分的最后四个字节 代表着PE头所在的偏移量
显然是80 00 00 00也就是指向了00000080 如图:
注意到在DOS MZ文件头和PE头中间还有一部分数据(橘黄色部分)
也就是我们所谓的DOS块
而DOS块不是结构体,而是由单个字节组成的数据,可以填写任何内容。
而注意到其中的值是可以翻译为可读字符串的(右方)
“This program cannot be run in DOS mode.”
这涉及到Dos头的另一个作用
DOS头是用来兼容MS-DOS操作系统的,目的是当这个文件在MS-DOS上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode.还有一个目的,就是指明NT头在文件中的位置。(PE头有时又叫NT头)
这部分数据在实际操作中常常可以忽略
1.2 IMAGE_NT_HEADERS–PE头(NT头)
在MS-DOS头下main,就是PE头,PE头是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,其中包含许多PE装载器用到的重要字段。
NT头仍然是一个结构体样式
1 |
|
跟随偏移我们找到了NT头的起始位置
很容易注意到开头四个字节(DWORD)是有意义的字符PE
也就是所谓的PE标识部分(Signature)
全部应该为”PE\0\0” 标志着PE文件头的开始 而DOS头最后的e_lfanew数据实际指向的也是这个位置
接下来两个成员为文件头(IMAGE_FILE_HEADER)和可选文件头(IMAGE_OPTIONAL_HEADER32)
注意到是没有大小定义的 实际上它们也是结构体
1.2.1 IMAGE_FILE_HEADER
同样也有结构体定义
1 |
|
注意到WORD二字节 DWORD四字节 所以我们可以给程序分段
第一个框说明了运行的平台标识
1 |
|
说明该程序是AMD64的
第三个字段是时间戳
用于表示该PE文件创建的时间,时间是从国际协调时间也就是1970年1月1日00:00
起开始计数的,计数单位是秒
第四到第五个字段略 见注释即可
第六个字段SizeOfOptionalHeader(倒数第二个二字节框)
标识了IMAGE_OPTIONAL_HEADER结构的大小 也能看出是64位还是32位的文件
32位文件值通常是00EOh,对于64位值通常为00F0h
可见该程序是64位的
最后一个字段是文件属性
不一定是以下某个值 也可以是某几个异或得到
1 |
|
实际上 010仍然很贴心的给出了结构体定义 鼠标放在某段数据上即可 例如在本程序中
1.2.2 IMAGE_OPTIONAL_HEADER
注:在不同的平台下是不一样的,例如32位下是IMAGE_OPTIONAL_HEADER32,而在64位下是IMAGE_OPTIONAL_HEADER64。
这个结构是IMAGE_FILE_HEADER结构的补充。这两个结构合起来才能对整个PE文件头进行描述。
结构体如下(左边是相对于文件头的偏移量 也就是刚刚图片红框后面的部分开始)
1 |
|
第一个字段
1 |
|
注意
1 |
|
AddressOfEntryPoint
是入口地址 如果想在一个可执行文件中附加了一段代码并且要让这段代码首先被执行,就可以通过更改入口地址到目标代码上,然后再跳转回原有的入口地址
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的倍数,值的计算方式为:
1 |
|
1.2.2.1 DataDirectory表
1 |
|
对于这个结构体数组 每一个成员都包含一个地址和一个大小 定义了一个区域 事实上 数组中的每一项对应一个特定的数据结构,包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构
1 |
|
注意到这里是NumberOfRvaAndSizes字段 那么之后的就是这个结构体数组 16*8共128个字节
1.2.2.1.1 IMAGE_EXPORT_DIRECTORY——导出表
1 |
|
导出表简介:在导出表中前四个成员基本没有用,我们就不用去管他,但是剩下的成员都是非常重要的。现在我们来说说导出表的作用,简单来说导出表就是用来描述模块中的导出函数的结构,导出函数就是将功能的提供给外部使用的函数,如果一个PE文件导出了函数,那么这个函数的信息就会记录PE文件的导出表中,方便外部程序加载该文件进行动态调用。可能有时函数在导出表中只有一个序号而没有名字,也就造成了导出表中有了三个子表的存在,分别是:函数地址表、函数名称表和函数序号表。使得外部程序可以通过函数名称和函数序号两种方式获取该函数的地址。
1 |
|
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.2.2.1.2 IMAGE_IMPORT_DESCRIPTOR——导入表
1 |
|
导入表简介: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的结构体:
1 |
|
IMAGE_IMPORT_BY_NAME 结构体:
1 |
|
1.2.2.1.3 IMAGE_RESOURCE_DIRECTORY——资源表
1 |
|
资源表简介:在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:
1 |
|
在资源目录项中该字段是一个联合体类型,大小为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字段的大小。
1.2.2.1.4 IMAGE_BASE_RELOCATION——重定位表
1 |
|
重定位表简介:正如我们所知,在程序运行时系统首先会给程序分配一个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.3 节表
一个节表(IMAGE_SECTION_TABLE)是由很多个节(IMAGE_SECTION_HEADER)组成的,实际上是一个IMAGE_SECTION_HEADER类型的数组,数组的成员个数被定义在IMAGE_FILE_HEADER中的NumberOfSections成员
1.3.1 IMAGE_SECTION_HEADER
1 |
|
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 |
|
1.4 实践
1.4.1 扩大最后节
在文件的最后新增4096个字节(0x1000)
修改最后一个节的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.4.2 新增节
在节表后面新增一个节,拷贝.text这个节的信息,因为我们的节和它一样也是可执行的
Name改为.abcd
VirtualSize内存中块大小:0x1000H
VirtaualAddress内存中块RVA值:最后一个节rsrc的VirtaualAddress + VirtualSize = BA000 + 7000 = C1000
SizeOfRawData文件中块大小:0x1000H
PointerToRawData文件中块偏移:最后一个节rsrc的PointerToRawData + SizeOfRawData = A9000 + 7000 = B0000
后面的和.text一样即可
修改标准PE头中的块数目(NumberOfSections) 4改成5
修改SizeOfImage的大小 原值+0x1000
在文件的最后插入0x1000字节