软件破解入门教程和解密手册——PE文件格式

时间:2021-07-26 18:01:10
 

第8章 压缩与脱壳

第一节 PE文件格式

教程1 : PE 文件格式一览

考虑到早期写的PE教程1是自己所有教程中最糟糕的一篇,此番决心彻底重写一篇以飨读者。

PE 的意思就是 Portable Executable(可移植的执行体)。它是 Win32环境自身所带的执行体文件格式。它的一些特性继承自 Unix Coff (common object file format)文件格式。"portable executable"(可移植的执行体)意味着此文件格式是跨win32平台的 : 即使Windows运行在非IntelCPU上,任何win32平台的PE装载器都能识别和使用该文件格式。当然,移植到不同的CPUPE执行体必然得有一些改变。所有 win32执行体 (除了VxD16位的Dll)都使用PE文件格式,包括NT的内核模式驱动程序(kernel mode drivers)。因而研究PE文件格式给了我们洞悉Windows结构的良机。

本教程就让我们浏览一下 PE文件格式的概要。

DOS MZ header
DOS stub
PE header
Section table
Section 1
Section 2
Section ...
Section n

上图是 PE文件结构的总体层次分布。所有 PE文件(甚至32位的 DLLs) 必须以一个简单的 DOS MZ header 开始。我们通常对此结构没有太大兴趣。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随 MZ header 之后的 DOS stubDOS stub实际上是个有效的 EXE,在不支持 PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串 "This program requires Windows" 或者程序员可根据自己的意图实现完整的DOS代码。通常我们也不对 DOS stub 太感兴趣: 因为大多数情况下它是由汇编器/编译器自动生成。通常,它简单调用中断21h服务9来显示字符串"This program cannot run in DOS mode"

紧接着 DOS stub 的是PE header PE header PE相关结构 IMAGE_NT_HEADERS 的简称,其中包含了许多PE装载器用到的重要域。当我们更加深入研究PE文件格式后,将对这些重要域耳目能详。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从 DOS MZ header 中找到 PE header 的起始偏移量。因而跳过了 DOS stub 直接定位到真正的文件头 PE header

PE文件的真正内容划分成块,称之为sections(节)。每节是一块拥有共同属性的数据,比如代码/数据、读/写等。我们可以把PE文件想象成一逻辑磁盘,PE header 是磁盘的boot扇区,而sections就是各种文件,每种文件自然就有不同属性如只读、系统、隐藏、文档等等。 值得我们注意的是 ---- 节的划分是基于各组数据的共同属性: 而不是逻辑概念。重要的不是数据/代码是如何使用的,如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。不必关心节中类似于"data", "code"或其他的逻辑概念: 如果数据和代码拥有相同属性,它们就可以被归入同一个节中。(译者注:节名称仅仅是个区别不同节的符号而已,类似"data", "code"的命名只为了便于识别,惟有节的属性设置决定了节的特性和功能)如果某块数据想付为只读属性,就可以将该块数据放入置为只读的节中,当PE装载器映射节内容时,它会检查相关节属性并置对应内存块为指定属性。

如果我们将PE文件格式视为一逻辑磁盘,PE headerboot扇区而sections是各种文件,但我们仍缺乏足够信息来定位磁盘上的不同文件,譬如,什么是PE文件格式中等价于目录的东东?别急,那就是 PE header 接下来的数组结构section table(节表)。 每个结构包含对应节的属性、文件偏移量、虚拟偏移量等。如果PE文件里有5个节,那么此结构数组内就有5个成员。因此,我们便可以把节表视为逻辑磁盘中的根目录,每个数组成员等价于根目录中目录项。

以上就是PE文件格式的物理分布,下面将总结一下装载一PE文件的主要步骤:

  1. PE文件被执行,PE装载器检查 DOS MZ header 里的 PE header 偏移量。如果找到,则跳转到 PE header
  2. PE装载器检查 PE header 的有效性。如果有效,就跳转到PE header的尾部。
  3. 紧跟 PE header 的是节表。PE装载器读取其中的节信息,并采用文件映射方法将这些节映射到内存,同时付上节表里指定的节属性。
  4. PE文件映射入内存后,PE装载器将处理PE文件中类似 import table(引入表)逻辑部分。

上述步骤是基于本人观察后的简述,显然还有一些不够精确的地方,但基本明晰了执行体被处理的过程。

你应该下载 LUEVELSMEYER的《PE文件格式》 该文的描述相当详细,可用作案头的参考手册。

PE教程2: 检验PE文件的有效性

本教程中我们将学习如何检测给定文件是一有效PE文件。
下载 
范例

理论:

如何才能校验指定文件是否为一有效PE文件呢? 这个问题很难回答,完全取决于想要的精准程度。您可以检验PE文件格式里的各个数据结构,或者仅校验一些关键数据结构。大多数情况下,没有必要校验文件里的每一个数据结构,只要一些关键数据结构有效,我们就认为是有效的PE文件了。下面我们就来实现前面的假设。

我们要验证的重要数据结构就是 PE header。从编程角度看,PE header 实际就是一个 IMAGE_NT_HEADERS 结构。定义如下:

IMAGE_NT_HEADERS STRUCT 
   Signature dd ? 
   FileHeader IMAGE_FILE_HEADER <> 
   OptionalHeader IMAGE_OPTIONAL_HEADER32 <> 
IMAGE_NT_HEADERS ENDS

Signature dword类型,值为50h, 45h, 00h, 00hPE\0\0)。 本域为PE标记,我们可以此识别给定文件是否为有效PE文件。
FileHeader 
该结构域包含了关于PE文件物理分布的信息, 比如节数目、文件执行机器等。
OptionalHeader 
该结构域包含了关于PE文件逻辑分布的信息,虽然域名有"可选"字样,但实际上本结构总是存在的。

我们目的很明确。如果IMAGE_NT_HEADERSsignature域值等于"PE\0\0",那么就是有效的PE文件。实际上,为了比较方便,Microsoft已定义了常量IMAGE_NT_SIGNATURE供我们使用。

IMAGE_DOS_SIGNATURE equ 5A4Dh 
IMAGE_OS2_SIGNATURE equ 454Eh 
IMAGE_OS2_SIGNATURE_LE equ 454Ch 
IMAGE_VXD_SIGNATURE equ 454Ch 
IMAGE_NT_SIGNATURE equ 4550h

接下来的问题是: 如何定位 PE header? 答案很简单: DOS MZ header 已经包含了指向 PE header 的文件偏移量。DOS MZ header 又定义成结构IMAGE_DOS_HEADER 。查询windows.inc,我们知道IMAGE_DOS_HEADER 结构的e_lfanew成员就是指向 PE header 的文件偏移量。

现在将所有步骤总结如下:

  1. 首先检验文件头部第一个字的值是否等于 IMAGE_DOS_SIGNATURE是则 DOS MZ header 有效。
  2. 一旦证明文件的 DOS header 有效后,就可用e_lfanew来定位 PE header 了。
  3. 比较 PE header 的第一个字的值是否等于IMAGE_NT_HEADER。如果前后两个值都匹配,那我们就认为该文件是一个有效的PE文件。

Example:

.386 
.model flat,stdcall 
option casemap:none 
include \masm32\include\windows.inc 
include \masm32\include\kernel32.inc 
include \masm32\include\comdlg32.inc 
include \masm32\include\user32.inc 
includelib \masm32\lib\user32.lib 
includelib \masm32\lib\kernel32.lib 
includelib \masm32\lib\comdlg32.lib 

SEH struct 
PrevLink dd ?    ; the address of the previous seh structure 
CurrentHandler dd ?    ; the address of the exception handler 
SafeOffset dd ?    ; The offset where it's safe to continue execution 
PrevEsp dd ?      ; the old value in esp 
PrevEbp dd ?     ; The old value in ebp 
SEH ends

.data 
AppName db "PE tutorial no.2",0 
ofn OPENFILENAME <> 
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0 
                 db "All Files",0,"*.*",0,0 
FileOpenError db "Cannot open the file for reading",0 
FileOpenMappingError db "Cannot open the file for memory mapping",0 
FileMappingError db "Cannot map the file into memory",0 
FileValidPE db "This file is a valid PE",0 
FileInValidPE db "This file is not a valid PE",0 

.data? 
buffer db 512 dup(?) 
hFile dd ? 
hMapping dd ? 
pMapping dd ? 
ValidPE dd ? 

.code 
start proc 
LOCAL seh:SEH 
mov ofn.lStructSize,SIZEOF ofn 
mov ofn.lpstrFilter, OFFSET FilterString 
mov ofn.lpstrFile, OFFSET buffer 
mov ofn.nMaxFile,512 
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY 
invoke GetOpenFileName, ADDR ofn 
.if eax==TRUE 
    invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL 
    .if eax!=INVALID_HANDLE_VALUE 
       mov hFile, eax 
       invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0 
       .if eax!=NULL 
          mov hMapping, eax 
          invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0 
          .if eax!=NULL 
             mov pMapping,eax 
             assume fs:nothing 
             push fs:[0] 
             pop seh.PrevLink 
             mov seh.CurrentHandler,offset SEHHandler 
             mov seh.SafeOffset,offset FinalExit 
             lea eax,seh 
             mov fs:[0], eax 
             mov seh.PrevEsp,esp 
             mov seh.PrevEbp,ebp 
             mov edi, pMapping 
             assume edi:ptr IMAGE_DOS_HEADER 
             .if [edi].e_magic==IMAGE_DOS_SIGNATURE 
                add edi, [edi].e_lfanew 
                assume edi:ptr IMAGE_NT_HEADERS 
                .if [edi].Signature==IMAGE_NT_SIGNATURE 
                   mov ValidPE, TRUE 
                .else 
                   mov ValidPE, FALSE 
                .endif 
             .else 
                 mov ValidPE,FALSE 
             .endif 
FinalExit: 
             .if ValidPE==TRUE 
                 invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION 
             .else 
                invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
             .endif 
             push seh.PrevLink 
             pop fs:[0] 
             invoke UnmapViewOfFile, pMapping 
          .else 
             invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR 
          .endif 
          invoke CloseHandle,hMapping 
       .else 
          invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR 
       .endif 
       invoke CloseHandle, hFile 
    .else 
       invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR 
    .endif 
.endif 
invoke ExitProcess, 0 
start endp 

SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD 
    mov edx,pFrame 
    assume edx:ptr SEH 
    mov eax,pContext 
    assume eax:ptr CONTEXT 
    push [edx].SafeOffset 
    pop [eax].regEip 
    push [edx].PrevEsp 
    pop [eax].regEsp 
    push [edx].PrevEbp 
    pop [eax].regEbp 
    mov ValidPE, FALSE 
    mov eax,ExceptionContinueExecution 
    ret 
SEHHandler endp 
end start

分析:

本例程打开一文件,先检验DOS header是否有效,有效就接着检验PE header的有效性,ok就认为是有效的PE文件了。这里,我们还运用了结构异常处理(SEH),这样就不必检查每个可能的错误: 如果有错误出现,就认为PE检测失效所致,于是给出我们的报错信息。其实Windows内部普遍使用SEH来检验参数传递的有效性。若对SEH感兴趣的话,可阅读Jeremy Gordon 文章

程序调用打开文件通用对话框,用户选定执行文件后,程序便打开文件并映射到内存。并在有效性检验前建立一 SEH:

   assume fs:nothing 
   push fs:[0] 
   pop seh.PrevLink 
   mov seh.CurrentHandler,offset SEHHandler 
   mov seh.SafeOffset,offset FinalExit 
   lea eax,seh 
   mov fs:[0], eax 
   mov seh.PrevEsp,esp 
   mov seh.PrevEbp,ebp

一开始就假设寄存器 fs为空(assume fs:nothing)。 记住这一步不能省却,因为MASM假设fs寄存器为ERROR。接下来保存 Windows使用的旧SEH处理函数地址到我们自己定义的结构中,同时保存我们的SEH处理函数地址和异常处理时的执行恢复地址,这样一旦错误发生就能由异常处理函数安全地恢复执行了。同时还保存当前espebp的值,以便我们的SEH处理函数将堆栈恢复到正常状态。

   mov edi, pMapping 
   assume edi:ptr IMAGE_DOS_HEADER 
   .if [edi].e_magic==IMAGE_DOS_SIGNATURE

成功建立SEH后继续校验工作。置目标文件的首字节地址给edi,使其指向DOS header的首字节。为便于比较,我们告诉编译器可以假定edi正指向IMAGE_DOS_HEADER结构(事实亦是如此)。然后比较DOS header的首字是否等于字符串"MZ",这里利用了windows.inc中定义的IMAGE_DOS_SIGNATURE常量。若比较成功,继续转到PE header,否则设ValidPE 值为FALSE,意味着文件不是有效PE文件。

      add edi, [edi].e_lfanew 
      assume edi:ptr IMAGE_NT_HEADERS 
      .if [edi].Signature==IMAGE_NT_SIGNATURE 
         mov ValidPE, TRUE 
      .else 
         mov ValidPE, FALSE 
      .endif

要定位到PE header,需要读取DOS header中的e_lfanew域值。该域含有PE header在文件中相对文件首部的偏移量。edi加上该值正好定位到PE header的首字节。这儿可能会出错,如果文件不是PE文件,e_lfanew值就不正确,加上该值作为指针就可能导致异常。若不用SEH,我们必须校验e_lfanew值是否超出文件尺寸,这不是一个好办法。如果一切OK,我们就比较PE header的首字是否是字符串"PE"。这里在此用到了常量IMAGE_NT_SIGNATURE,相等则认为是有效的PE文件。
如果e_lfanew的值不正确导致异常,我们的SEH处理函数就得到执行控制权,简单恢复堆栈指针和基栈指针后,就根据safeoffset的值恢复执行到FinalExit标签处。

FinalExit: 
   .if ValidPE==TRUE 
      invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION 
   .else 
      invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION 
   .endif

上述代码简单明确,根据ValidPE的值显示相应信息。

   push seh.PrevLink 
   pop fs:[0]

一旦SEH不再使用,必须从SEH链上断开。

翻译:iamgufeng [Iczelion's Win32 Assembly Homepage][LuoYunBin's Win32 ASM Page]

PE教程3: File Header (文件头)

本课我们将要研究 PE header  file header(文件头)部分。

至此,我们已经学到了哪些东东,先简要回顾一下:

  • DOS MZ header 又命名为IMAGE_DOS_HEADER.。其中只有两个域比较重要: e_magic 包含字符串"MZ"e_lfanew 包含PE header在文件中的偏移量。
  • 比较e_magic 是否为IMAGE_DOS_SIGNATURE以验证是否是有效的DOS header。比对符合则认为文件拥有一个有效的DOS header
  • 为了定位PE header,移动文件指针到e_lfanew所指向的偏移。
  • PE header的第一个双字包含字符串"PE\0\0"。该双字与IMAGE_NT_SIGNATURE比对,符合则认为PE header有效。

本课我们继续探讨关于 PE header 的知识。 PE header 的正式命名是 IMAGE_NT_HEADERS。再来回忆一下这个结构。

IMAGE_NT_HEADERS STRUCT 
    Signature dd ? 
    FileHeader IMAGE_FILE_HEADER <> 
    OptionalHeader IMAGE_OPTIONAL_HEADER32 <> 
IMAGE_NT_HEADERS ENDS

Signature PE标记,值为50h, 45h, 00h, 00hPE\0\0)。 
FileHeader 该结构域包含了关于PE文件物理分布的一般信息。
OptionalHeader 该结构域包含了关于PE文件逻辑分布的信息。

最有趣的东东在 OptionalHeader 里。不过,FileHeader 里的一些域也很重要。本课我们将学习FileHeader下一课研究OptionalHeader

IMAGE_FILE_HEADER STRUCT 
    Machine WORD ? 
    NumberOfSections WORD ? 
    TimeDateStamp dd ? 
    PointerToSymbolTable dd ? 
    NumberOfSymbols dd ? 
    SizeOfOptionalHeader WORD ? 
    Characteristics WORD ? 
IMAGE_FILE_HEADER ENDS

Field name Meanings
Machine 该文件运行所要求的CPU。对于Intel平台,该值是IMAGE_FILE_MACHINE_I386(14Ch)。我们尝试了LUEVELSMEYERpe.txt声明的14Dh14Eh,但Windows不能正确执行。看起来,除了禁止程序执行之外,本域对我们来说用处不大。
NumberOfSections 文件的节数目。如果我们要在文件中增加或删除一个节,就需要修改这个值。
TimeDateStamp 文件创建日期和时间。我们不感兴趣。
PointerToSymbolTable 用于调试。
NumberOfSymbols 用于调试。
SizeOfOptionalHeader 指示紧随本结构之后的 OptionalHeader 结构大小,必须为有效值。
Characteristics 关于文件信息的标记,比如文件是exe还是dll

简言之,只有三个域对我们有一些用: Machine, NumberOfSections  Characteristics。通常不会改变Machine Characteristics 的值,但如果要遍历节表就得使用 NumberOfSections
为了更好阐述 NumberOfSections 的用处,这里简要介绍一下节表。

节表是一个结构数组,每个结构包含一个节的信息。因此若有3个节,数组就有3个成员。 我们需要NumberOfSections值来了解该数组中到底有几个成员。 也许您会想检测结构中的全0成员起到同样效果。Windows确实采用了这种方法。为了证明这一点,可以增加NumberOfSections的值,Windows仍然可以正常执行文件。据我们的观察,Windows读取NumberOfSections的值然后检查节表里的每个结构,如果找到一个全0结构就结束搜索,否则一直处理完NumberOfSections指定数目的结构。 为什么我们不能忽略NumberOfSections的值?有几个原因。PE说明中没有指定节表必须以全0结构结束。Thus there may be a situation where the last array member is contiguous to the first section, without empty space at all. Another reason has to do with bound imports. The new-style binding puts the information immediately following the section table's last structure array member. 因此您仍然需要NumberOfSections

翻译:iamgufeng [Iczelion's Win32 Assembly Homepage][LuoYunBin's Win32 ASM Page]

PE教程4: Optional Header

我们已经学习了关于 DOS header  PE header 中部分成员的知识。这里是 PE header 中最后、最大或许也是最重要的成员,optional header

回顾一下,optional header 结构是 IMAGE_NT_HEADERS 中的最后成员。包含了PE文件的逻辑分布信息。该结构共有31个域,一些是很关键,另一些不太常用。这里只介绍那些真正有用的域。

这儿有个关于PE文件格式的常用术语: RVA 
RVA 
代表相对虚拟地址。 知道什么是虚拟地址吗?相对那些简单的概念而言,RVA有些晦涩。简言之,RVA是虚拟空间中到参考点的一段距离。我打赌您肯定熟悉文件偏移量: RVA就是类似文件偏移量的东西。当然它是相对虚拟空间里的一个地址,而不是文件头部。举例说明,如果PE文件装入虚拟地址(VA)空间的400000h处,且进程从虚址401000h开始执行,我们可以说进程执行起始地址在RVA 1000h。每个RVA都是相对于模块的起始VA的。
为什么PE文件格式要用到RVA呢? 这是为了减少PE装载器的负担。因为每个模块多有可能被重载到任何虚拟地址空间,如果让PE装载器修正每个重定位项,这肯定是个梦魇。相反,如果所有重定位项都使用RVA,那么PE装载器就不必操心那些东西了: 它只要将整个模块重定位到新的起始VA。这就象相对路径和绝对路径的概念: RVA类似相对路径,VA就象绝对路径。

