导入表
导入表的引入
当一个PE文件(如.dll/.exe等)需要使用别的模块的函数,也叫做依赖某模块,就需要一个清单来记录使用的模块(一般为.dll文件,为方便理解,以后我们将模块都认为是.dll文件)及使用的函数的相关信息(如使用了哪些DLL、使用了这个DLL里的哪些函数、叫什么名、去哪找等),这个清单就叫做导入表。
定位导入表
一个PE文件的数据目录第二个结构体就是导入表数据目录,根据导入表数据目录的VirtualAddress成员经过RVA转FOA找到导出表
导入表结构
如下是导入表结构:
struct _IMAGE_IMPORT_DESCRIPTOR{
union{
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
};
一个导入表大小为20字节
DWORD OriginalFirstThunk:该值为指向导入名称表(即INT表)的RVA
DWORD FirstThunk:该值为导出地址表(即IAT表)的RVA
DWORD TimeDateStamp:时间戳,用于判断该.dll文件是否有绑定导入表或IAT表中是否已经绑定绝对地址。当该值为0x00000000时,表示表示这个导入表结构对应的DLL中的函数绝对地址没有绑定到IAT表中。当该值为0xFFFFFFFF时,表示这个导入表结构对应的DLL中函数绝对地址已经绑定到IAT表中
DWORD Name:指向使用到的.dll名字字符串的RVA,该字符串以0结尾。如果Name指向的.dll名称为“user32.dll”,那么这个导入表中记录的就是该PE文件使用user32.dll的相关信息
注意:
1.当一个导入表后跟了一个导入表大小即20个字节长度的0时,表示该PE文件的所有导入表结束。
2.一般来说,程序通过导入表中的信息来装载对应的.dll文件到虚拟内存中(比如通过导入表的所有Name成员,获取系统要装载的.DLL的名字)。但导入表中的某些信息则需要在.DLL装载内存完成后(比如IAT表)才能修复
3.如果导入表的某个结构中OriginalFirstThunk和FirstThunk的值都为0,这意味着该PE文没有使用这个DLL中的任何函数。此时操作系统就不会加载这个导入表结构对应的DLL
4.一个PE文件使用到的.dll文件的数量就是该PE文件的导入表数量。
INT表
定位INT表
INT表定位:通过OriginalFirstThunk找到INT表
INT表结构
结构如下图:
上图是一般情况下INT表的样子,元素有IMAGE_THUNK_DATA也有数值,但实际上这些元素都是属于IMAGE_THUNK_DATA32这一种结构体的不同形式表现。实际上的INT表如下:
图中表的成员为结构体,结构如下:
struct _IMAGE_THUNK_DATA32{
union{
BYTE ForwarderString;
DWORD Function;
DWORD Ordinal;
_IMAGE_IMPORT_BY_NAME* AddressOfData;
};
};
这个结构体其实就是一个联合体,4字节大小。
元素表现形式不同的原因:一个.DLL中的函数可以以函数名称导出,也可以以序号(NONAME)导出,所以当一个PE文件使用别的.DLL中的函数时要考虑这个函数的名字和序号两种情况。因此INT表中的元素有IMAGE_THUNK_DATA和序号两种形式。
元素形式的判断方法如下:
如果INT表的元素最高位为1:那么除去最高位剩下31位的值,就是函数的导出序号
如果INT表的元素最高位为0:那么这个值为指向IMAGE_IMPORT_BY_NAME结构体的RVA
注意:
1.INT表中的元素个数就是该PE文件使用INT表对应的.dll文件函数的数量
2.当找到INT表后往后依次遍历出现有连续四字节长度的0时,表示该INT表结束
定位导入函数名称表
定位导入函数名称表:通过INT表中 _IMAGE_IMPORT_BY_NAME* AddressOfData元素,即可找到函数名称表的偏移地址
导入函数名称表结构
_IMAGE_IMPORT_BY_NAME结构体:
struct _IMAGE_IMPORT_BY_NAME{
WORD Hint;
BYTE Name[1]; //函数名称,以0结尾
};
Hint:这个值由编译器决定是否为空,如果不为空,表示函数在导出表中的索引。一般不使用这个值
Name:由于函数名称的字符个数是不确定的,所以此处只给出一个1字节的字符数组用来存储函数名称的第一个字符。通过Name获取该字符串的首地址,再依次往后遍历,直到遇到字符串的结尾字符\0,便是完整的函数名称
IAT表
IAT表的引入
当我们在程序中使用其他模块中的函数时,我们以程序调用MessageBoxA()为例,进行演示。程序运行后我们进入反汇编开始观察。
我们会发现,系统在调用该函数时,call后跟的不是一个绝对地址,而是一个间接地址。
我们在内存中查找该地址
我们发现,这个地址内容是一张表,记录着我们使用的函数的实际地址。我们在这个程序只使用了MessageBoxA()函数,所以上表中只有第一行四个字节记录了数据,也就是MessageBoxA()函数的地址。而这张表就是IAT表,也叫做导入函数地址表
定位IAT表
方法一:通过导入表中的FirstThunk(RVA)成员,找到IAT表。
方法二:通过PE文件数据目录的第13个结构体中的VirtualAddress(RVA)成员,找到IAT表
导入表和IAT表
IAT表在程序运行前后是不一样的,因此导入表和IAT表的具体关系结构有以下两种:
PE文件运行前:
PE文件运行后:
PE文件运行前:IAT表中的内容和INT表的内容是一样的
PE文件运行后:IAT表中的内容就变成了该PE文件使用的对应的.dll中的函数在内存中的绝对地址
IAT表修改过程
1.装载PE文件到内存:当PE文件运行后,系统先装载.exe,再装载各个使用到的.dll到PE文件的虚拟内存中。
2.调用GetProcAddress()函数:内存装载完成后,系统首先遍历导出表中每个结构的INT表,无论遍历到的是函数名还是序号,都会作为其中一个参数传给系统函数GetProcAddress()
3. IAT表修改:GetProcAddress()函数根据函数名或序号返回对应函数在内存中的绝对地址,接着把绝对地址存入到IAT表的对应位置
4.修改call语句地址:
PE文件运行前:call [0x....]后面间接寻址的地址0x....是INT表中某个元素的地址;
PE文件运行后:call [0x....]后面间接寻址的地址0x....改成了IAT表中某个元素的地址;
INT表和IAT表的关系
原因一:PE文件加载后,INT表作为函数名字的备份。
原因二:当PE文件加壳或加密以后,函数地址发生改变。当程序运行后,IAT表中原本的记录的函数的地址就错了,就无法通过IAT表找函数了。此时通过INT表查找函数到函数地址就可以修复IAT表了。具体流程如下:PE文件运行前后INT表中的元素都指向函数名称或者序号。我们通过遍历该程序导入表的INT表,最终在某一个导出表结构中的INT表中找到要查找的函数名称(或序号)。此时通过将LoadLibrary()返回该函数所属的.dll的句柄(即所属DLL的ImageBase)和函数名称传入GetProcAddress()函数,就可以得到该函数地址。此时就可以修改IAT表了