专栏首页逆向技术PE知识复习之PE的导出表

PE知识复习之PE的导出表

                   PE知识复习之PE的导出表

一丶简介

 在说明PE导出表之前.我们要理解.一个PE可执行程序.是由一个文件组成的吗.

答案: 不是.是由很多PE文件组成.DLL也是PE文件.如果我们PE文件运行.那么就需要依赖DLL.系统DLL就是Kerner32.dll user32.dll等等.这些都是PE文件.

什么是导出表:

    导出表就是当前的PE文件提供了那些函数.给别人用. 举个例子: PE文件相当于一个饭店.那么菜单就是导出表.

导出表解盲:

    有人认为exe可执行文件.没有导出表.而DLL有导出表.这个是错误的. 不管是exe.还是DLL 本质都是PE文件. exe文件也可以导出函数给别人使用. 一般EXE没有.但不是不可以有. 注意分清.

二丶导出表讲解

    在讲解导出表之前.我们要确定导出表在哪里.

在讲解扩展头的时候.里面有一个结构体数组.我们称之为数据目录.里面有16项成员.

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;                        虚拟地址
    DWORD   Size;                                  大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

导入表.导出表都在数据目录中存储着.

这个结构存储的是导出表在哪里.以及导出表有多大.

其中数据目录每一项都是保存着不同的表

例如第一项就是导出表. 记录了导出表的虚拟地址 以及大小.

如下:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor

因为结构体记录的是导出表的RVA. 所以我们需要转换为FOA 去PE文件中查看.

RVA 判断在那个节. RVA-节.VirtuallAddress == 差值偏移

FOA == 差值偏移+ 节.PointerToRawData

前边所说.是定位导出表在哪里. 定位之后.才是真正的导出表结构体.

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;    // 不加红的不重要
    DWORD   TimeDateStamp;      //时间戳.  编译的时间. 把秒转为时间.可以知道这个DLL是什么时候编译出来的.
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;           //指向该导出表文件名的字符串,也就是这个DLL的名称  辅助信息.修改不影响  存储的RVA 如果想在文件中查看.自己计算一下FOA即可.
    DWORD   Base;           // 导出函数的起始序号
    DWORD   NumberOfFunctions;     //所有的导出函数的个数
    DWORD   NumberOfNames;         //以名字导出的函数的个数
    DWORD   AddressOfFunctions;     // 导出的函数地址的 地址表  RVA  也就是 函数地址表  
    DWORD   AddressOfNames;         // 导出的函数名称表的  RVA      也就是 函数名称表
    DWORD   AddressOfNameOrdinals;  // 导出函数序号表的RVA         也就是 函数序号表
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

一个导出表大小是 0x28个字节. 也就是两行半.

其中重要成员都标红了. 最重要的是导出表中最后三个成员.是三个子表.

都是RVA

PS: 数据目录中的 Size成员.保存的是导出表中以及导出表子表中的所有成员大小. 这个值不影响.编译器计算后填写好的.

三丶导出表各成员解析

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;    // 不加红的不重要
    DWORD   TimeDateStamp;      //时间戳.  编译的时间. 把秒转为时间.可以知道这个DLL是什么时候编译出来的.
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;           //指向该导出表文件名的字符串,也就是这个DLL的名称  辅助信息.修改不影响  存储的RVA 如果想在文件中查看.自己计算一下FOA即可.
    DWORD   Base;           // 导出函数的起始序号
    DWORD   NumberOfFunctions;     //所有的导出函数的个数
    DWORD   NumberOfNames;         //以名字导出的函数的个数
    DWORD   AddressOfFunctions;     // 导出的函数地址的 地址表  RVA  也就是 函数地址表  
    DWORD   AddressOfNames;         // 导出的函数名称表的  RVA      也就是 函数名称表
    DWORD   AddressOfNameOrdinals;  // 导出函数序号表的RVA         也就是 函数序号表
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

解析导出表.所需要的是一个DLL. 这里我拷贝一下系统的DLL kerner32.dll进行解析.

解析导出表的第一步就是定位导出表.求出FOA. 也就是在文件中的位置.

数据目录中查看导出表RVA

在数据目录中得出导出表RVA == 0x90380 大小 == D4DC

查看属于那个节.求出FOA

得出在.rdata节中. 节.虚拟地址 == 0x80000 节.文件偏移 == 0x65000

FOA = 0x90380 - 0x80000 + 0x65000 == 0x75380

所以在PE文件中.文件偏移 0x75380为导出表结构.

1.Name成员解析

首先解析导出表重要的成员

Nmae: 在导出表一行位置处. 存储0x9416A. 这是一个RVA 所以我们要进行FOA转换. 这里直接计算了. FOA == 7916A

可见这个成员保存的就是自己DLL的名称.

2.Base成员解析. 导出函数起始序号

导出函数的序号起始位置. 你DLL导出的函数.如果给序号了.那么就从这个序号开始.

3.NumberOfFunctions 以及 NumberOfNmaes 函数导出总个数.以及函数以名字导出的个数

