PE学习(5)

导出表

当.exe文件调用dll文件里面的函数时,需要给.exe文件提供一个记录了dll文件中函数或变量的名称和地址的清单,这个清单就是导出表。导出表的主要作用是将PE中存在的函数引出到外部,以便其他人或程序可以使用这些函数,实现代码的重用。大多数情况下,exe文件没有导出表,dll文件有导出表(exe文件也可能有)。

导出表位置

在IMAGE_OPTIONAL_HEADER头里的最后一个成员结构体数组里,有16个结构体,第一个结构体就是导出表相关的信息

这个结构体有两个成员,一个是导出表偏移位置RVA(内存对齐后),另一个是导出表大小(实际上没什么用,可以随便改)。

手动定位导出表位置:

先找到可选PE头最后一个成员的位置

该结构体数组中的第一个结构体,里面就包含了导出表的地址和大小:

VirtualAddress: 0x0001B900

Size: 00000160

这里得到的是在内存中的偏移(RVA),但是我们现在是在010editor里面找 ,所以要先转换为文件中的偏移。

可以算到FOA是0xa900,接下来定位到该地址

导出表结构

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics; // 未使用
    DWORD   TimeDateStamp; // 时间戳,表示当前PE文件(DLL)编译时的时间
    WORD    MajorVersion; // 未使用
    WORD    MinorVersion; // 未使用
    DWORD   Name; // 当前导出表文件名字符串的地址
    DWORD   Base; // 导出函数起始序号
    DWORD   NumberOfFunctions; // 所有导出函数的个数
    DWORD   NumberOfNames; // 以函数名字导出的函数个数
    DWORD   AddressOfFunctions;// RVA,导出函数地址表
    DWORD   AddressOfNames; // 导出函数名称表RVA
    DWORD   AddressOfNameOrdinals; // 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

这个AddressOfFunctions指向了一个表(导出函数地址表),也就是说他是这个表的地址,这个表记录了每一个导出函数的地址。这个表的大小由NumberOfFunctions决定。

这个AddressOfNames指向了一个表(导出函数名称表),也就是说他是这个表的地址,这个表记录了每一个以名字导出的导出函数的名字的地址。这个表的大小由NumberOfNames决定。注意:导出函数地址表不一定会等于导出函数名称表,因为在创建导出函数时,可以创建没有名字的函数,这些函数在导入时按序号导入,不会出现在导出函数名称表里。

这个AddressOfNameOrdinals指向了一个表(导出函数序号表),也就是说他是这个表的地址,这个表与导出函数名称表一一对应,他存的是待寻函数在导出函数地址表的索引。

寻找函数

法一:通过名字找

假如我们要寻找的函数名字是“test”,先去导出函数名称表一一遍历,发现在索引位置为2处的地址指向的就是这个函数名,然后寻找导出函数序号表索引2处存的数,发现是4,然后据此去导出函数地址表寻找索引为4的函数的地址,发现是Fn5,然后根据这个地址定位这个test函数。

法二:通过序号找

假设给出一个需要找的函数,他的序号是10,那么用这个10减去Base(Base是一个类似于序号的基址一样的东西),假设Base是2,那么值是8,那么导出函数地址表里的索引为8的就是这个函数的地址。

重定位表

一个PE文件可以调用很多模块,每一个模块都有自己的ImageBase,当其中的模块ImageBase相同时,就会产生冲突,同一个地址只能导入一个模块,而另一个模块就只能被迫向后偏移,这样做的后果就是后者的ImageBase将会改变。对于一些只计算偏移的数据来说,这并不会造成影响,而对于直接给出地址的数据来说,由于ImageBase的改变,他们的地址也会随之改变。PE文件需要告诉计算机如果ImageBase改变的话,哪些数据需要变动,而重定位表就担任这一职责。

重定位表的主要作用是记录程序在加载到不同内存地址时,需要修正的地址值的相关信息。由于程序在编译时通常会基于一个预设的基地址(ImageBase)进行编译,但在实际运行时,操作系统可能会将程序加载到不同的内存地址。这时,程序中使用的绝对地址就会失效,需要通过重定位表来修正这些地址,以确保程序能够正常运行。大部分exe文件没有重定位表,大部分dll文件有,因为exe文件是最先强占内存的。

重定位表的位置

重定位表的位置信息同样在IMAGE_OPTIONAL_HEADER头里的最后一个成员结构体数组里,他是里面的第6个结构体,这个结构体与导出表相同,由VirtualAddress和Size组成。

typedef struct IMAGE_DATA_DIRECTORY{
  DWORD  VirtualAddress; RVA,重定位表的位置
  DWORD  Size;
} IMAGE_DATA_DIRECTORY, * PIMAGE_DATA_DIRECTORY;

VirtualAddress同样存的是重定位表的位置,根据这个地址可以找到重定位表。如:

先找到可选PE头的最后一个成员结构体:

这里一共16个结构体,每个占8个字节,要找的是第6个

VirtualAddress:0x00024000,接下来转换成FOA

FOA:0xe600

重定位表结构

typedef struct _IMAGE_BASE_RELOCATION {

  DWORD  VirtualAddress; //记录内存页的基址RVA 
  DWORD  SizeOfBlock;  //当前重定位块结构的大小。这个值减8就是TypeOffset数组的大小

} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

可以看到,如果仅仅是这一个结构的话那么重定位表将只有8个字节,但是我们找到的重定位表确明显不止8个。这是因为这个结构不止一个,许多个这种结构共同构成了重定位表,他们在内存里面是如下排列:

每一块的前8个字节分别存这个结构里面的两个成员VirtualAddress和SizeOfBlock,其他的空间存需要修改的地方。假设需要修改的地方的地址有三个,分别是0x00800123、0x00800456、0x800789,那么这一块重第9个字节开始就要存这些数,每一个存4个字节。如果是这样一个存法,会有一个明显的缺点:太占用内存。如果可以将0x00800000当做基址,只存他们的偏移量,那么每一个地址只需要占2个字节,而这,就是他真正的存法。0x00800000(基址)会存储在VirtualAddress里面,SizeOfBlock存这个块的大小(会直接影响下一个块的偏移),其他位置存需要改的地方的偏移。为什么重定位表要分块呢?在内存中0x1000为一页,他会把一页的需要修改的数据放在同一块,这是为了把地址相近的数据存在一起,方便上面这个用基址和偏移的方法存地址。

一个页大小是0x1000,换算成10进制就是4096,所以一个页的偏移范围是0~4095,即0x0~0xFFF,因此只需要分配12位二进制位给他即可完成对一个页的寻址。但是内存都是一个字节一个字节给的,没法给1.5个字节,所以内存会直接给他分配2个字节,这样的话就会多出4个二进制位,这4个二进制位还有其他用处。存在这里面的地址的是所有的会因为ImageBase改变而影响的值,而这里面有一些是有用的,一些是没用的(垃圾数据),这就需要有一个标志来辨别,而刚才提到的多余的4位就承担着这样的作用。高4位是0000的代表他是垃圾数据,而高4位是0011的代表他是有用的数据,因此RVA = VirtualAddress + 除去前4个位的偏移。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值