导读
本文主要介绍了导入表相关内容,涉及到C语言中结构体指针和pe文件结构相关知识。
导入表简介
导入表用于记录当前exe或dll需要用到哪些dll和哪些函数,举个例子,去饭店吃饭,通常会有一份菜单,饭店提供的菜单就是导出表,用于提供当前pe文件有哪些函数可以被别人用,而我们会在菜单上勾选出我们要吃的菜,这就相当于导入表,用于表示当前pe文件需要用到哪些dll和哪些函数。
导入表结构
导入表结构其实就是一个结构体,大小是20字节,注意并不是只有一个导入表,因为一张导入表对应一个dll,下面来看导入表的成员解析。
```
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
```
一个联合体,主要使用OriginalFirstThunk
成员,这个成员是一个rva,这个rva指向INT表(导入函数名称表),下面会说到这张表。
TimeDateStamp
一个时间戳,如果值是0代表导入表没有绑定,如果值是-1代表导入表绑定了。
Name
是一个rva,指向要使用的dll的名字,以0结尾。
FirstThunk
又是一个rva,指向IAT表,在程序加载前也就是在硬盘中INT表和IAT表存放的数据是一样的。
如果不是很理解这两个成员,可以看下面的图,就是OriginalFirstThunk
成员与FirstThunk
成员各指向一张表,在pe文件加载前,这两张表里存储的数据是一样的,结构也一样,就是表的地址不同,而在pe文件加载后IAT表中的数据会被修改成函数的地址,也就是说文件加载后,IAT表中的值会改变。
现在我们来看INT表的结构,因为INT表中成员都在一个联合体里,而且宽度都是DWORD
,不要看它有这么多的成员,其实这些成员中存储的数据都是一样的,也就是名字不一样但是内容是一样的,我们主要通过这个结构里的成员来定位IMAGE_IMPORT_BY_NAME
结构。
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
我们再来看下IMAGE_IMPORT_BY_NAME
结构,这个结构体用来存储函数的名称和函数序号(不一定准确),结构体的定义如下。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Hint
用来存储函数序号。
Name
用来存储函数名称,因为函数名称的宽度是不确定的所以这里是一个char
类型来存储名字,遇到0就代表结束了。
从上图中的内存窗口可以看到IMAGE_IMPORT_BY_NAME
的结构,Hint
成员值在内存中值是2e00
而Name
成员则是5f 5f 76 63 72 74 5f 47 65 74 4d 6f 64 75 6c 65 46 69 6c 65 4e 61 6d 65 57 00
(在内存中字符是以16进制的方式来存储的),因为上面说过Name成员宽度是char类型也就是一个字节,那为什么会占25个字节呢,因为函数的名称不是固定不变的,没办法固定的分配空间,只能给一个字节的空间来存储函数名的开头往后填,也就是函数名从这一个字节的地方开始到0结束。
导入表代码解析
大致思路:首先通过数据目录表的第2张表的VirtualAddress
得到导入表的rva在把它转换成foa加上基址即可,然后因为一个pe文件不可能只有一个导入表所以要进行循环,因为导入表结束的地方是一个全0的结构所以循环条件就是判断导入表的成员是不是0即可,到了这里我们已经定位到了导入表,下面来定位导入名称表和IMAGE_IMPORT_BY_NAME
结构,上面说过,导入表的OriginalFirstThunk
成员指向导入名称表,但是导入名称表中存储的值也是一个rva所以要先转换成foa在加上基址即可定位到导入名称表,因为导入名称表中存储的数据宽度是DWORD
所以我们用PDWORD
类型的指针来指向导入名称表即可。导入名称表也不只有一个,因为导入名称表结束的地方也是一个全0结构,所以判断导入名称表中存放的数据是不是0即可,导入名称表中存储的值有可能是一个序号也可能是一个指向IMAGE_IMPORT_BY_NAME
结构的偏移,这里先说下字节和位之间的转换1byte=8bit
也就是1个字节=32位,好了我们继续看导入名称表,导入名称表中存储的四字节数据可能是一个序号也可能是一个偏移,可以通过这四字节的最高位是不是1来判断这四字节里面存储的是序号还是偏移,如果最高位是1那么低31位存储的是一个序号,如果不是最高位1存储的就是一个偏移,这里使用if
判断即可,如果是偏移还是要把它转换成foa在加上基址定位到IMAGE_IMPORT_BY_NAME
结构用一个结构体指针指向这个地址,在打印它的成员即可。
```
include
include
define dir "C:\Users\blue\Desktop\dbg.exe"
DWORD rtf(PBYTE buffer, DWORD rva)
{
PIMAGE_DOS_HEADER doshd = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS nthd = (PIMAGE_NT_HEADERS)(buffer + doshd->e_lfanew);
PIMAGE_FILE_HEADER filehd = (PIMAGE_FILE_HEADER)(buffer + doshd->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 optionhd = (PIMAGE_OPTIONAL_HEADER32)(buffer + doshd->e_lfanew + 24);
PIMAGE_SECTION_HEADER sectionhd = IMAGE_FIRST_SECTION(nthd);
if (rva < optionhd->SizeOfHeaders)
{
return rva;
}
for (int i = 0; i < filehd->NumberOfSections; i++)
{
if (rva >= sectionhd[i].VirtualAddress && rva <= sectionhd[i].VirtualAddress + sectionhd[i].SizeOfRawData)
{
return rva - sectionhd[i].VirtualAddress + sectionhd[i].PointerToRawData;
}
}
}
void main()
{
FILE* fp = fopen(dir, "rb");
fseek(fp, 0, SEEK_END);
DWORD Size = ftell(fp);
rewind(fp);
PBYTE ptr = (PBYTE)malloc(Size);
memset(ptr, 0, Size);
fread(ptr, Size, 1, fp);
PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;
PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 0x4);
PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(ptr + Dos->e_lfanew + 24);
PIMAGE_DATA_DIRECTORY Data = Option->DataDirectory;
PIMAGE_IMPORT_DESCRIPTOR Import = (PIMAGE_IMPORT_DESCRIPTOR)(ptr + rtf(ptr, Data[1].VirtualAddress));
//return;
while (Import->OriginalFirstThunk != 0 && Import->FirstThunk != 0)
{
printf("DLL:%s\n", ptr + rtf(ptr, Import->Name));
printf("DLL:%x\n", Import->Name);
printf("Foa Dll:%x\n", rtf(ptr, Import->Name));
printf("TimeDateStamp:%x\n", Import->TimeDateStamp);
printf("OriginalFirstThunk:%x\n", Import->OriginalFirstThunk);
printf("Foa OriginalFirstThunk:%x\n", rtf(ptr, Import->OriginalFirstThunk));
printf("FirstThunk:%x\n", Import->FirstThunk);
printf("Foa FirstThunk:%x\n", rtf(ptr, Import->FirstThunk));
PDWORD ptr2 = (PDWORD)(ptr + rtf(ptr, Import->OriginalFirstThunk));
while (ptr2)
{
if (ptr2 & 0X80000000)
{
printf("%x\n", *ptr2 & 0xfff);
}
else
{
PIMAGE_IMPORT_BY_NAME ptr3 = (PIMAGE_IMPORT_BY_NAME)(ptr + rtf(ptr, *ptr2));
printf("Function:%s\n", ptr3->Name);
printf("Hint:%x\n", ptr3->Hint);
}
ptr2 = (PDWORD)((DWORD)ptr2 + sizeof(IMAGE_THUNK_DATA));
}
printf("IAT\n");
PDWORD ptr4 = (PDWORD)(ptr + rtf(ptr, Import->FirstThunk));
while (*ptr4)//因为程序加载前int表和iat表的数据都一样所以直接那上面的代码改一下就好了
{
if (ptr4 & 0X80000000)//判断高位是不是1如果不是以那就是rva通过rva转换foa加上基址得到地址用PIMAGE_IMPORT_BY_NAME结构指向这个地址
{//打印值
printf("%x\n", ptr4 & 0xfff);
}
else
{
PIMAGE_IMPORT_BY_NAME ptr5 = (PIMAGE_IMPORT_BY_NAME)(ptr + rtf(ptr, *ptr4));
printf("Function:%s\n", ptr5->Name);
printf("Hint:%x\n", ptr5->Hint);
}
ptr4 = (PDWORD)((DWORD)ptr4 + sizeof(IMAGE_THUNK_DATA));
}
Import++;
}
getchar();
}
```
因为之前的文章已经说过rva转换foa的函数所以在这就不多说了,大致思路:
通过参数传入一个rva和基址,然后循环判断这个rva在哪一个节表里面,减去rva所在节表的VirtualAddress加上rva所在节表的PointerToRawData即可
FILE* fp = fopen(dir, "rb");
fseek(fp, 0, SEEK_END);
DWORD Size = ftell(fp);
rewind(fp);
PBYTE ptr = (PBYTE)malloc(Size);
memset(ptr, 0, Size);
fread(ptr, Size, 1, fp);
FILE* fp = fopen(dir, "rb");
定义一个文件类型的指针来接收fopen
函数返回值,
fopen
函数第一个参数是要打开文件的路径,第二个参数是以什么方式打开,这里是rb也就是以二进制方式打开一个文件,只能读不可以写。
fseek(fp, 0, SEEK_END);
第一个参数是要设置文件的文件指针,第二个参数是一个相对于第三个参数是一个偏移量,第三个参数SEEK_END
代表文件的末尾,代码大致意思文件流重定向到文件末尾。
DWORD Size = ftell(fp);
定义一个变量用来接收ftell函数的返回值,ftell
函数作用是计算文件的大小,第一个参数是要计算那个文件的文件指针。
rewind(fp);
将文件流重定向到文件开头,为下面读取数据做准备。
PBYTE ptr = (PBYTE)malloc(Size);
定义一个指针来接受malloc
函数的返回值,malloc
函数第一个参数是申请内存的大小,PBYTE
就是char*
,malloc
函数返回值是申请内存块的地址。
memset(ptr, 0, Size);
填充刚才申请的内存块为0,第一个参数内存块的地址,第二个参数用什么填充,第三个参数填充多大。
fread(ptr, Size, 1, fp);
用于读取数据到内存,第一个参数是要读到哪里,第二个参数是读多少字节,第三个参数读多少次,第四个参数要读取文件的文件指针。
```
PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;
PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 0x4);
PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(ptr + Dos->e_lfanew + 24);
PIMAGE_DATA_DIRECTORY Data = Option->DataDirectory;
PIMAGE_IMPORT_DESCRIPTOR Import = (PIMAGE_IMPORT_DESCRIPTOR)(ptr + rtf(ptr, Data[1].VirtualAddress));
```
PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;
定义一个PIMAGE_DOS_HEADER
类型的结构体指针来指向刚才申请的那块内存,也就是用这块内存中的数据来填充这个结构体指针所指向的结构体,因为ptr
是PBYTE
类型和PIMAGE_DOS_HEADER
类型不同所以要把ptr
从PBYTE
强转成PIMAGE_DOS_HEADER
类型。
PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
定义一个PIMAGE_NT_HEADERS
类型的结构体指针,再通过基址+偏移的方式定位到Nt头,也就是ptr加上Dos头的e_lfanew成员得到一个地址,这个地址就是Nt头开始的地方,也可理解为用这个地址中的数据填充这个结构体中的成员。
PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);
还是定义一个结构体指针PIMAGE_FILE_HEADER类型
,首先通过基址加Dos头的e_lfanew成员定位到nt头,根据nt头的结构体定义可以知道nt头的Signature
成员,后面就是File头而Signature
成员大小是四字节,所以要加4这样就可以定位到File头,然后用这个结构体指针指向这个地址即可。
PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(ptr + Dos->e_lfanew + 20 + 4);
定义一个PIMAGE_OPTIONAL_HEADER32
类型的结构体指针,然后通过基址+Dos头e_lfanew成员定位到nt头再加上nt头的Signature
成员(大小4字节)得到File头地址,在加上File头的大小(20字节),它们相加结果是一个地址这个地址是可选头开始的地方,用结构体指针指向这个地址即可。
PIMAGE_DATA_DIRECTORY Data = Option->DataDirectory;
定义一个PIMAGE_DATA_DIRECTORY
类型的结构体指针,通过可选头的DataDirectory
成员定位到数据目录表。
PIMAGE_IMPORT_DESCRIPTOR Import = (PIMAGE_IMPORT_DESCRIPTOR)(ptr + rtf(ptr, Data[1].VirtualAddress));
还是定义一个结构体指针,PIMAGE_IMPORT_DESCRIPTOR
类型因为数据目录表的第二项的VirtualAddress
指向导入表,而它又是一个rva所以把它转换成foa再加上基址,用这个结构体指针指向这个地址即可。
到了这里我们已经定位到导入表了,下面我们来看定位导入名称表和IMAGE_IMPORT_BY_NAMe
结构的代码。
while (Import->OriginalFirstThunk != 0 && Import->FirstThunk != 0)
{
Import++;
}
因为不是只有一张导入表,所以要循环判断,通过Import++
来定位下一张导入表其实就是加上当前导入表的大小来定位下一张导入表,而再导入表结束的地方是一个全0结构所以我们判断导入表成员是不是0即可,下面我们来看while
循环中的代码。
printf("DLL:%s\n", ptr + rtf(ptr, Import->Name));
printf("DLL:%x\n", Import->Name);
printf("Foa Dll:%x\n", rtf(ptr, Import->Name));
printf("TimeDateStamp:%x\n", Import->TimeDateStamp);
printf("OriginalFirstThunk:%x\n", Import->OriginalFirstThunk);
printf("Foa OriginalFirstThunk:%x\n", rtf(ptr, Import->OriginalFirstThunk));
printf("FirstThunk:%x\n", Import->FirstThunk);
printf("Foa FirstThunk:%x\n", rtf(ptr, Import->FirstThunk));
上面的代码功能是打印出当前导入表结构的成员,需要注意的是Name
成员它是一个rva需要转换成foa加上基址才可找到真正的dll名字。
PDWORD ptr2 = (PDWORD)(ptr + rtf(ptr, Import->OriginalFirstThunk));
因为导入名称表中的数据宽度是4字节所以这里用一个PDWORD类型的指针,因为OriginalFirstThunk
成员是一个rva所以转换成foa加基址,在用一个指针来指向这块内存即可。
```
while (ptr2)
{
if (ptr2 & 0X80000000)
{
printf("%x\n", *ptr2 & 0xfff);
}
else
{
PIMAGE_IMPORT_BY_NAME ptr3 = (PIMAGE_IMPORT_BY_NAME)(ptr + rtf(ptr, *ptr2));
printf("Function:%s\n", ptr3->Name);
printf("Hint:%x\n", ptr3->Hint);
}
ptr2 = (PDWORD)((DWORD)ptr2 + sizeof(IMAGE_THUNK_DATA));
}
```
因为上面说过导入名称表结束的地方也是一个全0结构所以这里用*ptr2
来判断导入名称表中的值是不是0,如果是0就不循环代表导入名称表结束了,上面说过导入名称表中存储的值可以是一个指向IMAGE_IMPORT_BY_NAME
结构的偏移,也可以是一个函数的序号,*ptr2
代表取出这个地址中的值,取出这个值和0X80000000
与一下,这里涉及到逻辑运算这里只说与运算,也就是如果都是1才是1如果是1和0与那么结果是0,可以看下面的例子。
```
1101 &1001
1101
1001
1001
```
回到主题,这里通过一个if
来判断数据目录表的最高位是不是1,如果是就取出低31位的值通过*ptr2 & 0xfff
来取低31位的值,如果最高位不是1那么就执行else
下的代码。
PIMAGE_IMPORT_BY_NAME ptr3 = (PIMAGE_IMPORT_BY_NAME)(ptr + rtf(ptr, *ptr2));
首先定义一个PIMAGE_IMPORT_BY_NAME
类型的结构体指针,再把导入名称表中的值转换成foa加上基址来定位到IMAGE_IMPORT_BY_NAME
结构体。
printf("Function:%s\n", ptr3->Name);
printf("Hint:%x\n", ptr3->Hint);
打印IMAGE_IMPORT_BY_NAME
结构体的成员,Name
是函数的名字,Hint
是序号。
ptr2 = (PDWORD)((DWORD)ptr2 + sizeof(IMAGE_THUNK_DATA));
代码主要功能是定位下一个IMAGE_IMPORT_BY_NAME
结构,也就是当前IMAGE_IMPORT_BY_NAME
结构的地址加上IMAGE_IMPORT_BY_NAME
结构的大小。
下面的代码时定位IAT表的,因为IAT表与INT表(导入名称表)中存放的数据是一样的所以代码也一样。只是定义的变量改了一下,大致思路就是:通过导入表FirstThunk
成员来定位IAT表,循环判断IAT表中的成员是不是0如果是0就代表IAT表结束了,如果不是就取出IAT表总的数据和0X80000000
进行与运算的到最高位的值,如果最高位的值是1就代表当前的数据是一个序号,那么就再进行与运算去除低31位的值,并打印出来,如果最高位不是1,就把当前数据转换成foa再加上基址定位到IMAGE_IMPORT_BY_NAME
结构,定义一个结构体指针指向这个地址,然后再打印出结构体的成员,最后通过当前结构的地址+当前结构的大小来定位到下一个结构体的位置。
```
printf("IAT\n");
PDWORD ptr4 = (PDWORD)(ptr + rtf(ptr, Import->FirstThunk));
while (*ptr4)//因为程序加载前int表和iat表的数据都一样所以直接那上面的代码改一下就好了
{
if (ptr4 & 0X80000000)//判断高位是不是1如果不是以那就是rva通过rva转换foa加上基址得到地址用PIMAGE_IMPORT_BY_NAME结构指向这个地址
{//打印值
printf("%x\n", ptr4 & 0xfff);
}
else
{
PIMAGE_IMPORT_BY_NAME ptr5 = (PIMAGE_IMPORT_BY_NAME)(ptr + rtf(ptr, *ptr4));
printf("Function:%s\n", ptr5->Name);
printf("Hint:%x\n", ptr5->Hint);
}
ptr4 = (PDWORD)((DWORD)ptr4 + sizeof(IMAGE_THUNK_DATA));
}
Import++;
}
```
程序运行结果
结语
主要是讲了了pe文件结构导入表的作用和导入表的一些成员,以及如何使用代码打印出导入白哦,涉及到了指针和结构体与逻辑运算相关的知识。需要注意的是指针的类型和对导入表的理解。
由于作者水平有限,文章如有错误欢迎指出。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论