PE文件格式学习(三):导出表

1.回顾

上篇文章中介绍过,可选头中的数据目录表是一个大小为0x10的数组,导出表就是这个数组中的第一个元素。

我们再回顾下数据目录表的结构体:

struct _IMAGE_DATA_DIRECTORY
{
    DWORD VirtualAddress;
    DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

可以看出来,这个结构占8个字节,其中第一个字段是数据目录表的RVA,第二个字段是表的大小。跟RVA相关的有几个概念需要了解一下:

ImageBase:装载基址,PE文件在内存中的基地址,一般EXE文件的ImageBase是0x400000

VA:数据在内存中的地址,即虚拟地址。VA = ImageBase+RVA(相对虚拟地址)

RVA:相对虚拟地址,故名思义,它是相对于镜像基址(ImageBase)的虚拟地址, 假如ImageBase是0x400000,RVA是0x1000,那么VA就是0x401000

offset:数据在文件中的偏移

数据目录表中保存的是RVA,如果我们要在硬盘中分析文件格式,需要将RVA转换成offset,为什么要将RVA转为offset呢?直接用不是更方便吗?

其实转换的原因涉及到一个区段对齐的概念,接下来我会进行详细的讲解,先说结论:程序在内存中的区段对齐值和在磁盘中的区段对齐值是不一样的,所以区段数据在内存和在磁盘中的偏移一定是不一样的,换句话说就是要进行转换才能够找到数据地址。

程序在被加载到内存中时,每个区段都会被“拉长”到0x1000的倍数,因为在32位系统中一个内存分页的大小是4KB,也就是0x1000,程序需要遵循系统的分页机制。(分页机制以后会写一篇详细的文章进行讲解)

程序在磁盘中时,区段的大小都是0x200的倍数,也就是512,这是因为硬盘的扇区大小是512字节。

需要注意的是,无论是在内存中还是在磁盘中,区段的起始点和大小都必须符合相应的对齐值的整数倍!

我们来分析一下上面的图,一个程序的.text区段在磁盘文件中的rva是0x1345,对齐值是0x1000,所以0x1345处的数据在区段内的偏移是0x345。

当这个程序被加载到内存中时,.text区段的对齐值被“拉长”到了0x2000,可以看到左边图中的区段起始点是0x402000,它边上的阴影区就是与在磁盘中时区别的地方,段内偏移不变,还是0x345。

总结一下,加载到内存前,数据的偏移是0x1345,加载到内存后,数据的RVA是0x2345。

以上说明了区段对齐,既然知道了RVA需要转为offset的原因,现在就可以学习转为offset的方法了。

有个很重要的公式,即RVA转offset公式:


offset = (VA-ImageBase)-数据所在区段RVA+数据所在区段Offset
offset = RVA-数据所在区段RVA+数据所在区段offset

用上图的例子套这个公式:

offset = 0x2345-0x2000+0x1000
rva = 0x1345 - 0x1000 + 0x2000

2.导出表简介

一般情况下DLL文件都有导出表,导出的都是DLL文件中的函数。生成导出表的方法一般有两种,一种是声明extern "C" __declspec(dllexport) ,还有一种是.def文件声明。

因为本文只要着重讲解导出表的解析所以对于怎么样生成一个带导出表的文件就不做过多阐述了,毕竟这样的文章网上到处都是,况且几乎每一个要被外部使用的DLL文件都有导出表,我们只要在系统中随便找一个出来分析即可。

3.导出表结构

导出表的结构体:

struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;    
    DWORD   TimeDateStamp;      
    WORD    MajorVersion;       
    WORD    MinorVersion;       
    DWORD   Name;              
    DWORD   Base;               
    DWORD   NumberOfFunctions;  
    DWORD   NumberOfNames;      
    DWORD   AddressOfFunctions;    
    DWORD   AddressOfNames;         
    DWORD   AddressOfNameOrdinals; 
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

DWORD Characteristics:未使用,对应上图的0x00000000

DWORD TimeDateStamp:时间戳,文件头也有这个字段,对应上图的0x4dc5f2dc

WORD MajorVersion:未使用,对应上图的0x0000

WORD MinorVersion:未使用,对应上图的0x0000

DWORD Name:DLL文件名RVA,对应上图的0x000025c0

Base:导出表的起始序号,一般是0x01。这个起始的序号的使用场景是:系统会利用调用序号,调用序号-起始序号(Base)=函数原始序号,这个原始序号就是保存在PE文件中,也就是AddressOfNameOrdinals表中的序号。
为什么要使用这个序号呢?这个需要详细说说。导出表中有3个数组,分别是AddressofOfFunctions函数地址表、AddressOfNames函数名称表、AddressOfNameOrdinals函数序号表,PE装载器在加载文件的时候,可以通过函数名称表获取函数名称,但是无法与这个函数的地址建立联系,因此函数序号表就派上了用场,序号表跟名称表的数量是绝对一样并且一一对应的,因此装载器在获取到函数名时就可以获取到序号了,这个序号也就是调用序号,用它减去起始序号也就是Base得到的就是原始序号,这个原始序号对应函数地址表中的位置,因此就可以将函数名和函数地址对应起来了。对应上图的0x00000001

DWORD NumberOfFunctions:函数地址表中的个数,对应上图的0x00000004

DWORD NumberOfNames:函数名称表中的个数,函数序号表中的个数因为跟名称表中绝对一致,所以没有必要弄出一个专门的字段记录序号表的个数,对应上图的0x00000004

DWORD AddressOfFunctions:函数地址表RVA,对应上图的0x00002598

DWORD AddressOfNames:函数名称表RVA,存储函数名字符串所在的地址,这个RVA转换得到的offset处仍然是一个RVA,再将这个RVA转换得到的offset处还是一个RVA,最后再转一次得到的offset处存的就是函数的名称数组,每个元素以00结尾,对应上图的0x000025a8

DWORD AddressOfNamesOrdinals: 函数序号表RVA,序号表数组每个元素2个字节,对应上图的0x000025b8

猜你喜欢

转载自www.cnblogs.com/tutucoo/p/9927306.html