PE结构(9)导入表

Last updated on May 9, 2024 pm

导入表工作原理

PE文件加载过程中,OS会先将文件“拉伸”,而后OS会将其“贴”在虚拟内存,此时即可根据导入表内申请的名单来寻找该Module运行所需要的其他支持。

一般来说是因为某个Module运行需要外部提供某些函数的实现支持。

导入表内描述了需要的函数的名称或者函数序号,以及这些函数位于的Module名称(一般是XXX.DLL文件),OS根据依赖的Module名称就可以将该Module也加载到内存中,并检查该Module的导出表,按序号或名称的方式从导出表中找到所需的函数地址。

关于从导出表内寻找函数地址,可参考过往的帖子:
PE结构(4)导出表

当函数地址被找到后,OS即可将该函数的绝对地址直接贴回到申请导入的PE文件的导入表中(导入表中的IAT结构就专门干这事,下文会详细探讨)。

当我们联系汇编代码时,就可以想通这个道理:

静态引用的函数地址,例如某程序内部自实现的函数,其地址在汇编代码中表现为绝对地址(ImageBase + RVA): call XXXX

动态引用的函数地址,其地址依赖外部导入,故汇编代码中表现为 call dword ptr [XXXX]。这里的XXXX即为IAT中存储导入函数地址的位置,可以理解为一个函数指针。

导入表位置

1
2
3
4
typedef struct _PE_HEADERS_OPTIONAL {
...
PE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBERSOF_DIRECTORY_ENTRY];
}PEHOPTIONAL, *lpPEHOPTIONAL;

导入表位于OP头的DataDirectory数组的第2个元素,即DataDirectory[1] 的位置,存储导入表地址。

还记得导出表在哪里吗?正是 DataDirectory[0] 的位置。

导入表结构

1
2
3
4
5
6
7
8
9
10
typedef struct _DATA_IMPORT_DIRECTORY {
union {
DWORD Characteristics; //导入表结束标志
DWORD OriginalFirstThunk; //RVA指向一个结构体数组(INT表)
};
DWORD TimeDateStamp; //时间戳,该值为0时,说明IAT表未提前绑定,为-1时IAT已被绑定导入表提前导入函数地址
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; //RVA指向dll名字,以0结尾
DWORD FirstThunk; //RVA指向一个结构体数组(IAT表)
} DATA_IMPORT_DIRECTORY, * PDATA_IMPORT_DIRECTORY;

一个PE程序会依赖多少外部的DLL ? 不知道,可能是一个,也可能是多个。

因此导入表注定不会只有一张,往往会有多张。

通过结构体的联合体成员Characteristics是否等于0,即可判断是否已遍历所有的导入表。

其实可以将导入表的入口地址,解释为一个存储了DATA_IMPORT_DIRECTORY结构的数组成员。

INT与IAT

INT,即Import Name Table,直译为导入名称表。

IAT,即Import Address Table,直译为导入地址表。

DATA_IMPORT_DIRECTORY结构的OriginalFirstThunk指向INT,FirstThunk成员指向IAT。

需要注意的是,IAT表也可以通过OP头的DataDirectory数组来找到,它位于DataDirectory数组的第13个元素,即DataDirectory[12]。

INT表与IAT结构相同:

1
2
3
4
5
6
7
8
typedef struct _DATA_THUNK_DATA {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; //RVA 指向_IMAGE_IMPORT_BY_NAME
};
} DATA_THUNK_DATA, * PDATA_THUNK_DATA;

看似复杂的结构,其实整个结构都是联合体,一般来说直接将其视为四字节整型DWORD即可。

当该值为0时,即到达末尾结束,因此可将INT和IAT都看作一个四字节整型数组,即DWORD[]。

函数名称 or 函数序号

我们通过AddressOfData来获取数据。

AddressOfData为一个32bit的整型, 解析该值时需分两种情况:

  1. 该值最高位为0,那么该值实际上是一个RVA,指向NAME结构,通过该结构的Name成员即可获取函数名称:
1
2
3
4
typedef struct _DATA_IMPORT_BY_NAME {
WORD Hint; //函数序号,无意义,一般无视该成员
BYTE Name[1]; //函数名称,以0结尾
} DATA_IMPORT_BY_NAME, * PDATA_IMPORT_BY_NAME;
  1. 该值最高位为1,那么该值是一个函数序号,去掉最高位后剩下的31位即为函数序号。

PE加载前后的不同之处

在PE文件加载前,INT和IAT两张表中存储的内容完全一样,都是函数的序号或函数名称。

而当PE文件加载完成之后,即OS已经将全部所需的导入函数找到后,IAT表中存储的不再是函数名称或序号,而是导入函数的绝对地址。

即AddressOfData存储的内容,已经变成一个绝对地址,该地址即导入函数的入口地址。

此时INT表与IAT互为对照,INT内存储名字,IAT内存储地址。

编码实现打印导入表

主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void work06() {
//将指定文件加载到内存,并解析PE信息到PEFILE结构体
PEFILE peFile{ 0 };
FILE* pFile = fopen("D:\\1.exe", "rb");

int fileLength = LoadFileToMemory(pFile, &peFile);
int r = fclose(pFile);

if (peFile.h_pe == nullptr) {
return;
}

//获取导入表
PDATA_IMPORT_DIRECTORY pImportTable = nullptr;
GetImportTable(&pImportTable, &peFile);

PrintImportTable(pImportTable, &peFile);
}

打印导入表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void PrintImportTable(PDATA_IMPORT_DIRECTORY pImportTable, lpPEFILE lpPeFile) {
DWORD imageBase = (DWORD)lpPeFile->h_dos;

while (pImportTable->Characteristics != NULL) {
char* dllName = (char*)Rva2Foa(pImportTable->Name, lpPeFile) + imageBase;
printf(" %s : \n OriginalFirstThunk :0x%x \n FirstThunk : 0x%x \n",
dllName, pImportTable->OriginalFirstThunk, pImportTable->FirstThunk);

//INT表
PDATA_THUNK_DATA pImportNameTable =
(PDATA_THUNK_DATA)(imageBase + Rva2Foa(pImportTable->OriginalFirstThunk, lpPeFile));
printf("INT:\n");
PrintThunkData(pImportNameTable, lpPeFile);
//IAT表
PDATA_THUNK_DATA pImportAddressTable =
(PDATA_THUNK_DATA)(imageBase + Rva2Foa(pImportTable->FirstThunk, lpPeFile));
printf("IAT:\n");
PrintThunkData(pImportNameTable, lpPeFile);

for (int i = 0; i < 10; i++) {
printf("**********");
}
printf("\n");
pImportTable++;
}
}

打印IAT或INT表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//此处假定PE文件未使用绑定导入表提前导入函数地址
void PrintThunkData(PDATA_THUNK_DATA thunkData, lpPEFILE lpPeFile) {
printf("%8s\t%30s\t%10s\t%10s\t\n", "Adress:", "Name:", "Hint:", "OrderNo:");

DWORD imageBase = (DWORD)lpPeFile->h_dos;
DWORD AddressOfData = thunkData->AddressOfData;
while (AddressOfData != NULL) {
DWORD orderNo = AddressOfData >> 31;

if (orderNo == 1) {
orderNo = AddressOfData & 0x7FFFFFFF;
printf("%8x\t%30s\t%10s\t%10d \n", AddressOfData, "Null", "Null", orderNo);
}
else {
PDATA_IMPORT_BY_NAME name =
(PDATA_IMPORT_BY_NAME)(Rva2Foa(AddressOfData, lpPeFile) + imageBase);
printf("%8x\t%30s\t%10d\t%10d \n", AddressOfData, name->Name, name->Hint, orderNo);
}

thunkData++;
AddressOfData = thunkData->AddressOfData;
}

}

PE结构(9)导入表
http://dubhehub.github.io/blogs/202405091121009749.html
Author
Sin
Posted on
May 9, 2024
Licensed under