PE文件导入表简介

我们在编写程序的时候经常要导入外部的一些函数,通常我们只需要将包含函数的模块导入进来,然后我们直接用函数的名字就可以调用函数了。那么这具体是怎么做到的呢?

当我们使用动态链接库中的函数,只有在程序运行时,库里面的代码才会被调入内存,程序不运行时程序只包含了一些关于要导入的链接库和函数的信息,这些信息就在导入表中。(如果有多个程序使用同一个动态链接库,WIndows在物理内中只留一份库的代码,仅通过分页机制将这份代码映射到不同进程的地址空间中)PE文件被装载的时候, Windows装载器会根据导入表中的某个结构处的RVA找到要导入的函数名,然后再根据这个函数名在内存中找到函数的地址,最后再用函数地址将导入表的又某个结构处的内容替换成真正的函数地址。然后我们我们每次用函数名调用函数的时候,系统就会通过函数名可以直接找到函数地址,然后就调用函数了。

为了方便介绍,先直接放一张导入表在磁盘文件中的大概的样子的图:

 最左边的那一块就是PE文件头中IMAGE_OPTIONAL_HEADER32结构中数据目录索引为0的IMAGE_DATA_DIRECTOYR结构所指向的数据,也就是导入表。导入表由一系列IMAGE_IMPORT_DESCIPTOR结构组成,该结构以一个字段全为0的结构结尾。结构定义如下:

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

首先要说明一下,一个IMAGE_IMPORT_DESCRIPTOR结构只表示一个动态链接库里面的函数,所以数据目录所指向的是一系列导入表结构的一个起始RVA。然后导入表函数里面最重要的三个字段:OriginalFirstThunk,Name1,FirstThunk,其中第二个字段存储的是一个指向导入库名称的RVA。然后第一个字段和第三个字段也都是一个RVA,它们都指向包含导入函数的信息的结构,并且他们指向的结构是相同的,也就是说导入表里面有两个相同的结构。但是这两个相同的结构在文件装载进内存后又会变得不一样。下面就来介绍一下这个结构。

包含导入函数信息的结构也是一个结构只包含一个导入函数的信息,所以OriginalFirstThunk和FirstThunk指向的都是一系列导入函数结构的起始RVA。该结构以一个字段全为0的结构结尾。结构的定义如下:

IMAGE_THUNK_DATA    STRUCT
    union u1
        ForwarderString    dd    ?
        Function           dd    ?
        Ordinal            dd    ?
        AddressOfData      dd    ?
    ends
IMAGE_THUNK_DATA    ENDS

可以看到这个结构的定义是用了一个共用体,所以其实这个结构的大小就是一个双字。导入函数有两种方式,一种是通过序号导入,一种是通过函数名称导入。当双字(也就是这个结构)的高位为1时,这个函数以序号的方式导入,这时双字的低位就是函数的序号。当双字的高位为0时,这个函数以函数名的方式导入,双字表示一个RVA,这个RVA又指向一个结构,这个结构用来定义导入函数名。这个结构的定义如下:

IMAGE_IMPORT_BY_NAME    STRUCT
    Hint    dw    ?
    Name1   db    ?        ;因为这里字节数是不确定的,并不是一个字节
IMAGE_IMPORT_BY_NAME    ENDS

第一个字段Hint存储的是函数的序号,不过这个是个可选字段。第二个字段Name1就是函数的名称字符串了。之前说到IMAGE_IMPORT_DESCRIPTOR结构中的OriginalFirstThunk和FirstThunk都是指向相同的IMAGE_THUNK_DATA数组,但是为什么要有两个相同的导入函数结构数组呢?原因是PE文件在磁盘文件中时导入表中只包含导入函数的序号或者名称,只有在被Windows装载器装入内存时函数的地址才被加进来,这时,FirstThunk指向的IMAGE_THUNK_DATA数组里面的内容就全都被替换成相应的真正的函数的地址,所以上面说两个相同的结构在装载进内存后又会变得不一样。之所以要保留一个原来的结构是方便反过来可以通过函数地址查找函数的名称。导入表装载进内存后的样子如下图:

 这个被真正的函数地址替换后的IMAGE_THUNK_DATA结构数组现在叫做导入地址表(IAT),通过数据目录中索引值为12的项也可以找到这个导入地址表的起始RVA。

那么知道了这些信息后就可以写一个小程序来显示PE文件中导入表的信息了。资源和框架和上个显示PE文件头结构的程序是一样的,所以这里就只给出功能实现部分的代码。

因为我们对文件的所有操作都是基于用内存映射文件函数将文件映射到内存后来的,所以这个文件此刻的排列偏移都是和在磁盘上的一样,但是我们只有数据的RVA,没有数据在文件中的偏移。所以我们需要一个函数来将数据的RVA转换为在文件中的偏移(文件头不需要,因为文件头从磁盘装载到内存时不进行任何处理,就没有那些对齐操作)。

那么这个函数怎么实现呢?我们可以通过取得数据的RVA和该数据所在的节的RVA的差,得到该数据相对于所在节的偏移,然后我们将这个偏移加上该节在文件中的偏移,就得到了该数据在磁盘文件中的偏移。但是我们要怎么确定该数据在哪个节呢?我们可以用该节的节表中的VirtualAddress字段加上SizeOfRawData字段得到这个节的数据的结尾。为什么要拿RVA加上磁盘文件中的大小呢?因为我们是用RVA来进行转换,所以首先区间的小端得是RVA,所以用的是VirtualAddress,然后其实节在内存中对齐或在文件中对齐都是在后面填0,前面有用的数据并没有动,所以节在内存中的RVA加上节在文件中对齐后的大小依然能够包括这个节的所有有用数据。所以利用这个,我们循环操作每个节,看看要转换的RVA是否在这个节里面,在的话就按照上述转换一下然后返回。

然后我们还需要一个函数用来获取数据所在节的名称,原理和上面一样。

这个两个函数单独放在一个文件中,因为它们在其他程序中也用得到。

然后显示导入表的信息就比较简单了,通过NT文件头中IMAGE_OPTIONAL_HEADER32结构中的数据目录得到导入表的起始RVA后,通过OriginalFirstThunk找到导入函数结构数组,然后循环处理数组内容,如果双字高位是1,那么就显示函数序号,如果是0,则通过这个RVA再定位到IMAGE_IMPORT_BY_NAME结构数组,然后显示序号和函数名字。这一个IMAGE_IMPORT_DESCRIPTOR结构的导入函数信息全部显示完后定位到下一个IMAGE_IMPORT_DESCRIPTOR结构,循环显示完所有导入表信息。

转换RVA以及找出数据所在节的函数:

				.const
szNotFound		db		'无法查找',0
				.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;将RVA转换成文件偏移
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_RVAToOffset	proc	_lpFileHead,_dwRVA			;文件读取到内存后的起始位置和数据的RVA
				local	@dwReturn
				
				pushad
				mov		esi,_lpFileHead
				assume	esi:ptr IMAGE_DOS_HEADER	;将文件起始位置与DOS文件头的结构联系到一起
				add		esi,[esi].e_lfanew			;加上PE文件头的偏移量,得到PE文件头的位置
;*************************************************************************************************************
;得到文件头的位置,它再加上文件头的长度就是节表的位置
;*************************************************************************************************************
				assume	esi:ptr IMAGE_NT_HEADERS	;将PE文件头的位置与PE文件头的结构联系到一起
				mov		edi,_dwRVA
				mov		edx,esi
				add		edx,sizeof IMAGE_NT_HEADERS	;将PE文件头的位置加上PE文件头的大小得到节表的位置
				assume	edx:ptr IMAGE_SECTION_HEADER
				movzx	ecx,[esi].FileHeader.NumberOfSections	;获取节的数目