Field Meanings
AddressOfEntryPoint PE装载器准备运行的PE文件的第一个指令的RVA。若您要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。
ImageBase PE文件的优先装载地址。比如,如果该值是400000h,PE装载器将尝试把文件装到虚拟地址空间的400000h处。字眼"优先"表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。
SectionAlignment 内存中节对齐的粒度。例如,如果该值是4096 (1000h),那么每节的起始地址必须是4096的倍数。若第一节从401000h开始且大小是10个字节,则下一节必定从402000h开始,即使401000h和402000h之间还有很多空间没被使用。
FileAlignment

文件中节对齐的粒度。例如,如果该值是(200h),,那么每节的起始地址必须是512的倍数。若第一节从文件偏移量200h开始且大小是10个字节,则下一节必定位于偏移量400h: 即使偏移量512和1024之间还有很多空间没被使用/定义。

MajorSubsystemVersion
MinorSubsystemVersion
win32子系统版本。若PE文件是专门为Win32设计的,该子系统版本必定是4.0否则对话框不会有3维立体感。
SizeOfImage 内存中整个PE映像体的尺寸。它是所有头和节经过节对齐处理后的大小。
SizeOfHeaders 所有头+节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。可以以此值作为PE文件第一节的文件偏移量。
Subsystem NT用来识别PE文件属于哪个子系统。 对于大多数Win32程序,只有两类值: Windows GUI 和 Windows CUI (控制台)。
DataDirectory IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA,比如引入地址表等。

翻译:iamgufeng [Iczelion's Win32 Assembly Homepage][LuoYunBin's Win32 ASM Page]

PE教程5: Section Table(节表)

请下载 范例

理论:

到本课为止,我们已经学了许多关于 DOS header  PE header 的知识。接下来就该轮到 section table(节表)了。节表其实就是紧挨着 PE header 的一结构数组。该数组成员的数目由 file header (IMAGE_FILE_HEADER)结构中 NumberOfSections 域的域值来决定。节表结构又命名为 IMAGE_SECTION_HEADER

IMAGE_SIZEOF_SHORT_NAME equ 8

IMAGE_SECTION_HEADER STRUCT 
   Name1 db IMAGE_SIZEOF_SHORT_NAME dup(?) 
   union Misc 
      PhysicalAddress dd ? 
      VirtualSize dd ? 
   ends 
   VirtualAddress dd ? 
   SizeOfRawData dd ? 
   PointerToRawData dd ? 
   PointerToRelocations dd ? 
   PointerToLinenumbers dd ? 

   NumberOfRelocations dw ? 
   NumberOfLinenumbers dw ? 
   Characteristics dd ? 
IMAGE_SECTION_HEADER ENDS

同样,不是所有成员都是很有用的,我们只关心那些真正重要的。

Field Meanings
Name1 事实上本域的名称是"name",只是"name"已被MASM用作关键字,所以我们只能用"Name1"代替。这儿的节名长不超过8字节。记住节名仅仅是个标记而已,我们选择任何名字甚至空着也行,注意这里不用null结束。命名不是一个ASCIIZ字符串,所以不用null结尾。
VirtualAddress 本节的RVA(相对虚拟地址)。PE装载器将节映射至内存时会读取本值,因此如果域值是1000h,而PE文件装在地址400000h处,那么本节就被载到401000h
SizeOfRawData 经过文件对齐处理后节尺寸,PE装载器提取本域值了解需映射入内存的节字节数。(译者注: 假设一个文件的文件对齐尺寸是0x200,如果前面的 VirtualSize域指示本节长度是0x388字节,则本域值为0x400,表示本节是0x400字节长)。
PointerToRawData 这是节基于文件的偏移量,PE装载器通过本域值找到节数据在文件中的位置。
Characteristics 包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未初始数据,是否可写、可读等。

现在我们已知晓 IMAGE_SECTION_HEADER 结构,再来模拟一下 PE装载器的工作吧:

  1. 读取 IMAGE_FILE_HEADER  NumberOfSections域,知道文件的节数目。
  2. SizeOfHeaders 域值作为节表的文件偏移量,并以此定位节表。
  3. 遍历整个结构数组检查各成员值。
  4. 对于每个结构,我们读取PointerToRawData域值并定位到该文件偏移量。然后再读取SizeOfRawData域值来决定映射内存的字节数。将VirtualAddress域值加上ImageBase域值等于节起始的虚拟地址。然后就准备把节映射进内存,并根据Characteristics域值设置属性。
  5. 遍历整个数组,直至所有节都已处理完毕。

注意我们并没有使用节名: 这其实并不重要。

示例:

本例程打开一PE文件遍历其节表,并在列表框控件显示各节的信息。

.386 
.model flat,stdcall 
option casemap:none 
include \masm32\include\windows.inc 
include \masm32\include\kernel32.inc 
include \masm32\include\comdlg32.inc 
include \masm32\include\user32.inc 
include \masm32\include\comctl32.inc 
includelib \masm32\lib\comctl32.lib 
includelib \masm32\lib\user32.lib 
includelib \masm32\lib\kernel32.lib 
includelib \masm32\lib\comdlg32.lib 

IDD_SECTIONTABLE equ 104 
IDC_SECTIONLIST equ 1001 

SEH struct


PrevLink dd ? ; the address of the previous seh structure 
CurrentHandler dd ? ; the address of the new exception handler 
SafeOffset dd ? ; The offset where it's safe to continue execution 
PrevEsp dd ? ; the old value in esp 
PrevEbp dd ? ; The old value in ebp 
SEH ends 

.data 
AppName db "PE tutorial no.5",0 
ofn OPENFILENAME <> 
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0 
             db "All Files",0,"*.*",0,0 
FileOpenError db "Cannot open the file for reading",0 
FileOpenMappingError db "Cannot open the file for memory mapping",0 
FileMappingError db "Cannot map the file into memory",0 
FileInValidPE db "This file is not a valid PE",0 
template db "%08lx",0 
SectionName db "Section",0 
VirtualSize db "V.Size",0 
VirtualAddress db "V.Address",0 
SizeOfRawData db "Raw Size",0 
RawOffset db "Raw Offset",0 
Characteristics db "Characteristics",0 

.data? 
hInstance dd ? 
buffer db 512 dup(?) 
hFile dd ? 
hMapping dd ? 
pMapping dd ? 
ValidPE dd ? 
NumberOfSections dd ? 

.code 
start proc 
LOCAL seh:SEH 
   invoke GetModuleHandle,NULL 
   mov hInstance,eax 
   mov ofn.lStructSize,SIZEOF ofn 
   mov ofn.lpstrFilter, OFFSET FilterString 
   mov ofn.lpstrFile, OFFSET buffer 
   mov ofn.nMaxFile,512 
   mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY 
   invoke GetOpenFileName, ADDR ofn 
   .if eax==TRUE 
      invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL 
      .if eax!=INVALID_HANDLE_VALUE 
         mov hFile, eax 
         invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0 
         .if eax!=NULL 
            mov hMapping, eax 
            invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0 
            .if eax!=NULL 
               mov pMapping,eax 
               assume fs:nothing 
               push fs:[0] 
               pop seh.PrevLink 
               mov seh.CurrentHandler,offset SEHHandler 
               mov seh.SafeOffset,offset FinalExit 
               lea eax,seh 
               mov fs:[0], eax 
               mov seh.PrevEsp,esp 
               mov seh.PrevEbp,ebp 
               mov edi, pMapping 
               assume edi:ptr IMAGE_DOS_HEADER 
               .if [edi].e_magic==IMAGE_DOS_SIGNATURE 
                  add edi, [edi].e_lfanew 
                  assume edi:ptr IMAGE_NT_HEADERS 
                  .if [edi].Signature==IMAGE_NT_SIGNATURE 
                     mov ValidPE, TRUE 
                  .else 
                     mov ValidPE, FALSE 
                  .endif 
               .else 
                  mov ValidPE,FALSE 
               .endif 
FinalExit: 
               push seh.PrevLink 
               pop fs:[0] 
               .if ValidPE==TRUE 
                  call ShowSectionInfo 
               .else 
                  invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION 
               .endif 
               invoke UnmapViewOfFile, pMapping 
           .else 
               invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR 
          .endif 
          invoke CloseHandle,hMapping 
       .else 
          invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR 
       .endif 
       invoke CloseHandle, hFile 
    .else 
       invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR 
    .endif 
  .endif 
  invoke ExitProcess, 0 
  invoke InitCommonControls 
start endp 

SEHHandler proc uses edx pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD 
   mov edx,pFrame 
   assume edx:ptr SEH 
   mov eax,pContext 
   assume eax:ptr CONTEXT 
   push [edx].SafeOffset 
   pop [eax].regEip 
   push [edx].PrevEsp 
   pop [eax].regEsp 
   push [edx].PrevEbp 
   pop [eax].regEbp 
   mov ValidPE, FALSE 
   mov eax,ExceptionContinueExecution 
   ret 
SEHHandler endp 

