导入表的概念
可执行文件在进行加载时,需要导入一些动态库,即Dll文件。通过导入这些动态库,我们可以使用文件中提供的函数和功能,来为我们的可执行文件服务。这些Dll中的函数就称为导入函数。为了记录需要的动态库和导入函数,可执行文件在可选头的数据目录中维护了一张表,这张表记录了文件需要的所有动态库以及导入函数的信息,方便Windows加载器在加载PE文件时使用。这张表就是所谓的导入表,它被存储在数据目录的第二项。
可执行文件在使用动态库中的函数和代码时,一般有两种方式。一种是隐式链接,这完全通过windows加载器来完成;另一种方式是显式链接,即我们可以根据需要手动载入一个动态库文件,并使用其中的函数,这一过程需要使用LoadLibrary和GetProcAddress。
导入表的结构
导入表的结构定义如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
_ANONYMOUS_UNION union {
ULONG Characteristics;
ULONG OriginalFirstThunk;
} DUMMYUNIONNAME;
ULONG TimeDateStamp;
ULONG ForwarderChain;
ULONG Name; //动态库的名字
ULONG FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
看到这个数据结构的时候,我们可能会一脸懵- - ! 这OriginalFirstThunk,FirstThunk都是什么啊?别急,下面我用一张图来为大家解释一下:
前面的文章中我们讲到过,PE文件分为未加载和加载后两种状态。在未加载时,我们的PE文件还不知道自己即将被加载到进程空间的何处(Dll文件会出现地址重定位的问题),因此,动态库文件在真正被加载前,是无法确定其中的函数地址的。所以,如图所示,OriginalFirstThunk,FirstThunk两成员在PE文件被加载前,都指向一个数组,这个数组中的每一成员都是一个地址,每个地址指向一个字符串,这些字符串就是每个导入函数的名称。换言之,在PE文件被加载前,OriginalFirstThunk,FirstThunk指向两个数组的内容是完全相同的,都是保存指向导入函数名称的地址的一个数组。
那么,在Dll文件被加载到进程空间后,我们的可执行文件需要使用导入函数,这个时候,就需要确认每个导入函数的地址了。Windows加载器会根据OriginalFirstThunk中每个函数的名称,查找该函数的真正地址,也就是保存实现函数功能的代码地址,并将这些函数的真正地址填写到FirstThunk指向的数组中,这样,我们的可执行文件就可以通过函数地址调用具体的函数了。这一过程也被称为导入表的修正。
还有一个成员Name,它指向导入的动态库的名称,如"kernel32.dll"、"User32.dll"等。可执行文件中通常需要不止一个动态库文件,所以导入表中会存储若干个IMAGE_IMPORT_DESCRIPTOR结构,每个结构指向一个具体的动态库文件,我们可以通过查看结构中的Name来确定每个结构对应的Dll文件名。
使用LordPE工具查看一个具体的PE文件,查看导入表,可以查看到QQMusic.exe需要加载的动态库和导入函数。
手动加载导入表
前面我们说到,Dll文件有两种链接方式。显式链接需要我们使用LoadLibrary和GetProcAddress函数,这两个函数可以帮助我们将一个存在的Dll文件加载到进程空间内并获得导入函数的地址。这种显式链接方式也被称为注入,因为如果我们在想要攻击的目标进程中加载了我们自定义的Dll文件,就可以让目标进程调用我们Dll文件中的函数,从而实现修改进程中数据和代码的目的。
显式链接还有一种更复杂的方式,那就是手动加载PE文件。大概步骤为:我们将一个PE文件的数据读入到目标进程的地址空间内,然后按内存对齐的方式进行存储,同时按照Windows加载器的要求对导入表、重定位表进行修正。手动修正导入表的过程如下:
①定位到PE文件中数据目录的导入表项
②查看每一个IMAGE_IMPORT_DIRECTORY结构中的OriginalFirstThunk数组,根据数组中存储的函数名,调用GetProcAddress函数获得函数的地址,并手动填写到FirstThunk指向的数组中。
③如果是使用索引导入的方式,那么需要到指定的Dll文件中,查找该索引在Dll导出表对应的函数地址,然后填写到FirstThunk数组中(关于导出表的内容后续会讲到)。
参考代码
// 导入表项
ImageDataDirectory = (ULONG_PTR)&((PIMAGE_NT_HEADERS)ImageNtHeaders)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
// 导入表起始地址
ImageImportDescriptor = (VirtualAddress + ((PIMAGE_DATA_DIRECTORY)ImageDataDirectory)->VirtualAddress);
// 从起始地址开始,加载每一个Dll和Dll中的导入函数
while (((PIMAGE_IMPORT_DESCRIPTOR)ImageImportDescriptor)->Name)
{
// Dll基地址
ModuleBase = (ULONG_PTR)LoadLibraryA(
(LPCSTR)(VirtualAddress + ((PIMAGE_IMPORT_DESCRIPTOR)ImageImportDescriptor)->Name));
// 函数名数组的基地址
OriginalFirstThunk = (VirtualAddress + ((PIMAGE_IMPORT_DESCRIPTOR)ImageImportDescriptor)->OriginalFirstThunk);
// 需要修正的导入函数地址表
FirstThunk = (VirtualAddress + ((PIMAGE_IMPORT_DESCRIPTOR)ImageImportDescriptor)->FirstThunk);
while (DEREFERENCE(FirstThunk))
{
// 索引导入
if (OriginalFirstThunk && ((PIMAGE_THUNK_DATA)OriginalFirstThunk)->u1.Ordinal & IMAGE_ORDINAL_FLAG)
{
// 按索引号搜索Dll中对应的导出表中的导出函数
ImageNtHeaders = ModuleBase + ((PIMAGE_DOS_HEADER)ModuleBase)->e_lfanew;
// 需要修正某个Dll的导入表,查看该Dll的导出函数
ImageDataDirectory = (ULONG_PTR)&((PIMAGE_NT_HEADERS)ImageNtHeaders)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
ImageExportDirectory = (ModuleBase + ((PIMAGE_DATA_DIRECTORY)ImageDataDirectory)->VirtualAddress);
AddressOfFunctions = (ModuleBase + ((PIMAGE_EXPORT_DIRECTORY)ImageExportDirectory)->AddressOfFunctions);
//导入函数地址 = (导入表索引 - Base) * 4 + 起始地址
AddressOfFunctions +=
((IMAGE_ORDINAL(((PIMAGE_THUNK_DATA)OriginalFirstThunk)->u1.Ordinal) -
((PIMAGE_EXPORT_DIRECTORY)ImageExportDirectory)->Base) * sizeof(DWORD));
//将FirstThunk中的内容修改为真正函数地址
DEREFERENCE(FirstThunk) = (ModuleBase + DEREFERENCE_32(AddressOfFunctions));
}
else
{
//修正名称导入的函数地址
ImageImportByName = (VirtualAddress + DEREFERENCE(OriginalFirstThunk));
//将函数名导入的函数地址填入到IAT中
DEREFERENCE(FirstThunk) = (ULONG_PTR)GetProcAddress((HMODULE)ModuleBase,
(LPCSTR)((PIMAGE_IMPORT_BY_NAME)ImageImportByName)->Name);
}
FirstThunk += sizeof(ULONG_PTR);
if (OriginalFirstThunk)
OriginalFirstThunk += sizeof(ULONG_PTR);
}
// 查找下一个导入表项
ImageImportDescriptor += sizeof(IMAGE_IMPORT_DESCRIPTOR);
}