昨天给大家把《加密与解密》第三版上的PE工具源代码给大家分析了一下,感觉用VC去写PE工具还是不能很好的休现PE的文件内部结构,可能是封装了一些函数的吧,上一篇文章主是为那些没有学过Win32汇编的朋友们准备的,下面我开始重注讲解用Win32汇编进行PE工具的编程,你绝对会对PE文件工具有一个全新的认识,因为用汇编去写PE工具更能体现PE文件的内部组成结构,这是我个人的观点,不代表所有人!!
如果对汇编不是很熟悉的朋友,建设去看老罗的《Win32汇编语言程序设计》写的还不错!
这里我主要以老罗书的章节为引导给大家讲解一下PE工具的编写,如果大家还有什么不清楚的,请直接参考老罗的《Win32汇编语言程序设计》,里面讲的非常详细!!谢谢!
要讲解PE工具,首先我们要弄清的是RVA与File Offset的转换,在VC中微软给我们在IMAGEHLP.H中给我们封装了一个函数ImageRvaToVa,这个函数就实现了上面的功能,但汇编中我们必须自己去实现它们两者的转换。如果大家有不清楚RVA和File Offset的朋友请参考前面的几篇文章,里面已经将这两个讲的很清楚的,我这里只讲编程,理论就不讲了!
当处理PE文件时,任何的RVA必须经过到文件偏移的换算,才能用来定位并访问文件中的数据,但换算却无法用一个简单的公式来完成,我们可以通过下面的一个算法来实现:
循环扫描节表并得到每个节在内存中的起始RVA(根据VirtualAddress字段),并根据节的大小(SizeOfRawData字段)算出节的结束RVA,最后比较判断目标RVA是否落在某个节之内。
如果目标RVA处于某个节之内,那么用目标RVA减去节的起始RVA,这样就得到了目标RVA相对于节起始地址的偏移量RVA。
在节表中获取节在文件中所处的偏移(PointerToRawData字段),将这个偏移值加上上一步得到的RVA值,这才是数据在文件中的真正偏移位置。
这里将两个函数封装在一个_RvaToFileOffset.asm文件中,以便于以后使用方便,在这个文件中有两个函数,其中_RVAToOffset函数是将RVA转换成文件偏移,输入的参数是已经读取到内存中的文件头的地址和RVA值;_GetRVASection函数用来获取RVA所在的节的名称。
_RVAToOffset将RVA转换成实际的数据位置,函数如下所示,这里我用图示的方法,这样看起来会更加清楚一点!!
_GetRVASection查找RVA所在节区,函数如下所示,这里我同样用图示的方法,注释的都很详解了,大家只要对照上面的算法,看下面的图,就会很清楚!!
这两个函数很重要,在后面的应用中会经常用到,这里作为重点讲解,请在家一定要弄明白!!
在老罗的书上讲的PE分析工具使用的方法,基本上都是利用映射文件的方法将PE文件映射到内存中以供处理,处理使用的代码就是根据具体情况去编写,这里我先把映射文件的代码给大家
在老罗的书上还使用的SEH异常处理,代码如下,如有什么不懂的请参考《Win32汇编语言程序设计》第十四章中介绍的SEH来设置一个异常处理回调函数
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 错误 Handler
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Handler proc C _lpExceptionRecord,_lpSEH,_lpContext,_lpDispatcherContext
pushad
mov esi,_lpExceptionRecord
mov edi,_lpContext
assume esi:ptr EXCEPTION_RECORD,edi:ptr CONTEXT
mov eax,_lpSEH
push [eax + 0ch]
pop [edi].regEbp
push [eax + 8]
pop [edi].regEip
push eax
pop [edi].regEsp
assume esi:nothing,edi:nothing
popad
mov eax,ExceptionContinueExecution
ret
_Handler endp
文件映射到内存的函数如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_OpenFile proc
local @stOF:OPENFILENAME
local @hFile,@dwFileSize,@hMapFile,@lpMemory
invoke RtlZeroMemory,addr @stOF,sizeof @stOF ;将OPENFILENAME结构填充为0
mov @stOF.lStructSize,sizeof @stOF
push hWinMain
pop @stOF.hwndOwner
mov @stOF.lpstrFilter,offset szExtPe
mov @stOF.lpstrFile,offset szFileName
mov @stOF.nMaxFile,MAX_PATH
mov @stOF.Flags,OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST ;分别给OPENFILENAME结构赋值
invoke GetOpenFileName,addr @stOF ;调用通过对话框打开文件
.if ! eax
jmp @F ;如果打开失败则返回
.endif
;********************************************************************
; 打开文件并建立文件 Mapping
;********************************************************************
invoke CreateFile,addr szFileName,GENERIC_READ,FILE_SHARE_READ or \
FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
.if eax != INVALID_HANDLE_VALUE ;将文件映射到内存中
mov @hFile,eax
invoke GetFileSize,eax,NULL ;得到文件大小
mov @dwFileSize,eax
.if eax
invoke CreateFileMapping,@hFile,NULL,PAGE_READONLY,0,0,NULL ;创建文件映射
.if eax
mov @hMapFile,eax
invoke MapViewOfFile,eax,FILE_MAP_READ,0,0,0 ;映射文件
.if eax
mov @lpMemory,eax
;********************************************************************
; 创建用于错误处理的 SEH 结构
;********************************************************************
assume fs:nothing ;处理SEH异常
push ebp
push offset _ErrFormat
push offset _Handler
push fs:[0]
mov fs:[0],esp
;********************************************************************
; 检测 PE 文件是否有效
;********************************************************************
mov esi,@lpMemory
assume esi:ptr IMAGE_DOS_HEADER
.if [esi].e_magic != IMAGE_DOS_SIGNATURE ;判断DOS头是否为MZ
jmp _ErrFormat
.endif
add esi,[esi].e_lfanew ;定位到PE头
assume esi:ptr IMAGE_NT_HEADERS
.if [esi].Signature != IMAGE_NT_SIGNATURE ;判断PE头是否为PE
jmp _ErrFormat ;如果不是,跳到相应错误处理
.endif
invoke _ProcessPeFile,@lpMemory,esi,@dwFileSize ;根据情况处理内存映射中的文件
jmp _ErrorExit ;处理完结,处理相关结尾工作
_ErrFormat:
invoke MessageBox,hWinMain,addr szErrFormat,NULL,MB_OK
_ErrorExit:
pop fs:[0]
add esp,0ch
invoke UnmapViewOfFile,@lpMemory ;对应前面的映射
.endif
invoke CloseHandle,@hMapFile ;对应前面打开的映射文件
.endif
invoke CloseHandle,@hFile ;关闭文件打开的句柄
.endif
.endif
@@:
ret
_OpenFile endp
在老罗的书中的相关编程中都使用了上面的这个函数,唯一不同的就是调用_ProcessPeFile函数进行的操作不同!
上面简单对上面的这个函数时行分析,函数使用了SEH异常处理,一旦发生异常的话,则将程序转移到_ErrFormat标号处执行并认为文件的格式存在异常。由于PE文件的分析中涉及到很多指针操作,对任何一个指针都进行检测并判断它们是否已经越出了内存映射文件的范围是很麻烦的,使用SEH可以让这方面的工作开销的最少。
当一切准备结束之后,函数中简单的判断了一下打开的文件是否是一个PE文件,具体请参考上面的代码,算法其实很简单,ESI一开始被指向文件的头部,程序首先判断DOS文件头的标识符是否和"MZ"(也就是IMAGE_DOS_SIGNATURE)符合,如果符合的话,那么从003Ch处(也就是e_lfanew字段)取出PE文件头的偏移,并比较PE文件头的标识是否为IMAGE_NT_SIGNATURE,这两个步骤都通过的话,那么就可以认定这是一个合法的PE文件了,程序就真正开始分析工作了,调用_ProcessPeFile子程序进行分析。
得到区块信息
调用_ProcessPeFile得到区块的信息函数如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile proc _lpFile,_lpPeHead,_dwSize ;传入三个参数,文件,PE文件头,大小
local @szBuffer[1024]:byte,@szSectionName[16]:byte
pushad
mov edi,_lpPeHead
assume edi:ptr IMAGE_NT_HEADERS ;定义PE文件头
;********************************************************************
; 显示 PE 文件头中的一些信息
;********************************************************************
movzx ecx,[edi].FileHeader.Machine ;得到文件运行平台
movzx edx,[edi].FileHeader.NumberOfSections ;得到PE文件节区数量
movzx ebx,[edi].FileHeader.Characteristics ;得到PE文件的文件标记
invoke wsprintf,addr @szBuffer,addr szMsg,\ ;格式化输出
addr szFileName,ecx,edx,ebx,\
[edi].OptionalHeader.ImageBase
invoke SetWindowText,hWinEdit,addr @szBuffer
;********************************************************************
; 循环显示每个节区的信息
;********************************************************************
invoke _AppendInfo,addr szMsgSection
movzx ecx,[edi].FileHeader.NumberOfSections ;以区块数作为循环条件
add edi,sizeof IMAGE_NT_HEADERS ;PE文件头加上PE文件头的大小,定位到区块表
assume edi:ptr IMAGE_SECTION_HEADER
.repeat
push ecx
;********************************************************************
; 节区名称
;********************************************************************
invoke RtlZeroMemory,addr @szSectionName,sizeof @szSectionName
push esi
push edi
mov ecx,8 ;以8个字节为循环条件
mov esi,edi
lea edi,@szSectionName
cld ;设置方向传递方向
@@:
lodsb ;装载字符串
.if ! al ;判断字符串是否为空
mov al,' ' ;如果为空则赋为空
.endif
stosb ;字符串传递DS:ESI---->ES:EDI中
loop @B ;循环
pop edi
pop esi
;********************************************************************
invoke wsprintf,addr @szBuffer,addr szFmtSection,\ ;格式化输出
addr @szSectionName,[edi].Misc.VirtualSize,\
[edi].VirtualAddress,[edi].SizeOfRawData,\
[edi].PointerToRawData,[edi].Characteristics
invoke _AppendInfo,addr @szBuffer ;输出格式化信息
add edi,sizeof IMAGE_SECTION_HEADER ;定位到下一个区块
;********************************************************************
pop ecx
.untilcxz
assume edi:nothing
popad
ret
_ProcessPeFile endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
这里我就不过多的解释了,相信我上面的注释的很详细了,上面的注释我只是根据自己的理解来标注的,如果有什么失误的地方,请各位大侠们指出,谢谢!
导出PE文件中的输入表
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile proc _lpFile,_lpPeHead,_dwSize ;传入三个参数,文件,PE文件头,大小
local @szBuffer[1024]:byte,@szSectionName[16]:byte
pushad
mov edi,_lpPeHead
assume edi:ptr IMAGE_NT_HEADERS ;定义PE文件头
;********************************************************************
mov eax,[edi].OptionalHeader.DataDirectory[8].VirtualAddress ;从PE文件头中定位到导入表的位置
.if ! eax
invoke MessageBox,hWinMain,addr szErrNoImport,NULL,MB_OK
jmp _Ret
.endif
invoke _RVAToOffset,_lpFile,eax ;将导入表的RVA转换为File Offset地址
add eax,_lpFile
mov edi,eax
assume edi:ptr IMAGE_IMPORT_DESCRIPTOR ;定义导入表
;********************************************************************
; 显示 PE 文件名
;********************************************************************
invoke _GetRVASection,_lpFile,[edi].OriginalFirstThunk ;调用前面的函数得到导入表所处的节的名称
invoke wsprintf,addr @szBuffer,addr szMsg,addr szFileName,eax
invoke SetWindowText,hWinEdit,addr @szBuffer
;********************************************************************
; 循环处理 IMAGE_IMPORT_DESCRIPTOR 直到遇到全零的则结束
;********************************************************************
.while [edi].OriginalFirstThunk || [edi].TimeDateStamp || \
[edi].ForwarderChain || [edi].Name1 || [edi].FirstThunk
invoke _RVAToOffset,_lpFile,[edi].Name1 ;将导入库的Name字段RVA转换为File Offset地址
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 列表地址 ---> ebx
;********************************************************************
.if [edi].OriginalFirstThunk ;判断OriginalFirstThunk是否为0
mov eax,[edi].OriginalFirstThunk ;如果不为0,则以OriginalFirstThunk定位
.else
mov eax,[edi].FirstThunk ;如果为0,则以FirsThunk定位
.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 ;取出双字的低位就是函数的序号
invoke wsprintf,addr @szBuffer,addr szMsgOrdinal,eax
.else
;********************************************************************
; 按函数名导入
;********************************************************************
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
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
上面的注释,已经讲的很详细,我就不多此一举了!!!请大家对着前面的理论慢慢研究,其实很简单的!!
导出PE文件中的输出表
输出表的处理函数中会用到两个算法,在这里给大家说明一下:
从序号查找入口地址
Windows装载器查找导出函数入口地址的过程,如果已知函数的导出序号,如何得到入口地址呢?
步骤如下所示:
定位到PE文件头。
从PE文件头中的IMAGE_OPTIONAL_HEADER32结构中取出数据目录表,并从第一个数据目录中得到导出表的地址。
从导出表的nBase字段得到起始序号。
将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引。
检测索引值是否大于导出表的NumberOfFunctions字段的值,如果大于后者的话,说明输入的序号是无效的。
用这个索引值在AddressOfFunctions字段指向的导出函数入口地址表中取出相应的项目,这就是函数的入口地址RVA值,当函数被装入内存的时候,这个RVA值加上模块实际装入的基址,就得到了函数真正的入口地址。
从函数名称查找入口地址
最初的步骤是一样的,就是首先得到导出表的地址。
从导出表的NumberOfNames字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环。
从AddressOfNames字段指向的函数名称地址表的第一项开始,在循环中将第一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数。
如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在AddressOfNameOrdinals指向的数组中以同样的索引值取出数组项的值,暂且假定这个值为X。
最后,以X值作为索引的值,在AddressOfFunctions字段指向的函数入口地址表中获取的RVA就是函数的入口地址。同样当函数被装入内存的时候,这个RVA值加上模块实际装入的基址,就得到了函数的真正的入口地址。
从函数名称查找入口地址的代码在病毒中经常见到,因为病毒是作为一段额外的代码被附加到可执行文件中,如果病毒代码中用到了某些的API的话,这些API的地址不可能在宿住文件导入表中为病毒代码准备,只能通过在内存中动态查找的办法来实现。
_ProcessPeFile函数的具体实现如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile proc _lpFile,_lpPeHead,_dwSize ;传入三个参数,文件,PE头,大小
local @szBuffer[1024]:byte,@szSectionName[16]:byte
local @dwIndex,@lpAddressOfNames,@lpAddressOfNameOrdinals
pushad
mov esi,_lpPeHead
assume esi:ptr IMAGE_NT_HEADERS ;定义PE头
;********************************************************************
; 从数据目录中获取导出表的位置
;********************************************************************
mov eax,[esi].OptionalHeader.DataDirectory.VirtualAddress ;从PE头的结构中定位到导出表的位置
.if ! eax
invoke MessageBox,hWinMain,addr szErrNoExport,NULL,MB_OK
jmp _Ret
.endif
invoke _RVAToOffset,_lpFile,eax ;调用前面两个函数,将文件RVA地址转换为File Offset地址
add eax,_lpFile
mov edi,eax
;********************************************************************
; 显示一些常用的信息
;********************************************************************
assume edi:ptr IMAGE_EXPORT_DIRECTORY
invoke _RVAToOffset,_lpFile,[edi].nName ; 将导出输表的RVA转换为File Offset
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 ;将函数名地址表的RVA转换为File Offset
add eax,_lpFile
mov @lpAddressOfNames,eax
invoke _RVAToOffset,_lpFile,[edi].AddressOfNameOrdinals ;将函数名序号表的RVA转换为File Offset
add eax,_lpFile
mov @lpAddressOfNameOrdinals,eax
invoke _RVAToOffset,_lpFile,[edi].AddressOfFunctions ;将导出函数地址表的RVA转换为File Offset
add eax,_lpFile
mov esi,eax ;ESI中存放函数地址表
;********************************************************************
; 循环显示导出函数的信息
;********************************************************************
mov ecx,[edi].NumberOfFunctions ;以导出函数的总数为循环
mov @dwIndex,0 ;索引
@@:
pushad
;********************************************************************
; 在按名称导出的索引表中
;********************************************************************
mov eax,@dwIndex
push edi
mov ecx,[edi].NumberOfNames ;以名称导出的函数总数为循环条件
cld ;设置方向位
mov edi,@lpAddressOfNameOrdinals
repnz scasw ;字符串查找,看有没有符合的函数名
.if ZERO? ;找到函数名称
sub edi,@lpAddressOfNameOrdinals ;由于AddressOfNameOrdinals指定的数组是WORD类型的,所以查找
sub edi,2 ;指令用的是SCASW而不是SCASB,当查找结束后,如果标志位为0则表示查找成功,这时
shl edi,1 ;EDI的值指向找到的项目后面一个WORD位置,将EDI去数组的基址并减去2(一个WORD的长度),
;得到的就是找到的项目的位置偏移。由于这个数组是WORD类型的,而AddressOfNames指向
add edi,@lpAddressOfNames ;的数组是DWORD类型的,所以还要将偏移乘以2来修正,用修正后的偏移在AddressOfNames
invoke _RVAToOffset,_lpFile,dword ptr [edi] ;表中就可以得到指向函数名称字符串的RVA了。
add eax,_lpFile
.else
mov eax,offset szExportByOrd
.endif
pop edi
;********************************************************************
; 序号 --> ecx
;********************************************************************
mov ecx,@dwIndex
add ecx,[edi].nBase ;用函数在入口表的索引加上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
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
上面的注释已经讲解的很详细了,如果还有什么不懂,请参考相关资料!
得到PE文件的资源
在书中,笔者用了一个单独的函数来处理资源,并用_ProcessPeFile来调用
先来看看_ProcessPeFile这个函数:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_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 ;根据PE文件头结构定位到资源
.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
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
这个函数很简音,没什么好说的,只是在函数中调用了一个处理资源的函数_ProcessRes,我们下面来讲解一下这个函数。
_ProcessRes函数如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessRes proc _lpFile,_lpRes,_lpResDir,_dwLevel ;传入四个参数,文件,资源,资源目录,目录的层次
local @dwNextLevel,@szBuffer[1024]:byte
local @szResName[256]:byte
pushad
mov eax,_dwLevel ;传一个数值过来,为1,表示前当所处的目录的层次为1
inc eax ;将值自加1
mov @dwNextLevel,eax ;将自加1的值存储在局部变量@dwNextLevel中,作为递归调用时的_dwLevel参数使用
;********************************************************************
; 检查资源目录表,得到资源目录项的数量
;********************************************************************
mov esi,_lpResDir
assume esi:ptr IMAGE_RESOURCE_DIRECTORY
mov cx,[esi].NumberOfNamedEntries ;得到以名称命名的入口数量
add cx,[esi].NumberOfIdEntries ;以ID命名的入口数量加上以名称命名的入口数量,得到本目录的目录项总和
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 ;OffsetToData字段的位31是否为1
and ebx,7fffffffh ;取出低位,低位数据指向下一层目录块的起始地址
add ebx,_lpRes ;指向下一层资源,作为递归函数的参数
.if _dwLevel == 1
;********************************************************************
; 第一层:资源类型
;********************************************************************
mov eax,[esi].Name1 ;取得资源目录的名称或者ID
.if eax & 80000000h ;判断Name1的最高位是为1,还是为0
and eax,7fffffffh ;如果最高位为1,则低位数据当指针使用,指向下面的结构
add eax,_lpRes
movzx ecx,word ptr [eax] ;IMAGE_RESOURCE_DIR_STRING_U结构
add eax,2
mov edx,eax
invoke WideCharToMultiByte,CP_ACP,WC_COMPOSITECHECK,\ ;字符串转换
edx,ecx,addr @szResName,sizeof @szResName,\
NULL,NULL
lea eax,@szResName
.else
.if eax <= 10h ;如果最高位为0,则表示字段的值作为ID使用
dec eax ;如果ID在1到16之间,表示是系统预定义的类型
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 ;IMAGE_RESOURCE_DIR_STRING_U结构
movzx ecx,word ptr [edx]
add edx,2
invoke WideCharToMultiByte,CP_ACP,WC_COMPOSITECHECK,\
edx,ecx,addr @szResName,sizeof @szResName,\
NULL,NULL
invoke wsprintf,addr @szBuffer,\
addr szLevel2byName,addr @szResName
.else
;********************************************************************
; 资源以 ID 命名
;********************************************************************
invoke wsprintf,addr @szBuffer,\
addr szLevel2byID,edx
.endif
.else
.break
.endif
invoke _AppendInfo,addr @szBuffer
invoke _ProcessRes,_lpFile,_lpRes,ebx,@dwNextLevel ;递归处理资源
;********************************************************************
; 不是资源目录则显示资源详细信息
;********************************************************************
.else
add ebx,_lpRes
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 ;指向下一层资源项
pop ecx
dec ecx ;目录下减一
.endw
_Ret:
assume esi:nothing
assume ebx:nothing
popad
ret
_ProcessRes endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
上面的算法,可以表示为如下所示:
if OffsetToData字段的位31=1
(表明OffsetToData字段指向的是下一层的目录块)
.if 当前是第1层
(表明Name1字段代表的是资源类型)
.if Name1字段的位31=1
Name1指向的是一个UNICODE字符串
.else
Name1中包含的是资源类型ID
.endif
.elseif 当前是第2层
(表明Name1字段代表的是资源名称)
.if Name1字段的位31=1
Name1指向的是一个UNICODE字符串
.else
Name1中包含的是资源名称ID
.endif
.endif
将层次加1继续递归处理OffsetToData所指的下一层目录块
.else
(表明OffsetToData字段指向的是IMAGE_RESOURCE_DATA_ENTRY结构)
(表明Name1字段代表的是资源的代码页)
IMAGE_RESOURCE_DATA_ENTRY结构地址=OffsetToData字段
资源RVA=IMAGE_RESOURCE_DATA_ENTRY.OffsetToData
资源大小=IMAGE_RESOURCE_DATA_ENTRY.Size1
.endif
上面的这个算法,很清楚的明达了函数所进行的操作,不得不说算法是程序的灵魂!!
这里说明一点,代码在每次处理一个目录项或者资源数据的时候,都将它们的名称或ID等信息显示出来。如果例子中的代码被移植到了其他地方用来寻找资源的话,这些显示信息的语句就可以全部去掉了,因为这时程序的最终目的就是最后两句获取资源RVA和大小的指令。
得到PE文件的重定位表
重定位所使用的一个_ProcessPeFile来处理,我们来看看这个函数是怎么样实现的:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile proc _lpFile,_lpPeHead,_dwSize ;传入三个参数,文件,PE文件头,大小
local @szBuffer[1024]:byte,@szSectionName[16]:byte
pushad
mov esi,_lpPeHead
assume esi:ptr IMAGE_NT_HEADERS ;定义PE文件头
;********************************************************************
; 根据 IMAGE_DIRECTORY_ENTRY_BASERELOC 目录表找到重定位表位置
;********************************************************************
mov eax,[esi].OptionalHeader.DataDirectory[8*5].VirtualAddress ;根据PE文件头定位到重定位表
.if ! eax
invoke MessageBox,hWinMain,addr szErrNoReloc,NULL,MB_OK
jmp _Ret
.endif
push eax
invoke _RVAToOffset,_lpFile,eax ;将重定位表的RVA转换为File Offset
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
assume esi:ptr IMAGE_BASE_RELOCATION
;********************************************************************
; 循环处理每个重定位块
;********************************************************************
.while [esi].VirtualAddress ;以重定位表在内存中的起始RVA为循环条件
cld ;设置方向标志DF
lodsd ;eax = [esi].VirtualAddress 重定位内存页的起始RVA
mov ebx,eax
lodsd ;eax = [esi].SizeOfBlock 重定位块的长度
sub eax,sizeof IMAGE_BASE_RELOCATION
shr eax,1 ;重定位项的数量n就等于(SizeOfBlock-sizeof IMAGE_BASE_RELOCATION)/2
push eax ;eax = 重定位项数量
invoke wsprintf,addr @szBuffer,addr szMsgRelocBlk,ebx,eax
invoke _AppendInfo,addr @szBuffer
pop ecx
xor edi,edi
.repeat
push ecx
lodsw
mov cx,ax
and cx,0f000h ;取重定位项的高四位得到重定位项的类型
;********************************************************************
; 仅处理 IMAGE_REL_BASED_HIGHLOW 类型的重定位项
;********************************************************************
.if cx == 03000h
and ax,0fffh ;取重定位项的低十二位得到重定位项的地址
movzx eax,ax
add eax,ebx ;低十二位加上前面得到的重定位内存页的RVA(虚拟地址)
.else
mov eax,-1
.endif
invoke wsprintf,addr @szBuffer,addr szMsgReloc,eax ;格式化输出
inc edi ;EDI自增1
.if edi == 4 ;每显示4个项目换行
invoke lstrcat,addr @szBuffer,addr szCrLf
xor edi,edi
.endif
invoke _AppendInfo,addr @szBuffer
pop ecx
.untilcxz
.if edi
invoke _AppendInfo,addr szCrLf
.endif
.endw
_Ret:
assume esi:nothing
popad
ret
_ProcessPeFile endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
参考资料《Win32汇编语言程序设计》老罗的第二版!
上面的注释已经讲的很清楚了,我就不重复了,如果大家真的把前面的几篇文章弄懂了,我想看上面的代码并不困难,就是将前面几章的内容进行编程实现,然后按规定的格式显示出来了,PE文件的复习就基本上就完了,在接下来的两天里,我会讲解几个PE文件的编程的实际应用的例子与代码,希望对大家理解PE文件有所帮助!!
有点累了,先睡了,鼻子很不舒服,可能是感冒了,不断流鼻涕
,不过还是写完了!
上传的附件: