PE文件-引入表[IMPORT TABLE]--转自iczelion,附vc示范

原创 2011年07月16日 23:01:29

 有关引入表内部结构图例

 

首先,您得了解什么是引入函数。一个引入函数是被某模块调用的但又不在调用者模块中的函数,因而命名为"import(引入)"。引入函数实际位于一个或者更多的DLL里。调用者模块里只保留一些函数信息,包括函数名及其驻留的DLL名。现在,我们怎样才能找到PE文件中保存的信息呢? 转到 data directory 寻求答案吧。再回顾一把,下面就是 PE header:

IMAGE_NT_HEADERS STRUCT
   Signature dd ?
   FileHeader IMAGE_FILE_HEADER <>
   OptionalHeader IMAGE_OPTIONAL_HEADER <>
IMAGE_NT_HEADERS ENDS

optional header 最后一个成员就是 data directory(数据目录):

IMAGE_OPTIONAL_HEADER32 STRUCT
   ....
   LoaderFlags dd ?
   NumberOfRvaAndSizes dd ?
   DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>)
IMAGE_OPTIONAL_HEADER32 ENDS

data directory 是一个 IMAGE_DATA_DIRECTORY 结构数组,共有16个成员。如果您还记得节表可以看作是PE文件各节的根目录的话,也可以认为 data directory 是存储在这些节里的逻辑元素的根目录。明确点,data directory 包含了PE文件中各重要数据结构的位置和尺寸信息。每个成员包含了一个重要数据结构的信息。

MemberInfo inside
0Export symbols
1Import symbols
2Resources
3Exception
4Security
5Base relocation
6Debug
7Copyright string
8Unknown
9Thread local storage (TLS)
10Load configuration
11Bound Import
12Import Address Table
13Delay Import
14COM descriptor

上面那些金色显示的是我熟悉的。了解 data directory 包含域后,我们可以仔细研究它们了。data directory 的每个成员都是 IMAGE_DATA_DIRECTORY 结构类型的,其定义如下所示:

IMAGE_DATA_DIRECTORY STRUCT
  VirtualAddress dd ?
  isize dd ?
IMAGE_DATA_DIRECTORY ENDS

VirtualAddress 实际上是数据结构的相对虚拟地址(RVA)。比如,如果该结构是关于import symbols的,该域就包含指向IMAGE_IMPORT_DESCRIPTOR 数组的RVA
isize 含有VirtualAddress所指向数据结构的字节数。

下面就是如何找寻PE文件中重要数据结构的一般方法:

  1. DOS header 定位到 PE header
  2. optional header 读取 data directory 的地址。
  3. IMAGE_DATA_DIRECTORY 结构尺寸乘上找寻结构的索引号: 比如您要找寻import symbols的位置信息,必须用IMAGE_DATA_DIRECTORY 结构尺寸(8 bytes)乘上1import symbolsdata directory中的索引号)。
  4. 将上面的结果加上data directory地址,我们就得到包含所查询数据结构信息的 IMAGE_DATA_DIRECTORY 结构项。

现在我们开始真正讨论引入表了。data directory数组第二项的VirtualAddress包含引入表地址。引入表实际上是一个 IMAGE_IMPORT_DESCRIPTOR 结构数组。每个结构包含PE文件引入函数的一个相关DLL的信息。比如,如果该PE文件从10个不同的DLL中引入函数,那么这个数组就有10个成员。该数组以一个全0的成员结尾。下面详细研究结构组成:

IMAGE_IMPORT_DESCRIPTOR STRUCT
  union
    Characteristics dd ?
    OriginalFirstThunk dd ?
  ends
  TimeDateStamp dd ?
  ForwarderChain dd ?
  Name1 dd ?
  FirstThunk dd ?
IMAGE_IMPORT_DESCRIPTOR ENDS

结构第一项是一个union子结构。事实上,这个union子结构只是给 OriginalFirstThunk 增添了个别名,您也可以称其为"Characteristics"。 该成员项含有指向一个 IMAGE_THUNK_DATA 结构数组的RVA
什么是
IMAGE_THUNK_DATA? 这是一个dword类型的集合。通常我们将其解释为指向一个 IMAGE_IMPORT_BY_NAME 结构的指针。注意 IMAGE_THUNK_DATA 包含了指向一个 IMAGE_IMPORT_BY_NAME 结构的指针: 而不是结构本身。
请看这里
: 现有几个 IMAGE_IMPORT_BY_NAME 结构,我们收集起这些结构的RVA (IMAGE_THUNK_DATAs)组成一个数组,并以0结尾,然后再将数组的RVA放入 OriginalFirstThunk
IMAGE_IMPORT_BY_NAME 结构存有一个引入函数的相关信息。再来研究 IMAGE_IMPORT_BY_NAME 结构到底是什么样子的呢:

IMAGE_IMPORT_BY_NAME STRUCT
  Hint dw ?
  Name1 db ?
IMAGE_IMPORT_BY_NAME ENDS

Hint 指示本函数在其所驻留DLL的引出表中的索引号。该域被PE装载器用来在DLL的引出表里快速查询函数。该值不是必须的,一些连接器将此值设为0
Name1 含有引入函数的函数名。函数名是一个ASCIIZ字符串。注意这里虽然将Name1的大小定义成字节,其实它是可变尺寸域,只不过我们没有更好方法来表示结构中的可变尺寸域。The structure is provided so that you can refer to the data structure with descriptive names.

TimeDateStamp ForwarderChain 可是高级东东: 让我们精通其他成员后再来讨论它们吧。

Name1 含有指向DLL名字的RVA,即指向DLL名字的指针,也是一个ASCIIZ字符串。

FirstThunk OriginalFirstThunk 非常相似,它也包含指向一个 IMAGE_THUNK_DATA 结构数组的RVA(当然这是另外一个IMAGE_THUNK_DATA 结构数组)
好了,如果您还在犯糊涂,就朝这边看过来
: 现在有几个 IMAGE_IMPORT_BY_NAME 结构,同时您又创建了两个结构数组,并同样寸入指向那些 IMAGE_IMPORT_BY_NAME 结构的RVAs,这样两个数组就包含相同数值了(可谓相当精确的复制啊)。最后您决定将第一个数组的RVA赋给 OriginalFirstThunk第二个数组的RVA赋给 FirstThunk,这样一切都很清楚了。

OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk

|

   |
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Function 1
Function 2
Function 3
Function 4
...
Function n
<---
<---
<---
<---
<---
<---
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA

现在您应该明白我的意思。不要被IMAGE_THUNK_DATA这个名字弄糊涂: 它仅是指向 IMAGE_IMPORT_BY_NAME 结构的RVA。 如果将 IMAGE_THUNK_DATA 字眼想象成RVA,就更容易明白了。OriginalFirstThunk FirstThunk 所指向的这两个数组大小取决于PE文件从DLL中引入函数的数目。比如,如果PE文件从kernel32.dll中引入10个函数,那么IMAGE_IMPORT_DESCRIPTOR 结构的 Name1域包含指向字符串"kernel32.dll"RVA,同时每个IMAGE_THUNK_DATA 数组有10个元素。

下一个问题是: 为什么我们需要两个完全相同的数组? 为了回答该问题,我们需要了解当PE文件被装载到内存时,PE装载器将查找IMAGE_THUNK_DATA IMAGE_IMPORT_BY_NAME 这些结构数组,以此决定引入函数的地址。然后用引入函数真实地址来替代由FirstThunk指向的 IMAGE_THUNK_DATA 数组里的元素值。因此当PE文件准备执行时,上图已转换成:

OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk

|

   |
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Function 1
Function 2
Function 3
Function 4
...
Function n
 
 
 
 
 
Address of Function 1
Address of Function 2
Address of Function 3
Address of Function 4
...
Address of Function n

OriginalFirstThunk 指向的RVA数组始终不会改变,所以若还反过头来查找引入函数名,PE装载器还能找寻到。
当然再简单的事物都有其复杂的一面。有些情况下一些函数仅由序数引出,也就是说您不能用函数名来调用它们: 您只能用它们的位置来调用。此时,调用者模块中就不存在该函数的 IMAGE_IMPORT_BY_NAME 结构。不同的,对应该函数的 IMAGE_THUNK_DATA 值的低位字指示函数序数,而最高二进位 (MSB)设为1。例如,如果一个函数只由序数引出且其序数是1234h,那么对应该函数的 IMAGE_THUNK_DATA 值是80001234hMicrosoft提供了一个方便的常量来测试dword值的MSB位,就是 IMAGE_ORDINAL_FLAG32,其值为80000000h
假设我们要列出某个
PE文件的所有引入函数,可以照着下面步骤走:

  1. 校验文件是否是有效的PE
  2. DOS header 定位到 PE header
  3. 获取位于 OptionalHeader 数据目录地址。
  4. 转至数据目录的第二个成员提取其VirtualAddress值。
  5. 利用上值定位第一个 IMAGE_IMPORT_DESCRIPTOR 结构。
  6. 检查 OriginalFirstThunk值。若不为0,顺着 OriginalFirstThunk 里的RVA值转入那个RVA数组。若 OriginalFirstThunk 0,就改用FirstThunk值。有些连接器生成PE文件时会置OriginalFirstThunk值为0,这应该算是个bug。不过为了安全起见,我们还是检查 OriginalFirstThunk值先。
  7. 对于每个数组元素,我们比对元素值是否等于IMAGE_ORDINAL_FLAG32如果该元素值的最高二进位为1, 那么函数是由序数引入的,可以从该值的低字节提取序数。
  8. 如果元素值的最高二进位为0,就可将该值作为RVA转入 IMAGE_IMPORT_BY_NAME 数组,跳过 Hint 就是函数名字了。
  9. 再跳至下一个数组元素提取函数名一直到数组底部(它以null结尾)。现在我们已遍历完一个DLL的引入函数,接下去处理下一个DLL
  10. 即跳转到下一个 IMAGE_IMPORT_DESCRIPTOR 并处理之,如此这般循环直到数组见底。(IMAGE_IMPORT_DESCRIPTOR 数组以一个全0域元素结尾)

