我们在编写程序的时候经常要导入外部的一些函数,通常我们只需要将包含函数的模块导入进来,然后我们直接用函数的名字就可以调用函数了。那么这具体是怎么做到的呢?
当我们使用动态链接库中的函数,只有在程序运行时,库里面的代码才会被调入内存,程序不运行时程序只包含了一些关于要导入的链接库和函数的信息,这些信息就在导入表中。(如果有多个程序使用同一个动态链接库,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
编译链接后运行就是这个样子: