PE 导出表
知识点:
1、exe程序一般只有导入表,但并不是一定,有可能也有导出表
2、dll程序一般导出表和导入表都有
一个可执行程序是由一堆PE文件构成的!
如下图可解释:
我加载的是一个.exe
,但是后面还有需要的.dll
文件,大家都知道.dll
也有PE文件,这里也就又有一个问题了,为什么要引入这么多的.dll
文件呢?
因为一个exe还需要使用这些.dll中所提供的函数,这些dll中就有相应的导出表,然后exe用LoadLibrary
动态加载,最后通过GetProcAddress
到获取函数的地址!
导出表的结构
在扩展PE头是一个名为_IMAGE_OPTIONAL_HEADER
的结构体
其中存在一个结构体数组为IMAGE_DATA_DIRECTORY
,个数有16个,总占128字节
其中关于导出表的结构体的名称为:导出表IMAGE_DIRECTORY_ENTRY_EXPORT
该结构体数组中第一个成员结构体如下:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //虚拟地址,存储当前导出表的地址 DWORD Size; //存储 当前导出表的大小 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
那么蓝颜色的就是该结构体数组
那么IMAGE_DIRECTORY_ENTRY_EXPORT
的结构体就是前八个字节,如下:
存储导出表的结构体 80 AD 02 00 78 01 00 00
存储导出表的虚拟地址为:0002AD80
存储导出表的大小为:00000178
这里的文件对齐和内存对齐的大小是一样的
注意:如果不一样还需要先将RVA地址转换为FOA的地址
导出表的结构体如下:
typedef 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 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
该结构体占103个字节,但是现在只看到了40字节,那还有63个字节呢?
其实这里面还有几个成员是指针,比如:
Name
:指向的地址是该导出表文件的字符串
AddressOfFunctions
:子表,导出函数地址表 RVA
AddressOfNames
:子表,导出函数名称表 RVA
AddressOfNameOrdinals
:子表,导出函数序号表 RVA
所以可以认为IMAGE_DIRECTORY_ENTRY_EXPORT
中的Size
可以忽视了
那么现在跟到0x0002AD80
中查看_IMAGE_EXPORT_DIRECTORY
导出表的结构体:
Name为如下:CE AD 02 00
NumberOfFunctions
:所有导出函数的个数有五个
NumberOfNames
:以函数名字导出的函数个数三个
AddressOfFunctions
:导出函数地址表 RVA
AddressOfNames
:导出函数名称表 RVA
AddressOfNameOrdinals
:导出函数序号表 RVA
理解图:
如何寻找导出函数的地址
第一种寻找导出的函数地址的方法: 通过导出函数名称表,再对应到导出函数序号表,最后从导出函数地址表中寻找!
如下图对应:
实现代码:
//1、导出函数名称表来寻找导出函数地址表,AddressOfNames是一个指向函数名称的地址 RVA_TO_FOA(pFileBuffer,pExportDirectory->AddressOfNames,&FOA); //printf("pExportDirectory->AddressOfNames导出函数名称表: 0x%x\n",FOA); //2、再加上pFileBuffer,转换为文件地址,得到函数名称存储的地方的首地址,当前的首地址是内存中的地址,也需要进行转换 AddressOfNamesTable = (PVOID)(*(PDWORD)((DWORD)pFileBuffer+(DWORD)FOA)); RVA_TO_FOA(pFileBuffer,(DWORD)AddressOfNamesTable,&FOA); AddressOfNamesTable = (PVOID)FOA; AddressOfNamesTable = (PVOID)((DWORD)pFileBuffer + (DWORD)AddressOfNamesTable); printf("\n"); printf("\n"); //3、得到函数名称表的文件地址,每个函数的名称 占四个字节,然后进行遍历判断 for(j=0;j<pExportDirectory->NumberOfNames;j++){ //(PDWORD)((DWORD)AddressOfNamesTable + 4*j); //获取当前函数名称表中的函数名称,然后循环判断 strcpy(FunName,(PVOID)((DWORD)AddressOfNamesTable + (strlen(AddressOfNamesTable)+1)*j)); if(0 == memcmp((PDWORD)((DWORD)AddressOfNamesTable + (DWORD)(strlen(AddressOfNamesTable)+1)*j),(PDWORD)FunName,strlen(FunName))){ //4、找到序号表AddressOfNameOrdinals下标所对应的的值,序号表中每个成员占2字节 word类型 RVA_TO_FOA(pFileBuffer,pExportDirectory->AddressOfNameOrdinals,&FOA); AddressOfNameOrdinalsNumber = *(PWORD)((DWORD)FOA + (DWORD)pFileBuffer + (DWORD)j*2); //5、通过序号表中下标对用的值去导出函数地址表AddressOfFunctions中寻找 该值下标对应的值 RVA_TO_FOA(pFileBuffer,pExportDirectory->AddressOfFunctions,&FOA); printf("函数序号: %d\t",AddressOfNameOrdinalsNumber); printf("函数名称为: %s\t",FunName); printf("导出函数地址表的地址为:0x%.8x\n",*(PDWORD)(PVOID)((DWORD)FOA + (DWORD)pFileBuffer + AddressOfNameOrdinalsNumber*4)); } }
小技巧:这里找的话同样可以通过导数函数序号表来找,导出函数序号表中的值 - _IMAGE_EXPORT_DIRECTORY中base的值,然后从导出函数地址表的下标寻找!
注意:上面的代码可以发现这种方法是找不到 def定义过的NONAME的函数名字,所以如果想要找到被隐藏的函数就需要通过第二种方法,逆着方法来找
第二种寻找导出的函数地址的方法: 通过导出函数地址表反推
也就是先从导出函数地址表开始进行寻找,上面的代码是先从导出名称表中 -> 导出序号表 -> 导出地址表,那么如果想要找到被def定义隐藏的函数,就需要逆着来,导出地址表 -> 导出序号表 -> 导出名称表中
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY