2.6 PE结构:导出表详细解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 导出表(Export Table)是Windows可执行文件中的一个结构,记录了可执行文件中某些函数或变量的名称和地址,这些名称和地址可以供其他程序调用或使用。当PE文件执行时Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中函数的导出信息对可执行文件的导入表(IAT)进行修正。

导出表(Export Table)是Windows可执行文件中的一个结构,记录了可执行文件中某些函数或变量的名称和地址,这些名称和地址可以供其他程序调用或使用。当PE文件执行时Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中函数的导出信息对可执行文件的导入表(IAT)进行修正。

导出表中包含了三种信息:

  • 函数名称:记录了可执行文件中导出函数的名称,在其他程序中调用时需要用到这个名称。
  • 函数地址:记录了可执行文件中导出函数的地址,使用时需要调用该函数的地址。
  • 函数序号:记录了每个导出函数的序号,可以通过序号直接调用函数。

导出函数的DLL文件中,导出信息被保存在导出表,导出表就是记载着动态链接库的一些导出信息。通过导出表,DLL文件可以向系统提供导出函数的名称、序号和入口地址等信息,以便Windows装载器能够通过这些信息来完成动态链接的整个过程。

导出函数存储在PE文件的导出表里,导出表的位置存放在PE文件头中的数据目录表中,与导出表对应的项目是数据目录中的首个IMAGE_DATA_DIRECTORY结构,从这个结构的VirtualAddress字段得到的就是导出表的RVA值,导出表同样可以使用函数名或序号这两种方法导出函数。

导出表的起始位置有一个IMAGE_EXPORT_DIRECTORY结构与导入表中有多个IMAGE_IMPORT_DESCRIPTOR结构不同,导出表只有一个IMAGE_EXPORT_DIRECTORY结构,该结构定义如下:

typedef struct _IMAGE_EXPORT_DIRECTORY
{
   
   
    DWORD   Characteristics;       // 保留,恒为0x00000000
    DWORD   TimeDateStamp;         // 文件的产生时间戳
    WORD    MajorVersion;          // 主版本号
    WORD    MinorVersion;          // 次版本号
    DWORD   Name;                  // 指向文件名的RVA
    DWORD   Base;                  // 导出函数的起始序号
    DWORD   NumberOfFunctions;     // 导出函数总数
    DWORD   NumberOfNames;         // 以名称导出函数的总数
    DWORD   AddressOfFunctions;    // 导出函数地址表的RVA
    DWORD   AddressOfNames;        // 函数名称地址表的RVA
    DWORD   AddressOfNameOrdinals; // 函数名序号表的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

上面的_IMAGE_EXPORT_DIRECTORY结构如果总结成一张图,如下所示:

在上图中最左侧AddressOfNames结构成员指向了一个数组,数组里保存着一组RVA,每个RVA指向一个字符串即导出的函数名,与这个函数名对应的是AddressOfNameOrdinals中的结构成员,该对应项存储的正是函数的唯一编号并与AddressOfFunctions结构成员相关联,形成了一个导出链式结构体。

获取导出函数地址时,先在AddressOfNames中找到对应的名字MyFunc1,该函数在AddressOfNames中是第1项,然后从AddressOfNameOrdinals中取出第1项的值这里是1,然后就可以通过导出函数的序号AddressOfFunctions[1]取出函数的入口RVA,然后通过RVA加上模块基址便是第一个导出函数的地址,向后每次相加导出函数偏移即可依次遍历出所有的导出函数地址,代码如下所示:

int main(int argc, char * argv[])
{
   
   
    BOOL PE = IsPeFile(OpenPeFile("c://pe/lyshark.dll"), 0);

    if (PE == TRUE)
    {
   
   
        // 0. 获取到ImageBase镜像基地址
        DWORD ImageBase = NtHeader->OptionalHeader.ImageBase;

        // 1. 从数据目录表的下标为 0 的项找到rva
        DWORD rav = NtHeader->OptionalHeader.DataDirectory[0].VirtualAddress;

        // 2. 找到导入表结构体
        auto ExportTable = (PIMAGE_EXPORT_DIRECTORY)(RVAtoFOA(rav) + GlobalFileBase);

        // 3. 获取有名字的个数和函数总个数
        DWORD NameCount = ExportTable->NumberOfNames;
        DWORD FunctionCount = ExportTable->NumberOfFunctions;

        // 4. 获取三张表,分别是 地址表,名称表,序号表,其中序号表是WORD
        DWORD* Addr_Table = (DWORD*)(RVAtoFOA(ExportTable->AddressOfFunctions) + GlobalFileBase);
        DWORD* Name_Table = (DWORD*)(RVAtoFOA(ExportTable->AddressOfNames) + GlobalFileBase);
        WORD* Id_Table = (WORD*)(RVAtoFOA(ExportTable->AddressOfNameOrdinals) + GlobalFileBase);

        printf("序号 \t 导出RVA地址 \t 导出VA地址 \t 导出FOA地址 \t 导出函数 \t \n");

        // 5. 遍历地址表
        for (DWORD i = 0; i < FunctionCount; ++i)
        {
   
   
            bool HaveName = FALSE;

            // 6. 判断是否有名字,有名字的话,下标会存在序号表中
            for (DWORD j = 0; j < NameCount; ++j)
            {
   
   
                // 如果有名字则执行此处
                if (i == Id_Table[j])
                {
   
   
                    HaveName = TRUE;
                    // 对应序号表下标的名称表内保存的是名字
                    CHAR* Name = (CHAR*)(RVAtoFOA(Name_Table[j]) + GlobalFileBase);
                    printf("%5d \t %10p \t 0x%08X \t 0x%08X \t %-35s \n",
                        i + ExportTable->Base, Addr_Table[i], ImageBase + Addr_Table[i], RVAtoFOA(Addr_Table[i]), Name);
                    break;
                }
            }
            // 如果全部找完还没有名字
            if (HaveName == FALSE)
            {
   
   
                printf("%5d \t %10p \t 0x%08X \t 0x%08X \t None \n",
                    i + ExportTable->Base, Addr_Table[i], ImageBase + Addr_Table[i], RVAtoFOA(Addr_Table[i]));
            }
        }
    }
    else
    {
   
   
        printf("非标准程序 \n");
    }

    system("pause");
    return 0;
}

运行如上程序片段,则会输出lyshark.dll动态链接库里面所有的导出函数,其输出效果如下图所示;

本文作者: 王瑞
本文链接: https://www.lyshark.com/post/407f7b06.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章
|
2月前
|
人工智能
歌词结构的巧妙安排:写歌词的方法与技巧解析,妙笔生词AI智能写歌词软件
歌词创作是一门艺术,关键在于巧妙的结构安排。开头需迅速吸引听众,主体部分要坚实且富有逻辑,结尾则应留下深刻印象。《妙笔生词智能写歌词软件》提供多种 AI 功能,帮助创作者找到灵感,优化歌词结构,写出打动人心的作品。
|
2月前
|
机器学习/深度学习 搜索推荐 大数据
深度解析:如何通过精妙的特征工程与创新模型结构大幅提升推荐系统中的召回率,带你一步步攻克大数据检索难题
【10月更文挑战第2天】在处理大规模数据集的推荐系统项目时,提高检索模型的召回率成为关键挑战。本文分享了通过改进特征工程(如加入用户活跃时段和物品相似度)和优化模型结构(引入注意力机制)来提升召回率的具体策略与实现代码。严格的A/B测试验证了新模型的有效性,为改善用户体验奠定了基础。这次实践加深了对特征工程与模型优化的理解,并为未来的技术探索提供了方向。
104 2
深度解析:如何通过精妙的特征工程与创新模型结构大幅提升推荐系统中的召回率,带你一步步攻克大数据检索难题
|
27天前
|
机器学习/深度学习 自然语言处理 数据管理
GraphRAG核心组件解析:图结构与检索增强生成
【10月更文挑战第28天】在当今数据科学领域,自然语言处理(NLP)和图数据管理技术的发展日新月异。GraphRAG(Graph Retrieval-Augmented Generation)作为一种结合了图结构和检索增强生成的创新方法,已经在多个应用场景中展现出巨大的潜力。作为一名数据科学家,我对GraphRAG的核心组件进行了深入研究,并在此分享我的理解和实践经验。
52 0
|
2月前
|
存储 编译器 C++
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
【初阶数据结构】掌握二叉树遍历技巧与信息求解:深入解析四种遍历方法及树的结构与统计分析
|
1月前
光纤电缆(FOC)的结构深度解析
【10月更文挑战第21天】
51 0
|
3月前
|
数据采集 存储 JavaScript
如何使用Cheerio与jsdom解析复杂的HTML结构进行数据提取
在现代网页开发中,复杂的HTML结构给爬虫技术带来挑战。传统的解析库难以应对,而Cheerio和jsdom在Node.js环境下提供了强大工具。本文探讨如何在复杂HTML结构中精确提取数据,结合代理IP、cookie、user-agent设置及多线程技术,提升数据采集的效率和准确性。通过具体示例代码,展示如何使用Cheerio和jsdom解析HTML,并进行数据归类和统计。这种方法适用于处理大量分类数据的爬虫任务,帮助开发者轻松实现高效的数据提取。
如何使用Cheerio与jsdom解析复杂的HTML结构进行数据提取
|
4月前
|
存储 缓存 NoSQL
深入解析Memcached:内部机制、存储结构及在大数据中的应用
深入解析Memcached:内部机制、存储结构及在大数据中的应用
|
16天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
70 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
57 0

推荐镜像

更多