PE文件导出表简介

导出表一般存在于 .dll 文件中,偶尔也存在于EXE文件中。PE文件被执行时,Windows装载器将文件装入内存并将导入表登记的DLL文件装入,再把需要导入的函数的地址根据DLL文件导出表中的信息对被执行文件导入表中的IAT表进行修正。所以导出表就是存储了一个文件的导出信息的表,通过导出表DLL文件向系统提供导出函数名称,序号和入口地址等信息,以便Windows装载器来完成动态链接的过程。

导出表的定义如下:

IMAGE_EXPORT_DIRECTORY    STRUCT
    Characteristics            DWORD        ? 
    TimeDateStamp              DWORD        ? ;文件产生时刻
    MajorVersion               WORD         ? 
    MinorVersion               WORD         ?  
    nName                      DWORD        ? ;指向文件名的RVA
    nBase                      DWORD        ? ;导出函数的起始序号
    NumberOfFunctions          DWORD        ? ;导出函数的总数
    NumberOfNames              DWORD        ? ;以名称导出的函数的函数总数
    AddressOfFunctions         DWORD        ? ;指向导入函数地址表的一个RVA
    AddressOfNames             DWORD        ? ;指向函数名地址表的一个RVA
    AddressOfNameOrdinals      DWORD        ? ;指向函数名序号表的一个RVA
IMAGE_EXPORT_DIRECTORY    ENDS

注意别把导出表和导入表的一些性质搞混了,导入表是由一系列IMAGE_IMPORT_DESCRIPTOR结构组成的,而导出表一个文件中就只有一个,所以一个文件就只有一个IMAGE_EXPORT_DIRECTORY结构。

导入表中导入函数时可以通过函数名或者序号来导入,那么对应的,导出表也可以通过序号或者函数名来导出函数。当函数是通过函数名导出时它也同时可以通过序号来导出,而当函数通过序号来导出时它就只能通过序号来导出。

接下来分析一下导出表结构里面的重要字段:

  • nName:指向文件名的RVA,就是DLL的文件名或者少有的EXE文件名的RVA,这个地方的字符串说明了模块的原始文件名。
  • nBase:这个是导出函数的起始序号,我们待会会看到,用函数地址表里面函数的索引加上这个序号才是函数真正的导出序号。
  • NumberOfFunctions:导出函数的总数,就是用名字导出的加上只能用序号导出的函数的总数
  • AddressOfFunctions:一个指向导出函数地址表的RVA,导出函数地址表又是一个双字数组,数组里面存的都是RVA,这些RVA指向导出函数的真正的入口地址。这个数组的长度等于NumberOfFunctions的值
  • AddressOfNames:一个指向函数名地址表的RVA,函数名地址表又是一个双字数组,数组里面存放的也都是RVA,然后这些RVA又指向导出函数名字符串。这个数组的长度等于NumberOfNames的值
  • AddressOfNameOrdinals:一个指向函数名序号表的RVA,函数名序号表是一个单字数组,这次里面存的不是RVA了,里面存放的是函数名地址表中函数在函数地址表里面的索引。这样说似乎很模糊,放个图就明白了

导出函数地址表中函数的索引是按照0,1,2,3,4.....这样的顺序一直排列下去的,而导出函数名地址表中的函数名并不是按照地址表中的顺序依次排列下来的,里面的函数名的顺序是乱的,而它右边的(图片上对应是右边)函数名序号表,按照顺序下来和这个函数名表一 一对应,并且函数名序号表里面存的正好就是对应的函数名在地址表中的索引,这样的话我们就可以通过函数名找到函数名在地址表中的索引,然后再找到函数地址了。

知道这些后,我们又可以写一个查看文件导出表的信息的小程序了,导出表里最重要的肯定就是导出函数了。但是我们不知道一个文件里又哪些导出函数,但是我们知道导出表中导出函数地址的那些函数的索引是按照0,1,2,3......这样的顺序依次下来的,所以加入有n个函数,那么索引就是0到n-1,所以只要我们知道导出函数的数量,然后就可以从0到n遍历查找导出函数序号表,找到每个序号对应的导出函数名,这样就得到了导出函数的名字,然后再通过这个序号(就是索引)找到函数的真正地址,这样就得到了函数的地址。有了索引,函数的真正序号也得到了。

所以大致步骤如下:

  1. 首先定位到PE文件头
  2. 从PE文件头的IMAGE_OPTIONAL_HEADER32结构中取出数据目录第一项的,得到导出表的RVA,进而得到地址
  3. 从导出表中得到起始序号nBase,得到导出函数的总数NumberOfFunctions
  4. 循环遍历导出函数序号表从0到NumberOfFunctions-1,对于每个序号通过计算得到对应的导出函数名的地址,如果该序号没有对应的导出函数名,那么说明这个函数是只能序号导出的。
  5. 再通过序号(索引)在导出函数地址表中找到导出函数的真正地址

资源和框架用的还是和之前的一样,然后转换RVA到文件偏移的函数以节找出数据所在节的函数也是和之前的一样。主要功能实现部分代码如下:
 

​

				.const
				
