引入导出表
Win32下的一个PE文件,是由多个PE文件组成。比如通过OD打开一个Ipmsg.exe,查看模块M,会发现模块有一个ipmsg.exe文件和多个动态链接库.dll文件。
当一个exe文件通过使用动态链接库.dll的方式导出某.dll文件某函数进行使用时,就需要通过导入表获取该函数的各种信息。导入表记录了当前PE文件使用的所有其他PE文件的相关信息,比如使用到的.dll中的函数信息,使用到的其他PE文件的名字等等
与导入表相对的导出表,则记录了当前的PE文件的提供了哪些函数给其他PE文件使用
注意:一个安全的.exe文件通常不提供导出表,这就意味着.exe不提供函数给其他PE文件使用,但这并不意味着.exe文件不能提供函数给其他PE文件使用
而.dll文件通常提供导出表和导入表,这就意味着.dll文件既可以提供函数给其他PE文件使用,也可以使用其他PE文件的函数
导出表的位置
先定位到可选PE头,再找到可选PE头的最后一个成员:即一个结构体数组,结构体中每个成员大小都是四字节。第一个结构体就是导出表数据目录。结构体的第一个成员是导出表的内存偏移地址RVA,第二个成员是导出表的大小。
通过导出表数据目录的RVA转FOA就可以找到导出表
导出表其实就在这个PE文件的某个节中(后续的各种表也是在某个节中)
导出表的结构
struct _IMAGE_EXPORT_DIRECTORY{ //40字节
DWORD Characteristics; //未使用
DWORD TimeDateStamp; //时间戳,用于获取该PE文件编译时的时间
WORD MajorVersion; //未使用
WORD MinorVersion; //未使用
DWORD Name; //指向该导出表文件名字符串 *
DWORD Base; //导出函数起始序号 *
DWORD NumberOfFunctions; //所有导出函数的个数 *
DWORD NumberOfNames; //以函数名字导出的函数个数 *
DWORD AddressOfFunctions; //导出函数地址表RVA * DWORD AddressOfNames; //导出函数名称表RVA *
DWORD AddressOfNameOrdinals; //导出函数序号表RVA *
};
注意:我们在可选PE头中观察到的导出表的大小可能并不是40字节,这是因为在导出表中有几个指向其他表的指针,而这其他表便是导出表的子表。子表的大小加上导出表的大小才是我们在可选PE头中实际观察到的大小
1.Name
指向该导出表文件名字符串的RVA,比如一个DBGHELP.dll的PE文件具有导出表,其表中Name指向的字符串为为dbghelp.dll,注意字符串在内存中以0结尾
2.Base
导出函数起始序号(最小的序号)
比如有序号为14、6、10、8的导出函数,那么Base的值为6
3.NumberOfFunctions
所有导出函数的个数
注意:这个值是通过导出函数的最大序号 - 最小序号 + 1算出来的。正常来说这个值是多少,那么此PE文件中导出函数的个数就是多少。但是如果序号定义是不是连续的,中间有空缺的序号,那么此时NumberOfFunctions的值会比实际的定义的导出函数个数多
比如说一个PE文件在.def中定义的函数为Plus @12 、Sub @15 NONAME、 Mul @13 、Div @16
那么NumberOfFunctions值 = 16 - 12 + 1 = 5,而不是4
4.NumberOfNames
以函数名字导出的函数个数:比如以动态链接库的方式导出,导出时函数加了NONAME关键字,那么该函数就不计数(注意和只以序号导出函数区分)
5.AddressOfFunctions
导出函数地址表RVA,即该地址指向一个表。这个表中记录了此PE文件的所有导出函数的地址。我们举一个例子画出下表进行形象的表示
该表中每个元素宽度为4个字节
元素个数:由NumberOfFunctions决定
注意:导出函数是有序号的,上图中的下标0 1 2 3都是与序号最小的函数相比的相对序号,
解析此图:该PE文件以.def的方式自定义序号导出函数,定义了序号13、14、16、17、19的导出函数,那么Base的值应为13,那么序号为13的函数相对相对下标就是0,序号14的导出函数相对下标就是1,序号为15的导出函数虽然没有,但是会把位置空出来,定义地址值为NULL,即0x00000000,序号16的导出函数相对下标就是3…以此类推
当我们获取了函数地址以后,找到这个函数,复制该函数二进制数据在OD等软件打开即可获取该函数的反汇编,我们也就可以进一步推理该函数的作用
6.AddressOfNames
导出函数名称表RVA我们在硬盘数据找该表地址时要先转成FOA,这个地址指向的表记录了导出函数的名称字符串RVA首地址,而不是导出函数名称。从首地址开始到00结束,便是整个函数的名称
该表中元素宽度:4个字节
函数名称表是按名字ASCII码排序排序的
表中元素的数量:由NumberOfNames决定
如果函数导出时添加了NONAME,即函数没有名称,那么这个表中就不会出现这个函数名地址
注意:一般来说AddressOfNames表中元素个数比AddressOfFunctions表中元素个数少。这是因为AddressOfFunctions表中不管导出函数有没有名字,都会有地址。但是特殊情况下AddressOfNames表中元素个数比AddressOfFunctions表中元素个数多,这是因为导出函数时可以让多个不同名字的函数指向同一个函数地址
NONAME导出函数注意事项:
如果导出时,定义的无名字函数,即Div @13 NONAME,那么函数名称表中就不会有指向Div函数名的元素,同样函数序号表中也不会有Div的相对序号,但是在函数地址表中会留出来一个元素位置存储Div函数地址
7.addressOfNameOrdinals
导出函数序号表RVA,该地址指向一个存储导出函数的相对序号的表
该表中元素宽度:2个字节
表中存储内容与AddressOfName表一一对应的,先确定AddressOfName表的地址对应的函数,之后按照函数对应AddressOfNameOrdinals表中的相对序号
该表中存储的内容 + Base = 函数的导出序号,base指的是序号最小的函数的序号
表中元素个数:由NumberOfNames决定
如下图,很形象的表现了两表之间的关系:
比如:现在要找名字叫Sub的导出函数,经查找在AddressOfNames指向的表的下标为1的位置,那么这个函数所对应的相对序号则在AddressOfNameOrdinals指向的表中下标为1的位置,之后我们取相对序号就可以在AddressOfFunctions指向的表找到我们想要查找的表了
导出表获取函数地址
Windows中有一个API:
FARPROC GetProcAddress
(
HMODULE hModule, // DLL模块句柄
LPCSTR lpProcName // 函数名
);
通过使用这个函数,我们可以获取导出表中的函数的地址。
该函数使用有以下两种方法:1.按名字找函数地址2. 按序号找函数地址
按名字找函数地址
找到导出表后,先根据AddressOfNames,将RVA转成FOA,即可定位到函数名称表,遍历函数名称表,用名字依次与函数名称表每个元素指向的字符串做比较,直到名字匹配,记录下此时元素在函数名称表的下标i
再根据AddressOfNameOrdinals,将RVA转成FOA,即可定位到函数序号表,得到此表下标为i的元素值n
最后根据AddressOfFunctions,将RVA转成FOA,即可定位到函数地址表,则此表下标为n的元素值就是该名字对应函数的RVA地址,将RVA转成FOA就得到了该导出函数在FileBuffer中的地址
比如要根据名字查找导出函数mul(),假设在函数名称表中下标为2指向的位置找了"mul"字符串与函数名称匹配,所以再去查找函数序号表下标为2的元素为0x0004,最后再函数地址表中找下标为4的元素值,即为mul函数的RVA,转成FOA即可
按序号找函数地址
找到导出表后,根据AddressOfFunctions,将RVA转成FOA,定位到函数地址表
再用给定的序号 - Base = 相对序号i,得到相对序号i
最后找函数地址表中下标为i的元素值即为该序号对应函数RVA地址,将RVA转成FOA就得到了该函数在FileBuffer中的地址
比如要根据序号找导出函数mul(),因为mul的序号为17,Base值为13,那么17 - 13 = 4,所以直接再函数地址表中找到下标为4的元素值,即为mul函数的RVA,最后转成FOA即可
所以根据导出序号找,跟函数序号表没有关系!函数序号表的存在就是为了用于按名字查找
上图可以发现,t_method函数用NONAME导出,所以函数名称表和函数序号表都没有t_method的信息,但是由于此函数序号为19,相对序号为19 - 13 = 6,所以会在函数地址表中下标为6的位置存储t_method函数的RVA