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)