szMsg	db		'文件名: %s',0dh,0ah
		db		'-------------------------------------------------',0dh,0ah
		db		'导出表所处的节: %s',0dh,0ah
		db		'-------------------------------------------------',0dh,0ah
		db		'原始文件名          %s',0dh,0ah
		db		'nBase			  %08X',0dh,0ah
		db		'NumberOfFunctions	  %08X',0dh,0ah				;导出函数的数量
		db		'NumberOfNames		  %08X',0dh,0ah
		db		'AddressOfFunctions        %08X',0dh,0ah
		db		'AddressOfNames		  %08X',0dh,0ah
		db		'AddressOfNameOrd	  %08X',0dh,0ah
		db		'-------------------------------------------------',0dh,0ah
		db		'导出序号  虚拟地址  导出函数名称',0dh,0ah
		db		'------------------------------------------',0dh,0ah,0
szMsgName		db		'%08X  %08X  %s',0dh,0ah,0
szExportByOrd	db		'(按照序号导出)',0
szErrNoExport	db		'这个文件中没有导出函数!',0

				.code
include			_RvaToFileOffset.asm
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile	proc	_lpFile,_lpPeHead,_dwSize
				local	@szBuffer[1024]:byte,@szSectionName[16]:byte
				local	@dwIndex,@lpAddressOfNames,@lpAddressOfNameOrdinals,@Names_RVA
				
				pushad
				mov		esi,_lpPeHead
				assume	esi:ptr IMAGE_NT_HEADERS
;***********************************************************************************************************
;从数据目录中获取导出表的位置
;***********************************************************************************************************
				mov		eax,[esi].OptionalHeader.DataDirectory.VirtualAddress				;获取导出表的起始RVA,因为导出表在数据目录中的索引是0,所以数据目录起始位置就是导出表的目录结构
				.if		! eax
						invoke	MessageBox,hWinMain,\
								addr szErrNoExport,NULL,MB_OK
						jmp		_Ret
				.endif
				invoke	_RVAToOffset,_lpFile,eax
				add		eax,_lpFile								;获取导出表在文件中的地址
				mov		edi,eax
;***********************************************************************************************************
;显示一些常用的信息
;***********************************************************************************************************
				assume	edi:ptr IMAGE_EXPORT_DIRECTORY
				invoke	_RVAToOffset,_lpFile,[edi].nName		;获取导出表文件名在文件中的偏移
				add		eax,_lpFile								;获取导出表的文件名的地址
				mov		ecx,eax
				invoke	_GetRVASection,_lpFile,[edi].nName		;获取导出表所处的节的名称
				invoke	wsprintf,addr @szBuffer,addr szMsg,\
						addr szFileName,eax,ecx,[edi].nBase,\
						[edi].NumberOfFunctions,[edi].NumberOfNames,\
						[edi].AddressOfFunctions,[edi].AddressOfNames,\
						[edi].AddressOfNameOrdinals
				invoke	SetWindowText,hWinEdit,addr @szBuffer
;***********************************************************************************************************
				invoke	_RVAToOffset,_lpFile,[edi].AddressOfNames
				add		eax,_lpFile								;获取导出函数名地址表的地址
				mov		@lpAddressOfNames,eax
				invoke	_RVAToOffset,_lpFile,[edi].AddressOfNameOrdinals
				add		eax,_lpFile								;获取导出函数序号表的地址
				mov		@lpAddressOfNameOrdinals,eax
				invoke	_RVAToOffset,_lpFile,[edi].AddressOfFunctions
				add		eax,_lpFile								;获取导出函数地址表的地址
				mov		esi,eax	
;***********************************************************************************************************
;循环显示导出函数的信息
;***********************************************************************************************************
				mov		ecx,[edi].NumberOfFunctions				;导出函数的数目
				mov		@dwIndex,0
				@@:
				pushad
;***********************************************************************************************************
;按名称导出的索引表中
;***********************************************************************************************************
				mov		eax,@dwIndex
				push	edi
				mov		ecx,[edi].NumberOfNames					;以函数名字的方式导出的函数的个数
				cld
				mov		edi,@lpAddressOfNameOrdinals			;现在EDI指向函数名序号表
				repnz	scasw								
				.if		ZERO?									;在序号表中找到了@dwIndex这个序号,ZF标志位置1
						sub		edi,@lpAddressOfNameOrdinals
						sub		edi,2							;因为找到序号后EDI又向后移动了一个单位,序号表又是以字为单位,所以减2再减去表的起始位置就得到项目相对于函数序号表的偏移
						shl		edi,1							;因为函数名表的单位是双字节,而序号表是字,所以要乘2得到该序号对应的函数名相对于函数名表中的偏移
						add		edi,@lpAddressOfNames			;用函数名相对于导出函数名表的RVA加上导出函数名表的地址,得到该函数名在导出函数名表中对应的地址,而该地址指向一个RVA,该RVA指向该函数名字符串
						invoke	_RVAToOffset,_lpFile,dword ptr [edi]
						add		eax,_lpFile						;得到函数名字的字符串的地址
				.else
						mov		eax,offset szExportByOrd
				.endif
				pop		edi										;edi重新指向导出表结构
;**********************************************************************************************************
				mov		ecx,@dwIndex
				add		ecx,[edi].nBase
				invoke	wsprintf,addr @szBuffer,addr szMsgName,\
						ecx,dword ptr [esi],eax					;序号,地址,名字
				invoke	_AppendInfo,addr @szBuffer				;显示内容
				popad
				add		esi,4									;移动到下一个函数的地址
				inc		@dwIndex								;开始寻找下一个序号的函数
				loop	@B
_Ret:
				assume	esi:nothing
				assume  edi:nothing
				popad
				ret
				
_ProcessPeFile	endp
						

[点击并拖拽以移动]
​

遍历链接后运行:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值