PE-导出表

导出表

导出表描述了导出表所在PE文件向其他程序提供的可供调用的函数的情况。

一般情况下,PE中的导出表存在于动态链接库文件里。导出表的主要作用是将PE中存在的函数引出到外部,以便其他人可以使用这些函数,实现代码的重用。

3.1导出表作用

代码重用机制提供了重用代码的动态链接库,它会向调用者说明库里的哪些函数是可以被别人使用的,这些用来说明的信息便组成了导出表。

通常情况下,导出表存在于动态链接库文件里。

导出表的存在可以让程序的开发者很容易清楚PE中到底有多少可以使用的函数,但如果没有函数使用说明,开发者也只能通过名称、反汇编代码或者运行结果对函数的调用方式、 函数的功能等进行猜测

Windows装载器在进行PE装载时,会将导入表中登记的所有DLL—并装入,然后根据 DLL的导出表中对导入函数的描述修正导入表的IAT值。通过导出表,DLL文件向调用它的 程序或系统提供导出函数的名称、序号,以及入口地址等信息。

导出表的作用有两个:
一是可以通过导出表分析不认识的动态链接库文件所能提供的功能。
二是向调用者提供输出函数指令在模块中的起始地址。

3.1.1 分析动态链接库功能

当得不到某些动态链接库中输出函数的完整说明,这时候只能通过猜测,猜测的依据就是导出表中输出的函数的名字。

对于猜测出功能的函数.可以尝试通过程序调用来测试函数的参数以及调用方法,从而为自己所用。这样不仅避免了重复开发,还大大提高了开发效率。

3.1.2 获得导出函数地址

对一个动态链接库里导出的函数的调用,既吋以通过函数名称来进行,也可以通过函数 在导出表的索引来进行。Windows加载器将与进程相关的DLL加载到虚拟地址空间以后,会 根据导人表中登记的与该动态链接库相关的由INT指向的名称或编号来遍历DLL所在虚拟地 址空间,通过函数名或编号査找导出表结构,从而确定该导出函数在虚拟地址空间中的起始地址VA,并将该VA覆盖导人表的IAT相关项。
在覆盖IAT的过程中,导出表起到了参照和指引的作用。如果一个动态链接库没有定义导出表,其内部包含的所有函数都无法被其他程序透明地调用。这里所说的透明,是指公开调用。当然,只要你掌握了动态链接库的内部编码,即使没有导出表,你也可以随意地引用 里面的函数,哪怕这些函数是私有的。

3.2 含导出表的PE文件

使用一个DLL文件需要经过以下四步:
步骤1:编写DLL文件的源代码。
步骤2:编写函数导出声明文件(扩展名为def)。
步骤3:使用特殊参数编译链接生成最终的DLL文件。
步骤4:编写包含文件(扩展名为inc)。
在其他源代码中,可以通过静态引用和动态加载两种技术使用新编写的DLL文件中声明的导出函数

3.2.1 DLL源码、编译、链接、调用

编写DLL源代码和编写其他程序唯一不同之处是,在源代码内部必须定义DLL的入口函数。该入口函数必须符合一定的格式。
入口函数的名字可以随意命名,但格式必须遵循一定规范(这些规范包括入口参数的个数和返回值的类型)
参考:

https://blog.csdn.net/qianchenglenger/article/details/21599235
https://www.cnblogs.com/MikeZhang/archive/2013/01/07/dllCall_20130107.html

3.3 导出表数据结构

3.3.1 导出表定位

导出表数据为数据目录中注册的数据类型之一,其描述信息处于数据目录的第1个目录项中。

导出表所在地址RVA
导出表数据大小

3.3.2 导出目录 IMAGE_EXPORT_DIRECTORY

导出数据的第一个结构是IMAGE_EXPORT_DIRECTORY
定义如下:

IMAGE_EXPORT_DIRECTORY STRUCT
     Characteristics          //DWORD OOOOh -	标志,未用
     TimeDatestamp	         //DWORD  0004h -	时间戳
     Majorversion	         //WORD	  0008h -	未用
     MinorVersion	         //WORD	  000ah -	未用
     nName	                 //DWORD  000ch -	指向该导出表的文件名字符串
     nBase	                 //DWORD  0010h -	争出凼数的起始序号
     NumberOfFunctions	     //DWORD  0014h -	所有的导出函数个数
     NumberOfNames	         //DWORD  0018h -	以A数名导出的A数个数
     AddressOfFunctions	     //DWORD  00lch -	导出函数地址表RVA
     AddressOfNames	         //DWORD  0020h -	A致名称地址表RVA
     AddressOfNameOrdinals	//DWORD	  0024h -	A敫序号地址表
IMAGE_EXPORT_DIRECTORY ENDS

导入表的IMAGE_IMPORT_DESCRIPTOR个数与调用的动态链接库个数相等,而导出表的IMAGE_EXPORT_DIRECTORY只有一个。

64.IMAGE_EXPORT_DIRECTORYnName
+000Ch,双字。该字段指示的地址指向了一个以“\0”结尾的字符串,字符串记录了导出表所在的文件的最初文件名。

65.IMAGE_EXPORT_DIRECTORY.NumberOfFunctions
+0014h,双字。该字段定义了文件中导出函数的总个数。

66.IMAGE_EXPORT_DIRECTORY.NumberOfNames

+0018h,双字。在导出表中,有些函数是定义名字的,有些是没有定义名字的。该字 段记录了所有定义名字函数的个数。如果这个值是0,则表示所有的函数都没有定义名字。 NumberOfNames和NumberOfFunctions的关系是前者小于等于后者。

