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)

PE知识复习之PE的导出表的更多相关文章

  1. PE知识复习之PE的导入表

    PE知识复习之PE的导入表 一丶简介 上一讲讲解了导出表. 也就是一个PE文件给别人使用的时候.导出的函数  函数的地址 函数名称 序号 等等. 一个进程是一组PE文件构成的.  PE文件需要依赖那些 ...

  2. PE知识复习之PE的各种头属性解析

    PE知识复习之PE的各种头属性解析 一丶DOS头结构体 typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // M ...

  3. PE知识复习之PE的绑定导入表

    PE知识复习之PE的绑定导入表 一丶简介 根据前几讲,我们已经熟悉了导入表结构.但是如果大家尝试过打印导入表的结构. INT IAT的时候. 会出现问题. PE在加载前 INT IAT表都指向一个名称 ...

  4. PE知识复习之PE的重定位表

    PE知识复习之PE的重定位表 一丶何为重定位 重定位的意思就是修正偏移的意思.  如一个地址位 0x401234 ,Imagebase = 0x400000 . 那么RVA就是 1234.  如果Im ...

  5. PE知识复习之PE合并节

    PE知识复习之PE合并节 一丶简介 根据上一讲.我们为PE新增了一个节. 并且属性了各个成员中的相互配合. 例如文件头记录节个数.我们新增节就要修改这个个数. 那么现在我们要合并一个节.以上一讲我们例 ...

  6. PE知识复习之PE新增节

    PE知识复习之PE新增节 一丶为什么新增节.以及新增节的步骤 例如前几讲.我们的PE文件在空白区可以添加代码.但是这样是由一个弊端的.因为你的空白区节属性可能是只读的不能执行.如果你修改了属性.那么程 ...

  7. PE知识复习之PE扩大节

    PE知识复习之PE扩大节 一丶为什么扩大节 上面我们讲了,空白区添加我们的代码.但是有的时候.我们的空白区不够了怎么办.所以需要进行扩大节. 扩大节其实很简单.修改节数据对齐后的大小即可. 并且在PE ...

  8. PE知识复习之PE文件空白区添加代码

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

  9. PE知识复习之PE的RVA与FOA的转换

    PE知识复习之PE的RVA与FOA的转换 一丶简介PE的两种状态 首先我们知道PE有两种状态.一种是内存展开.一种是在文件中的状态.那么此时我们有一个需求. 我们想改变一个全局变量的初始值.此时应该怎 ...

随机推荐

  1. UWB DWM1000 跟随小车原理---一张图演示

    更多内容参考论坛:bphero.com.cn

  2. php |= 什么意思

  3. Logstash 6.4.3 导入 csv 数据到 ElasticSearch 6.4.3

    本文实践最新版的Logstash从csv文件导入数据到ElasticSearch. 本文目录: 1.初始化ES.Kibana.Logstash 2.安装logstash文件导入.过滤器等插件 3.配置 ...

  4. centos7搭建zabbix3.0监控系统

    关闭防火墙和selinux systemctl stop firewalld.service                (停止防火墙) systemctl disable firewalld.se ...

  5. Caused by: org.apache.ibatis.builder.BuilderException: Parsing error was found in mapping #{}. Check syntax #{property|(expression), var1=value1, var2=value2, ...}

    解决办法:查看与该项目中的所有#{},应该是 #{}的中间没有写值

  6. yii2 注册一个新事件(trigger Event)

    有些时候我们需要在某个方法的中间注册一个新事件,确保某些业务的可拓展性. 下面我介绍一下注册一个新事件的方法: 第一步:需要的地方(比如控制器或模型)中定义一个事件常量(如:const EVENT_C ...

  7. python3 stack/ queue和deque模块

    '''栈stack 先进后出FILO (first in last out)'''lst = []lst.append("张一山")lst.append("杨紫" ...

  8. 最小化JIT示例(仅限Intel x86+Windows)

    #include <Windows.h> #include <cstdint> #include <cstring> #define BACK_FILL (0) i ...

  9. Python开发虚拟环境使用virtualenvwrapper的搭建及pycharm链接步骤

    virtualenv 是一个创建隔绝的Python环境的工具.virtualenv创建一个包含所有必要的可执行文件的文件夹,用来使用Python工程所需的包.创建的环境是独立的,互不干扰,无需sudo ...

  10. sweetalert提示框

    文档 sweetalert Api:http://t4t5.github.io/sweetalert/ 开源项目源码:https://github.com/t4t5/sweetalert 在文件中首先 ...