一、导出表概述
1.引入导出表
-
前面学过一个Win32下的.exe文件,是由多个PE文件组成。比如通过OD打开一个Ipmsg.exe,查看模块
M,会发现一个ipmsg.exe和多个.dll(即模块)构成。
-
这种以动态链接库.dll的方式导出函数时,如果此.exe需要用到某个.dll中的函数,那么.exe怎么知道:
- 引入的.dll中有哪些函数可以供.exe使用
- .dll中的各种函数都存在哪里
-
所以.dll中应当有一个目录或者结构提供给.exe文件,来记录.dll中的函数有哪些,函数的起始地址等信息。所以如果.dll或者.exe文件想要给别的程序提供函数,就必须同时给别的程序一个“清单”,这个“清单”就是导出表
-
故当我们分析上面例子中的ole32.dll,或者LPK.dll等.dll文件结构时,就可以看到有导出表
2.常见误区
- 不是只有.dll才提供函数给别的程序使用,有些.exe也可以提供函数
- 有些.exe的导出表数据目录不是全0,表示有此.exe程序有导出表,即可对外提供函数;有些没有导出表,就不对外提供函数
- 所以一个PE文件(如.dll/.exe/.sys)如果对外提供函数,那么就需要有导出表,且需要遵守一定的格式
- 一个安全的PE文件,是不会显示导出表的,但可以对外提供函数:正常情况下如果一个PE文件不提供导出表,别人是无法使用其函数的;但是逆向一个游戏时,如果知道了别人的导出表,就不用找call了(不现实)。所以游戏会做安全加固,我们不知道哪些函数是导出的,就算给我函数地址,我也没办法拿这个地址去调用,只能分析反汇编代码,分析程序入口是什么,分析函数的参数是什么
3.导出表在哪里
-
先定位到可选PE头 --> 再找到可选PE头的最后一个成员:即一个结构体数组,第一个结构体就是导出表数据目录,第一个成员是导出表的内存偏移地址RVA,第二个成员是导出表的大小

-
通过导出表数据目录的RVA转FOA就可以找到导出表
-
导出表其实就在这个PE文件的某个节中(后面要学的各种表也是在某个节中)
-
注意事项:学到现在解析PE结构时就不用拉伸了,用FileBuffer分析即可。因为拉伸只是有助于理解,如果直接在FileBuffer中做,只是需要多使用一个==RVA–>FOA==的地址转换函数,每次遇到RVA地址时转换一下即可,就省去了将文件整个拉伸的过程,会减少很多代码量和多余操作,而且市面上关于PE的书籍几乎都是直接用FileBuffer讲解操作的,所以现在开始,我们就要学会不拉伸的情况下去分析
4.导出表的结构
struct _IMAGE_EXPORT_DIRECTORY{
//40字节
DWORD Characteristics; //未使用
DWORD TimeDateStamp; //时间戳
WORD MajorVersion; //未使用
WORD MinorVersion; //未使用
DWORD Name; //指向该导出表文件名字符串 *
DWORD Base; //导出函数起始序号 *
DWORD NumberOfFunctions; //所有导出函数的个数 *
DWORD NumberOfNames; //以函数名字导出的函数个数 *
DWORD AddressOfFunctions; //导出函数地址表RVA *
DWORD AddressOfNames; //导出函数名称表RVA *
DWORD AddressOfNameOrdinals; //导出函数序号表RVA *
};
1.Name
-
指向该导出表文件名字符串的RVA,比如一个DBGHELP.dll的PE文件提供函数,那么这个PE文件的Name指向的字符串为为dbghelp.dll
可以用LordPE软件,选择
PE Editor,选择你要打开的PE文件,查看其导出表
2.Base
-
导出函数起始序号(最小的序号)
比如有序号为14、6、10、8的导出函数,那么Base的值为6
3.NumberOfFunctions
-
所有导出函数的个数
-
注意:day35中讲过,这个值是通过导出函数的最大序号 - 最小序号 + 1算出来的;正常来说这个值是多少,那么此PE文件中导出函数的个数就是多少。但是如果使用自定义序号,序号定义时不是连续的,而是中间有空缺的序号,那么此时NumberOfFunctions的值会比实际的定义的导出函数个数多
- 比如在.def中定义为
Plus @12、Sub @15 NONAME、Mul @13、Div @16 - 那么NumberOfFunctions值 = 16 - 12 + 1 = 5;而不是4!
- 比如在.def中定义为
4.NumberOfNames
- 以函数名字导出的函数个数:比如以动态链接库的方式导出(注意和只以序号导出函数区分)
- 如果导出时加了
NONAME关键字,那么就不计数
5.AddressOfFunctions
-
导出函数地址表RVA,即这个地址指向一个表!这个表中记录了此PE文件的所有导出函数的地址
-
该表中元素宽度:4个字节
-
元素个数:由NumberOfFunctions决定
-
注意:day35中讲过导出函数是有序号的,上图中的下标等于导出函数的相对序号
(参考上图)比如使用自定义序号导出函数,即.def的方式导出,定义了序号13、14、16、17、19的导出函数,那么Base的值应为13,那么序号为13的函数相对相对下标就是0,序号14的导出函数相对下标就是1,序号为15的导出函数虽然没有,但是会把位置空出来,只是地址值为NULL,即0x00000000,序号16的导出函数相对下标就是3…以此类推
6.AddressOfNames
-
导出函数名称表RVA(拉伸后的内存地址偏移,所以要先转成FOA),即这个地址指向一个表,这个表中记录的是导出函数的名称字符串地址!!不是直接存储名称(且此字符串地址也是RVA)
就像C语言中的字符串
char* name = "abc";,name变量值为abc这个字符串的首地址。所以导出函数名称表中存储的是name值(即名称的地址),而不是直接存储的abc这个名称
-
该表中元素宽度:4个字节
-
表中元素的数量:由NumberOfNames决定
-
注意:如果函数导出时添加了
NONAME,即函数没有名称,那么这个表中就不会出现这个函数名地址- 所以AddressOfNames表中元素个数可能比AddressOfFunctions表中元素个数少!(AddressOfFunctions表中不管导出函数有没有名字,都会有地址)
- 也有可能AddressOfNames表中元素个数比AddressOfFunctions表中元素个数多!因为导出函数时可以让多个不同名字的函数指向同一个函数地址
-
函数名称表中是按名字排序的(根据ASCII码排序)
7.AddressOfNameOrdinals
-
导出函数序号表RVA,即这个地址指向一个表,表中存的是导出函数的相对序号
-
即==该表中存储的内容 + Base = 函数的导出序号==
-
表中元素个数:由NumberOfNames决定,且和AddressOfName表一一对应的
比如:现在要找名字叫Sub的导出函数,经查找在AddressOfNames表的下标为1的位置,那么这个函数所对应的相对序号则在AddressOfNameOrdinals表中下标为1的位置,即由上图可知Sub函数对应的序号为0x0001 + Base
-
该表中元素宽度:2个字节
5.NONAME导出函数注意事项
- 如果导出时,定义的无名字函数,即
Div @13 NONAME,那么函数名称表中就不会有指向Div函数名的元素,同样函数序号表中也不会有Div的相对序号,但是在函数地址表中会留出来一个元素位置存储Div函数地址
二、由导出表获取函数地址
1.按名字找函数地址
- 找到导出表后,先根据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即可
2.按序号找函数地址
-
找到导出表后,根据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
三、作业
1.编写程序打印导出表的信息
#include "stdafx.h"
#include <stdlib.h>
typedef unsigned short WORD;
typedef unsigned int DWORD;
typedef unsigned char BYTE;
#define MZ 0x5A4D
#define PE 0x4550
#define IMAGE_SIZEOF_SHORT_NAME 8
//DOS头
struct _IMAGE_DOS_HEADER {
WORD e_magic; //MZ标记
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
DWORD e_lfanew; //PE文件真正开始的偏移地址
};
//标准PE头
struct _IMAGE_FILE_HEADER {
WORD Machine; //文件运行平台
WORD NumberOfSections; //节数量
DWORD TimeDateStamp; //时间戳
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; //可选PE头大小
WORD Characteristics; //特征值
};
//数据目录
struct _IMAGE_DATA_DIRECTORY{
DWORD VirtualAddress;
DWORD Size;
};
//可选PE头
struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; //文件类型
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode; //代码节文件对齐后的大小
DWORD SizeOfInitializedData; //初始化数据文件对齐后的大小
DWORD SizeOfUninitializedData; //未初始化数据文件对齐后大小
DWORD AddressOfEntryPoint; //程序入口点(偏移量)
DWORD BaseOfCode; //代码基址
DWORD BaseOfData; //数据基址
DWORD ImageBase; //内存镜像基址
DWORD SectionAlignment; //内存对齐粒度
DWORD FileAlignment; //文件对齐粒度
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; //文件装入虚拟内存后大小
DWORD SizeOfHeaders; //DOS、NT头和节表大小
DWORD CheckSum; //校验和
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve; //预留堆栈大小
DWORD SizeOfStackCommit; //实际分配堆栈大小
DWORD SizeOfHeapReserve; //预留堆大小
DWORD SizeOfHeapCommit; //实际分配堆大小
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; //目录项数目
_IMAGE_DATA_DIRECTORY DataDirectory[16]; //数据目录
};
//NT头
struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE签名
_IMAGE_FILE_HEADER FileHeader;
_IMAGE_OPTIONAL_HEADER OptionalHeader;
};
//节表
struct _IMAGE_SECTION_HEADER{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //节表名
union{
DWORD PhysicalAddress;
DWORD VirtualSize; //内存中未对齐大小
}Misc;
DWORD VirtualAddress; //该节在内存中偏移地址
DWORD SizeOfRawData; //该节在硬盘上文件对齐后大小
DWORD PointerToRawData; //该节在硬盘上文件对齐后偏移地址
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; //该节特征属性
};
//导出表
struct _IMAGE_EXPORT_DIRECTORY{
DWORD Characteristics; //未使用
DWORD TimeDateStamp; //时间戳
WORD MajorVersion; //未使用
WORD MinorVersion; //未使用
DWORD Name; //指向该导出表文件名字符串 *
DWORD Base; //导出函数起始序号 *
DWORD NumberOfFunctions; //所有导出函数的个数 *
DWORD NumberOfNames; //以函数名字导出的函数个数 *
DWORD AddressOfFunctions; //导出函数地址表RVA *
DWORD AddressOfNames; //导出函数名称表RVA *
DWORD AddressOfNameOrdinals; //导出函数序号表RVA *
};
/*计算文件大小函数
参数:文件绝对路径
返回值:返回文件大小(单位字节)
*/
int compute_file_size(char* filePath){
FILE* fp = fopen(filePath,"rb");
if(!fp){
printf("打开文件失败");
exit(0);
}
fseek(fp,0,2);
int size = ftell(fp);
//fseek(fp,0,0); 单纯计算文件大小,就不需要还原指针了
fclose(fp);
return size;
}
/*将文件读入FileBuffer函数
参数:文件绝对路径
返回值:FileBuffer起始地址
*/
char* to_FileBuffer(char* filePath){
FILE* fp = fopen(filePath,"rb");
if(!fp){
printf("打开文件失败");
exit(0);
}
int size = compute_file_size(filePath);
char* mp = (char*)malloc(sizeof(char) * size); /


281

被折叠的 条评论
为什么被折叠?