67.IMAGE_EXPORT_DIRECTORY.AddressOfFunctions

+001Ch,双字。该指针指向了全部导出函数的人口地址的起始。从入口地址开始为双字 数组,数组的个数由字段IMAGE_EXPORT_DIRECTORY.NumberOfFunctions决定。导出函 数的每一个地址按函数的编号顺序依次往后排开。在内存中,我们可以通过函数编号来定位 某个函数的地址。大致代码如下:

mov eax, [esi] .AddressOfFunctions ;esi 相向早出表结构 
IMAGE_EXPORT_DIRECTORY 的起始地址
mov ebx, num               ;假设eax为函数编号
sub ebx,[esi].nBase
add eax,[num*4]
mov eax, [eax]              ;到这里已获取函数的虚拟地址RVA,加上模块实际装入地址
                            ;就是在虚拟地址空间里真实的地址VA

68.IMAGE_EXPORT_DIRECTORY.nBase

+0010h,双字。导出函数编号的起始值。DLL中的第一个导出函数并不是从0开始的, 某导出函数的编号等于从AddressOfFunctions开始的顺序号加上这个值
在这里插入图片描述

​ nBase字段决定导出函数编号

69.IMAGE_EXPORT_DIRECTORY.AddressOfNames

+0020h,双字。该值为一个指针。该指针指向的位置是一连串的双字值,这些 双字值均指向了对应的定义了函数名的函数的字符串地址。这一连串的双字个数为 NumberOf Names。

70.IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals

+0024h,双字。该值也是一个指针,与AddressOfNames是一一对应关系(注意,是一一对 应),所不同的是,AddressOfNames指向的是字符串的指针数组,而AddressOfNameOrdinals则指向了该函数在AddressOfFunctions中的索引值。

索引值是一个字,而非双字。该值与函数编号是两个不同的概念,两者之间的关系为:
索引值=编号-nBase

在这里插入图片描述

3.4 导出表编程

通过编程査找函数地址有两个不同方法,分别是:
1.根据编号查找函数地址
2.根据名字査找函数地址

3.4.1根据编号查找函数地址

要通过编号査找函数地址,其步骤如下:
步骤1:定位到PE头。
步骤2:从PE文件头中找到数据目录表,表项的第一个双字值是导出表的起始RVA。
步骤3:从导出表的nBase字段得到起始序号。
步骤4:函数编号减去起始序号得到的是函数在AddressOfFunctions中的索引号。
步骤5:通过査询AddressOfFunctions指定索引位置的值,找到虚拟地址。
步骤6:将虚拟地址加上该动态链接库在被导入到地址空间后的基地址,即为函数的真实入口地址。
不建议使用编号査找函数地址。因为有很多的动态链接库中标识的编号与对应的函数并不一致,通过这种方法找到的函数地址往往是错误的。

3.4.2 根据名字查找函数地址

要根据函数名字从导出表结构中査找函数的地址,步骤如下:
步骤1:定位到PE头。
步骤2:从PE文件头中找到数据目录表,表项的第一个双字值是导出表的起始RVA。
步骤3:从导出表中获取NumberOfNames字段的值,以便构造一个循环,根据此值确定 循环的次数。
步骤4:从AddressOfNames字段指向的函数名称数组的第一项开始,与给定的函数名字进行匹配;如果匹配成功,则记录从AddressOfNames开始的索引号。
步骤5:通过索引号再去检索AddressOfNameOrdinals数组,从同样索引的位置找到函数的地址索引。
步骤6:通过査询AddressOfFunctions指定函数地址索引位S的值,找到虚拟地址。
步骤7:将虚拟地址加上该动态链接库在被导入到地址空间的基地址,即为函数的真实入口地址。

3.5 导出表的应用

导出表常见的应用主要包括对导出表函数的覆盖,以及对动态链接库内部私有函数的导出等。通过对导出表函数进行覆盖,可以更改代码流程或代码功能,为应用程序实施补丁:通过对动态链接库内部私有函数的导出,可以更充分地利用已有的代码,减轻二次开发的工量。

3.5.1导出函数覆盖

两种常见的导出函数覆盖技术:
1.修改导出结构中的函数地址
2.覆盖函数地址部分的指令代码

1.修改导出结构中的函数地址
需要注意的是,在使用导出函数地址覆盖技术的时候,首先要保证所涉及的两个函数参数入口要一致,否则调用完成后栈不平衡,这会导致应用程序调用失败;其次,要求用户对两 个函数的内部实现要有充分的了解,使得地址转向后,能够保证应用程序在功能上可以全面兼容并运行良好。

2.覆盖函数地址部分的指令代码
第二种常见的覆盖技术,是将AddressOfFunctions指向的地址空间指令字节码实施覆盖。 这种技术又衍生出两种:
1.暴力覆盖,即将所有的代码全部替换为新代码。新代码可能含有原来代码的全部功能, 也可能不包含原有代码功能。
2.完美覆盖,通过构造指令,实施新代码与原代码的共存和无遗漏运行。

3.5.2 导出私有函数

第二种常见的覆盖技术,是将AddressOfFunctions指向的地址空间指令字节码实施覆盖。 这种技术又衍生出两种:
1.暴力覆盖,即将所有的代码全部替换为新代码。新代码可能含有原来代码的全部功能, 也可能不包含原有代码功能。
2.完美覆盖,通过构造指令,实施新代码与原代码的共存和无遗漏运行。

3.5.2 导出私有函数

https://blog.csdn.net/u013761036/article/details/53933350

猜你喜欢

转载自blog.csdn.net/weixin_51732593/article/details/122022929