这个两个成员很简单. 一个就是所有函数导出的个数.一个就是以名字进行导出的个数. DLL是可以以序号导出的.

所有函数导出是 62d个函数. 名字导出是 62d个函数. 如果有按照序号导出.那么以函数名导出的个数就会跟所有函数导出个数不一样.

那么就有公式可以计算出. 未导出的函数是多少个.

所有导出函数个数 - 以名字导出的个数 == 差值个数. 未导出的.或者是以序号导出的.

4.1函数地址表

  前面的都很简单.下面的就是子表了.

三个子表都是RVA 我们直接都进行一下FOA转换.

函数地址表 FOA == 0x753A8

函数名称表 FOA == 0x76c5c

函数序号表 FOA == 0x78510

  函数地址表: 函数地址表指向一个偏移. 这个偏移存放了函数所有导出个数的 函数的地址.

例如所有导出函数有2个. 那么函数地址表中就有2项. 没一个占4个字节. 存放的是函数地址的 RVA偏移.

函数地址表. 4个字节进行存储. 总共有函数所有导出函数个数大小个字节. 例如第一项 RVA偏移为 0x0162A0 函数地址偏移 + ImageBase 就是函数地址.

例如我电脑上Kerner32.dll加载的Imagebase为 76360000 我们在文件中看的函数偏移为 0x162A0 相加就得出一个导出函数地址了 0x763762A0

PS: 因为我们在文件中查看导出表.所以一直在转换FOA ,如果在内存中查看就很简单了.

数据目录的RVA + ImageBase 定位导出表位置.

导出表结构体中定义的RVA偏移+Imagebase就能得出其它表的位置.就不用我们进行转换了.

还需要注意的就是,如果你按照序号导出. 1 3 4 5导出了4个函数. 在导入表中我们的函数地址表中的地址会有5个.原因就是.序号会给我们用0填充. 1 2 3 4 5 虽然第二项并没有.但是也会给我们导出.

如果函数地址我们已经知道了.我们要怎么只有函数地址的情况下.确定是哪个函数?

4.2 函数名称表

  函数名称表也是存储的名称RVA. 4个字节存储一个. 存储的大小 跟导出表的以函数名字导出个数 这个成员来决定的.

以名称导出函数的个数 例如为10 .那么函数名称表就可以存储10个RVA. 每一个为4个字节.

里面的RVA指向了当前导出函数的函数名称.

例如上面已经算出 函数地址表的FOA位置

函数名称表 FOA == 0x76c5c

那么我们去函数名称表中查看.

表中存储的都是RVA. 如果在内存中.我们直接RVA + 当前PE的ImageBase就可以看到函数导出的名称了.不过我们现在算一下.

FOA = 0x941D6 - 0x80000 + 0x65000 = 0x791D6

我们表中的第一项的FOA位置为0x791d6 在文件中就保存这导出函数的名称

例如下图文件偏移处:

注意: 函数名称表保存的并不是函数名称.而是指向函数名称的RVA偏移. 还有RVA偏移是按照字母排序的.并不是按照你导出的时候函数的顺序进行排序的.

例如:

  EXPORT

    SUB

    ADD

    MUL

导出三个函数.那么第一项就为 ADD.因为按照字母排序.A在前边.后面依次类推. 所以我们上面看到的函数名称 ACquireSRW 这个函数名称.并不是Kerner32.dll第一个导出的函数.

4.3函数序号表

  我们DLL导出函数的时候.会有序号进行导出.但是并不是说.如果按照名字导出名称表中有.序号表中就没有.

序号表的个数跟函数名称表个数是一样的.都依赖成员 导出表.函数名称导出表个数 这个成员来决定的.

序号表是给名称表的使用的. 序号表占两个字节.存储序号.

函数序号表 FOA == 0x78510

0300 0400 0500 序号.两个字节进行存储的

常用函数 GetProcAddress(模块,名字或者序号)

我们这个函数就是遍历PE文件中导出表进行返回的. 那么他是如何实现的.如何通过名字查找函数地址. 或者如何通过序号进行查找函数地址的?

首先我们要分成三张表,函数地址表中序号开始的位置是导出表成员Base指定的.假设为0开始.

函数地址表 序号表 函数名称表

0  0x1010 sub 0 0x0100 0 Add

1 0x2020 Add 1 0x0000 1 Sub

2  0x3030 Div 2 0x0200 2 DiV

首先GetProcAddress 如果按照名称查找的话.会先去遍历函数名称表. 比如我们要获取Sub的地址. 遍历函数名称表的时候.找到了Sub. 并获取当前Sub的索引. sub是在第二项中.所以索引为1 (从0开始)

然后拿着这个索引.去序号表中进行查找对比. 在序号表中查到了.对比成功.序号表中第2项的值跟这个索引一样的.所以就拿序号表的序号. 去函数地址表中获取函数地址.

序号为0x0000. 那么他就在函数地址表中.找到了第0项. 当函数地址进行返回. (并不是直接返回,加上了当前DLL模块的ImageBase才返回的,所以为什么需要DLL模块地址)

所以上面就是GetProcAddress的名字查找的实现流程

