输入表是PE文件结构中不可或缺的部分,输入表也称之为"导入表"。
要想了解输入表,首先还得先从DLL文件入手。日常生活中我们会看见一些大型软件有很多的DLL格式的文件,这些文件中有很多的导入函数,这些函数不会直接被执行。当一个程序(EXE)运行时,导入函数是被程序调用执行的,其执行的代码是不在主程序(EXE)中的一小部分函数,其真正的代码却在DLL文件中。这时我们就会想,那么EXE主程序是如何找到这些需要导入的函数呢,这就要归结于“输入表”了,输入表就相当于EXE文件与DLL文件沟通的钥匙,所有的导入函数信息都会写入输入表中,在PE文件映射到内存后,windows将相应的DLL文件装入,EXE文件通过“输入表”找到相应的DLL中的导入函数,从而完成程序的正常运行,这一动态连接的过程都是由“输入表”参与的。
下面以32位PE文件为例,分析如何从PE文件中获取输入表信息。
先看一下PE文件结构,PE文件由DOS首部,PE文件头,块表,块和调试信息组成,有关PE文件的数据结构信息在winnt.h中定义。
输入表的位置和大小可以从PE文件的IMAGE_OPTIONAL_HEADER32结构的数据目录表中获取。数据目录表有16项组成,每一项都是一个IMAGE_DATA_DIRECTORY结构,
其中第二项存放输入表信息,即OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress就是输入表的相对虚拟地址(RVA)
如果在内存中查找输入表,那么将RVA值加上PE文件装入的基址就是实际的地址,如果在PE文件中查找输入表,那么需要将RVA转换成文件偏移。
DWORD ITLibrary::RVA2Offset(PCHAR pImageBase,DWORD dwRVA) { DWORD dwOffset=0; int iSections=0; PIMAGE_SECTION_HEADER pSection=NULL; if(m_dwType==PE_X86) { iSections=m_pImage_nt_header32->FileHeader.NumberOfSections; pSection=(PIMAGE_SECTION_HEADER)((PCHAR)m_pImage_nt_header32 + sizeof(IMAGE_NT_HEADERS32)); } else if(m_dwType==PE_X64) { iSections=m_pImage_nt_header64->FileHeader.NumberOfSections; pSection=(PIMAGE_SECTION_HEADER)((PCHAR)m_pImage_nt_header64 + sizeof(IMAGE_NT_HEADERS64)); } if(m_pImage_section_header==NULL) { for(int i=0;i<iSections;i++) { if((pSection->VirtualAddress) && (dwRVA<=(pSection->VirtualAddress+pSection->SizeOfRawData))) { m_pImage_section_header=pSection; break; } pSection++; } if(m_pImage_section_header==NULL) { return 0; } } dwOffset=dwRVA+m_pImage_section_header->PointerToRawData-m_pImage_section_header->VirtualAddress; return dwOffset; }
输入表的地址指向的是IMAGE_IMPORT_DESCRIPTOR结构,输入表由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成,结构的数量取决于程序要使用的DLL文件的数量,每一个结构对应一个DLL文件,在所有这些结构的最后,由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构作为结束。
OriginalFirstThunk
指向一个包含一系列IMAGE_THUNK_DATA32结构的数组,数组中的每个IMAGE_THUNK_DATA32结构定义了一个导入函数的信息,数组最后以一个内容为0的IMAGE_THUNK_DATA32结构作为结束。
当 IMAGE_THUNK_DATA32 值的最高位为1时,表示函数以序号方式输入,这时候低31位被看作一个函数序号,可以使用预定义IMAGE_SNAP_BY_ORDINAL32来判断函数是否以序号方式输入。
当 IMAGE_THUNK_DATA32 值的最高位为0时,表示函数以字符串类型的函数名方式输入,这时其值是一个RVA,指向一个IMAGE_IMPORT_BY_NAME 结构。
结构中的 Hint 字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为 0,Name字段定义了导入函数的名称,这是一个以 0 为结尾的字符串。
Name
它表示DLL 名称的相对虚地址,指向的是该导入DLL文件的名称,如Kernel32.dll
FirstThunk
指向一个包含一系列IMAGE_THUNK_DATA32结构的数组,在PE文件未装入内存时,其指向的信息和OriginalFirstThunk字段指向的信息相同,当PE文件装入内存后,由FirstThunk字段指向的那个数组中的每个双字都被替换成了真正的函数入口地址,
之所以在PE文件中使用两份IMAGE_THUNK_DATA32数组的拷贝并修改其中的一份,是为了最后还可以留下一份拷贝用来反过来查询地址所对应的导入函数名。
在PE文件中,所有DLL对应的导入地址数组是被排列在一起的,全部这些数组的组合也被称为导入地址表(IAT,Import Address Table),输入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的FirstThunk字段指向的就是IAT的起始地址。也可以通过数据目录表的第13项找到IAT数据块的位置和大小。
至此,32位PE文件的输入表分析完毕,对于64位PE文件,获取输入表信息的方法和32位PE文件类似,但64位PE文件和32位PE文件在具体的数据结构字段和类型上有所差别。
最后,奉献上一个完整的获取32PE文件输入表信息和64PE文件输入表信息的程序,运行界面如下: