绑定导入数据的存在主要是为了优化导入信息,提高PE的加载效率.
当PE文件被加载到内存时,加载器会先检查导入表,然后把需要加载的DLL载入到地址空间中.
加载器还有一项比较重要的工作是根据导入信息的描述使用动态链接库里输入函数的实际地址去替换IAT表的内容,这个步骤会花去一部分时间.但是如果知道函数实际的地址,就可以直接把数组中的元素替换为地址,这能节省相当多的时间.这种方法就称为绑定(Binding).
绑定导入表定位以及解析(手动FileBuffer)
测试文件Win7 x86下notepad.exe
IMAGE_BOUND_IMPORT_DESCRIPTOR
结构及成员含义如下:
typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {
DWORD TimeDateStamp; //时间戳
WORD OffsetModuleName; //DLL名的地址相当于第一个IMAGE_BOUND_IMPORT_DESCRIPTOR结构的偏移
WORD NumberOfModuleForwarderRefs; //该结构后IMAGE_BOUND_FORWARDER_REF结构的数量
// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_BOUND_FORWARDER_REF {
DWORD TimeDateStamp; //时间戳
WORD OffsetModuleName; //DLL名的地址相当于第一个IMAGE_BOUND_IMPORT_DESCRIPTOR结构的偏移
WORD Reserved; //保留
} IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;
定位导入表:
IMAGE_OPTIONAL_HEADER -> DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT(11)].VirtualAddress + ImageBase.(如果在文件中解析只需将RVA转换为FOA加上当前文件首地址即可定位导入表).
VirtualAddress = 00000278h(在SizeOfHeaders内,RVA = FOA)(RVA与FOA转换讲解)
Size = 00000128h
绑定导入表数据如下:
绑定导入表在内存中是连续存储的,结束标记为8字节全0数据.
成员含义:
TimeDateStamp该成员为时间戳,该值必须与导入DLL的IMAGE_FILE_HEADER中的TimeDateStamp值相同绑定导入才会生效,否则就会重新计算IAT表中的函数地址.这种情况一般发生在DLL版本不同或者DLL被重定位.
可通过函数gmtime_s获取对应时间.
OffsetModuleName该成员为DLL名,计算方式为第一个IMAGE_BOUND_IMPORT_DESCRIPTOR的地址为基址 + OffsetModuleName为指向模块名字符串.
IMAGE_BOUND_IMPORT_DESCRIPTOR地址为:0x278
OffsetModuleName值为:0x0080
NumberOfModuleForwarderRefs该值为当前IMAGE_BOUND_IMPORT_DESCRIPTOR结构后存在的IMAGE_BOUND_FORWARDER_REF结构数组的元素个数.
第二个绑定导入结构中NumberOfModuleForwarderRefs值为1,可以直接理解为第二个绑定导入表后紧跟一个IMAGE_BOUND_FORWARDER_REF结构.
微软提供了一个绑定工具bind.exe程序,该程序可以把导入表中IAT表项IMAGE_THUNK_DATA32的内容都静态替换成虚拟内存地址,然后在数据目录表的第12项指定的位置声明这些更改.Windows在加载目标PE相关的动态链接库时,会首先检查这些地址是否正确,这些检查包括当前系统的DLL版本是否符合绑定导入结构中的版本号,如果不符合或者DLL需要被重新定位,加载器就会去遍历OriginalFirstThunk指向的数组(INT),计算新的地址.
代码定位绑定导入表并打印其数据
读取文件代码
PVOID FileToMem(IN PCHAR szFilePath, OUT LPDWORD dwFileSize)
{
//打开文件
FILE* pFile = fopen(szFilePath, "rb");
if (!pFile)
{
printf("FileToMem fopen Fail \r\n");
return NULL;
}
//获取文件长度
fseek(pFile, 0, SEEK_END); //SEEK_END文件结尾
DWORD Size = ftell(pFile);
fseek(pFile, 0, SEEK_SET); //SEEK_SET文件开头
//申请存储文件数据缓冲区
PCHAR pFileBuffer = (PCHAR)malloc(Size);
if (!pFileBuffer)
{
printf("FileToMem malloc Fail \r\n");
fclose(pFile);
return NULL;
}
//读取文件数据
fread(pFileBuffer, Size, 1, pFile);
//判断是否为可执行文件
if (*(PSHORT)pFileBuffer != IMAGE_DOS_SIGNATURE)
{
printf("Error: MZ \r\n");
fclose(pFile);
free(pFileBuffer);
return NULL;
}
if (*(PDWORD)(pFileBuffer + *(PDWORD)(pFileBuffer + 0x3C)) != IMAGE_NT_SIGNATURE)
{
printf("Error: PE \r\n");
fclose(pFile);
free(pFileBuffer);
return NULL;
}
if (dwFileSize)
{
*dwFileSize = Size;
}
fclose(pFile);
return pFileBuffer;
}
定位并输出绑定导入表数据
VOID PrintBoundImportInfo()
{
//读取文件二进制数据
DWORD dwFileSize = 0;
PCHAR pFileBuffer = FileToMem(FILE_PATH_IN, &dwFileSize);
if (!pFileBuffer)
{
return;
}
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pFileBuffer + pDos->e_lfanew);
PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PUCHAR)pNth + 4);
PIMAGE_OPTIONAL_HEADER32 pOpo = (PIMAGE_OPTIONAL_HEADER32)((PUCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);
//判断该PE文件是否有重定位表
if (!pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress)
{
printf("该PE文件不存在导入表 \r\n");
return;
}
PIMAGE_BOUND_IMPORT_DESCRIPTOR pBou = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)(pFileBuffer + RvaToFoa(pFileBuffer, pOpo->DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress));
//第一个绑定导入基址
PUCHAR pBase = (PUCHAR)pBou;
//循环遍历
do
{
printf("TimeDateStamp -> [0x%08x] ", pBou->TimeDateStamp);
printf("ModuleName -> [%s] \r\n", pBase + pBou->OffsetModuleName);
//可以直接*(PULONGLONG)addr 判断不需要这么麻烦
//方便理解结构
if (pBou->NumberOfModuleForwarderRefs != 0)
{
PIMAGE_BOUND_FORWARDER_REF pRef = pBou + 1;
for (size_t i = 0; i < pBou->NumberOfModuleForwarderRefs; i++)
{
printf("TimeDateStamp -> [0x%08x] ", pRef->TimeDateStamp);
printf("ModuleName -> [%s] \r\n", pBase + pRef->OffsetModuleName);
pRef++;
pBou++;
}
}
} while (*(PDWORD)(++pBou));
}