DlgProc proc uses edi esi hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD 
   LOCAL lvc:LV_COLUMN 
   LOCAL lvi:LV_ITEM 
   .if uMsg==WM_INITDIALOG 
      mov esi, lParam 
      mov lvc.imask,LVCF_FMT or LVCF_TEXT or LVCF_WIDTH or LVCF_SUBITEM 
      mov lvc.fmt,LVCFMT_LEFT 
      mov lvc.lx,80 
      mov lvc.iSubItem,0 
      mov lvc.pszText,offset SectionName 
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,0,addr lvc inc lvc.iSubItem 
      mov lvc.fmt,LVCFMT_RIGHT 
      mov lvc.pszText,offset VirtualSize 
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,1,addr lvc 
      inc lvc.iSubItem 
      mov lvc.pszText,offset VirtualAddress 
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,2,addr lvc 
      inc lvc.iSubItem 
      mov lvc.pszText,offset SizeOfRawData 
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,3,addr lvc 
      inc lvc.iSubItem 
      mov lvc.pszText,offset RawOffset 
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,4,addr lvc 
      inc lvc.iSubItem 
      mov lvc.pszText,offset Characteristics 
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,5,addr lvc 
      mov ax, NumberOfSections 
      movzx eax,ax 
      mov edi,eax       
      mov lvi.imask,LVIF_TEXT 
      mov lvi.iItem,0 
      assume esi:ptr IMAGE_SECTION_HEADER 
      .while edi>0 
         mov lvi.iSubItem,0 
         invoke RtlZeroMemory,addr buffer,9 
         invoke lstrcpyn,addr buffer,addr [esi].Name1,8 
         lea eax,buffer 
         mov lvi.pszText,eax 
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi 
         invoke wsprintf,addr buffer,addr template,[esi].Misc.VirtualSize 
         lea eax,buffer 
         mov lvi.pszText,eax 
         inc lvi.iSubItem 
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi 
         invoke wsprintf,addr buffer,addr template,[esi].VirtualAddress 
         lea eax,buffer 
         mov lvi.pszText,eax 
         inc lvi.iSubItem 
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi 
         invoke wsprintf,addr buffer,addr template,[esi].SizeOfRawData 
         lea eax,buffer 
         mov lvi.pszText,eax 
         inc lvi.iSubItem 
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi 
         invoke wsprintf,addr buffer,addr template,[esi].PointerToRawData 
         lea eax,buffer 
         mov lvi.pszText,eax 
         inc lvi.iSubItem 
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi 
         invoke wsprintf,addr buffer,addr template,[esi].Characteristics 
         lea eax,buffer 
         mov lvi.pszText,eax 
         inc lvi.iSubItem 
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi 
         inc lvi.iItem 
         dec edi 
         add esi, sizeof IMAGE_SECTION_HEADER 
      .endw 
   .elseif 
      uMsg==WM_CLOSE 
         invoke EndDialog,hDlg,NULL 
   .else 
      mov eax,FALSE 
      ret 
   .endif 
   mov eax,TRUE 
   ret 
DlgProc endp 

ShowSectionInfo proc uses edi 
   mov edi, pMapping 
   assume edi:ptr IMAGE_DOS_HEADER 
   add edi, [edi].e_lfanew 
   assume edi:ptr IMAGE_NT_HEADERS 
   mov ax,[edi].FileHeader.NumberOfSections 
   movzx eax,ax 
   mov NumberOfSections,eax 
   add edi,sizeof IMAGE_NT_HEADERS 
   invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi
   ret 
ShowSectionInfo endp 
end start

分析:

本例重用了PE教程2的代码,校验PE文件的有效性后,继续调用函数ShowSectionInfo显示各节信息。

ShowSectionInfo proc uses edi 
   mov edi, pMapping 
   assume edi:ptr IMAGE_DOS_HEADER 
   add edi, [edi].e_lfanew
   assume edi:ptr IMAGE_NT_HEADERS

我们将edi用作指向PE文件数据的指针。首先,将指向DOS header地址的pMapping赋给edi,再加上e_lfanew域值等于PE header的地址。

   mov ax,[edi].FileHeader.NumberOfSections
   mov NumberOfSections,ax

因为我们要遍历节表,所以必须先获取文件的节数目。这就得靠file header里的NumberOfSections域了,切记这是个word域。

   add edi,sizeof IMAGE_NT_HEADERS

现在edi正指向PE header的起始地址,加上PE header结构大小后恰好指向节表了。

   invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi

调用 DialogBoxParam 显示列表对话框,注意我们已将节表地址作为最后一个参数传递过去了,该值可从WM_INITDIALOG 消息的lParam参数中提取。

在对话框过程里我们响应WM_INITDIALOG消息,将lParam (节表地址)存入esi,节数目赋给edi并设置列表控件。万事俱备后,进入循环将各节信息插入到列表控件中,这部分相当简单。

      .while edi>0 
         mov lvi.iSubItem,0

字符串置入第一列。

         invoke RtlZeroMemory,addr buffer,9 
         invoke lstrcpyn,addr buffer,addr [esi].Name1,8 
         lea eax,buffer 
         mov lvi.pszText,eax

要显示节名,当然要将其转换为ASCIIZ字符串先。

         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi

然后显示第一列。
继续我们伟大的工程,显示完本节中最后一个欲呈现的值后,立马下一个结构。

         dec edi 
         add esi, sizeof IMAGE_SECTION_HEADER 
      .endw

每处理完一节就递减edi,然后将esi加上IMAGE_SECTION_HEADER 结构大小,使其指向下一个IMAGE_SECTION_HEADER 结构。

遍历节表的步骤:

  1. PE文件有效性校验。
  2. 定位到 PE header 的起始地址。
  3.  file header NumberOfSections域获取节数。
  4. 通过两种方法定位节表: ImageBase+SizeOfHeaders 或者 PE header的起始地址+ PE header结构大小。(节表紧随 PE header)。如果不是使用文件映射的方法,可以用SetFilePointer 直接将文件指针定位到节表。节表的文件偏移量存放在 SizeOfHeaders域里。(SizeOfHeaders IMAGE_OPTIONAL_HEADER 的结构成员)
  5. 处理每个 IMAGE_SECTION_HEADER 结构。

翻译:iamgufeng [Iczelion's Win32 Assembly Homepage][LuoYunBin's Win32 ASM Page]

PE教程6: Import Table(引入表)

本课我们将学习引入表。先警告一下,对于不熟悉引入表的读者来说,这是一堂又长又难的课,所以需要多读几遍,最好再打开调试器来好好分析相关结构。各位,努力啊!

下载范例

理论:

首先,您得了解什么是引入函数。一个引入函数是被某模块调用的但又不在调用者模块中的函数,因而命名为"import(引入)"。引入函数实际位于一个或者更多的DLL里。调用者模块里只保留一些函数信息,包括函数名及其驻留的DLL名。现在,我们怎样才能找到PE文件中保存的信息呢? 转到data directory 寻求答案吧。再回顾一把,下面就是 PE header:

IMAGE_NT_HEADERS STRUCT
   Signature dd ?
   FileHeader IMAGE_FILE_HEADER <>
   OptionalHeader IMAGE_OPTIONAL_HEADER <>
IMAGE_NT_HEADERS ENDS

optional header 最后一个成员就是 data directory(数据目录):

IMAGE_OPTIONAL_HEADER32 STRUCT
   .... 
   LoaderFlags dd ? 
   NumberOfRvaAndSizes dd ? 
   DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>) 
IMAGE_OPTIONAL_HEADER32 ENDS

data directory 是一个 IMAGE_DATA_DIRECTORY 结构数组,共有16个成员。如果您还记得节表可以看作是PE文件各节的根目录的话,也可以认为 data directory 是存储在这些节里的逻辑元素的根目录。明确点,data directory 包含了PE文件中各重要数据结构的位置和尺寸信息。 每个成员包含了一个重要数据结构的信息。

Member Info inside
0 Export symbols
1 Import symbols
2 Resources
3 Exception
4 Security
5 Base relocation
6 Debug
7 Copyright string
8 Unknown
9 Thread local storage (TLS)
10 Load configuration
11 Bound Import
12 Import Address Table
13 Delay Import
14 COM descriptor

上面那些金色显示的是我熟悉的。了解 data directory 包含域后,我们可以仔细研究它们了。data directory 的每个成员都是 IMAGE_DATA_DIRECTORY 结构类型的,其定义如下所示:

IMAGE_DATA_DIRECTORY STRUCT 
  VirtualAddress dd ? 
  isize dd ? 
IMAGE_DATA_DIRECTORY ENDS

VirtualAddress 实际上是数据结构的相对虚拟地址(RVA)。比如,如果该结构是关于import symbols的,该域就包含指向IMAGE_IMPORT_DESCRIPTOR 数组的RVA 
isize 含有VirtualAddress所指向数据结构的字节数。

下面就是如何找寻PE文件中重要数据结构的一般方法:

  1.  DOS header 定位到 PE header
  2.  optional header 读取 data directory 的地址。
  3. IMAGE_DATA_DIRECTORY 结构尺寸乘上找寻结构的索引号: 比如您要找寻import symbols的位置信息,必须用IMAGE_DATA_DIRECTORY 结构尺寸(8 bytes)乘上1import symbolsdata directory中的索引号)。
  4. 将上面的结果加上data directory地址,我们就得到包含所查询数据结构信息的IMAGE_DATA_DIRECTORY 结构项。

现在我们开始真正讨论引入表了。data directory数组第二项的VirtualAddress包含引入表地址。引入表实际上是一个 IMAGE_IMPORT_DESCRIPTOR 结构数组。每个结构包含PE文件引入函数的一个相关DLL的信息。比如,如果该PE文件从10个不同的DLL中引入函数,那么这个数组就有10个成员。该数组以一个全0的成员结尾。下面详细研究结构组成:

IMAGE_IMPORT_DESCRIPTOR STRUCT 
  union 
    Characteristics dd ? 
    OriginalFirstThunk dd ? 
  ends 
  TimeDateStamp dd ? 
  ForwarderChain dd ? 
  Name1 dd ? 
  FirstThunk dd ? 
IMAGE_IMPORT_DESCRIPTOR ENDS

结构第一项是一个union子结构。 事实上,这个union子结构只是给 OriginalFirstThunk 增添了个别名,您也可以称其为"Characteristics"。 该成员项含有指向一个 IMAGE_THUNK_DATA 结构数组的RVA
什么是 
IMAGE_THUNK_DATA? 这是一个dword类型的集合。通常我们将其解释为指向一个IMAGE_IMPORT_BY_NAME 结构的指针。注意 IMAGE_THUNK_DATA 包含了指向一个IMAGE_IMPORT_BY_NAME 结构的指针: 而不是结构本身。
请看这里
: 现有几个 IMAGE_IMPORT_BY_NAME 结构,我们收集起这些结构的RVA (IMAGE_THUNK_DATAs)组成一个数组,并以0结尾,然后再将数组的RVA放入 OriginalFirstThunk
 
IMAGE_IMPORT_BY_NAME 结构存有一个引入函数的相关信息。再来研究 IMAGE_IMPORT_BY_NAME结构到底是什么样子的呢:

IMAGE_IMPORT_BY_NAME STRUCT 
  Hint dw ? 
  Name1 db ? 
IMAGE_IMPORT_BY_NAME ENDS

Hint 指示本函数在其所驻留DLL的引出表中的索引号。该域被PE装载器用来在DLL的引出表里快速查询函数。该值不是必须的,一些连接器将此值设为0
Name1 含有引入函数的函数名。函数名是一个ASCIIZ字符串。注意这里虽然将Name1的大小定义成字节,其实它是可变尺寸域,只不过我们没有更好方法来表示结构中的可变尺寸域。The structure is provided so that you can refer to the data structure with descriptive names.

TimeDateStamp  ForwarderChain 可是高级东东: 让我们精通其他成员后再来讨论它们吧。

Name1 含有指向DLL名字的RVA,即指向DLL名字的指针,也是一个ASCIIZ字符串。

FirstThunk  OriginalFirstThunk 非常相似,它也包含指向一个 IMAGE_THUNK_DATA 结构数组的RVA(当然这是另外一个IMAGE_THUNK_DATA 结构数组) 
好了,如果您还在犯糊涂,就朝这边看过来
: 现在有几个 IMAGE_IMPORT_BY_NAME 结构,同时您又创建了两个结构数组,并同样寸入指向那些 IMAGE_IMPORT_BY_NAME 结构的RVAs,这样两个数组就包含相同数值了(可谓相当精确的复制啊)。 最后您决定将第一个数组的RVA赋给 OriginalFirstThunk第二个数组的RVA赋给FirstThunk,这样一切都很清楚了。

OriginalFirstThunk   IMAGE_IMPORT_BY_NAME   FirstThunk

|

      |
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Function 1
Function 2
Function 3
Function 4
...
Function n
<---
<---
<---
<---
<---
<---
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA

现在您应该明白我的意思。不要被IMAGE_THUNK_DATA这个名字弄糊涂: 它仅是指向IMAGE_IMPORT_BY_NAME 结构的RVA。 如果将 IMAGE_THUNK_DATA 字眼想象成RVA,就更容易明白了。OriginalFirstThunk  FirstThunk 所指向的这两个数组大小取决于PE文件从DLL中引入函数的数目。比如,如果PE文件从kernel32.dll中引入10个函数,那么IMAGE_IMPORT_DESCRIPTOR 结构的 Name1域包含指向字符串"kernel32.dll"RVA,同时每个IMAGE_THUNK_DATA 数组有10个元素。

下一个问题是: 为什么我们需要两个完全相同的数组? 为了回答该问题,我们需要了解当PE文件被装载到内存时,PE装载器将查找IMAGE_THUNK_DATA  IMAGE_IMPORT_BY_NAME 这些结构数组,以此决定引入函数的地址。然后用引入函数真实地址来替代由FirstThunk指向的 IMAGE_THUNK_DATA 数组里的元素值。因此当PE文件准备执行时,上图已转换成:

OriginalFirstThunk   IMAGE_IMPORT_BY_NAME   FirstThunk

|

      |
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Function 1
Function 2
Function 3
Function 4
...
Function n
 
 
 
 
 
Address of Function 1
Address of Function 2
Address of Function 3
Address of Function 4
...
Address of Function n

OriginalFirstThunk 指向的RVA数组始终不会改变,所以若还反过头来查找引入函数名,PE装载器还能找寻到。
当然再简单的事物都有其复杂的一面。有些情况下一些函数仅由序数引出,也就是说您不能用函数名来调用它们: 您只能用它们的位置来调用。此时,调用者模块中就不存在该函数的IMAGE_IMPORT_BY_NAME 结构。不同的,对应该函数的 IMAGE_THUNK_DATA 值的低位字指示函数序数,而最高二进位 (MSB)设为1。例如,如果一个函数只由序数引出且其序数是1234h,那么对应该函数的 IMAGE_THUNK_DATA 值是80001234hMicrosoft提供了一个方便的常量来测试dword值的MSB位,就是 IMAGE_ORDINAL_FLAG32,其值为80000000h
假设我们要列出某个
PE文件的所有引入函数,可以照着下面步骤走:

  1. 校验文件是否是有效的PE
  2.  DOS header 定位到 PE header
  3. 获取位于 OptionalHeader 数据目录地址。
  4. 转至数据目录的第二个成员提取其VirtualAddress值。
  5. 利用上值定位第一个 IMAGE_IMPORT_DESCRIPTOR 结构。
  6. 检查 OriginalFirstThunk值。若不为0,顺着 OriginalFirstThunk 里的RVA值转入那个RVA数组。若OriginalFirstThunk 0,就改用FirstThunk值。有些连接器生成PE文件时会置OriginalFirstThunk值为0,这应该算是个bug。不过为了安全起见,我们还是检查 OriginalFirstThunk值先。
  7. 对于每个数组元素,我们比对元素值是否等于IMAGE_ORDINAL_FLAG32如果该元素值的最高二进位为1, 那么函数是由序数引入的,可以从该值的低字节提取序数。
  8. 如果元素值的最高二进位为0,就可将该值作为RVA转入 IMAGE_IMPORT_BY_NAME 数组,跳过 Hint就是函数名字了。
  9. 再跳至下一个数组元素提取函数名一直到数组底部(它以null结尾)。现在我们已遍历完一个DLL的引入函数,接下去处理下一个DLL
  10. 即跳转到下一个 IMAGE_IMPORT_DESCRIPTOR 并处理之,如此这般循环直到数组见底。(IMAGE_IMPORT_DESCRIPTOR 数组以一个全0域元素结尾)

示例:

本例程打开一PE文件,将所有引入函数名读入一编辑控件,同时显示 IMAGE_IMPORT_DESCRIPTOR 结构各域值。

.386 
.model flat,stdcall 
option casemap:none 
include \masm32\include\windows.inc 
include \masm32\include\kernel32.inc 
include \masm32\include\comdlg32.inc 
include \masm32\include\user32.inc 
includelib \masm32\lib\user32.lib 
includelib \masm32\lib\kernel32.lib 
includelib \masm32\lib\comdlg32.lib 

IDD_MAINDLG equ 101 
IDC_EDIT equ 1000 
IDM_OPEN equ 40001 
IDM_EXIT equ 40003 

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD 
ShowImportFunctions proto :DWORD 
ShowTheFunctions proto :DWORD,:DWORD 
AppendText proto :DWORD,:DWORD 

SEH struct 
PrevLink dd ? ; the address of the previous seh structure 
CurrentHandler dd ? ; the address of the new exception handler 
SafeOffset dd ? ; The offset where it's safe to continue execution 
PrevEsp dd ? ; the old value in esp 
PrevEbp dd ? ; The old value in ebp 
SEH ends 

.data 
AppName db "PE tutorial no.6",0 
ofn OPENFILENAME <> 
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0 
             db "All Files",0,"*.*",0,0 
FileOpenError db "Cannot open the file for reading",0 
FileOpenMappingError db "Cannot open the file for memory mapping",0 
FileMappingError db "Cannot map the file into memory",0 
NotValidPE db "This file is not a valid PE",0 
CRLF db 0Dh,0Ah,0 
ImportDescriptor db 0Dh,0Ah,"================[ IMAGE_IMPORT_DESCRIPTOR ]=============",0 
IDTemplate db "OriginalFirstThunk = %lX",0Dh,0Ah 
           db "TimeDateStamp = %lX",0Dh,0Ah 
           db "ForwarderChain = %lX",0Dh,0Ah 
           db "Name = %s",0Dh,0Ah 
           db "FirstThunk = %lX",0 
NameHeader db 0Dh,0Ah,"Hint Function",0Dh,0Ah 
           db "-----------------------------------------",0 
NameTemplate db "%u %s",0 
OrdinalTemplate db "%u (ord.)",0 

.data? 
buffer db 512 dup(?) 
hFile dd ? 
hMapping dd ? 
pMapping dd ? 
ValidPE dd ? 

.code 
start: 
invoke GetModuleHandle,NULL 
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0 
invoke ExitProcess, 0 

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD 
.if uMsg==WM_INITDIALOG 
  invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0 
.elseif uMsg==WM_CLOSE 
  invoke EndDialog,hDlg,0 
.elseif uMsg==WM_COMMAND 
  .if lParam==0 
    mov eax,wParam 
    .if ax==IDM_OPEN 
      invoke ShowImportFunctions,hDlg 
    .else ; IDM_EXIT 
      invoke SendMessage,hDlg,WM_CLOSE,0,0 
    .endif 
  .endif 
.else 
  mov eax,FALSE 
  ret 
.endif 
mov eax,TRUE 
ret 
DlgProc endp 

SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD 
  mov edx,pFrame 
  assume edx:ptr SEH 
  mov eax,pContext 
  assume eax:ptr CONTEXT 
  push [edx].SafeOffset 
  pop [eax].regEip 
  push [edx].PrevEsp 
  pop [eax].regEsp 
  push [edx].PrevEbp 
  pop [eax].regEbp 
  mov ValidPE, FALSE 
  mov eax,ExceptionContinueExecution 
  ret 
SEHHandler endp 

