PE文件结构之导入表和导出表

IAT:

【我觉得,这些都是别人微软定义好了的,知道一个概念现在就足够了,不需要刻意去记住吧】

Import Address Table , 导入地址表

Dll中隐式链接的调用过程:

  • 以调用CreateFileW()为例

  • 该函数位于kernel32.dll中

  • call dword ptr ds:[01001104] 实现函数的调用

  • 调用CreateFileW()函数时并非直接调用,而是通过获取01001104地址处的值来实现(所有API调用均

    采用这种方式)。

  • 地址01001 104是notepad.exe中.text节区的内存区域(更确切地说是IAT内存区域)。

  • 01001104地址处的值为7C8107F0

  • 指令与call 7C8107F0 为一个效果

相当于式有一个表,这个表里面放了地址。

IMAGE_IMPORT_DESCRIPTOR

  • IMAGE_ IMPORT_ DESCRIPTOR结构体中记录着PE文件要导入哪些库文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //INT的地址
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; //库名称字符串地址
DWORD FirstThunk; //IAT的地址
} IMAGE_IMPORT_DESCRIPTOR;

typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //ordinal
BYTE Name[1]; //function name string
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

INT :

Import_Name_Table

image-20230524215523921

它不在PE头而在PE体中,但查找其位置的信息在PE头中

OPTIONAL_ HEADER32.DataDirectory[ 1].VirtualAddress的值即是IMAGE_ IMPORT_ DESCRIPTOR结构体数组的起始地址( RVA值)。

IMAGE IMPORT_ DESCRIPTOR结构体数组也被称为IMPORT Directory Table (只有了解上述全部称谓,与他人交流时才能没有障碍)

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

EAT:

通过EAT才能准确求得从相应库中导出函数的起始地址。

  • 与前面讲解的IAT一样,PE文件内的特定结构体( IMAGE_ EXPORT_ DIRECTORY )保存着导出信息,且PE文件中仅有一个用来说明库EAT的IMAGE_ EXPORT DIRECTORY结构体

(用来说明IAT的IMAGE_ IMPORT_ DESCRIPTOR 结构体以数组形式存在,且拥有多个成员。这样是因为PE文件可以同时导入多个库。)

  • 可以在PE文件的PE头中查找到IMAGE_EXPORT_DIRECTORY结构体的位置。IMAGE_OPTIONAL_HEADER32.DataDirectory[0]. VirtualAddress值即是IMAGE_ EXPORT_ DIRECTORY结构体数组的起始地址(也是RVA的值)

IMAGE_EXPORT_DIRECTORY:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; //导出文件名的字符串的地址
DWORD Base;
DWORD NumberOfFunctions; //实际Export函数的个数
DWORD NumberOfNames; //Export函数中有名字的函数的个数
DWORD AddressOfFunctions; //Export函数数组地址
DWORD AddressOfNames; //函数名称数组地址
DWORD AddressOfNameOrdinals; //Ordinal数组地址( 数组元素个数=NumberOfNames )
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

image-20230524220407472

  • Ordinal:数组中的每一项与AddressOfNames中的每一项对应,表示该名字的函数在

    AddressOfFunctions中的序号

从库中获得函数地址的API为GetProcAddress()函数。该API引用EAT来获取指定API的地址。通过GetProcAddress() 获取的函数地址的函数拥有函数名称

(对于没有函数名称的导出函数,可以通过Ordinal 查找到它们的地址。从Ordinal值中减去IMAGEEXPORT_ DIRECTORY.Base 成员后得到一个值,使用该值作为“函数地址数组”的索引,即可查找到相应函数的地址。

GetProcAddress()原理:

(1)利用AddressOfNames成员转到“函数名称数组”。

(2)“函数名称数组”中存储着字符串地址。通过比较( strcmp)字符串,查找指定的函数名称(此时数组的索

引称为name_index)。

(3)利用AddressOfNameOrdinals成员,转到orinal数组。

(4)在ordinal 数组中通过name_ index 查找相应ordinal 值。

(5)利用AddressOfFunctions成员转到“ 函数地址数组”( EAT )。

(6)在“函数地址数组”中将刚刚求得的ordinal用作数组索引,获得指定函数的起始地址。

PE文件结构之重新定位表

重定位

重定位就是你本来这个程序理论上要占据这个地址,但是由于某种原因,这个地址现在不能让你占用,你必须转移到别的地址,这就需要基址重定位。

你可能会问,不是说过每个进程都有自己独立的虚拟地址空间吗?既然都是自己的,怎么会被占据呢?对于EXE应用程序来说,是这样的。

但是动态链接库就不一样了,我们说过动态链接库都是寄居在别的应用程序的空间的,所以出现要载入的基地址被应用程序占据了或者被其它的DLL占据了,也是很正常的,这时它就不得不进行重定位了。

重定位表的位置

重定位表一般会被单独存放在一个可丢弃的以“.reloc”命名的节中,但是和资源一样,这并不是必然的,因为重定位表放在其他节中也是合法的,惟一可以肯定的是,如果重定位表存在的话,它的地址肯定可以在PE文件头中的数据目录中找到。

image-20230524230757191

【这里就充分说明了,洋文还是得学】

  • VirtualAddress字段是当前页面起始地址的RVA值,本块中所有重定位项中的12位地址加上这个起始地址后就得到了真正的RVA值。

  • SizeOfBlock字段定义的是当前重定位块的大小,从这个字段的值可以算出块中重定位项的数量

  • 由于SizeOfBlock=4+4+2×n,(4字节VritualAddress,4字节SizeOfBlock,每个重定位项2字节),也就是sizeof IMAGE_BASE_RELOCATION+2×n。

  • 所以重定位项的数量n就等于(SizeOfBlock-sizeof IMAGE_BASE_RELOCATION)÷2。

  • IMAGE_BASE_RELOCATION结构后面跟着的n个字就是重定位项,每个重定位项的16位数据位中的低12位就是需要重定位的数据在页面中的地址,剩下的高4位也没有被浪费,它们被用来描述当前重定位项的种类。

  • 虽然高4位定义了多种重定位项的属性,但实际上在PE文件中只能看到0和3这两种情况。

所有的重定位块最终以一个VirtualAddress字段为0的IMAGE_BASE_RELOCATION结构作为结束,读者现在一定明白了为什么可执行文件的代码总是从装入地址的1000h处开始定义的了(比如装入00400000h处的.exe文件的代码总是从00401000h开始,而装入10000000h处的.dll文件的代码总是从10001000h处开始),要是代码从装入地址处开始定义,那么第一页代码的重定位块的VirtualAddress字段就会是0,这就和重定位块的结束方式冲突了。

但凡涉及到直接寻址的指令都需要进行重定位处理

把内存中需要重定位的数据按页的大小0x1000分为若干个块,而这个VirtualAddress就是每个块的起始RVA。在程序没有被真正加载(得到真实的起始地址)之前,就用ImageBase作为基址(这时的ImageBase是00400000)

PE文件的重定位表中保存的就是一大堆需要修正的代码。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <Windows.h>
#include <tlhelp32.h>

using namespace std;

void HelloWorld() {
cout << "HelloWorld" << endl;
}
int main()
{
HMODULE hMod = GetModuleHandle(NULL);
cout << hex << "Base:" << (DWORD)hMod << endl;
HelloWorld();
system("pause");
return 0;
}

用VS2022生成一个x32的exe

image-20230524230315724

这里没表示成[0004XXXX],而是用 j_?HelloWorld@@YAXXZ代替了,但是双击跟进发现:

image-20230524230327528

实际就是[004112CB]

下面我们查看它的PE结构中的OptionalHeader.ImageBase的值:

image-20230524225835721

即每次它都自身被加载到00400000处。

现在,我们利用GetModuleHandle()函数,运行testRelHello,看看实际上会被加载到哪里:

image-20230524230411502

我在运行之前先设好断点image-20230524230417692

现在在VS中查看其汇编代码:

image-20230524230425840

对HelloWorld()的调用就变成了call [0B312CB],而不再是[004112CB]。这就是因为进行了重定位。

现在我们计算:004112CB+B20000-400000,结果等于

image-20230524230435397

这就是对HelloWorld()函数进行重定位的一个过程