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

pe,结构,导出,详细,解析 · 浏览次数 : 20

小编点评

**导出表(Export Table)** 导出表是一个用于记录可执行文件导出函数的结构,包含以下三种信息: * **函数名称:**可执行文件中导出函数的名称。 * **函数地址:**可执行文件中导出函数的地址。 * **函数序号:**可执行文件中导出函数的序号。 导出表包含在 PE 文件的导出表中,导出表的位置存储在 PE 文件头中的数据目录表中,与导出表对应的项目是数据目录中的首个 IMAGE_DATA_DIRECTORY 结构。 通过导出表,DLL 文件可以向系统提供导出函数的名称、序号和入口地址等信息,以便 Windows 装载器能够通过这些信息来完成动态链接的整个过程。

正文

导出表(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.6 PE结构:导出表详细解析相似的内容:

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

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

[转帖]Perf 笔记

https://www.cnblogs.com/jyi2ya/p/16278495.html 环境 Linux Syameimaru-Aya 5.17.0-2-amd64 #1 SMP PREEMPT Debian 5.17.6-1 (2022-05-11) x86_64 GNU/Linux。 Pe

Blazor前后端框架Known-V1.2.6

# V1.2.6 Known是基于C#和Blazor开发的前后端分离快速开发框架,开箱即用,跨平台,一处代码,多处运行。 - Gitee: [https://gitee.com/known/Known](https://gitee.com/known/Known) - Github:[https:/

[转帖]2.6 The jcmd Utility

https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr006.html#BABEJDGE The jcmd utility is used to send diagnostic command req

操作系统开发:BIOS/MBR基础与调试

这里在实验之前需要下载 Bochs-win32-2.6.11 作者使用的是Linux版本的,在Linux写代码不太舒服,所以最好在Windows上做实验,下载好虚拟机以后还需要下载Nasm汇编器,以及GCC编译器,为了能够使用DD命令实现磁盘拷贝,这里你可以安装windows 10 下面的子系统Ub

Langchain-Chatchat项目:1.2-Baichuan2项目整体介绍

由百川智能推出的新一代开源大语言模型,采用2.6万亿Tokens的高质量语料训练,在多个权威的中文、英文和多语言的通用、领域benchmark上取得同尺寸最佳的效果,发布包含有7B、13B的Base和经过PPO训练的Chat版本,并提供了Chat版本的4bits量化。 一.Baichuan2模型 B

正则表达式

正则表达式 import re 限定符(quantifier) a* a出现0次或者多次 a+ a出现1次或者多次 a? a出现0次或者一次 a{6} a出现6次 a{2,6} a出现2-6次 a{2,} a出现两次以上 或运算符(OR Operator) (a|b) 匹配 (ab)|(cd) 匹配

[转帖]【redis】redis各稳定版本特性(更新到6.0版本)

1.Redis2.6 Redis2.6在2012年正是发布,经历了17个版本,到2.6.17版本,相对于Redis2.4,主要特性如下: 1)服务端支持Lua脚本。 2)去掉虚拟内存相关功能。 3)放开对客户端连接数的硬编码限制。 4)键的过期时间支持毫秒。 5)从节点支持只读功能。 6)两个新的位

[转帖]Redis里使用Lua

http://me.52fhy.com/lua-book/chapter11.html 版本:自2.6.0起可用。 时间复杂度:取决于执行的脚本。 使用Lua脚本的好处: 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。 原子操作。redis会将整个脚本作为一个整体执行,中间不会被

[转帖]redis惰性删除 lazy free 源码剖析,干货满满

目录 前言 数据删除场景 lazy free 概念 配置 源码剖析(版本 6.2.6) 场景一:客户端执行的显示删除/清除命令 场景二:某些指令带有的隐式删除命令 场景三:删除过期数据 场景四:内存淘汰数据删除 场景五:主从同步清空从库 小结 前言 都说 redis 是单线程的,其实并不是说 red