现在的位置: 首页 > 综合 > 正文

【原创】国庆PE总复习(五)

2018年04月06日 ⁄ 综合 ⁄ 共 29760字 ⁄ 字号 评论关闭

昨天给大家把《加密与解密》第三版上的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转换成实际的数据位置,函数如下所示,这里我用图示的方法,这样看起来会更加清楚一点!!
点击图片以查看大图图片名称:	图1.jpg查看次数:	526文件大小:	128.2 KB文件 ID :	48589
_GetRVASection查找RVA所在节区,函数如下所示,这里我同样用图示的方法,注释的都很详解了,大家只要对照上面的算法,看下面的图,就会很清楚!!
点击图片以查看大图图片名称:	图2.jpg查看次数:	524文件大小:	97.8 KB文件 ID :	48590

这两个函数很重要,在后面的应用中会经常用到,这里作为重点讲解,请在家一定要弄明白!!

在老罗的书上讲的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文件有所帮助!!

有点累了,先睡了,鼻子很不舒服,可能是感冒了,不断流鼻涕,不过还是写完了! 


我倒,本来我上面的图片很清楚的,不知道为什么传上去了,这么模糊!!算了,还是在下面帖出这两个函数的源代码吧!!
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 将 RVA 转换成实际的数据位置
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_RVAToOffset  proc  _lpFileHead,_dwRVA                  ;这个函数带两个参数,一个是映射头,还有一个就是RVA的值
    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              ;PE文件头加上PE文件头的大小,得到区块的首地址
    assume  edx:ptr IMAGE_SECTION_HEADER             ;定义EDX为区块
    movzx  ecx,[esi].FileHeader.NumberOfSections    ;以PE文件的区块个数作为循环条件,进行循环
;********************************************************************
; 扫描每个节区并判断 RVA 是否位于这个节区内
;********************************************************************
    .repeat                               
      mov  eax,[edx].VirtualAddress         ;VirtualAddress是区块的RVA地址,即表示这个区块的开头位置
      add  eax,[edx].SizeOfRawData     ;SizeofRawData是在文件对齐后的尺寸,即表示这个区块的结尾位置
      .if  (edi >= [edx].VirtualAddress) && (edi < eax)
        mov  eax,[edx].VirtualAddress ;VirtualAddress是区块的RVA地址,即表示这个区块的开头位置
        sub  edi,eax       ;EDI中存放的是这个RVA在内存中相对于起始RVA的偏移量
        mov  eax,[edx].PointerToRawData ;PointerToRawData表示文件中的偏移起始地址
        add  eax,edi       ;EAX存放的是RVA转换成的File Offset的值
        jmp  @F                       ;结束循环
      .endif
      add  edx,sizeof IMAGE_SECTION_HEADER  ;EDX指向下一个区块
    .untilcxz                                
    assume  edx:nothing                              
    assume  esi:nothing
    mov  eax,-1                                   ;如果不在区块之类,则返回-1
@@:
    mov  @dwReturn,eax                            ;将返回值传到@dwReturn中保存
    popad
    mov  eax,@dwReturn
    ret

_RVAToOffset  endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 查找 RVA 所在的节区
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_GetRVASection  proc  _lpFileHead,_dwRVA          ;这个函数带两个参数,一个是映射文件头,一个是RVA的值
    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              ;PE文件头加上PE文件头的大小,得到区块的首地址
    assume  edx:ptr IMAGE_SECTION_HEADER             ;定义EDX为区块
    movzx  ecx,[esi].FileHeader.NumberOfSections    ;以PE文件的区块个数作为循环条件,进行循环
;********************************************************************
; 扫描每个节区并判断 RVA 是否位于这个节区内
;********************************************************************
    .repeat
      mov  eax,[edx].VirtualAddress         ;VirtualAddress是区块的RVA地址,即表示这个区块的开头位置
      add  eax,[edx].SizeOfRawData     ;SizeofRawData是在文件对齐后的尺寸,即表示这个区块的结尾位置
      .if  (edi >= [edx].VirtualAddress) && (edi < eax)
        mov  eax,edx       ;EAX存储的区块的名字
        jmp  @F
      .endif
      add  edx,sizeof IMAGE_SECTION_HEADER  ;指向下一个区块
    .untilcxz
    assume  edx:nothing
    assume  esi:nothing
    mov  eax,offset szNotFound
@@:
    mov  @dwReturn,eax                            ;将区块的名字传给@dwReturn
    popad
    mov  eax,@dwReturn
    ret

_GetRVASection  endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 

抱歉!评论已关闭.