导出表一般存在于 .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遍历查找导出函数序号表,找到每个序号对应的导出函数名,这样就得到了导出函数的名字,然后再通过这个序号(就是索引)找到函数的真正地址,这样就得到了函数的地址。有了索引,函数的真正序号也得到了。
所以大致步骤如下:
- 首先定位到PE文件头
- 从PE文件头的IMAGE_OPTIONAL_HEADER32结构中取出数据目录第一项的,得到导出表的RVA,进而得到地址
- 从导出表中得到起始序号nBase,得到导出函数的总数NumberOfFunctions
- 循环遍历导出函数序号表从0到NumberOfFunctions-1,对于每个序号通过计算得到对应的导出函数名的地址,如果该序号没有对应的导出函数名,那么说明这个函数是只能序号导出的。
- 再通过序号(索引)在导出函数地址表中找到导出函数的真正地址
资源和框架用的还是和之前的一样,然后转换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
[点击并拖拽以移动]
遍历链接后运行: