PE结构&导出表

PE体系

PE结构&整体叙述

PE结构&导入表

PE结构&导出表

PE结构&基址重定位表

PE结构&绑定导入实现

PE结构&延迟加载导入表

导出表的作用

代码重用机制提供了重用代码的动态链接库,它会向调用者说明库里的哪些函数是可以被别人使用的,这些用来说明的信息便组成了导出表。
通常情况下,导出表存在于动态链接库文件里。但不能简单地认为EXE中没有导出表,例如WinWord.exe文件里就有;也不能简单地认为所有的DLL中都有导出表,例如一些专门存放资源位文件的DLL里就没有导出表。
它的存在可以让程序开发者很容易清楚PE中到底有多少可以使用的函数,但如果没有函数使用说明,开发着只能通过函数名称,反汇编代码或者运行结果对函数的调用方式,函数的功能等进行猜测。
Windows装载器在进行PE装载时,会将导入表中登记的所有DLL一并装入,然后根据DLL的导出表中对导入函数的描述修正导入表的IAT值。通过导入表,DLL文件向调用它的程序或系统提供导出函数的名称,序号,以及入口地址等信息。
综上所述,作用如下:

  1. 可以通过导出表分析不认识的动态链接库文件所提供的功能
  2. 向调用者提供输出函数指令在模块中的起始地址

导出表的定位

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

在这里插入图片描述
在这里插入图片描述
0x2140转换成文件偏移后是0x940

导出目录IMAGE_EXPORT_DIRECTORY

导出数据的第一个结构是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.nName

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

IMAGE_EXPORT_DIRECTORY.NumberOfFunctions

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

IMAGE_EXPORT_DIRECTORY.NumberOfNames

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

IMAGE_EXPORT_DIRECTORY.AddressOfFunctions

+001ch,双字。该指针指向了全部导出函数的入口地址的起始。从入口地址开始为双字数组,数组的个数由字段IMAGE_EXPORT_DIRECTORY.NumberOfFunctions决定。导出函数的每一个地址按函数的编号顺序依次往后排开。

IMAGE_EXPORT_DIRECTORY.nBase

+0010h,双字。导出函数编号的起始值。DLL中的第一个导出函数并不从0开始的,某个导出函数的编号等于从AddressOfFunctions开始的顺序号加上这个值,如下:
在这里插入图片描述
如上图,Fun1的函数编号为nBase+0=200h,Fun2的函数编号为nBase+1=201h

IMAGE_EXPORT_DIRECTORY.AddressOfNames

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

IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals

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

注意:
索引值是一个字,而非双字。该值与函数编号是两个不同的概念,两者的之间的关系为:索引值=编号-nBase
导入表中“Hint/Name”中的Hint值是AddressOfFunctions 的索引值,并非编号。

结构图:
在这里插入图片描述
0x940

90	21	00	00

对应IMAGE_EXPORT_DIRECTORY.nName字段,转换成文件偏移后0x990
在这里插入图片描述

此字符串为winresult.dll,是动态链接库的最初的名字

01	00	00	00

对应IMAGE_EXPORT_DIRECTORY.nBase字段,表示起始编号为1

04	00	00	00

对应IMAGE_EXPORT_DIRECTORY.NumberOfFunctions字段,表示共有4个导出函数

04	00	00	00

对应IMAGE_EXPORT_DIRECTORY.NumberOfNames字段,表示4个导出函数均为按名称导出

68	21	00	00

转换为文件偏移是0x968
对应IMAGE_EXPORT_DIRECTORY.AddressOfFunctions字段。从该位置取出连续4个地址(个数由IMAGE_EXPORT_DIRECTORY.NumberOfFunctions字段决定),这些地址分别对应4个函数的RVA
在这里插入图片描述
索引0:0x00001183
索引1:0x00001022
索引2:0x00001082
索引3:0x00001323
用图表示:
在这里插入图片描述

78	21	00	00

转换为文件偏移为0x978
对应IMAGE_EXPORT_DIRECTORY.AddressOfNames字段。从该位置取出的连续4个地址依次为
在这里插入图片描述
0x0000219e—>‘AnimateClose’\0
0x000021ab—>‘AnimateOpen’\0
0x000021b7—>‘FadeInOpen’\0
0x000021c2—>‘FadeOutClose’\0

88	21	00	00

转换为文件偏移为0x988
在这里插入图片描述
对应IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals字段。从该位置取出的连续4个单字索引依次为:
0x0000
0x0001
0x0002
0x0003
这些索引的值存在于字段IMAGE_EXPORT_DIRECTORY.AddressOfFunctions所指向的函数地址列表中,最终的4个函数的编号将分别是此处的索引值加上nBase的值,函数的索引值可以在调用了该动态链接库的程序FirstWindows.exe的导入表数据中找到
在这里插入图片描述

根据编号查找函数地址:

  1. 定位到PE头
  2. 从PE文件中找到数据目录表,表项的第一个双字值是导出表的起始RVA
  3. 从导出表的nBase字段得到起始序号
  4. 函数编号减去起始序号得到的是函数在AddressOfFunctions中的索引号
  5. 通过查询AddressOfFunctions指定索引位置的值,找到虚拟地址
  6. 将虚拟地址加上该动态链接库在被导入到地址空间后的基地址,即为函数的真实入口地址

提示:
不建议使用编号查找函数地址。因为有很多的动态链接库汇总标识的编号与对应的函数并不一致,通过这种方法找到的函数地址往往是错误的

根据名字查找函数地址:

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

举例:
从库中获得函数地址的API为GetProcAddress()函数,该API引用EAT来获取指定API的地址。其过程大致如下:

  1. 利用AddressOfName成员转到“函数名称数组”
  2. “函数名称数组”中存储着字符串地址,通过比较(strcmp)字符串,查找指定的函数名称(此时数组的索引称为name_index)
  3. 利用AddressOfNameOrdinals成员,转到ordinal数组
  4. 在ordinal数组中通过name_index查找相应ordinal值
  5. 利用AddressOfFunctionis成员转到“函数地址数组”(EAT)
  6. 在“函数地址数组”中将刚刚求得的ordinal用作数组索引,获得指定函数的起始地址

kernel32.dll中所有到处函数均有相应名称,AddressOfNameOrdinals数组的值以index=ordinal的形式存在。但存在一部分dll中的导出函数没有名称,所以仅通过ordinal导出,从Ordinal值中减去IMAGE_EXPORT_DIRECTORY.Base 成员后得到一个值,使用该值作为“函数地址数组”的索引即可查找到相应函数的地址

导出表的应用

导出函数的覆盖

导出表编程中常见的技术是,不需要修改用户程序,便能将用户程序中调用的动态链接库函数转向或者实施代码覆盖,实现用户程序的调用转移。(这种技术在杀毒软件对用户程序防护过程中,针对这种渗透是无效的)

  1. 修改导出结构中的函数地址
  2. 覆盖函数地址部分的指令代码

1. 修改导出结构中的函数地址

直接利用二进制工具将 AddressOfFunctions索引1和2的地址(分别对应函数AnimateOpen和FadeInOpen)交换位置。
在这里插入图片描述
仅通过函数调用RVA地址0x000012820x00001022交换位置,即可实现导出函数的覆盖。

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

注意:
这种操作在实际操作中不赞成大家使用。

覆盖函数地址部分的指令代码

第二种常见的覆盖技术,是将AddressOfFunctions指向的地址空间指令字节码实施覆盖。这种技术又衍生处两种:

  1. 暴力覆盖,即将所有的代码全部替换为新代码,新代码可能含有原来代码的全部功能,也可能不包含原有代码功能
  2. 完美覆盖,通过构造指令,实施新代码和原代码的共存和无遗漏运行。

举例:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

导出私有函数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寻梦&之璐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值