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)

原文地址:https://www.cnblogs.com/iBinary/p/9739031.html

时间: 2024-10-11 22:34:06

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

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

PE知识复习之PE的RVA与FOA的转换 一丶简介PE的两种状态 首先我们知道PE有两种状态.一种是内存展开.一种是在文件中的状态.那么此时我们有一个需求. 我们想改变一个全局变量的初始值.此时应该怎么做.你知道虚拟地址.或者文件位置了.那么你怎么自己进行转换. 也就是说通过文件中的节数据找到在内存中这块数据的位置.或者反之. 寻找之前我们要先弄前几个概念. ImageBase:  模块基址.程序一开始的地址. VA: 全名virtualAddress 虚拟地址. 就是内存中虚拟地址. 例如 0

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

PE知识复习之PE文件空白区添加代码 一丶简介 根据上面所讲PE知识.我们已经可以实现我们的一点手段了.比如PE的入口点位置.改为我们的入口位置.并且填写我们的代码.这个就是空白区添加代码. 我们也可以利用这个知识.实现PEDLL注入. 原理就是 修改入口. 跳转到我们空白区执行我们的代码.我们空白区进行重定位.调用Loadlibary. 并且load的是我们的DLL 实现功能就是 我们只要给PE注入了代码.那么这个PE程序一旦启动就会加载我们的DLL 关于PEDLL注入,后面会有博客分类中会讲

PE知识复习之PE扩大节

PE知识复习之PE扩大节 一丶为什么扩大节 上面我们讲了,空白区添加我们的代码.但是有的时候.我们的空白区不够了怎么办.所以需要进行扩大节. 扩大节其实很简单.修改节数据对齐后的大小即可. 并且在PE文件中添加0数据进行填充即可. 首先看一下我们的节表 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //8个字节名字.自己可以起.编译器也可以给定.不重要. union { DWORD Physi

PE知识复习之PE新增节

PE知识复习之PE新增节 一丶为什么新增节.以及新增节的步骤 例如前几讲.我们的PE文件在空白区可以添加代码.但是这样是由一个弊端的.因为你的空白区节属性可能是只读的不能执行.如果你修改了属性.那么程序就可能出现问题.所以新增一个节可以实现我们的代码. 等等. 1.新增节的步骤 1.在最后一个节位置添加一个节.如果没有空白位置.自己需要给扩展头扩大.并且自己修正节的偏移. 2.修改文件头中节表个数. 3.添加的新节表修改节表的属性. 节.VirtualAddress .这个成员指定了这个节在内存

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

PE知识复习之PE的重定位表 一丶何为重定位 重定位的意思就是修正偏移的意思.  如一个地址位 0x401234 ,Imagebase = 0x400000 . 那么RVA就是 1234.  如果Imagebase 变了成了0x300000, 那么修正之后就是 ImageBase + RVA = 0X300000+1234 = 0x301234. 首先我们知道.一个EXE文件.会调用很多DLL(PE) 有多个PE文件组成. exe文件启动的基址 (ImageBase) 是0x40000. 假设我

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

一丶熟悉PE的整体结构 从下面依次网上看.可以得出PE结构 其中DOS头有DOS头结构 也就是 IMAGE_DOS_HEADER 关于结构体的各项属性.前边已经写过了.本系列博客就是加深PE印象.理解复杂的原理. IMAGE_DOS_HEADER 大小 64个字节    十六进制 0x40字节 IMAGE_FILE_HEADER 大小 20个字节     十六进制 0x14字节 IAMGE_OPTIONAL_HEADER 224个字节 十六进制  0xE0 IMAGE_SECTION_HEADE

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

一丶简介 根据前几讲,我们已经熟悉了导入表结构.但是如果大家尝试过打印导入表的结构. INT IAT的时候. 会出现问题. PE在加载前 INT IAT表都指向一个名称表. 这样说是没错的. 但是如果你打印过导入表.会发现一个问题. 有的EXE程序.在打印IAT表的时候.发现里面是地址. 原因: 我们的PE程序在加载的时候.我们知道. PE中导入表的子表. IAT表.会填写函数地址. 但是这就造成了一个问题.PE程序启动慢.每次启动都要给IAT表填写函数地址. 我们可不可以在文件中就给填写好.

考试备战系列--软考--02基础知识复习

这部分主要是计算机基础知识的概念介绍,相关系列文章如下所示. 考试备战系列--软考--01基础架构概念 考试备战系列--软考--02基础知识复习 考试备战系列--软考--03综合知识复习 考试备战系列--软考--04考题收集 考试备战系列--软考--05案例收集 考试备战系列--软考--06论文准备 操作系统时计算机系统的核心系统软件,其他软件均建立在其上,其分类包括:单用户操作系统和批处理操作系统.分时操作系统和实时操作系统.网络操作系统和分布式操作系统.嵌入式操作系统.其4大特征为并发性.共

php高级进阶系列文章--第二篇(PHP基础知识复习)

php基础知识复习 本文中对于简单的知识点只会提及一下(例如什么控制结构什么的),对于较有难度的详细复习下(例如面向对象的反射等等) 再次申明,本系列文章不是从最基础的开始复习,对于零基础的可能不适用,本文的初衷是我要准备攻读源码,在攻读前将之前的知识牢固下,太过简单的就写在文中了 1,echo print echo 可以输出一个或者多个字符串 print 只可以输出一个字符串,返回值总为1 2,PHP7中的组合比较符 echo 1 <==> 1 // 0 echo 1 <==>