ShowImportFunctions proc uses edi hDlg:DWORD 
  LOCAL seh:SEH 
  mov ofn.lStructSize,SIZEOF 
  ofn mov ofn.lpstrFilter, OFFSET FilterString 
  mov ofn.lpstrFile, OFFSET buffer 
  mov ofn.nMaxFile,512 
  mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY 
  invoke GetOpenFileName, ADDR ofn 
  .if eax==TRUE 
    invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL 
    .if eax!=INVALID_HANDLE_VALUE 
      mov hFile, eax 
      invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0 
      .if eax!=NULL 
        mov hMapping, eax 
        invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0 
        .if eax!=NULL 
          mov pMapping,eax 
          assume fs:nothing 
          push fs:[0] 
          pop seh.PrevLink 
          mov seh.CurrentHandler,offset SEHHandler 
          mov seh.SafeOffset,offset FinalExit 
          lea eax,seh 
          mov fs:[0], eax 
          mov seh.PrevEsp,esp 
          mov seh.PrevEbp,ebp 
          mov edi, pMapping 
          assume edi:ptr IMAGE_DOS_HEADER 
          .if [edi].e_magic==IMAGE_DOS_SIGNATURE 
            add edi, [edi].e_lfanew 
            assume edi:ptr IMAGE_NT_HEADERS 
            .if [edi].Signature==IMAGE_NT_SIGNATURE 
              mov ValidPE, TRUE 
            .else 
              mov ValidPE, FALSE 
            .endif 
          .else 
            mov ValidPE,FALSE 
          .endif 
FinalExit: 
          push seh.PrevLink 
          pop fs:[0] 
          .if ValidPE==TRUE 
            invoke ShowTheFunctions, hDlg, edi 
          .else 
            invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR 
          .endif 
          invoke UnmapViewOfFile, pMapping 
      .else 
          invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR 
      .endif 
      invoke CloseHandle,hMapping 
    .else 
      invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR 
    .endif 
    invoke CloseHandle, hFile 
   .else 
   invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR 
   .endif 
 .endif 
 ret 
ShowImportFunctions endp 

AppendText proc hDlg:DWORD,pText:DWORD 
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText 
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF 
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0 
   ret 
AppendText endp 

RVAToOffset PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD 
   mov esi,pFileMap 
   assume esi:ptr IMAGE_DOS_HEADER 
   add esi,[esi].e_lfanew 
   assume esi:ptr IMAGE_NT_HEADERS 
   mov edi,RVA ; edi == RVA 
   mov edx,esi 
   add edx,sizeof IMAGE_NT_HEADERS 
   mov cx,[esi].FileHeader.NumberOfSections 
   movzx ecx,cx 
   assume edx:ptr IMAGE_SECTION_HEADER 
   .while ecx>0 ; check all sections 
     .if edi>=[edx].VirtualAddress 
       mov eax,[edx].VirtualAddress 
       add eax,[edx].SizeOfRawData 
       .if edi<eax ; The address is in this section 
         mov eax,[edx].VirtualAddress 
         sub edi,eax
         mov eax,[edx].PointerToRawData 
         add eax,edi ; eax == file offset 
         ret 
       .endif 
     .endif 
     add edx,sizeof IMAGE_SECTION_HEADER 
     dec ecx 
   .endw 
   assume edx:nothing 
   assume esi:nothing 
   mov eax,edi 
   ret 
RVAToOffset endp 

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD 
   LOCAL temp[512]:BYTE 
   invoke SetDlgItemText,hDlg,IDC_EDIT,0 
   invoke AppendText,hDlg,addr buffer 
   mov edi,pNTHdr 
   assume edi:ptr IMAGE_NT_HEADERS 
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress 
   invoke RVAToOffset,pMapping,edi 
   mov edi,eax 
   add edi,pMapping 
   assume edi:ptr IMAGE_IMPORT_DESCRIPTOR 
   .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0) 
     invoke AppendText,hDlg,addr ImportDescriptor 
     invoke RVAToOffset,pMapping, [edi].Name1 
     mov edx,eax 
     add edx,pMapping 
     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp 
     .if [edi].OriginalFirstThunk==0 
        mov esi,[edi].FirstThunk 
     .else 
        mov esi,[edi].OriginalFirstThunk 
     .endif 
     invoke RVAToOffset,pMapping,esi 
     add eax,pMapping 
     mov esi,eax 
     invoke AppendText,hDlg,addr NameHeader 
     .while dword ptr [esi]!=0 
       test dword ptr [esi],IMAGE_ORDINAL_FLAG32 
       jnz ImportByOrdinal 
       invoke RVAToOffset,pMapping,dword ptr [esi] 
       mov edx,eax 
       add edx,pMapping 
       assume edx:ptr IMAGE_IMPORT_BY_NAME 
       mov cx, [edx].Hint 
       movzx ecx,cx 
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1 
       jmp ShowTheText 
ImportByOrdinal: 
       mov edx,dword ptr [esi] 
       and edx,0FFFFh 
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx 
ShowTheText: 
       invoke AppendText,hDlg,addr temp 
       add esi,4 
    .endw 
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR 
  .endw 
  ret 
ShowTheFunctions endp 
end start

分析:

本例中,用户点击打开菜单显示文件打开对话框,检验文件的PE有效性后调用 ShowTheFunctions

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD 
   LOCAL temp[512]:BYTE

保留512字节堆栈空间用于字符串操作。

   invoke SetDlgItemText,hDlg,IDC_EDIT,0

清除编辑控件内容。

   invoke AppendText,hDlg,addr buffer

PE文件名插入编辑控件。 AppendText 通过传递一个 EM_REPLACESEL 消息以通知向编辑控件添加文本。然后它又向编辑控件发送一个设置了 wParam=-1lParam=0EM_SETSEL 消息,使光标定位到文本末。

   mov edi,pNTHdr 
   assume edi:ptr IMAGE_NT_HEADERS 
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress

获取import symbolsRVAedi起初指向 PE header,以此我们可以定位到数据目录数组的第二个数组元素来得到虚拟地址值。

   invoke RVAToOffset,pMapping,edi 
   mov edi,eax 
   add edi,pMapping

这儿对PE编程初学者来说可能有点困难。在PE文件中大多数地址多是RVAs 而 RVAs只有当PE文件被PE装载器装入内存后才有意义。 本例中,我们直接将文件映射到内存而不是通过PE装载器载入,因此我们不能直接使用那些RVAs。必须先将那些RVAs转换成文件偏移量,RVAToOffset函数就起到这个作用。 这里不准备详细分析。指出的是,它还将给定的RVA和PE文件所有节的始末RVA作比较(检验RVA的有效性),然后通过IMAGE_SECTION_HEADER 结构中的PointerToRawData域(当然是所在节的那个PointerToRawData域啦)将RVA转换成文件偏移量。
函数使用需要传递两个参数: 内存映射文件指针和所要转换的RVA。eax里返回文件偏移量。上面代码中,我们必须将文件偏移量加上内存映射文件指针以转换成虚拟地址。是不是有点复杂? :)

   assume edi:ptr IMAGE_IMPORT_DESCRIPTOR 
   .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)

edi现在指向第一个 IMAGE_IMPORT_DESCRIPTOR 结构。接下来我们遍历整个结构数组直到遇上一个全0结构,这就是数组末尾了。

     invoke AppendText,hDlg,addr ImportDescriptor
     invoke RVAToOffset,pMapping, [edi].Name1 
     mov edx,eax 
     add edx,pMapping

我们要显示当前 IMAGE_IMPORT_DESCRIPTOR 结构的值。Name1 不同于其他结构成员,它含有指向相关dll名的RVA。因此必须先将其转换成虚拟地址。

     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp

显示当前 IMAGE_IMPORT_DESCRIPTOR 结构的值。

     .if [edi].OriginalFirstThunk==0 
        mov esi,[edi].FirstThunk 
     .else 
        mov esi,[edi].OriginalFirstThunk 
     .endif

接下来准备遍历 IMAGE_THUNK_DATA 数组。通常我们会选择OriginalFirstThunk指向的那个数组,不过,如果某些连接器错误地将OriginalFirstThunk 0,这可以通过检查OriginalFirstThunk值是否为0判断。这样的话,只要选择FirstThunk指向的数组了。

     invoke RVAToOffset,pMapping,esi 
     add eax,pMapping 
     mov esi,eax

同样的,OriginalFirstThunk/FirstThunk值是一个RVA。必须将其转换为虚拟地址。

     invoke AppendText,hDlg,addr NameHeader
     .while dword ptr [esi]!=0

现在我们准备遍历 IMAGE_THUNK_DATAs 数组以查找该DLL引入的函数名,直到遇上全0项。

       test dword ptr [esi],IMAGE_ORDINAL_FLAG32 
       jnz ImportByOrdinal

第一件事是校验IMAGE_THUNK_DATA 是否含有IMAGE_ORDINAL_FLAG32标记。检查IMAGE_THUNK_DATA MSB是否为1,如果是1,则函数是通过序数引出的,所以不需要更进一步处理了。直接从 IMAGE_THUNK_DATA 提取低字节获得序数,然后是下一个IMAGE_THUNK_DATA 双字。

       invoke RVAToOffset,pMapping,dword ptr [esi] 
       mov edx,eax 
       add edx,pMapping 
       assume edx:ptr IMAGE_IMPORT_BY_NAME

如果IMAGE_THUNK_DATA MSB0,那么它包含了IMAGE_IMPORT_BY_NAME 结构的RVA。需要先转换为虚拟地址。

       mov cx, [edx].Hint 
       movzx ecx,cx 
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1 
       jmp ShowTheText

Hint 是字类型,所以先转换为双字后再传递给wsprintf,然后我们将hint和函数名都显示到编辑控件中。

ImportByOrdinal: 
       mov edx,dword ptr [esi] 
       and edx,0FFFFh 
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx

在仅用序数引出函数的情况中,先清空高字再显示序数。

ShowTheText: 
       invoke AppendText,hDlg,addr temp 
       add esi,4

在编辑控件中插入相应的函数名/序数后,跳转到下个 IMAGE_THUNK_DATA

    .endw 
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR

处理完当前IMAGE_THUNK_DATA 数组里的所有双字,跳转到下个IMAGE_IMPORT_DESCRIPTOR 开始处理其他DLLs的引入函数了。

附录:

