PE体系
PE结构&整体叙述
PE结构&导入表
PE结构&导出表
PE结构&基址重定位表
PE结构&绑定导入实现
PE结构&延迟加载导入表
导出表的作用
代码重用机制提供了重用代码的动态链接库,它会向调用者说明库里的哪些函数是可以被别人使用的,这些用来说明的信息便组成了导出表。
通常情况下,导出表存在于动态链接库文件里。但不能简单地认为EXE中没有导出表,例如WinWord.exe文件里就有;也不能简单地认为所有的DLL中都有导出表,例如一些专门存放资源位文件的DLL里就没有导出表。
它的存在可以让程序开发者很容易清楚PE中到底有多少可以使用的函数,但如果没有函数使用说明,开发着只能通过函数名称,反汇编代码或者运行结果对函数的调用方式,函数的功能等进行猜测。
Windows装载器在进行PE装载时,会将导入表中登记的所有DLL一并装入,然后根据DLL的导出表中对导入函数的描述修正导入表的IAT值。通过导入表,DLL文件向调用它的程序或系统提供导出函数的名称,序号,以及入口地址等信息。
综上所述,作用如下:
- 可以通过导出表分析不认识的动态链接库文件所提供的功能
- 向调用者提供输出函数指令在模块中的起始地址
导出表的定位
导出表数据为数据目录中注册的数据类型之一,其描述信息处于数据目录的第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的导入表数据中找到
根据编号查找函数地址:
- 定位到PE头
- 从PE文件中找到数据目录表,表项的第一个双字值是导出表的起始RVA
- 从导出表的nBase字段得到起始序号
- 函数编号减去起始序号得到的是函数在AddressOfFunctions中的索引号
- 通过查询AddressOfFunctions指定索引位置的值,找到虚拟地址
- 将虚拟地址加上该动态链接库在被导入到地址空间后的基地址,即为函数的真实入口地址
提示:
不建议使用编号查找函数地址。因为有很多的动态链接库汇总标识的编号与对应的函数并不一致,通过这种方法找到的函数地址往往是错误的
根据名字查找函数地址:
- 定位到PE头
- 从PE文件中找到数据目录表,表项的第一个双字值是导出表的起始RVA
- 从导出表中获取NumberOfNames字段的值,以便构造一个循环,根据此值确定循环的次数
- 从AddressOfNames字段指向的函数名称数组的第一项开始,与给定的函数名字进行匹配;如果匹配成功,则记录从AddressOfNames开始的索引号
- 通过索引号再去检索AddressOfNameOridinals数组,从同样索引的位置找到函数的地址索引
- 通过查询AddressOfFuncs指定函数地址索引位置的值,找到虚拟地址
- 将虚拟地址加上该动态链接库在被导入到地址空间的基地址,即为函数的真实入口地址。
举例:
从库中获得函数地址的API为GetProcAddress()函数,该API引用EAT来获取指定API的地址。其过程大致如下:
- 利用AddressOfName成员转到“函数名称数组”
- “函数名称数组”中存储着字符串地址,通过比较(strcmp)字符串,查找指定的函数名称(此时数组的索引称为name_index)
- 利用AddressOfNameOrdinals成员,转到ordinal数组
- 在ordinal数组中通过name_index查找相应ordinal值
- 利用AddressOfFunctionis成员转到“函数地址数组”(EAT)
- 在“函数地址数组”中将刚刚求得的ordinal用作数组索引,获得指定函数的起始地址
kernel32.dll中所有到处函数均有相应名称,AddressOfNameOrdinals数组的值以index=ordinal的形式存在。但存在一部分dll中的导出函数没有名称,所以仅通过ordinal导出,从Ordinal值中减去IMAGE_EXPORT_DIRECTORY.Base 成员后得到一个值,使用该值作为“函数地址数组”的索引即可查找到相应函数的地址
导出表的应用
导出函数的覆盖
导出表编程中常见的技术是,不需要修改用户程序,便能将用户程序中调用的动态链接库函数转向或者实施代码覆盖,实现用户程序的调用转移。(这种技术在杀毒软件对用户程序防护过程中,针对这种渗透是无效的)
- 修改导出结构中的函数地址
- 覆盖函数地址部分的指令代码
1. 修改导出结构中的函数地址
直接利用二进制工具将 AddressOfFunctions
索引1和2的地址(分别对应函数AnimateOpen和FadeInOpen)交换位置。
仅通过函数调用RVA地址0x00001282
和0x00001022
交换位置,即可实现导出函数的覆盖。
需要注意的是,在使用导出函数地址覆盖技术的时候,首先保证所涉及的两个函数参数入口要一致,否则调用完成后栈不平衡。这将会导致应用程序调用失败;其次,要求用户对两个函数的内部实现要有充分了解,使得地址转向后,能够保证应用程序在功能上可以全面兼容并允许良好
注意:
这种操作在实际操作中不赞成大家使用。
覆盖函数地址部分的指令代码
第二种常见的覆盖技术,是将AddressOfFunctions指向的地址空间指令字节码实施覆盖。这种技术又衍生处两种:
- 暴力覆盖,即将所有的代码全部替换为新代码,新代码可能含有原来代码的全部功能,也可能不包含原有代码功能
- 完美覆盖,通过构造指令,实施新代码和原代码的共存和无遗漏运行。
举例:
导出私有函数