VC示范:

/* 分析PE导入表函数 

IMAGE_DATA_DIRECTORY.VirtualAddress												-->
IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk/IMAGE_IMPORT_DESCRIPTOR.FirstThunk	-->//IMAGE_IMPORT_DESCRIPTOR 数组以一个全0域元素结尾
IMAGE_THUNK_DATA																-->
IMAGE_IMPORT_BY_NAME.Name
*/
void PARSE_IMPORT_TABLE_CALLBACK(IMAGE_DATA_DIRECTORY* lpImageDataDirectory,PVOID pImageBase)
{
	if (!lpImageDataDirectory)
		return;
	
	WRITE_LINE(TEXT("----------------------------------PARSE IMPORT TABLE----------------------------------------"));
	int dllcounter = 0;
	IMAGE_IMPORT_DESCRIPTOR* lpImageImportDescriptor = (IMAGE_IMPORT_DESCRIPTOR*)((byte*)pImageBase+RVAToFileOffset(pImageBase,lpImageDataDirectory->VirtualAddress));
	while (lpImageImportDescriptor->Name!=NULL)//如果不是空元素
	{
		dllcounter++;
		WRITE_LINE((char*)((byte*)pImageBase+RVAToFileOffset(pImageBase,lpImageImportDescriptor->Name)));//输出当前的DLL名字
		DWORD thunk = (lpImageImportDescriptor->OriginalFirstThunk==NULL?lpImageImportDescriptor->FirstThunk:lpImageImportDescriptor->OriginalFirstThunk);
		PARSE_IMPORT_TABLE_FUNCTION_CALLBACK(pImageBase,(IMAGE_THUNK_DATA *)((byte*)pImageBase+RVAToFileOffset(pImageBase,thunk)));
		WRITE_LINE(TEXT("-------------------------------------------"));
		lpImageImportDescriptor++;
	}
	WRITE_LINE_EX(TEXT("导出DLL数目:"),TEXT(dllcounter));
}


 

void PARSE_IMPORT_TABLE_FUNCTION_CALLBACK(PVOID pImageBase,IMAGE_THUNK_DATA *lpImageThunkData)
{
	
	WRITE_LINE(TEXT("\t\tHint\t\tFunction"));
	while (lpImageThunkData->u1.Ordinal!=NULL)
	{
		//对于每个数组元素,我们比对元素值是否等于IMAGE_ORDINAL_FLAG32。
		//如果该元素值的最高二进位为1,那么函数是由序数引入的,可以从该值的低字节提取序数。 
		//如果元素值的最高二进位为0,就可将该值作为RVA转入 IMAGE_IMPORT_BY_NAME 数组,跳过 Hint 就是函数名字了
		if(lpImageThunkData->u1.Ordinal & IMAGE_ORDINAL_FLAG32)//如果该元素值的最高二进位为1,那么函数是由序数引入的
		{
			WORD ordinal = (lpImageThunkData->u1.Ordinal & 0xFFFF);//从该值的低字节提取序数
			WRITE_LINE_EX(TEXT("\t\t"),ordinal);//输出该函数编号
		}
		else
		{
			IMAGE_IMPORT_BY_NAME* lpImageImportByName = (IMAGE_IMPORT_BY_NAME*)((byte*)pImageBase+RVAToFileOffset(pImageBase,lpImageThunkData->u1.Ordinal));
			WRITE(TEXT("\t\t0x"));
			WRITE(lpImageImportByName->Hint);
			WRITE(TEXT("\t\t"));
			WRITE_LINE((char*)(lpImageImportByName->Name));
			
		}
		lpImageThunkData++;
	}
	
}


 

/* 将相对虚拟地址转换为文件偏移地址 */
DWORD RVAToFileOffset(PVOID pMappping,DWORD rva)
{
	//定位到DOS头
	IMAGE_DOS_HEADER* lpImageDosHeader = (IMAGE_DOS_HEADER*)pMappping;
	//定位到PE头
	IMAGE_NT_HEADERS* lpImageNtHeader = (IMAGE_NT_HEADERS*)((byte*)pMappping+lpImageDosHeader->e_lfanew);
	//定位到节表
	IMAGE_SECTION_HEADER* lpSectionTable = (IMAGE_SECTION_HEADER*)((byte*)lpImageNtHeader+sizeof(IMAGE_NT_HEADERS));
	//用节数量作循环次数
	int i = lpImageNtHeader->FileHeader.NumberOfSections;
	while(i>0)//检查所有块
	{
		if (rva>=lpSectionTable->VirtualAddress)//如果传入的rva大于等于当前节的虚拟地址
		{
			DWORD sectionEndAddr = lpSectionTable->VirtualAddress+lpSectionTable->SizeOfRawData;//当前节结束地址=当前节的虚拟地址+文件对齐处理后节尺寸
			if (rva<sectionEndAddr)//如果这个rva地址在这个块里面
			{
				DWORD r_rva = rva-lpSectionTable->VirtualAddress;//这个rva地址-当前节的虚拟地址[rva距离节的开始地址的距离]
				return lpSectionTable->PointerToRawData+r_rva;//当前节基于文件的偏移量+rva距离节的开始地址的距离
			}
		}
		lpSectionTable++;
		i--;
	}
	return rva;
}


 

void main()
{
	//将文件映射到内存
	PVOID pMapping = MapFileToView(PE_FILE_NAME);
	//获取文件头
	IMAGE_NT_HEADERS* lpImageNtHeader = GetPeHeader(pMapping);
	
	if (lpImageNtHeader)
	{
		//显示文件头信息
		PARSE_NT_HEADER_CALLBACK(lpImageNtHeader);
		//显示文件节头信息
		PARSE_SECTION_HEADER_CALLBACK(GET_IMAGE_SECTION_HEADER(lpImageNtHeader),GET_IMAGE_NUMBER_OF_SECTIONS(lpImageNtHeader));
		//分析导入表
		PARSE_IMPORT_TABLE_CALLBACK(&(lpImageNtHeader->OptionalHeader.DataDirectory[1]),pMapping);		
	}
	
	//取消映射
	UnmapViewOfFile(pMapping);
	pMapping = NULL;

}


 

PE文件结构详解(三)PE导出表

上篇文章 PE文件结构详解(二)可执行文件头 的结尾出现了一个大数组,这个数组中的每一项都是一个特定的结构,这次来看看第一项:导出表。...

Oracle数据库对象----序列、索引、视图、同义词

Oracle数据库对象—-序列、索引、视图、同义词

Android客户端采用Http 协议Post方式请求与服务端进行数据交互

本示例以Servlet为例,演示Android与Servlet的通信。 众所周知,Android与服务器通信通常采用HTTP通信方式和Socket通信方式,而HTTP通信方式又分get和post...

PE文件-导出表[EXPORT TABLE]--转自iczelion,附vc示范和图例

首先我贴上导出表结构图例:理论:当PE装载器执行一个程序,它将相关DLLs都装入该进程的地址空间。然后根据主程序的引入函数信息,查找相关DLLs中的真实函数地址来修正主程序。PE装载器搜寻的是DLLs...

PE文件-节表--转自iczelion,附vc示范

我们已经学了许多关于 DOS header 和 PE header 的知识。接下来就该轮到 section table(节表)了。节表其实就是紧挨着 PE header 的一结构数组。该数组成员的数目...

PE文件-检验PE文件的有效性--转自iczelion,附vc示范

同样我首先附上Iczelion的PE文档:如何才能校验指定文件是否为一有效PE文件呢? 这个问题很难回答,完全取决于想要的精准程度。您可以检验PE文件格式里的各个数据结构,或者仅校验一些关键数据结构。...

PE文件-分析vc示范所有代码[包含EXPORT TABLE]

纯属个人研究,请勿见笑#include #include #define PE_FILE_NAME TEXT("C:\\WINDOWS\\twain_32.dll") #define...

PE文件-分析vc示范所有代码[不包含EXPORT TABLE]

#include #include #define PE_FILE_NAME TEXT("C:\\WINDOWS\\system32\\notepad.exe") #define CREATE...

认识PE文件格式-输入表(Import)

exe文件在运行时会调用其他dll文件中的某个函数,被调函数的代码位于相关的dll文件中,而在exe文件中,只保留了该函数的函数名、该函数所在的dll文件名等信息,对exe文件来说,这样的函数就被称为...

Import Table(引入表)

首先,您得了解什么是引入函数。一个引入函数是被某模块调用的但又不在调用者模块中的函数,因而命名为"import(引入)"。引入函数实际位于一个或者更多的DLL里。调用者模块里只保留一些函数信息,包括函...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:PE文件-引入表[IMPORT TABLE]--转自iczelion,附vc示范
举报原因:
原因补充:

(最多只允许输入30个字)