PE结构导出表详细解析

只码重点

DLL导出方式:

按名称导出

1.__declspec(dllexport) 

2.

LIBRARY DLL
EXPORTS
  FuncDll  

按序号导出

LIBRARY DLL
EXPORTS
  FuncDll @ 1 NONAME  

按名称和序号导出

LIBRARY DLL
EXPORTS
  fnDll1 @ 1 NONAME  
  fnDll2 @ 2 //这个就是


关于导出,如果是按名字导出,编译器会自动为你编序号。
其实算是按名字和序号导出

导出表结构:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;//未使用,总是定义为0
    DWORD   TimeDateStamp;//文件生成时间
    WORD    MajorVersion;//未使用,总是定义为0
    WORD    MinorVersion;//未使用,总是定义为0
    DWORD   Name;	//模块的真实名称的RVA
    DWORD   Base;	//基数,加上序数就是函数地址数组的索引值
    DWORD   NumberOfFunctions;//导出函数的总数
    DWORD   NumberOfNames;	//以名称方式导出的函数的总数
    DWORD   AddressOfFunctions;     // RVA from base of image指向输出函数地址的RVA
    DWORD   AddressOfNames;         // RVA from base of image指向输出函数名字的RVA
    DWORD   AddressOfNameOrdinals;  // RVA from base of image向输出函数序号的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Name:
一个RVA 值,指向一个定义了模块名称的字符串。如即使Kernel32.dll 文件被改名为"Ker.dll",仍然可以从这个字符串中的值得知其在编译时的文件名是"Kernel32.dll"。
 
NumberOfFunctions:
文件中包含的导出函数的总数。
 
NumberOfNames:
被定义函数名称的导出函数的总数,显然只有这个数量的函数既可以用函数名方式导出。也可以用序号方式导出,剩下 的NumberOfFunctions 减去NumberOfNames 数量的函数只能用序号方式导出。该字段的值只会小于或者等于 NumberOfFunctions 字段的值,如果这个值是0,表示所有的函数都是以序号方式导出的。
 
AddressOfFunctions:
一个RVA 值,指向包含全部导出函数入口地址的双字数组。数组中的每一项是一个RVA 值,数组的项数等于NumberOfFunctions 字段的值。


Base:

导出函数序号的起始值,将AddressOfFunctions 字段指向的入口地址表的索引号加上这个起始值就是对应函数的导出 序号。假如Base 字段的值为x,那么入口地址表指定的第1个导出函数的序号就是x;第2个导出函数的序号就是x+1。总之,一个导出函数的导出序号等 于Base 字段的值加上其在入口地址表中的位置索引值。
 
AddressOfNames 和 AddressOfNameOrdinals:
均为RVA 值。前者指向函数名字符串地址表。这个地址表是一个双字数组,数组中的每一项指向一个函数名称字符串的RVA。数组的项数等于NumberOfNames 字段的值,所有有名称的导出函数的名称字符串都定义在这个表中;后者指向另一个word 类型的数组(注意不是双字数组)。数组项目与文件名地址表中的项目一一对应,项目值代表函数入口地址表的索引,这样函 数名称与函数入口地址关联起来。
 
(举个例子说,加入函数名称字符串地址表的第n 项指向一个字符串“MyFunction”,那么可以去查找 AddressOfNameOrdinals 指向的数组的第n 项,假如第n 项中存放的值是x,则表示AddressOfFunctions 字段描述的地址表中的第x 项函数入口地址对应的名称就是“MyFunction”)




Windows 装载器查找导出函数入口地址的整个过程

1.从序号查找函数入口地址:

定位到PE 文件头 
从PE 文件头中的 IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA 
从导出表的 Base 字段得到起始序号 
将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引 
检测索引值是否大于导出表的 NumberOfFunctions 字段的值,如果大于后者的话,说明输入的序号是无效的 
用这个索引值在 AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到了函数真正的入口地址 。

简单说就是:序号-base 对应找到AddressOfFunctions 的第几项

2.从函数名称查找函数入口地址

最初的步骤是一样的,那就是首先得到导出表的地址 
从导出表的 NumberOfNames 字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环 
从 AddressOfNames 字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数 
如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在 AddressOfNamesOrdinals 指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x 
最后,以 x 值作为索引值,在 AddressOfFunctions 字段指向的函数入口地址表中获取的 RVA 就是函数的入口地址。

简单说是:查找AddressOfNames ,对应到a项,取AddressOfNamesOrdinals 的第a项的值得到b,取AddressOfFunctions 的第b项


关于编程实现获取输出表函数信息:

遍历导出函数地址表的成员AddressOfFunctions  n从0开始(n<NumberOfFunctions)
  第n个函数地址:判断是否==0 ,不等于0代表确实存在此函数(等于0直接遍历下一个),导出序号=base+n
  判断是否也同时以函数名导出:导出序号是否在AddressOfNameOrdinals中(word元素的数值加上base)。
若存在,那么是哪个成员呢?这个与导出序号表是一一对应的。因为在导出序号表中是第一个成员,那么对应导出函数名称地址表也是第一个成员。这里需要(根据NumberOfNames的值建立一个一一对应的关系数组)
不存在 就不存在呗
 n+1遍历下一个


这里与上面讲的有些出入,我具体指明下那几点

第六个成员(Base)是个DWORD类型,是基数,实际上它等于所有函数导出序号中最小的值,该值也是导出地址表中第一个地址对应函数的导出序号。默认情况下导出序号从一开始递增,所以最小序号为一。但是也有特殊的情况,如下所述。
第七个成员(NumberOfFunctions)是个DWORD类型,表示模块中导出函数/符号个数,这里是0x00000002,由此得知此模块导出了两个函数(注意:这里不一定是真实导出函数的个数。实际上,该值等于所有函数中导出序号最小的值依次递增一到最大的值所经历的数的个数。)因为默认情况下导出序号从一开始递增,每次递增一,所以所经历的个数与导出函数个数相等。例如某模块导出了五个函数,而导出序号从一开始依次递增,每次递增一,递增到最大的是五,那么从一到五经历的的个数也是五。但是可以通过修改DEF文件为某个函数指定特定的导出序号,如果指定的结果并不是按照从一开始逐个递增一,那么将导致导出函数个数与递增个数不相等(也就是这里的值)。例如我们修改原来的DLL.dll程序,使其导出三个函数,并且在DEF文件中定义如下:
EXPORTS
  fnDll1  @ 3 NONAME  
  fnDll2  @ 2 
  fnDll3  @ 5
    该DEF定义的含义为函数fnDll1仅以序号方式导出,导出序号为3。函fnDll2分别以函数名和序号的方式导出,导出序号为2,函数fnDll3分别以函数名和序号方式导出,导出序号为5。由此可以看出,我们强制指定了各个函数的导出序号,其中最小的是2,最大的是5,那么从最小的2到最大的5要经历2、3、4、5总共4个数。因此在导出表中,基数Base的值为2,第七个成员的值为4。

第八个成员(NumberOfNames)是个DWORD类型,表示通过名字导出的函数/符号的数目。注意该值不是模块导出的函数/符号总数,总数由上面的NumberOfFunctions即第七个成员给出。本成员值可以为0,表示模块没有通过名字导出的函数,完全通过序数导出。我们这个模块(修改前的模块)导出了两个函数,一个按照序号导出,一个按照名字导出,所以看到此处的值为0x00000001。而修改后的模块fnDll2和fnDll3都按照名字导出,只有fnDll1仅以序号的方式导出,所以对于修改后的模块此处值为2。
    第九个成员(AddressOfFunctions)是个DWORD类型,指向一个地址表(即DWORD数组结构),被指向的地址表存放的是所有导出函数所在地址的RVA值。该地址表的地址个数(一个函数地址占4个字节)和第七个成员的值相等,而该地址表对应函数的导出序号从第六个成员(Base)开始,依次递增,每次增加一。
    第十个成员(AddressOfNames)是个DWORD类型,指向一个地址表(即DWORD数组结构),被指向的地址表存放的是所有导出函数名称字符串所在地址的RVA值。该地址表的地址个数和第八个成员(NumberOfNames)相等。
    第十一个成员(AddressOfNameOrdinals)是个DWORD类型,指向一个导出序号表(即WORD数组结构),被指向的表中每个成员是一个WORD类型,存放的是所有导出函数中以名称方式导出的函数的导出序号(实际导出序号是此表的成员值加上Base的值)。因此他与第十个成员(AddressOfNames)指向的地址表一一对应。所以该地址表的大小也和第八个成员(NumberOfNames)相等



  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值