;**************************************************************************************************************
;扫描每个节并判断RVA是否位于这个节里面
;**************************************************************************************************************
				.repeat
						mov		eax,[edx].VirtualAddress		;获取节表中第一个结构指向的节的相对虚拟偏移地址
						add		eax,[edx].SizeOfRawData			;获取节表中第一个结构指向的节的末尾的虚拟偏移地址
						.if		(edi >= [edx].VirtualAddress) && (edi < eax)	;如果要查找的数据的位置在这个节里面
								mov		eax,[edx].VirtualAddress;获取节表中第一个结构指向的节的相对虚拟偏移地址
								sub		edi,eax					;获取数据相对于数据所处的节的起始位置的偏移量
								mov		eax,[edx].PointerToRawData	;获取该节在磁盘文件中的偏移
								add		eax,edi					;节在磁盘中的偏移加上数据相对于节的起始位置的偏移,得到数据在磁盘中的真正偏移
								assume	edx:nothing
								assume	esi:nothing
								jmp		@F
						.endif
						add		edx,sizeof	IMAGE_SECTION_HEADER	;加上一个节表结构的长度得到下一个节表结构的位置
				.untilcxz
				assume	edx:nothing
				assume	esi:nothing
				mov		eax,-1			;如果节中没有该数据则返回-1
@@:
				mov		@dwReturn,eax
				popad
				mov		eax,@dwReturn
				ret
_RVAToOffset	endp
;*******************************************************************************************************************
;获取RVA所在的节的名称
;*******************************************************************************************************************
_GetRVASection	proc	_lpFileHead,_dwRVA
				local	@dwReturn
				
				pushad
				mov		esi,_lpFileHead
				assume	esi:ptr IMAGE_DOS_HEADER
				add		esi,[esi].e_lfanew
				assume	esi:ptr IMAGE_NT_HEADERS
				mov		edi,_dwRVA
				mov		edx,esi
				add		edx,sizeof	IMAGE_NT_HEADERS
				assume	edx:ptr IMAGE_SECTION_HEADER
				movzx	ecx,[esi].FileHeader.NumberOfSections
;******************************************************************************************************************
;扫描每个节区并判断RVA是否位于这个节内
;******************************************************************************************************************
				.repeat
						mov		eax,[edx].VirtualAddress
						add		eax,[edx].SizeOfRawData
						.if		(edi >= [edx].VirtualAddress) && (edi < eax)
								mov		eax,edx					;将节表结构的起始位置放入AX(就是节的名称)
								jmp		@F
						.endif
						add		edx,sizeof	IMAGE_SECTION_HEADER
				.untilcxz
				assume	edx:nothing
				assume	esi:nothing
				mov		eax,offset szNotFound
@@:
				mov		@dwReturn,eax
				popad
				mov		eax,@dwReturn
				ret
_GetRVASection	endp
				
				
								
								
						
						
				

功能实现代码:

				.const
szMsg	db		'文件名: %s',0dh,0ah
		db		'-------------------------------------------------------',0dh,0ah
		db		'导入表所处的节:%s',0dh,0ah,0
szMsgImport		db		0dh,0ah
		db		'-------------------------------------------------------',0dh,0ah
		db		'导入库:%s',0dh,0ah
		db		'-------------------------------------------------------',0dh,0ah
		db		'OriginalFirstThunk %08X',0dh,0ah
		db		'TimeDateStamp	   %08X',0dh,0ah
		db		'ForwarderChain	   %08X',0dh,0ah
		db		'FirstThunk         %08X',0dh,0ah
		db		'-------------------------------------------------------',0dh,0ah
		db		'导入序号  导入函数名称',0dh,0ah
		db		'-------------------------------------------------------',0dh,0ah,0
szMsgName		db		'%8u  %s',0dh,0ah,0
szMsgOrdinal	db		'%8u  (按序号导入)',0dh,0ah,0
szErrNoImport	db		'这个文件不适用任何导入函数',0

				.code
include			_RvaToFileOffset.asm
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile	proc	_lpFile,_lpPeHead,_dwSize							;内存映射文件指针,PE文件头指针,文件大小
				local	@szBuffer[1024]:byte,@szSectionName[16]:byte
				pushad
				mov		edi,_lpPeHead
				assume	edi:ptr IMAGE_NT_HEADERS
;********************************************************************************************************************
				mov		eax,[edi].OptionalHeader.DataDirectory[8].VirtualAddress			;将数据目录中指向导入表的结构的第一个字段(就是导入表的起始RVA)放入AX
				.if		! eax																;如果没有导入表
						invoke	MessageBox,hWinMain,addr szErrNoImport,NULL,MB_OK
						jmp		_Ret
				.endif
				invoke	_RVAToOffset,_lpFile,eax											;将读入到内存后文件的起始地址和导入表的RVA作为参数得到导入表在文件中的偏移
				add		eax,_lpFile															;将偏移加上文件起始位置得到导入表的地址
				mov		edi,eax
				assume	edi:ptr	IMAGE_IMPORT_DESCRIPTOR										;导入表结构
;********************************************************************************************************************
;显示文件名以及节的名称
;********************************************************************************************************************
				invoke	_GetRVASection,_lpFile,[edi].OriginalFirstThunk						;获取导入表所在的节的名称
				invoke	wsprintf,addr @szBuffer,addr szMsg,addr szFileName,eax				;写入文件名以及节的名称
				invoke	SetWindowText,hWinEdit,addr @szBuffer
;********************************************************************************************************************
;循环处理IMAGE_IMPORT_DESCRIPTOP直到遇到全0的则结束
;********************************************************************************************************************
				.while	[edi].OriginalFirstThunk || [edi].TimeDateStamp || \					;当内容不全为0时进入循环
						[edi].ForwarderChain || [edi].Name1 || [edi].FirstThunk
						invoke	_RVAToOffset,_lpFile,[edi].Name1							;获取导入表所指向的动态链接库的名字的字符串在文件中的偏移
						add		eax,_lpFile													;获取动态链接库的名字的字符串在文件中的地址
						invoke	wsprintf,addr @szBuffer,addr szMsgImport,eax,\
								[edi].OriginalFirstThunk,[edi].TimeDateStamp,\
								[edi].ForwarderChain,[edi].FirstThunk						;将导入表的结构的信息写入
						invoke	_AppendInfo,addr @szBuffer									;显示信息
;********************************************************************************************************************
;获取IMAGE_THUNK_DATA列表地址
;********************************************************************************************************************
						.if		[edi].OriginalFirstThunk
								mov		eax,[edi].OriginalFirstThunk								;将指向导入函数的结构的地RVA填入AX
						.else
								mov		eax,[edi].FirstThunk
						.endif
;**********************************************************************************************************************

						invoke	_RVAToOffset,_lpFile,eax											;获取导入函数结构在文件中的偏移
						add		eax,_lpFile
						mov		ebx,eax
;********************************************************************************************************************
;循环处理所有的IMAGE_THUNK_DATA
;********************************************************************************************************************
						.while	dword ptr [ebx]
;********************************************************************************************************************
;按序号导入
;********************************************************************************************************************
								.if		dword ptr [ebx] & IMAGE_ORDINAL_FLAG32						;判断导入函数结构是按序号还是按函数名字,如果是按序号
										mov		eax,dword ptr [ebx]
										and		eax,0FFFFh											;将高位清0
										invoke	wsprintf,addr @szBuffer,\
												addr szMsgOrdinal,eax
								.else
;********************************************************************************************************************
;按函数名导入(则导入函数结构是IMAGE_IMPORT_BY_NAME的RVA
;********************************************************************************************************************
										invoke	_RVAToOffset,_lpFile,dword ptr [ebx]				;获取导入函数结构在文件中的偏移
										add		eax,_lpFile
										assume	eax:ptr IMAGE_IMPORT_BY_NAME
										movzx	ecx,[eax].Hint										;序号
										invoke	wsprintf,addr @szBuffer,\
												addr szMsgName,ecx,addr [eax].Name1  
										assume	eax:nothing
								.endif
								invoke	_AppendInfo,addr @szBuffer									;将信息显示出来
								add		ebx,4														;移到下一个导入函数的结构的位置
						.endw
						add		edi,sizeof IMAGE_IMPORT_DESCRIPTOR									;移动到下一个导入表结构的位置
				.endw
;***********************************************************************************************************************
_Ret:
				assume	edi:nothing
				popad
				ret
				
_ProcessPeFile	endp

编译链接后运行就是这个样子:

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值