PE学习3

发布时间 2023-07-12 16:26:27作者: 清雨中欣喜

9、导入表

一个进程是由一组PE文件构成的:

PE文件提供哪些功能 : 导出表 PE文件需要依赖哪些模块以及依赖这些模块中的哪些函数 : 导入表

扩展pe头中的最后一个成员是一个结构体数组,其中包含了十六个结构体

其中 _IMAGE_DIRECTORY_ENTRY_IMPORT 这个成员就是代表了导入表,而这个结构体中的成员

struct _IMAGE_DATA_DIRECTORY{
0x00 DWORD VirtualAddress;
0x04 DWORD Size,
};

其存储了导入表在哪里以及导入表有多大。即为扩展pe头最后一个成员的数组中的第二个成员,我们就可通过此方法来定位导入表。

 

导入表的结构如下,一共二十个字节

typedef struct _IMAGE_IMPORT_DESCRIPTOR{
union{
DWORD Characteristics;
DWORD OriginalFirstThunk;//RVA 指向IMAGE_THUNK_DATA结构数组
};
DWoRD TimeDateStamp;//时间戳
DWORD ForwarderChain;
DWORD Name; //RVA,指向dll名字,该名字以0结尾
DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组
}IMAGE_IMPORT_DESCRIPTOR;

首先我们通过VirtualAddress找到对应的导入表在哪里,不同于导出表只有一个,导入表有很多个,所以在对应的地址有很多个导入表,每二十个字节为一个导入表,直到出现连续的20个0,即为结尾。

首先我们来了解 Name 这个成员,其也是一个指针,其数值指向的是一个字符串,其所对应的是,导入表的名字。然后这个字符串的结尾是以0结尾。所对应我们就确定了该导入表依赖的是哪一个模块了。

image-20230106001929011

导入表的两个成员 OriginalFirstThunk 和 FirstThunk 分别指向不同的。OriginalFirstThunk 指向 INT(IMPORT_NAME_TABLE 导入名称表),FirstThunk 指向 IAT(IMPORT_ADDRESS_TABLE 导入地址表)。两张表的名字和地址都不同,但是两张表的内容相同。(PE文件加载前)。

导入名称表的结构如下

typedef struct _IMAGE_THUNK_DATA32{
union{
PBYTE ForwarderString,
PDWORD Function;
DWORD Ordinal; //序号
PIMAGE_IMPORT_BY_NAME AddressOfData;//指向IMAGE_IMPORT_BY_NAME
}u1;
}IMAGE_THUNK_DATA32;

因为导入名称表的这个表是由一个联合体组成,所以其所占的内存空间大小是由其中最大的一个决定。又由于四个成员都是四个字节,所以这个联合体所占内存空间大小是四个字节。之所以使用联合体,是因为提高代码的可读性,我们可以将这四个字节的空间取四个不同的名字。

 

指向的两张表中,每四个字节是一个函数,最后以0为结束符,有一个函数,就说明该模块使用了这个 dll 对应的多少个函数。

image-20230108105828995

 

确定导入的函数的时候,通过INT 中的成员的最高位是否是1,如果是的话,那么去掉最高位的值就是函数的导出序号;如果不是的话,这个值就是RVA 指向了IMAGE_IMPORT_BY_NAME 就是如下的这个结构体

typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint; //可能为空,编译器决定如果不为空是函数在导出表中的索引
BYTE Name[1]; //函数名称,以0结尾
}IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

这个结构体一共占三个字节,第一个成员 Hint 占两个字节,可能为空,如果不为空的话,其就是函数在导出表中的索引(导出函数地址表的索引)。如果第一个成员为空或者我们不想使用这个成员的话,我们就可以用第二个成员 Name[1] 函数的名称,因为节约空间,他只存放了名称的第一个值,顺着一直找,直到以0结尾,都是函数的名称。以此方法就可以确定依赖的函数了。

 

现在知道了如何确定导入表的名字和对应的那些函数,来找到对应的函数地址。在我们运行其他dll的函数的时候,我们在反汇编中看到的call 不是直接的操作数,而是一个间接的地址。只要调用的是其他dll文件的都会这样。而这个时候call 调用的值(间接的这个值)实质上指向的是一个表,即为函数地址表( FirstThunk 指向的IAT)。

10、重定位表

一个进程是如何加载到一个pe文件中的。因为一个进程是由许多个pe文件组成的。

一个进程都有4G的内存空间,但是只有对应的低2G对应用程序是有意义的。高2G是对内核去使用的。

在内存空间中,一个pe文件展开的时候,可能会出现两个 dll (或者是模块,之后用dll解释更加的清晰) 的imagebase的值相同,即为占用了同样的内存地址,导致两个dll发生冲突,操作系统这时候就会将发生冲突的dll重新放置到另一个内存地址。但是其中两个dll文件中的数值,可能会因为地址的移动,本身的位置发生了改变,而无法进行使用。所以这时候就需要一张重定位表,将因为改变位置的发生改变的数据成员的地址记住,并在编译的时候,修改对应的数值。

即可以解释为重定位表就是当模块在内存中展开的时候,没有占住自己想要占住的空间的时候,就需要更改imagebase,而重定位表中记录的就是需要修正的数据的地址。

如今我们利用的这些模块,从xp系统开始,都是动态加载的。如果没有重定位表,在每次执行模块的时候,更改定位的话,模块就无法加载了。

重定位表的位置:数据目录项的第六个结构

IMAGE_DIRECTORY_ENTRY_BASERELOC

struct _IMAGE_DATA_DIRECTORY{
0x00 DWORD VirtualAddress;
0x04 DWORD Size;
}

从这个结构里,就可以找到重定位表的位置了。

而所对应的重定位表的结构为:

image-20230110214720683SizeOfBlock 是以字节为单位,这个数据有多大,就代表了第一个重定位块有多大。

重定位表并非一个表组成,而是一堆表组成。每一个表在内存中都是一块,然后大小都是由SizeOfBlock决定。IMAGE_DIRECTORY_ENTRY_BASERELOC找到的是重定位表从哪里开始的。且表的大小都是由SizeOfBlock决定,直到找到连续8个字节都是零为止。

实际上很多需要修改的数据都是一个模块中的,所以数据大差不差,如果都以四个字节存储,比较的浪费空间,所以我们可以通过利用VirtualAddress记录高位,低位存入表中,这样就能减少许多空间的浪费。

而为什么重定位表设置为一块一块的,是因为内存是以4kb为单位的,也就是在物理内存上每个内存划分都是4kb ,4kb为一个物理页,重定位表也是据此设计的。一个物理页创建一张重定位表。又因为如此,一个物理页中的地址都是在4kb中进行偏移,4kb = 2^12 byte ,所以至少需要十二位数据,一个字节八位,所以表中存储低位数据的时候,是以两个字节为一个单位的。

但是由于用到的是两个字节,十六位数据,所以其中四个数据没有用到记录地址,最高四位挪作他用。当操作系统真正要修复这个位置的时候,它会判断这四位的数值是否等于3 ,如果等于3,才确定是需要修复的。地址即为VirtualAddress + 表中低12位。

而为什么会有需要修改和不需要修改的数据,是因为操作系统中存在内存对齐的操作,高四位不为3的数据,有可能是无用的数据,只是为了用来内存对齐,填补空缺用的。