让我们再来讨论一下bound import。当PE装载器装入PE文件时,检查引入表并将相关DLLs映射到进程地址空间。然后象我们这样遍历IMAGE_THUNK_DATA 数组并用引入函数的真实地址替换IMAGE_THUNK_DATAs 值。这一步需要很多时间。如果程序员能事先正确预测函数地址,PE装载器就不用每次装入PE文件时都去修正IMAGE_THUNK_DATAs 值了。Bound import就是这种思想的产物。
为了方便实现,Microsoft出品的类似Visual Studio的编译器多提供了bind.exe这样的工具,由它检查PE文件的引入表并用引入函数的真实地址替换IMAGE_THUNK_DATA 值。当文件装入时,PE装载器必定检查地址的有效性,如果DLL版本不同于PE文件存放的相关信息,或则DLLs需要重定位,那么装载器认为原先计算的地址是无效的,它必定遍历OriginalFirstThunk指向的数组以获取引入函数新地址。
Bound import
在本课中并非很重要,我们确省就是用到了OriginalFirstThunk。要了解更多信息可参见LUEVELSMEYERpe.txt

翻译:iamgufeng [Iczelion's Win32 Assembly Homepage][LuoYunBin's Win32 ASM Page]

PE教程7: Export Table(引出表)

上一课我们已经学习了动态联接中关于引入表那部分知识,现在继续另外一部分,那就是引出表。

下载 范例

理论:

当PE装载器执行一个程序,它将相关DLLs都装入该进程的地址空间。然后根据主程序的引入函数信息,查找相关DLLs中的真实函数地址来修正主程序。PE装载器搜寻的是DLLs中的引出函数。

DLL/EXE要引出一个函数给其他DLL/EXE使用,有两种实现方法: 通过函数名引出或者仅仅通过序数引出。比如某个DLL要引出名为"GetSysConfig"的函数,如果它以函数名引出,那么其他DLLs/EXEs若要调用这个函数,必须通过函数名,就是GetSysConfig。另外一个办法就是通过序数引出。什么是序数呢? 序数是唯一指定DLL中某个函数的16位数字,在所指向的DLL里是独一无二的。例如在上例中,DLL可以选择通过序数引出,假设是16,那么其他DLLs/EXEs若要调用这个函数必须以该值作为GetProcAddress调用参数。这就是所谓的仅仅靠序数引出。

我们不提倡仅仅通过序数引出函数这种方法,这会带来DLL维护上的问题。一旦DLL升级/修改,程序员无法改变函数的序数,否则调用该DLL的其他程序都将无法工作。

现在我们开始学习引出结构。象引出表一样,可以通过数据目录找到引出表的位置。这儿,引出表是数据目录的第一个成员,又可称为IMAGE_EXPORT_DIRECTORY。该结构*有11 个成员,常用的列于下表。

Field Name Meaning
nName 模块的真实名称。本域是必须的,因为文件名可能会改变。这种情况下,PE装载器将使用这个内部名字。
nBase 基数,加上序数就是函数地址数组的索引值了。
NumberOfFunctions 模块引出的函数/符号总数。
NumberOfNames 通过名字引出的函数/符号数目。该值不是模块引出的函数/符号总数,这是由上面的NumberOfFunctions给出。本域可以为0,表示模块可能仅仅通过序数引出。如果模块根本不引出任何函数/符号,那么数据目录中引出表的RVA为0。
AddressOfFunctions 模块中有一个指向所有函数/符号的RVAs数组,本域就是指向该RVAs数组的RVA。简言之,模块中所有函数的RVAs都保存在一个数组里,本域就指向这个数组的首地址。
AddressOfNames 类似上个域,模块中有一个指向所有函数名的RVAs数组,本域就是指向该RVAs数组的RVA。
AddressOfNameOrdinals RVA,指向包含上述 AddressOfNames数组中相关函数之序数的16位数组。

上面也许无法让您完全理解引出表,下面的简述将助您一臂之力。

引出表的设计是为了方便PE装载器工作。首先,模块必须保存所有引出函数的地址以供PE装载器查询。模块将这些信息保存在AddressOfFunctions域指向的数组中,而数组元素数目存放在NumberOfFunctions域中。 因此,如果模块引出40个函数,则AddressOfFunctions指向的数组必定有40个元素,而NumberOfFunctions值为40。现在如果有一些函数是通过名字引出的,那么模块必定也在文件中保留了这些信息。这些 名字的RVAs存放在一数组中以供PE装载器查询。该数组由AddressOfNames指向,NumberOfNames包含名字数目。考虑一下PE装载器的工作机制,它知道函数名,并想以此获取这些函数的地址。至今为止,模块已有两个模块: 名字数组和地址数组,但两者之间还没有联系的纽带。因此我们还需要一些联系函数名及其地址的东东。PE参考指出使用到地址数组的索引作为联接,因此PE装载器在名字数组中找到匹配名字的同时,它也获取了 指向地址表中对应元素的索引。 而这些索引保存在由AddressOfNameOrdinals域指向的另一个数组(最后一个)中。由于该数组是起了联系名字和地址的作用,所以其元素数目必定和名字数组相同,比如,每个名字有且仅有一个相关地址,反过来则不一定: 每个地址可以有好几个名字来对应。因此我们给同一个地址取"别名"。为了起到连接作用,名字数组和索引数组必须并行地成对使用,譬如,索引数组的第一个元素必定含有第一个名字的索引,以此类推。

AddressOfNames   AddressOfNameOrdinals
|   |
RVA of Name 1
RVA of Name 2
RVA of Name 3
RVA of Name 4
...
RVA of Name N
<-->
<-->
<-->
<-->
...
<-->
Index of Name 1
Index of Name 2
Index of Name 3
Index of Name 4
...
Index of Name N

下面举一两个例子说明问题。如果我们有了引出函数名并想以此获取地址,可以这么做:

  1. 定位到PE header。
  2. 从数据目录读取引出表的虚拟地址。
  3. 定位引出表获取名字数目(NumberOfNames)。
  4. 并行遍历AddressOfNamesAddressOfNameOrdinals指向的数组匹配名字。如果在AddressOfNames 指向的数组中找到匹配名字,从AddressOfNameOrdinals 指向的数组中提取索引值。例如,若发现匹配名字的RVA存放在AddressOfNames 数组的第77个元素,那就提取AddressOfNameOrdinals数组的第77个元素作为索引值。如果遍历完NumberOfNames 个元素,说明当前模块没有所要的名字。
  5. AddressOfNameOrdinals 数组提取的数值作为AddressOfFunctions 数组的索引。也就是说,如果值是5,就必须读取AddressOfFunctions 数组的第5个元素,此值就是所要函数的RVA。

现在我们在把注意力转向IMAGE_EXPORT_DIRECTORY 结构的nBase成员。您已经知道AddressOfFunctions 数组包含了模块中所有引出符号的地址。当PE装载器索引该数组查询函数地址时,让我们设想这样一种情况,如果程序员在.def文件中设定起始序数号为200,这意味着AddressOfFunctions 数组至少有200个元素,甚至这前面200个元素并没使用,但它们必须存在,因为PE装载器这样才能索引到正确的地址。这种方法很不好,所以又设计了nBase 域解决这个问题。如果程序员指定起始序数号为200,nBase 值也就是200。当PE装载器读取nBase域时,它知道开始200个元素并不存在,这样减掉一个nBase值后就可以正确地索引AddressOfFunctions 数组了。有了nBase,就节约了200个空元素。

注意nBase并不影响AddressOfNameOrdinals数组的值。尽管取名"AddressOfNameOrdinals",该数组实际包含的是指向AddressOfFunctions 数组的索引,而不是什么序数啦。

讨论完nBase的作用,我们继续下一个例子。
假设我们只有函数的序数,那么怎样获取函数地址呢,可以这么做:

  1. 定位到PE header。
  2. 从数据目录读取引出表的虚拟地址。
  3. 定位引出表获取nBase值。
  4. 减掉nBase值得到指向AddressOfFunctions 数组的索引。
  5. 将该值与NumberOfFunctions作比较,大于等于后者则序数无效。
  6. 通过上面的索引就可以获取AddressOfFunctions 数组中的RVA了。

可以看出,从序数获取函数地址比函数名快捷容易。不需要遍历AddressOfNames AddressOfNameOrdinals 这两个数组。然而,综合性能必须与模块维护的简易程度作一平衡。

总之,如果想通过名字获取函数地址,需要遍历AddressOfNames  AddressOfNameOrdinals 这两个数组。如果使用函数序数,减掉nBase值后就可直接索引AddressOfFunctions 数组。

如果一函数通过名字引出,那在GetProcAddress中可以使用名字或序数。但函数仅由序数引出情况又怎样呢? 现在就来看看。
"一个函数仅由序数引出"意味着函数在AddressOfNames  AddressOfNameOrdinals 数组中不存在相关项。记住两个域,NumberOfFunctions  NumberOfNames。这两个域可以清楚地显示有时某些函数没有名字的。函数数目至少等同于名字数目,没有名字的函数通过序数引出。比如,如果存在70个函数但AddressOfNames数组中只有40项,这就意味着模块中有30个函数是仅通过序数引出的。现在我们怎样找出那些仅通过序数引出的函数呢?这不容易,必须通过排除法,比如,AddressOfFunctions 的数组项在AddressOfNameOrdinals 数组中不存在相关指向,这就说明该函数RVA只通过序数引出。

示例:

本例类似上课的范例。然而,在显示IMAGE_EXPORT_DIRECTORY 结构一些成员信息的同时,也列出了引出函数的RVAs,序数和名字。注意本例没有列出仅由序数引出的函数。

.386 
.model flat,stdcall 
option casemap:none 
include \masm32\include\windows.inc 
include \masm32\include\kernel32.inc 
include \masm32\include\comdlg32.inc 
include \masm32\include\user32.inc 
includelib \masm32\lib\user32.lib 
includelib \masm32\lib\kernel32.lib 
includelib \masm32\lib\comdlg32.lib 

IDD_MAINDLG equ 101 
IDC_EDIT equ 1000 
IDM_OPEN equ 40001 
IDM_EXIT equ 40003 

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD 
ShowExportFunctions proto :DWORD 
ShowTheFunctions proto :DWORD,:DWORD 
AppendText proto :DWORD,:DWORD


SEH struct 
   PrevLink dd ? 
   CurrentHandler dd ?
   SafeOffset dd ?
   PrevEsp dd ?
   PrevEbp dd ?
SEH ends 

.data 
AppName db "PE tutorial no.7",0 
ofn OPENFILENAME <> 
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0 
             db "All Files",0,"*.*",0,0 
FileOpenError db "Cannot open the file for reading",0 
FileOpenMappingError db "Cannot open the file for memory mapping",0 
FileMappingError db "Cannot map the file into memory",0 
NotValidPE db "This file is not a valid PE",0 
NoExportTable db "No export information in this file",0 
CRLF db 0Dh,0Ah,0 
ExportTable db 0Dh,0Ah,"======[ IMAGE_EXPORT_DIRECTORY ]======",0Dh,0Ah 
            db "Name of the module: %s",0Dh,0Ah 
            db "nBase: %lu",0Dh,0Ah 
            db "NumberOfFunctions: %lu",0Dh,0Ah 
            db "NumberOfNames: %lu",0Dh,0Ah 
            db "AddressOfFunctions: %lX",0Dh,0Ah 
            db "AddressOfNames: %lX",0Dh,0Ah 
            db "AddressOfNameOrdinals: %lX",0Dh,0Ah,0 
Header db "RVA Ord. Name",0Dh,0Ah 
       db "----------------------------------------------",0 
template db "%lX %u %s",0 

.data? 
buffer db 512 dup(?) 
hFile dd ? 
hMapping dd ? 
pMapping dd ? 
ValidPE dd ? 

.code 
start: 
invoke GetModuleHandle,NULL 
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0 
invoke ExitProcess, 0 

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD 
.if uMsg==WM_INITDIALOG 
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0 
.elseif uMsg==WM_CLOSE 
   invoke EndDialog,hDlg,0 
.elseif uMsg==WM_COMMAND 
   .if lParam==0 
     mov eax,wParam 
     .if ax==IDM_OPEN 
       invoke ShowExportFunctions,hDlg 
     .else ; IDM_EXIT 
       invoke SendMessage,hDlg,WM_CLOSE,0,0 
     .endif 
   .endif 
.else 
   mov eax,FALSE 
   ret 
.endif 
mov eax,TRUE 
ret 
DlgProc endp 

SEHHandler proc uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD 
mov edx,pFrame 
assume edx:ptr SEH 
mov eax,pContext 
assume eax:ptr CONTEXT 
push [edx].SafeOffset 
pop [eax].regEip 
push [edx].PrevEsp 
pop [eax].regEsp 
push [edx].PrevEbp 
pop [eax].regEbp 
mov ValidPE, FALSE 
mov eax,ExceptionContinueExecution 
ret 
SEHHandler endp 

ShowExportFunctions proc uses edi hDlg:DWORD 
LOCAL seh:SEH 
mov ofn.lStructSize,SIZEOF ofn 
mov ofn.lpstrFilter, OFFSET FilterString 
mov ofn.lpstrFile, OFFSET buffer 
mov ofn.nMaxFile,512 
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY 
invoke GetOpenFileName, ADDR ofn 
.if eax==TRUE 
   invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL 
   .if eax!=INVALID_HANDLE_VALUE 
     mov hFile, eax 
     invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0 
     .if eax!=NULL 
       mov hMapping, eax 
       invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0 
       .if eax!=NULL 
         mov pMapping,eax 
         assume fs:nothing 
         push fs:[0] 
         pop seh.PrevLink 
         mov seh.CurrentHandler,offset SEHHandler 
         mov seh.SafeOffset,offset FinalExit 
         lea eax,seh 
         mov fs:[0], eax 
         mov seh.PrevEsp,esp 
         mov seh.PrevEbp,ebp 
         mov edi, pMapping 
         assume edi:ptr IMAGE_DOS_HEADER 
         .if [edi].e_magic==IMAGE_DOS_SIGNATURE 
           add edi, [edi].e_lfanew 
           assume edi:ptr IMAGE_NT_HEADERS 
           .if [edi].Signature==IMAGE_NT_SIGNATURE 
             mov ValidPE, TRUE 
           .else 
             mov ValidPE, FALSE 
           .endif 
         .else 
           mov ValidPE,FALSE 
         .endif 
FinalExit: 
         push seh.PrevLink 
         pop fs:[0] 
         .if ValidPE==TRUE 
           invoke ShowTheFunctions, hDlg, edi 
         .else 
           invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
         .endif 
         invoke UnmapViewOfFile, pMapping 
       .else 
         invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR 
       .endif 
       invoke CloseHandle,hMapping 
     .else 
       invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR 
     .endif 
     invoke CloseHandle, hFile 
   .else 
     invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR 
   .endif 
.endif 
ret 
ShowExportFunctions endp 

AppendText proc hDlg:DWORD,pText:DWORD 
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText 
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF 
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0 
ret 
AppendText endp 

RVAToFileMap PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD 
mov esi,pFileMap 
assume esi:ptr IMAGE_DOS_HEADER 
add esi,[esi].e_lfanew 
assume esi:ptr IMAGE_NT_HEADERS 
mov edi,RVA ; edi == RVA 
mov edx,esi 
add edx,sizeof IMAGE_NT_HEADERS 
mov cx,[esi].FileHeader.NumberOfSections 
movzx ecx,cx 
assume edx:ptr IMAGE_SECTION_HEADER 
.while ecx>0
   .if edi>=[edx].VirtualAddress 
     mov eax,[edx].VirtualAddress 
     add eax,[edx].SizeOfRawData 
     .if edi<eax
       mov eax,[edx].VirtualAddress 
       sub edi,eax
       mov eax,[edx].PointerToRawData 
       add eax,edi
       add eax,pFileMap 
       ret 
     .endif 
   .endif 
   add edx,sizeof IMAGE_SECTION_HEADER 
   dec ecx 
.endw 
assume edx:nothing 
assume esi:nothing 
mov eax,edi 
ret 
RVAToFileMap endp 

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD 
LOCAL temp[512]:BYTE 
LOCAL NumberOfNames:DWORD 
LOCAL Base:DWORD 

mov edi,pNTHdr 
assume edi:ptr IMAGE_NT_HEADERS 
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress 
.if edi==0 
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR 
  ret 
.endif 
invoke SetDlgItemText,hDlg,IDC_EDIT,0 
invoke AppendText,hDlg,addr buffer 
invoke RVAToFileMap,pMapping,edi 
mov edi,eax 
assume edi:ptr IMAGE_EXPORT_DIRECTORY 
mov eax,[edi].NumberOfFunctions 
invoke RVAToFileMap, pMapping,[edi].nName 
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp
invoke AppendText,hDlg,addr Header 
push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase 
pop Base 
invoke RVAToFileMap,pMapping,[edi].AddressOfNames 
mov esi,eax 
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals 
mov ebx,eax 
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions 
mov edi,eax
.while NumberOfNames>0 
   invoke RVAToFileMap,pMapping,dword ptr [esi] 
   mov dx,[ebx] 
   movzx edx,dx 
   mov ecx,edx 
   shl edx,2 
   add edx,edi 
   add ecx,Base 
   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax 
   invoke AppendText,hDlg,addr temp 
   dec NumberOfNames 
   add esi,4 
   add ebx,2 
.endw 
ret 
ShowTheFunctions endp 
end start

分析:

mov edi,pNTHdr 
assume edi:ptr IMAGE_NT_HEADERS 
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress 
.if edi==0 
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR 
  ret 
.endif

程序检验PE有效性后,定位到数据目录获取引出表的虚拟地址。若该虚拟地址为0,则文件不含引出符号。

mov eax,[edi].NumberOfFunctions 
invoke RVAToFileMap, pMapping,[edi].nName 
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp

在编辑控件中显示IMAGE_EXPORT_DIRECTORY 结构的一些重要信息。

push [edi].NumberOfNames 
pop NumberOfNames 
push [edi].nBase 
pop Base

由于我们要枚举所有函数名,就要知道引出表里的名字数目。nBase 在将AddressOfFunctions 数组索引转换成序数时派到用场。

invoke RVAToFileMap,pMapping,[edi].AddressOfNames 
mov esi,eax 
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals 
mov ebx,eax 
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions 
mov edi,eax

将三个数组的地址相应存放到esi,,ebx,edi中。准备开始访问。

.while NumberOfNames>0

直到所有名字都被处理完毕。

   invoke RVAToFileMap,pMapping,dword ptr [esi]

由于esi指向包含名字字符串RVAs的数组,所以[esi]含有当前名字的RVA,需要将它转换成虚拟地址,后面wsprintf要用的。

   mov dx,[ebx] 
   movzx edx,dx 
   mov ecx,edx
   add ecx,Base 

ebx指向序数数组,值是字类型的。因此我们先要将其转换成双字,此时edx和ecx含有指向AddressOfFunctions数组的索引。我们用edx作为索引值,而将ecx加上nBase得到函数的序数值。=

   shl edx,2 
   add edx,edi

索引乘以4 (AddressOfFunctions 数组中每个元素都是4字节大小) 然后加上数组首地址,这样edx指向的就是所要函数的RVA了。

   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax 
   invoke AppendText,hDlg,addr temp

在编辑控件中显示函数的RVA, 序数, 和名字。

   dec NumberOfNames 
   add esi,4 
   add ebx,2 
.endw

修正计数器,AddressOfNames  AddressOfNameOrdinals 两数组的当前指针,继续遍历直到所有名字全都处理完毕。

转自:http://www.ebookpk.com/ebook/olread_189683.jsp?pageNo=39