如果是序号来查找的话.比如我们寻找 14序号. 他会先根据导出表中Base成员属性.将表的起始位置进行一次定义.

例如上面.我们找的14序号并不存在. 但是他会先看看Base起始位置是多少. 假设为13. 那么我们函数地址表中 0索引 相当于 13 1索引相当于 14 2索引相当于15了.依次类推.

这样我们虽然说寻找14. 但是根据Base起始位置的指定.那么也会寻找到我们的函数地址.

总结来说 :

    1.遍历函数名称表 得出索引

    2.当前索引.去序号表中查找.如果有.则取出当前序号表的序号.当做函数地址表的下标

    3.得出下标. 返回函数地址 (RVA +IMAGEbase)

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!
本文分享自作者个人站点/博客:http://www.cnblogs.com/iBinary/复制
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • PE知识复习之PE的导入表

      上一讲讲解了导出表. 也就是一个PE文件给别人使用的时候.导出的函数 函数的地址 函数名称 序号 等等.

    IBinary
  • PE知识复习之PE的节表

    节表重要成员都标红了.我们知道.PE文件有两种状态.一种是内存状态.一种则是文件状态.

    IBinary
  • PE知识复习之PE的重定位表

        重定位的意思就是修正偏移的意思. 如一个地址位 0x401234 ,Imagebase = 0x400000 . 那么RVA就是 1234. 如果I...

    IBinary
  • PE知识复习之PE的绑定导入表

      根据前几讲,我们已经熟悉了导入表结构.但是如果大家尝试过打印导入表的结构. INT IAT的时候. 会出现问题.

    IBinary
  • PE知识复习之PE合并节

       根据上一讲.我们为PE新增了一个节. 并且属性了各个成员中的相互配合. 例如文件头记录节个数.我们新增节就要修改这个个数.

    IBinary
  • PE知识复习之PE新增节

        例如前几讲.我们的PE文件在空白区可以添加代码.但是这样是由一个弊端的.因为你的空白区节属性可能是只读的不能执行.如果你修改了属性.那么程序就可能出现问...

    IBinary
  • PE知识复习之PE扩大节

      上面我们讲了,空白区添加我们的代码.但是有的时候.我们的空白区不够了怎么办.所以需要进行扩大节.

    IBinary
  • PE知识复习之PE的两种状态

    关于结构体的各项属性.前边已经写过了.本系列博客就是加深PE印象.理解复杂的原理.

    IBinary
  • PE知识复习之PE的各种头属性解析

    DOS头是在16位程序下使用的.所以不用全部关心.只需要关心第一个跟最后一个成员记住即可.

    IBinary
  • PE知识复习之PE的RVA与FOA的转换

      首先我们知道PE有两种状态.一种是内存展开.一种是在文件中的状态.那么此时我们有一个需求.

    IBinary
  • PE知识复习之PE文件空白区添加代码

      根据上面所讲PE知识.我们已经可以实现我们的一点手段了.比如PE的入口点位置.改为我们的入口位置.并且填写我们的代码.这个就是空白区添加代码.

    IBinary
  • PE格式第六讲,导出表

                    PE格式第六讲,导出表 请注意,下方字数比较多,其实结构挺简单,但是你如果把博客内容弄明白了,对你受益匪浅,千万不要看到字...

    IBinary
  • PE文件和COFF文件格式分析——导出表

            在之前的《PE可选文件头》相关博文中我们介绍了可选文件头中很多重要的属性,而其中一个非常重要的属性是(转载请指明来源于breaksoftware...

    方亮
  • PE文件和COFF文件格式分析——导出表的应用——通过导出表隐性加载DLL

            通过导出表隐性加载DLL?导出表?加载DLL?还隐性?是的。如果觉得不可思议,可以先看《PE文件和COFF文件格式分析——导出表》中关于“导出地...

    方亮
  • PE格式第四讲,数据目录表之导入表,以及IAT表

               PE格式第四讲,数据目录表之导入表,以及IAT表 一丶IAT(地址表) 首先我们思考一个问题,程序加载的时候会调用API,比如我们以前写的...

    IBinary
  • PE文件和COFF文件格式分析——导出表的应用——一种插件模型

            可能在很多人想想中,只有DLL才有导出表,而Exe不应该有导出表。而在《PE文件和COFF文件格式分析——导出表》中,我却避开了这个话题。我就是...

    方亮
  • PE解析器的编写(三)——区块表的解析

    PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节...

    Masimaro
  • PE解析器的编写(四)——数据目录表的解析

    在PE结构中最重要的就是区块表和数据目录表,上节已经说明了如何解析区块表,下面就是数据目录表,在数据目录表中一般只关心导入表,导出表和资源这几个部分,但是资源实...

    Masimaro
  • PE文件和COFF文件格式分析——导出表的应用——一种摘掉Inline钩子(Unhook)的方法

            在日常应用中,某些程序往往会被第三方程序下钩子(hook)。如果被下钩子的进程是我们的进程,并且第三方钩子严重影响了我们的逻辑和流程,我们就需要...

    方亮

扫码关注腾讯云开发者

领取腾讯云代金券