PE文件中的资源部分占PE文件很大的一个比例,资源包括光标,位图,菜单等等还有十几种标准类型,处理标准类型还有自定义类型。资源的组织方式很像磁盘目录的组织的方式,或者说像数据结构中的树型结构。PE文件中资源的组织方式如下图:
从上图可以看到资源块是由很多种结构组成的,第一层目录也就是根目录的第一个结构是一个IMAGE_RESOURCE_DIRECTORY结构,它也就是IMAGE_OPTIONAL_HEADER32结构中的数据目录的第三项IMAGE_DATA_DIRECTORY结构中VirtualAddress字段RVA所指向的内容。从上图可以看到第一二三层目录都是以一个IMAGE_RESOURCE_DIRECTORY结构开始后面跟着一系列的IMAGE_RESOURCE_DIRECTORY_ENTRY结构构成的。
整个资源的组织方式:第一层根目录,目录项就是描述有哪些类型的资源,然后每个目录项又各指向一个第二层目录的IMAGE_RESOURCE_DIRECTORY结构,第二层目录主要就是说明同种类型有哪些具体的资源,每个目录项里面包含该资源名称或者ID,然后每个目录项又各自指向一个第三层目录的结构,第三层目录的组织方式和第一二层一样,但是都只有一个目录项,目录项主要就是说明本资源的代码页,然后再指向一个资源数据入口,就是上图从左往右第4列的结构,该结构说明了一些资源的信息,并给出了资源数据的RVA。
IMAGE_RESOURCE_DIRECTORY结构主要是说明本目录的各种属性信息,还说明了本目录的目录项。该结构的定义如下:
IMAGE_RESOURCE_DIRECTORY STRUCT
Characteristics dd ? ;理论上为资源的属性,不过经常为0
TimeDateStamp dd ? ;资源的产生时刻
MajorVersion dw ? ;理论上为资源的版本,不过也经常是0
MinorVersion dw ? ;
NumberOfNamedEntries dw ? ;以名称命名的入口数量
NumberOfIdEntries dw ? ;以ID命名的入口数量
IMAGE_RESOURCE_DIRECTORY ENDS
比较重要的就是最后面两个字段了,因为不管资源种类还是资源名称,都可以用ID或者字符串的名称来定义。比如:
100 ICON "Test.ico" 例1
HelpFile HELP "Test.chm" 例2
第一种方式就是以ID定义的,第二种方式就是以名称定义的。所以NumberOfNamedEntries字段加上NumberOfIdEntries字段才是本目录种所有的入口数量。然后后跟在这个结构后面的IMAGE_RESOURCE_DIRECTORY_ENTRY结构,该结构一个结构描述一个目录项,结构的定义如下:
IMAGE_RESOURCE_DIRECTORY_ENTRY STRUCT
Name1 dd ? ;目录项的名字字符串指针或ID
OffsetToData dd ? ;目录项指针
IMAGE_RESOURCE_DIRECTORY_ENTRY ENDS
该结构很简单,第一个字段Name1是一个指向本目录项的一个名字字符串或者ID,如果该目录是第一一层目录,那么它就指向资源类型的字符串或者是资源类型的ID,如果是第二层目录,它就指向资源名称的字符串或者是资源名称的ID,如果是第三层目录,那么它表示的就是代码页。当该字段表示ID时,32位比特足矣,但是要表示字符串的话就只能用RVA这种方式来表示了,但是指向字符串时它其实不是字符串的RVA。当Name1字段的最高位为0时,该字段为ID,当该字段的最高位为1时,该字段的低位表示一个相对于资源块起始位置的偏移。
当Name1字段指向字符串时,它也并不是直接指向字符串的地址的,它指向一个IMAGE_RESOURCE_DIR_STRING_U结构,该结构的定义如下:
IMAGE_RESOURCE_DIR_STRING_U STRUCT
Length1 dw ? ;字符串的长度
NameString dw ? ;UNICODE字符串,由于字符串是不定长的,所以这里只能用一个DW来表示一下
;实际上如果长度是10,那么应该是 dw 10 dup (?)
IMAGE_RESOURCE_DIR_STRING_U ENDS
第一个字段描述了字符串的长度,第二个字段就是字符串的位置了。
IMAGE_RESOURCE_DIRECTORY_ENTRY结构的第二个字段也是一个相对于资源起始位置的偏移,它指向下一层目录。如果该字段的最高位为1的话, 那么字段的低位指向指向一个IMAGE_RESOURCE_DIRECTORY结构,这样的情况只有在第一层和第二层种出现,当在第三层目录时,该字段高位为0,字段指向一个资源数据入口的结构。
在第三层目录中,目录项的Name1字段说明了资源的代码页,OffsetToData字段指向资源数据入口结构,该结构定义如下:
IMAGE_RESOURCE_DATA_ENTRY STRUCT
OffsetToData dd ? ;资源数据的RVA
Sizel dd ? ;资源数据的长度
CodePage dd ? ;代码页
Reserved dd ? ;保留字段
IMAGE_RESOURCE_DATA_ENTRY ENDS
这里的第一个字段是一个资源数据的RVA,而不再是一个相对于资源快起始位置的偏移了。第二个字段说明资源数据的长度。
那么这里也可以根据以上信息写一个查看PE文件资源的小程序。因为资源的组织方式类似于树形结构,所以我们可以像数据结构中的深度优先搜索遍历一样来遍历输出资源的信息。
遍历整个资源结构可以用一个循环,循环次数为该目录的目录项数,然后循环中要有一个判断,如果目录项指向的下一个结构是IMAGE_RESOURCE_DIRECTORY,那么则递归调用这个循环函数进入下一层目录,然后在下一层目录中再循环。如果目录项指向的是资源数据入口结构,那么就输出资源的各种信息。
资源文件和框架用的都是之前博客里的,实现RVA转换文件偏移和查找数据所在的节的函数也是之前博客里的。功能实现代码如下:
.const
szMsg db '文件名: %s',0dh,0ah
db '----------------------------------------------------------',0dh,0ah
db '资源所处的节:%s',0dh,0ah,0
szErrNoRes db '这个文件中没有包含资源!',0
szLevel1 db 0dh,0ah
db '----------------------------------------------------------',0dh,0ah
db '资源类型:%s',0dh,0ah
db '----------------------------------------------------------',0dh,0ah,0
szLevel1byID db '%d (自定义类型)',0
szLevel2byID db ' ID: %d',0dh,0ah,0
szLevel2byName db ' Name: %s',0dh,0ah,0
szResData db '文件偏移:%08x (代码页=%04x,长度%d字节)',0dh,0ah,0
szType db '光标 ',0 ;1
db '位图 ',0 ;2
db '图标 ',0 ;3
db '菜单 ',0 ;4
db '对话框 ',0 ;5
db '字符串 ',0 ;6
db '字体目录 ',0 ;7
db '字体 ',0 ;8
db '加速键 ',0 ;9
db '未格式化资源',0 ;10
db '消息表 ',0 ;11
db '光标组 ',0 ;12
db '未知类型 ',0 ;13
db '图标组 ',0 ;14
db '未知类型 ',0 ;15
db '版本信息 ',0 ;16
.code
include _RvaToFileOffset.asm
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessRes proc _lpFile,_lpRes,_lpResDir,_dwLevel
local @dwNextLevel,@szBuffer[1024]:byte
local @szResName[256]:byte
pushad
mov eax,_dwLevel ;目录的层数
inc eax
mov @dwNextLevel,eax
;********************************************************************************************************************
;检查资源目录表,得到资源目录的数量
;********************************************************************************************************************
mov esi,_lpResDir
assume esi:ptr IMAGE_RESOURCE_DIRECTORY
mov cx,[esi].NumberOfNamedEntries ;获取以名称命名的入口数量
add cx,[esi].NumberOfIdEntries ;获取全部的入口数量
movzx ecx,cx
add esi,sizeof IMAGE_RESOURCE_DIRECTORY ;加上目录的大小到达第一个目录项的位置
assume esi:ptr IMAGE_RESOURCE_DIRECTORY_ENTRY
;*********************************************************************************************************************
;循环处理每个资源目录项
;*********************************************************************************************************************
.while ecx > 0
push ecx
mov ebx,[esi].OffsetToData ;获取指向下一层目录的指针(这个指针实际上是相对于资源块起始的偏移量)
.if ebx & 80000000h ;测试最高位是否是1,如果是1那么它指向第二层目录的IMAGE_RESOURCE_DIRECTORY结构,如果是0则它指向第三层目录的IMAGE_RESOURCE_DATA_ENTRY
and ebx,7fffffffh ;将最高位清0
add ebx,_lpRes ;这个偏移加上资源数据块的地址得到它指向的第二层目录的文件地址
.if _dwLevel == 1 ;如果当前目录是第一层
;*********************************************************************************************************************
;第一层:资源类型
;*********************************************************************************************************************
mov eax,[esi].Name1 ;获得该目录项的名字字符串结构的偏移或者ID(当最高位为1时该字段标识偏移,为0时表示ID)
.if eax & 80000000h ;如果最高位为1
and eax,7fffffffh ;将最高位清0
add eax,_lpRes ;得到名字字符串结构的地址
;**********************************************************************************************************************
;处理IMAGE_RESOURCE_DIR_STRING_U结构并将UNICODE转换成ANSI字符串
;**********************************************************************************************************************
movzx ecx,word ptr [eax] ;获取字符串的长度
add eax,2 ;将EAX指向字符串的位置
mov edx,eax
invoke WideCharToMultiByte,CP_ACP,\
WC_COMPOSITECHECK,edx,ecx,\
addr @szResName,sizeof @szResName,\
NULL,NULL
lea eax,@szResName ;将转换后的字符串的地址放入AX
.else
;***********************************************************************************************************************
;当资源类型为标准类型的时候查表得到资源的字符串说明
;***********************************************************************************************************************
.if eax <= 10h ;如果是预定义的资源类型
dec eax ;因为序号是从1开始,减一变成从0开始
mov ecx,sizeof szType
mul ecx
add eax,offset szType ;获得该资源类型对应的字符串位置的地址
.else ;否则就是自定义类型
invoke wsprintf,addr @szResName,\
addr szLevel1byID,eax
lea eax,@szResName
.endif
.endif
invoke wsprintf,addr @szBuffer,addr szLevel1,eax ;将资源类型的字符串说明写入缓冲区
;***************************************************************************************************************************
;第二层:资源ID(或名称)
;***************************************************************************************************************************
.elseif _dwLevel == 2
mov edx,[esi].Name1
.if edx & 80000000h
;***************************************************************************************************************************
;资源以字符串方式命名
;***************************************************************************************************************************
and edx,7fffffffh
add edx,_lpRes
movzx ecx,word ptr [edx]
add edx,2
invoke WideCharToMultiByte,CP_ACP,\ ;转换UNICODE码
WC_COMPOSITECHECK,edx,ecx,\
addr @szResName,sizeof @szResName,\
NULL,NULL
invoke wsprintf,addr @szBuffer,\
addr szLevel2byName,addr @szResName
.else
;***************************************************************************************************************************
;资源以ID命名
;***************************************************************************************************************************
invoke wsprintf,addr @szBuffer,\ ;将ID写入缓冲区
addr szLevel2byID,edx
.endif
.else
.break ;如果是第三层则跳出循环,函数回到上一层递归
.endif
invoke _AppendInfo,addr @szBuffer ;显示资源信息
invoke _ProcessRes,_lpFile,_lpRes,ebx,@dwNextLevel ;ebx现在指向下一层目录的IMAGE_RESOURCE_DIRECTORY结构
;***************************************************************************************************************************
;不是资源目录则显示资源详细信息(在第3层情况)
;***************************************************************************************************************************
.else
add ebx,_lpRes ;ebx现在指向资源数据
mov ecx,[esi].Name1 ;代码页(第三层目录)
assume ebx:ptr IMAGE_RESOURCE_DATA_ENTRY
mov eax,[ebx].OffsetToData ;获取指向资源数据的RVA
invoke _RVAToOffset,_lpFile,eax
invoke wsprintf,addr @szBuffer,addr szResData,\
eax,ecx,[ebx].Size1 ;显示资源数据的文件偏移代码页和长度
invoke _AppendInfo,addr @szBuffer
.endif
add esi,sizeof IMAGE_RESOURCE_DIRECTORY_ENTRY ;esi移动到下一个目录项的位置
pop ecx
dec ecx
.endw
;*************************************************************************************************************************
_Ret: assume esi:nothing
assume ebx:nothing
popad
ret
_ProcessRes endp
_ProcessPeFile proc _lpFile,_lpPeHead,_dwSize
local @szBuffer[1024]:byte,@szSectionName[16]:byte
pushad
mov esi,_lpPeHead
assume esi:ptr IMAGE_NT_HEADERS
;********************************************************************************************************************
;检测是否存在资源
;********************************************************************************************************************
mov eax,[esi].OptionalHeader.DataDirectory[8*2].VirtualAddress ;从数据目录中取得资源结构的RVA
.if ! eax
invoke MessageBox,hWinMain,addr szErrNoRes,NULL,MB_OK ;如果结构为空则输出提示信息
jmp _Ret
.endif
push eax
invoke _RVAToOffset,_lpFile,eax
add eax,_lpFile ;获取资源在文件中的地址
mov esi,eax
pop eax
invoke _GetRVASection,_lpFile,eax ;获取数据所在节区名字
invoke wsprintf,addr @szBuffer,addr szMsg,addr szFileName,eax
invoke SetWindowText,hWinEdit,addr @szBuffer
invoke _ProcessRes,_lpFile,esi,esi,1
;***************************************************************************************************************************
_Ret:
assume esi:nothing
popad
ret
_ProcessPeFile endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
代码编译链接出来后是这个样子的: