导出表概念
一般情况下,PE中的导出表存在于动态链接库文件里。但不能简单地认为EXE中没有导出表,例如WinWord.exe文件里就有;也不能简单地认为所有的DLL中都有导出表,例如一些专门存放资源位文件的DLL里就没有导出表。
导出表的主要作用是将PE中存在的函数引出到外部,以便其他人可以使用这些函数,实现代码的重用。
导出表的作用
Windows装载器在进行PE装载时,会将导入表中登记的所有DLL一并装入,然后根据DLL的导出表中对导入函数的描述修正导入表的IAT值。 通过导入表,DLL文件向调用它的程序或系统提供导出函数的名称,序号,以及入口地址等信息。
综上所述,导出表的作用如下:
- 可以通过导出表分析不认识的动态链接库文件所提供的
功能
。 - 向调用者提供输出函数指令在模块中的
起始地址
。
导出表数据结构(IMAGE_EXPORT_DIRECTORY)
IMAGE_EXPORT_DIRECTORY STRUCT
Characteristics DWORD ? ;0000h -标志,未用
TimeDateStamp DWORD ? ;0004h -时间戳
MajorVersion WORD ? ;0008h -未用
MinorVersion WORD ? ;000ah -未用
nName DWORD ? ;000ch -指向该导出表的文件名字符串
nBase DWORD ? ;0010h -导出函数的起始序号
NumberOfFunctions DWORD ? ;0014h -所有的导出函数个数
NumberOfNames DWORD ? ;0018h -以函数名导出的函数个数
AddressOfFunctions DWORD ? ;001ch -导出函数地址表RVA
AddressOfNames DWORD ? ;0020h -函数名称地址表RVA
AddressOfNameOrdinals DWORD ? ;0024h -函数序列地址表
IMAGE_EXPORT_DIRECTORY ENDS
导入表的IMAGE_IMPORT_DESCRIPTOR
个数与调用的动态链接库个数相等,而导出表的IMAGE_EXPORT_DIRECTORY
只有一个。
IMAGE_EXPORT_DIRECTORY字段说明:
IMAGE_EXPORT_DIRECTORY.nName
+000ch,双字,该字段指示的地址指向了一个以“\0”结尾的字符串,字符串记录了导出表所在的文件的最初文件名
。
IMAGE_EXPORT_DIRECTORY.NumberOfFunctions
+0014h,双字。该字段定义了文件中导出函数的总个数
。
IMAGE_EXPORT_DIRECTORY.NumberOfNames
+0018h,双字。在导出表中,有些函数是定义名字的,有些事没有定义名字的。该字段记录了所有定义名字函数的个数。 如果此值为0,则表示所有的函数都没有定义名字。
NumberOfNames和NumberOfFunctions的关系是前者小于后者(NumberOfNames < NumberOfFunctions)。(其实这关系也不一定,如果多个名字指向一个函数则此结论不成立)
IMAGE_EXPORT_DIRECTORY.AddressOfFunctions
+001ch,双字。该指针指向了全部导出函数的入口地址的起始。 从入口地址开始为双字数组
,数组的个数由字段IMAGE_EXPORT_DIRECTORY.NumberOfFunctions决定。导出函数的每一个地址按函数的编号顺序依次往后排开。
IMAGE_EXPORT_DIRECTORY.nBase(编号)
+0010h,双字。导出函数编号
的起始值。DLL中的第一个导出函数并不从0开始的
,某个导出函数的编号等于从AddressOfFunctions开始的顺序号加上这个值,如下:
IMAGE_EXPORT_DIRECTORY.AddressOfNames
+0020h,双字,该值为一个指针,该指针指向的位置是一连串的双字值
,这些双字值均指向了对应的定义了函数名的函数的字符串地址。这一连串的双字个数为NumberOfNames
。
特别说明:
1、函数的真正的名字在文件中位置是不确定的。
2、但函数名称表中是按名字排序的。
也就是说,A开头的函数在AddressOfNames排在最前面;
但AXXXXXX这个真正的名字,可能排在BXXXXX后面。
3、如果想打印名字,要先将AddressOfNames转换为FOA。
IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals
+0024h,双字。该值也是一个指针,与AddressOfNames是一一对应关系
(注意,是一一对应),所不同的是,AddressOfNames
指向的是字符串的指针数组
,而AddressOfNameOrdinals
则指向了该函数在AddressOfFunction
中的索引值
。
注意:
索引值是一个字
,而非双字。 该值与函数编号是两个不同的概念,两者的之间的关系为:
索引值=编号-nBase。
如下图导入表中“Hint/Name”中的Hint值
是AddressOfFunctions 的索引值
,并非编号。
导出表结构各字段关系图
如图所示,AddressOfNames中的函数是从Function2开始的,也就是说,这里假设Function1只提供编号访问;其nBase为200h,所以对应的AddressOfNameOrdinals是0001h,但最终函数Function1的编号
为:索引值+nBase的值,即0201h。
注:
保存地址的相关表中的内容全是RVA。
总结(为什么要分成3张表?)
1、函数导出的个数与函数名的个数未必一样.所以要将函数地址表和函数名称表分开。
2、函数地址表是不是一定大于函数名称表?
未必,一个相同的函数地址,可能有多个不同的名字。
3、如何根据函数的名字获取一个函数的地址?
4、如何根据函数的导出序号(AddressOfNameOrdinals)获取一个函数的地址?
实例分析
1.查看导出表位置
RVA = 0X2140
SIZE = 0X8F(SIZE大小没什么用)
FOA = 0X940
2.找出对应字段值
90 21 00 00 // nName
01 00 00 00 //nBase
04 00 00 00 //NumberOfFunctions
04 00 00 00 //NumberOfNames
68 21 00 00 //AddressOfFunctions
RVA = 0x2168
FOA = 0x968
78 21 00 00 //AddressOfNames
RVA = 0x2178
FOA = 0x978
88 21 00 00 //AddressOfNameOrdinals
RVA = 0X2188
FOA = 0X988
查找函数地址(两种方法)
根据编号查找函数地址
一,定位到PE头;
二,从PE文件头中找到数据目录,表项的第一个双字值是导出表的起始RVA;
三,从导出表的nBase字段得到起始序号;
四,函数编号
减去起始序号
得到的是函数在AddressOfFunctions中的索引号
;
五,通过查询AddressOfFunctions指定索引位置的值,找到虚拟地址;
六,将虚拟地址加上该动态链接库在被导入到地址空间后的基址,即为函数的真实入口地址。
注:
不建议使用编号查找函数地址。因为有很多的动态链接库汇总标识的编号与对应的函数并不一致,通过这种方法找到的函数地址往往是错误的
根据名字查找函数地址
一,定位到PE头;
二,从PE文件中找到数据目录表,表项的第一个双字值是导出表的起始RVA;
三,从导出表中获取NumberOfNames字段的值,以便构造一个循环,根据此值确定循环的次数;
四,从AddressOfNames字段指向的函数名称数组的第一项开始,与给定的函数名字进行匹配;如果匹配成功,则记录从AddressOfNames开始的索引号
;
五,通过索引号再去检索AddressOfNameOridinals数组,从同样索引的位置找到函数的地址索引;
六,通过查询AddressOfFuncs指定函数地址索引位置的值,找到虚拟地址;
七,将虚拟地址加上该动态链接库在被导入到地址空间的基地址,即为函数的真实入口地址。
遍历导出表
需完成如下要求:
导出表的应用
导出函数覆盖
修改导出结构中的函数地址
还是上面实例中的导出表部分内容。
交换以下两个值的内容
程序改变运行状态。
注意:
在实际操作中不赞成使用该技术。(至于为什么?可能是环境控制不便容易失败吧)
覆盖函数地址部分的指令代码
第二种常见的覆盖技术,是将AddressOfFunctions指向的地址空间指令字节码实施覆盖。这种技术又衍生处两种:
- 暴力覆盖,即将所有的代码全部替换为新代码,新代码可能含有原来代码的全部功能,也可能不包含原有代码功能
- 完美覆盖,通过构造指令,实施新代码和原代码的共存和无遗漏运行。
说明:
有点类似ROP链的攻击方法,实例可自行百度参考。
注意:
一定要多考虑下代码的重定位
问题。
导出私有函数
这里就不码字和截图了,实现这个功能思想其实很简单,就是让程序自己告诉自己把藏着的私有函数吐出来。
这里分享一位大佬朋友的链接,最后面有导出私有函数的实例。
寻梦&之璐
里面还有其他的好文章,大家也能多学习学习。
结束
如有疑问欢迎私聊交流学习。