PE文件 02 导出表
0x01 导出表结构
导出表是由数据目录表中的第一个成员DataDirectory[0]指出的:
1 typedef struct _IMAGE_DATA_DIRECTORY { 2 DWORD VirtualAddress; 3 DWORD Size; 4 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
导出表就是记载着动态链接库的一些导出信息。通过导出表,DLL 文件可以向系统提供导出函数的名称、序号和入口地址等信息,比便Windows 加载器通过这些信息来完成动态连接的整个过程。扩展名为.exe 的PE 文件中一般不存在导出表,而大部分的.dll 文件中都包含导出表。但这并不是绝对的。例如纯粹用作资源的.dll 文件就不需要导出函数啦,另外有些特殊功能的.exe 文件也会存在导出函数。
当PE文件被加载为模块时候,Windows加载器会将导入表中登记的DLL文件一并装入,再根据DLL文件中的函数导出信息对被执行文件的IAT表进行修正。
Windows 在加载一个程序后就会在内存中为该程序开辟一个单独的4G虚拟地址空间(x86下)。有一些函数很多程序都会用到,为每一个程序所调用的相同的函数都占用一次内存空间显得很浪费,因此Windows提出了了动态链接库的概念,将一些常用的函数封装成动态链接库,等到需要的时候通过直接加载动态链接库,将需要的函数映射到自己的地址空间中,从而提高了内存的利用率。
导出表的结构成员: 1 typedef struct _IMAGE_EXPORT_DIRECTORY {
2 DWORD Characteristics; //未使用,总是定义为0 3 DWORD TimeDateStamp; //文件生成时间 4 WORD MajorVersion; 5 WORD MinorVersion; 6 DWORD Name; //模块的真实名称RVA 7 DWORD Base; //导出函数序号的起始值,将AddressOfFunctions 字段指向的入口地址表的索引号加上这个起始值就是对应函数的导出序号。假如Base 字段的值为x,那么入口地址表指定的第1个导出函数的序号就是x;第2个导出函数的序号就是x+1。总之,一个导出函数的导出序号等于Base 字段的值加上其在入口地址表中的位置索引值。 8 DWORD NumberOfFunctions; //导出函数的个数
9 DWORD NumberOfNames; //名称方式导出的函数的总数。有的导出函数是没有名字的,只有序号 10 DWORD AddressOfFunctions; //一个RVA 值,指向包含全部导出函数入口地址的双字数组(EAT)。数组中的每一项是一个RVA 值,数组的项数等于NumberOfFunctions 字段的值。 11 DWORD AddressOfNames; //一个RVA 值,指向函数名字符串地址表。这个地址表是一个双字数组,数组中的每一项指向一个函数名称字符串的RVA。数组的项数等NumberOfNames 字段的值,所有有名称的导出函数的名称字符串都定义在这个表中 12 DWORD AddressOfNameOrdinals; //一个RVA 值,指向另一个word 类型的数组(注意不是双字数组)。数组项目与文件名地址表中的项目一一对应,项目值代表函数入口地址表的索引,这样函 数名称与函数入口地址关联起来,起到一个桥梁的作用。 13 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
0x02 关键问题
(1)通过函数序号得到函数地址:
1>由DOS头定位到PE头。
2>由PE头定位到PE头中的中的 可选头IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA。
3>从导出表的 Base 字段得到起始序号。
4>将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引。
5>检测索引值是否大于导出表的 NumberOfFunctions 字段的值,如果大于后者的话,说明输入的序号是无效的。
6>用这个索引值在 AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到函数真正的入口地址。
(2)通过函数名称得到函数地址:
1>由DOS头定位到PE头。(同上)
2>由PE头定位到PE头中的中的 可选头IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA。(同上)
3>从导出表的NumberOfNames 字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环。
4>从AddressOfNames 字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数。
5>如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在AddressOfNamesOrdinals 指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x。
6>以 x 值作为索引值,在 AddressOfFunctions 字段指向的函数入口地址表中获取的 RVA 加上模块基地址就是函数的入口地址。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