《逆向工程核心原理》读书笔记——第13章 PE文件格式

13.1 介绍

  PE文件是Windows操作系统下使用的可执行文件格式。它是微软在UNIX平台的COFF(Common Object File Format,通用对象文件格式)基础上制作而成的。最初(正如Portable这个单词所代表的那样)设计用来提高程序在不同操作系统上的移植性,但实际上这种文件格式仅用在Windows系列的操作系统下。
  PE文件是指32位的可执行文件,也称为PE32。64位的可执行文件称为PE+或PE32+,是PE( PE32)文件的一种扩展形式(请注意不是PE64 )。

13.2、PE文件格式

PE文件种类如表13-1所示。

表13-1PE文件种类
种类主扩展名
可执行系列EXE、SCR
驱动程序系列SYS、VXD
库系列DLL、OCX、CPL、DRV
对象文件系列OBJ

  严格地说,OBJ(对象)文件之外的所有文件都是可执行的。DLL、SYS文件等虽然不能直接在Shell ( Explorer.exe)中运行,但可以使用其他方法(调试器、服务等)执行。
提示
  根据PE正式规范,编译结果OBJ文件也视为PE文件。但是OBJ文件本身不能以任何形式执行,在代码逆向分析中几乎不需要关注它。
  下面以记事本( notepad.exe)程序进行简单说明,首先使用Hex Editor打开记事本程序。
  图13-1是notepad.exe文件的起始部分,也是PE文件的头部分(PE header )。notepad.exe文件运行需要的所有信息就存储在这个PE头中。如何加载到内存、从何处开始运行、运行中需要的DLL有哪些、需要多大的栈/堆内存等,大量信息以结构体形式存储在PE头中。换言之,学习PE文件格式就是学习PE头中的结构体。
提示—
  书中将以Windows XP SP3的 notepad.exe为例进行说明,与其他版本Windows 下的notepad.exe文件结构类似,但是地址不同。
在这里插入图片描述

图13-1 notepad.exe文件

13.2.1基本结构

  notepad.exe具有普通PE文件的基本结构。图13-2描述了notepad.exe文件加载到内存时的情形。其中包含了许多内容,下面逐一学习。
  从DOS头 ( DOS header )到节区头 (Section header )是PE头部分,其下的节区合称PE体。文件中使用偏移(offset ),内存中使用VA (Virtual Address,虚拟地址)来表示位置。文件加载到内存时,情况就会发生变化(节区的大小、位置等)。文件的内容一般可分为代码(.text)、数据( .data)、资源( .rsrc)节,分别保存。
提示—
  根据所用的不同开发工具(VB/VC++/Delphi/etc )与编译选项,节区的名称、大小、个数、存储的内容等都是不同的。最重要的是它们按照不同的用途分类保存到不同的节中。
  各节区头定义了各节区在文件或内存中的大小、位置、属性等。
  PE头与各节区的尾部存在一个区域,称为NULL填充(NULL padding )。计算机中,为了提高处理文件、内存、网络包的效率,使用“最小基本单位”这一概念,PE文件中也类似。文件/内存中节区的起始位置应该在各文件/内存最小单位的倍数位置上,空白区域将用NULL填充(看图13-2,可以看到各节区起始地址的截断都遵循一定规则)。
在这里插入图片描述

图13-2 PE文件( notepad.exe)加载到内存中的情形

13.2.2 VA&RVA

  VA指的是进程虚拟内存的绝对地址,RVA(Relative Virtual Address,相对虚拟地址)指从某个基准位置(ImageBase)开始的相对地址。VA与RVA满足下面的换算关系。

RVA+ImageBase=VA

  PE头内部信息大多以RVA形式存在。原因在于,PE文件(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他PE文件(DLL)。此时必须通过重定位(Relocation)将其加载到其他空白的位置,若PE头信息使用的是VA,则无法正常访问。因此使用RVA来定位信息,即使发生了重定位,只要相对于基准位置的相对地址没有变化,就能正常访问到指定信息,不会出现任何问题。
提示

32位Windows OS中,各进程分配有4GB的虚拟内存,因此进程中VA值的范围是00000000~FFFFFFFF。 # 13.3 PE头
PE头由许多结构体组成,现在开始逐一学习各结构体。此外还会详细讲解在代码逆向分析中起着重要作用的结构体成员。 ## 13.3.1 DOS头
微软创建PE文件格式时,人们正广泛使用DOS文件,所以微软充分考虑了PE文件对DOS文件的兼容性。其结果是在PE头的最前面添加了一个IMAGE_DOS_HEADER结构体,用来扩展已有的DOS EXE头。
typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部
  USHORT e_magic; // 魔术数字
  USHORT e_cblp; // 文件最后页的字节数
  USHORT e_cp; // 文件页数
  USHORT e_crlc; // 重定义元素个数
  USHORT e_cparhdr; // 头部尺寸,以区块落为单位
  USHORT e_minalloc; // 所需的最小附加区块
  USHORT e_maxalloc; // 所需的最大附加区块
  USHORT e_ss; // 初始的SS值(相对偏移量)
  USHORT e_sp; // 初始的SP值
  USHORT e_csum; // 校验和
  USHORT e_ip; // 初始的IP值
  USHORT e_cs; // 初始的CS值(相对偏移量)
  USHORT e_lfarlc; // 重分配表文件地址
  USHORT e_ovno; // 覆盖号
  USHORT e_res[4]; // 保留字
  USHORT e_oemid; // OEM标识符(相对e_oeminfo)
  USHORT e_oeminfo; // OEM信息
  USHORT e_res2[10]; // 保留字
  LONG e_lfanew; // 新exe头部的文件地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

  IMAGE_DOS_HEADER结构体的大小为40个字节。在该结构体中必须知道2个重要成员:e_magic与e_lfanew。
e_magic: DOS签名( signature,4D5A=>ASCII值“MZ”)。
e_lfanew:指示NT头的偏移(根据不同文件拥有可变值)。
  所有PE文件在开始部分( e_magic)都有DOS签名(“MZ”)。e_lfanew值指向NT头所在位置(NT头的名称为IMAGE_NT_HEADERS,后面将会介绍)。
提示
  一个名叫 Mark Zbikowski的开发人员在微软设计了DOS可执行文件,MZ即取自其名字的首字母。
出处:http-/en.wikipedia.orglwiki/Mark_Zbikowski
  使用Hex Editor打开notepad.exe,查看IMAGE_DOS_HEADERS结构体,如图13-3所示。
在这里插入图片描述

图13-3 IMAGE_DOS_HEADERS

  根据PE规范,文件开始的2个字节为4D5A,e_lfanew值为000000EO(不是E0000000 )。
提示
  Intel系列的CPU以逆序存储数据,这称为小端序标识法。
  请尝试修改这些值,保存后运行。可以发现程序无法正常运行(因为根据PE规范,它已不再是PE文件了)。

13.3.2 DOS存根

  DOS存根(stub)在DOS头下方,是个可选项,且大小不固定(即使没有DOS存根,文件也能正常运行)。DOS存根由代码与数据混合而成,图13-4显示的就是notepad.exe的DOS存根。
在这里插入图片描述

图13-4 DOS存根

  图13-4中,文件偏移40~4D区域为16位的汇编指令。32位的Windows OS中不会运行该命令(由于被识别为PE文件,所以完全忽视该代码)。在DOS环境中运行Notepad.exe文件,或者使用DOS调试器(debug.exe)运行它,可使其执行该代码(不认识PE文件格式,所以被识别为DOS EXE文件)。
  打开命令行窗口(cmd.exe),输入如下命令(仅适用于Windows XP环境)。

debug C:\Windows\notepad.exe

  在出现的光标位置上输入“u”指令(Unassemble ),将会出现16位的汇编指令,如下所示:
-u

0D1E:0000 	0E      	PUSH CS
0D1E:0001 	1F        POP DS
0D1E:0002 BAOE00  	MOV DX,000E ;DX = OE:"This program cannot be
run in Dos mode"
0D1E:0805 B409       MOV AH, 09
0D1E:0007 CD21       INT 21 ;AH= 09 ;WriteString()
0D1E:0009 B8014C      MOV AX, 4C01
0D1E:000C CD21       INT 21         ;AX =4C01 : Exit()

在这里插入图片描述

  代码非常简单,在画面中输出字符串“This program cannot be run in DOS mode”后就退出。换言之,notepad.exe文件虽然是32位的PE文件,但是带有MS-DOS兼容模式,可以在DOS环境中运行,执行DOSEXE代码,输出“This program cannot be run in DOS mode”后终止。灵活使用该特性可以在一个可执行文件(EXE ))中创建出另一个文件,它在DOS与Windows中都能运行(在DOS环境中运行16位DOS代码,在Windows环境中运行32位Windows代码)。
  如前所述,DOS存根是可选项,开发工具应该支持它(VB、VC++、Delphi等默认支持DOS存根)。

13.3.3 NT头

下面介绍NT头IMAGE_NT_HEADERS。

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

  IMAGE_NT_HEADERS结构体由3个成员组成,第一个成员为签名(Signature)结构体,其值为50450000h(“PE”00)。另外两个成员分别为文件头( File Header )与可选头( Optional Header)结构体。使用Hex Editor打开notepad.exe,查看其IMAGE_NT_HEADERS,如图13-5所示。
在这里插入图片描述

图13-5 IMAGE_NT_HEADERS

IMAGE_NT_HEADERS结构体的大小为F8,相当大。下面分别讲解文件头与可选头结构体。

13.3.4 NT头中的文件头

文件头是表现文件大致属性的IMAGE_FILE_HEADER结构体。

typedef struct _IMAGE_FILE_HEADER {
  USHORT Machine; //运行平台
  USHORT NumberOfSections;//文件的区块数
  ULONG TimeDateStamp;//文件创建日期和时间
  ULONG PointerToSymbolTable;//指向符号表(用于调试)
  ULONG NumberOfSymbols;//符号表中符号的个数(用于调试)
  USHORT SizeOfOptionalHeader;//IMAGE_OPTIONAL_HEADER32结构的大小
  USHORT Characteristics;//文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

#define IMAGE_SIZEOF_FILE_HEADER 20

IMAGE_FILE_HEADERS结构体中有如下4种重要成员(若它们设置不正确,将导致文件无法正常运行)。

1.Machine

每个CPU都拥有唯一的Machine码,兼容32位Intel x86芯片的Machine码为14C。以下是定义在winnt.h文件中的Machine码。
代码13-4 Machine码

#define IMAGE_FILE_MACHINE_UNKNOWN  0
#define IMAGE_FILE MACHINE I386     0x014c //Intel 386.
#define IMAGE_FILE_MACHINE_R3000    0x0162 // MIPS little-endian,0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000    0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000   0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2

出处:Microsoft Platform SDK - winnt.h

2.NumberOfSections

  前面提到过,PE文件把代码、数据、资源等依据属性分类到各节区中存储。
  NumberOfSections用来指出文件中存在的节区数量。该值一定要大于0,且当定义的节区数量与实际节区不同时,将发生运行错误。

3.SizeOfOptionalHeader

  IMAGE_NT_HEADER结构体的最后一个成员为IMAGE_OPTIONAL_HEADER32结构体。SizeOfOptionaHeader成员用来指出IMAGE_OPTIONAL_HEADER32结构体的长度。IMAGE_OPTIONAL_HEADER32结构体由C语言编写而成,故其大小已经确定。但是Windows的PE装载器需要查看IMAGE_FILE_HEADER的SizeOfOptionalHeader值,从而识别出IMAGE_OPTIONAL_HEADER32结构体的大小。
  PE32+格式的文件中使用的是IMAGE_OPTIONAL_HEADER64结构体,而不是IMAGE_OPTIONAL_HEADER32结构体。2个结构体的尺寸是不同的,所以需要在SizeOfOptionalHeader成员中明确指出结构体的大小。
提示
  借助 IMAGE_DOS_HEADER的e_lfanew成员与 IMAGE_FILE_HEADER的SizeOfOptionalHeader成员,可以创建出一种脱离常规的PE文件(PE Patch )(也有人称之为“麻花”PE文件)。

4.Characteristics

  该字段用于标识文件的属性,文件是否是可运行的形态、是否为DLL文件等信息,以bit OR形式组合起来。
  以下是定义在winnt.h文件中的Characteristics值(请记住0002h与2000h这两个值)。
代码13-5 Characteristics

#define IMAGE_FILE_RELOCs STRIPPED     0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE    0x0002 // File is executable
//(i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED  0x0004 // Line numbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM   0x0010 // Agressively trim working set
#define IMAGEFILELARGE_ADDRESS_AwARE   0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO   0x0080 // byte of machine word are reversed .
#define IMAGE_FILE_32BIT_MACHINE       0x0100  //32 bit word machine.
#define IMAGE_FILE_DEBUGSTRIPPED       0x0200 // Debugging info stripped from
// file in .DBG file
#define IMAGE_FILE_RENOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media,
// copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 	0x0800 //If Image is on Net,
//copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 	 			 0x1000 // System File.
#define IMAGE_FILE_DLL 					 0x2000 //File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY     	 Ox4000 //File should only be
run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 	 0x8000 // byte of machine word are reversed.

出处:Microsoft Platform SDK- winnt.h
  另外,PE文件中Characteristics的值有可能不是0002h(不可执行)吗?是的,确实存在这种情况。比如类似*.obj的object文件及resource DLL文件等。
  最后讲一下IMAGE_FILE_HEADER的TimeDateStamp成员。该成员的值不影响文件运行,用来记录编译器创建此文件的时间。但是有些开发工具(VB、VC++)提供了设置该值的工具,而有些开发工具( Delphi)则未提供(且随所用选项的不同而不同)。
IMAGE_FILE_HEADER
  在Hex Editor中查看notepad.exe的IMAGE_FILE_HEADER结构体。
在这里插入图片描述

图13-6 IMAGE FILE HEADER

  为使大家理解图13-6,以结构体成员的形式表示如下。

OffsetValueDescription
000000E4014Cmachine
000000E60003number of sections
000000E848025287time date stamp (Mon Apr 14 03:35:51 2008)
000000EC00000000offset to symbol table
000000F000000000number of symbols
000000F400E0size of optional header
000000F6010Fcharacteristics
0x0001:IMAGE_FILE_RELOCS_STRIPPED
0x0002:IMAGE_FILE_EXECUTABLE_IMAGE
0x0004:IMAGE_FILE_LINE_NUMS_STRIPPED
0x0008:IMAGE_FILE_LOCAL_SYMS_STRIPPED
0x1000:IMAGE_FILE_32BIT_MACHINE

13.3.5 NT头中的可选头

IMAGE_ OPTIONAL_ HEADER32是PE头结构体中最大的。
typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //
    WORD    Magic;//标志字
    BYTE    MajorLinkerVersion;//链接器主版本号
    BYTE    MinorLinkerVersion;//链接器次版本号
    DWORD   SizeOfCode;//所有含有代码的区块的大小
    DWORD   SizeOfInitializedData;//所有初始化数据区块的大小
    DWORD   SizeOfUninitializedData;//所有未初始化数据区块的大小
    DWORD   AddressOfEntryPoint;//程序执行人口 RVA
    DWORD   BaseOfCode;//代码区块起始RVA
    DWORD   BaseOfData;//数据区块起始RVA

    //
    // NT additional fields.
    //
    DWORD   ImageBase;//程序默认载人基地址
    DWORD   SectionAlignment;//内存中区块的对齐值
    DWORD   FileAlignment;//文件中区块的对齐值
    WORD    MajorOperatingSystemVersion;//操作系统主版本号
    WORD    MinorOperatingSystemVersion;//操作系统次版本号
    WORD    MajorImageVersion;//用户自定义主版本号
    WORD    MinorImageVersion;//用户自定义次版本号
    WORD    MajorSubsystemVersion;//所需子系统主版本号
    WORD    MinorSubsystemVersion;//所需子系统次版本号
    DWORD   Win32VersionValue;//保留,通常被设置为0
    DWORD   SizeOfImage;//映像载入内存后的总尺寸
    DWORD   SizeOfHeaders;//MS-DOS头部、PE文件头、区块表总大小
    DWORD   CheckSum;//映像校验和
    WORD    Subsystem;//文件子系统
    WORD    DllCharacteristics;//显示 DLL特性的旗标
    DWORD   SizeOfStackReserve;初始化时栈的大小
    DWORD   SizeOfStackCommit;//初始化时实际提交栈的大小
    DWORD   SizeOfHeapReserve;//初始化时保留堆的大小
    DWORD   SizeOfHeapCommit;//初始化时实际保留堆的大小
    DWORD   LoaderFlags;//与调试相关,默认值为0
    DWORD   NumberOfRvaAndSizes;//数据目录表的项数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//数据目录表

} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct_IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress ;
DWORD Size;
] IMAGE DATA DIRECTORY, *PIMAGE DATA DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16在IMAGE 

  OPTIONAL_ HEADER32结构体中需要关注下列成员。这些值是文件运行必需的,设置错误将导致文件无法正常运行。

1.Magic

  为IMAGE_OPTIONAL_HEADER32结构体时,Magic码为10B;为IMAGE_OPTIONAL_HEADER64结构体时,Magic码为20B。

2.AddressOfEntryPoint

  AddressOfEntryPoint持有EP的RVA值。该值指出程序最先执行的代码起始地址,相当重要。

3.ImageBase

  进程虚拟内存的范围是0FFFFFFF ( 32位系统)。PE文件被加载到如此大的内存中时,ImageBase指出文件的优先装人地址。
  EXE、DLL文件被装载到用户内存的O~FFFFF中,SYS文件被载人内核内存的800000-FFFFFF中。一般而言,使用开发工具( VB/VC++/Delphi)创建好EXE文件后,其ImageBase的值为00400000,DLL 文件的ImageBase值为10000000 (当然也可以指定为其他值)。执行PE文件时,PE装载器先创建进程,再将文件载人内存,然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint。

4.SectionAlignment, FileAlignment

  PE文件的Body部分划分为若干节区,这些节存储着不同类别的数据。FileAlignment指定了节区在磁盘文件中的最小单位,而SectionAlignment则指定了节区在内存中的最小单位(一个文件中,FileAlignment与SectionAlignment的值可能相同,也可能不同)。磁盘文件或内存的节区大小必定为FileAlignment或SectionAlignment值的整数倍。

5.SizeOflmage

  加载PE文件到内存时,SizeOflmage指定了PE Image在虚拟内存中所占空间的大小。一般而言,文件的大小与加载到内存中的大小是不同的(节区头中定义了各节装载的位置与占有内存的大小,后面会讲到)。

6.SizeOfHeader

  SizeOfHeader用来指出整个PE头的大小。该值也必须是FileAlignment的整数倍。第一节区所在位置与SizeOfHeader距文件开始偏移的量相同。

7.Subsystem

  该Subsystem值用来区分系统驱动文件(.sys)与普通的可执行文件(.exe,*.dll)。Subsystem成员可拥有的值如表13-2所示。

表13-2 Subsystem
含义备注
1Driver文件系统驱动(如:nfs.sys)
2GUI文件窗口应用程序(如:notepad.exe)
3CUI文件控制台应用程序(如:cmd.exe)

#8.NumberOfRvaAndSizes
  NumberOfRvaAndSizes用来指定DataDirectory( IMAGE_ OPTIONAL_ HEADER32结构体的最后一个成员)数组的个数。虽然结构体定义中明确指出了数组个数为IMAGE NUMBEROF_DIRECTORY ENTRIES(16),但是PE装载器通过查看NumberOfRvaAndSizes值来识别数组大小,换言之,数组大小也可能不是16。
#9.DataDirectory
  DataDirectory是由IMAGE DATA_ DIRECTORY结构体组成的数组,数组的每项都有被定义的值。代码13-7列出了各数组项。
代码13-7 DataDirectory结构体数组

DataDirectory[0] = EXPORT Directory
DataDirectory[1] = IMPORT Directory
DataDirectory[2] = RESOURCE Directory
DataDirectory[3] = EXCEPTION Directory
DataDirectory[4] = SECURITY Di rectory
DataDirectory[5] = BASERELOC Directory
DataDirectory[6] = DEBUG Directory
DataDirectory[7] = COPYRIGHT Directory
DataDi rectory[8] = GLOBALPTR Directory
DataDirectory[9] = TLS Directory
DataDirectory[A] = LOAD CONFIG Directory
DataDirectory[B] = BOUND IMPORT Directory
DataDirectory[C] = IAT Directory
DataDirectory[D] = DELAY IMPORT Directory
DataDirectory[E] = COM DESCRIPTOR Directory
DataDirectory[F] = Reserved Directory

  将此处所说的Directory想成某个结构体数组即可。希望各位重点关注标红的EXPORT/IMPORT/RESOURCE、TLS Direction。特别需要注意的是IMPORT与EXPORT Directory,它们是PE头中非常重要的部分,后面会单独讲解。其余部分不怎么重要,大致了解一下即可。
IMAGE_OPTIONAL_HEADER
  前面简要介绍了重要成员组。现在查看notepad.exe的IMAGE_OPTIONAL_HEADER整个结构体。
在这里插入图片描述

图13-7 notepad.exe的IMAGE_OPTIONAL_HEADER

  图13-7描述的是notepad.exe的IMAGE_OPTIONAL_HEADER结构体区域。结构体各成员的值及其说明如代码13-8所示。
代码13-8 notepad.exe文 件的IMAGE OPTIONAL HEADER
[ IMAGE_ OPTIONAL_ HEADER ] - notepad . exe

offsetvaluedescription
000000F8010Bmagic
000000FA07major linker version
000000FB0Aminor linker version
000000FC00007800size of code
000001000000800size of initialized data
000001040000000size of uninitialized data
000001080000739Daddress of entry point
0000010C00001000base of code
0000011000009000base of data
0000011401000000image base
0000011800001000section alignment
0000001C0000200file alignment
000001200005major 0S version
000001220001minor 0S version
00000124 0005major image version
000001260001minor image version
000001280004major subsystem version
0000012A0000minor subsystem version
0000012C00000000win32 version value
0000013000014000size of image
0000013400000400size of headers
00000138000126CEChecksum
0000013C0002subsystem
0000013E8000 DLLcharacteristics
0000014000040000size of stack reserve
0000014400011000size of stack commit
0000014800000000size of heap reserve
0000014C00001000size of heap commit
0000015000000000loader flags
0000015400000010number of di rectories
0000015800000000RVA of EXPORT Directory
0000015C00000000size of EXPORT Directory
0000016000007604RVA of IMPORT Directory
00000164000000C8size of IMPORT Directory
0000016800008000RVA of RESOURCE Directory
0000016C00008304size of RESOURCE Di rectory
0000017000000000RVA of EXCEPTION Directory
0000017400000000size of EXCEPTION Directory
0000017800000000RVA of SECURITY Directory
0000017C00000000size of SECURITY Directory
0000018000000000RVA of BASERELOC Directory
0000018400000000size of BASERELOC Directory
0000018800013050RVA of DEBUG Directory
0000018C0000001Csize of DEBUG Directory
0000019000000000RVA of COPYRIGHT Directory
0000019400000000size of COPYRIGHT Directory
0000019800000000RVA of GLOBALPTR Directory
0000019C00000000size of GLOBALPTR Directory
000001A000000000RVA of TLS Directory
0000014400000000size of TLS Directory
000001A8000018A8RVA of LOAD CONFIG Directory
000001AC0000040size of LOAD CONFIG Directory
000001B000000250RVA of BOUND IMPORT Directory
000001B400000D0size of BOUND_ IMPORT Di rectory
000001B800001000RVA of IAT Directory
000001BC0000348size of IAT Directory
000001C00000000RVA of DELAY_ IMPORT Directory
000001C40000000size of DELAY IMPORT Directory
000001C80000000RVA of COM DESCRIPTOR Di rectory
000001CC0000000size of COM DESCRIPTOR Directory
000001D00000000RVA of Reserved Directory
000001D40000000size of Reserved Directory

13.3.6 节区头

  节区头中定义了各节区属性。看节区头之前先思考- -下:前面提到过,PE文件中的code(代码)、data(数据)、resource(资源)等按照属性分类存储在不同节区,设计PE文件格式的工程师们之所以这样做,一定有着某些好处。
  我认为把PE文件创建成多个节区结构的好处是,这样可以保证程序的安全性。若把code与data放在一个节区中相互纠缠(实际上完全可以这样做)很容易引发安全问题,即使忽略过程的烦琐。
  假如向字符串data写数据时,由于某个原因导致溢出(输入超过缓冲区大小时),那么其下的code(指令)就会被覆盖,应用程序就会崩溃。因此,PE文件格式的设计者们决定把具有相似属性的数据统一保存在一个被称为“节区”的地方,然后需要把各节区属性记录在节区头中(节区属性中有文件/内存的起始位置、大小、访问权限等)。
  换言之,需要为每个code/data/resource分别设置不同的特性、访问权限等,如表13-3所示。

表13-3不同 内存属性的访问权限
类别访问权限
Code执行,读取权限
Data非执行,读写权限
Resource非执行,读取权限

  至此,大家应当对节区头的作用有了大致了解。
IMAGE_SECTION_HEADER
  节区头是由IMAGE_ SECTION_ HEADER结构体组成的数组,每个结构体对应一个节区。

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];//8字节的块名区块尺寸
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;//区块的RVA地址
    DWORD   SizeOfRawData;//在文件中对齐后的尺寸
    DWORD   PointerToRawData;//在文件中的偏移
    DWORD   PointerToRelocations;//在 OBJ文件中使用,重定位的偏移
    DWORD   PointerToLinenumbers;//行号表的偏移(供调试用)
    WORD    NumberOfRelocations;//在OBJ文件中使用,重定位项数目
    WORD    NumberOfLinenumbers;//行号表中行号的数目
    DWORD   Characteristics;//区块的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

#define IMAGE_SIZEOF_SHORT_NAME              8
#define IMAGE_SIZEOF_SECTION_HEADER          40//区块表的长度为40个字节

  表13-4中列出了IMAGE_ SECTION_ HEADER结构体中要了解的重要成员(不使用其他成员)。

表13-4 IMAGE_SECTION_HEADER结构体的重要成员
项目含义
VirtualSize内存中节区所占大小
VirtualAddress内存中节区起始地址(RVA)
SizeOfRawData磁盘文件中节区所占大小
PointerToRawData磁盘文件中节区起始位置
Charateristics节区属性(bit OR)

  VirtualAddress与PointerToRawData不带有任何值,分别由( 定义在IMAGE _OPTIONAL_HEADER32中的) SectionAlignment 与FileAlignment确定。
  VirtualSize与SizeOfRawData一般具有不同的值,即磁盘文件中节区的大小与加载到内存中的节区大小是不同的。
  Characterisitics由代码13-10中显示的值组合(bit OR)而成。
代码13-10 Characterisitics

#define IMAGE_SCN_CNT_CODE 0x00000020 // Section contains code.
#define IMAGE_SCN_CNT_INITIALIZED DATA   0x00000040 // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED DATA 0x00000080 // Section contains uninitialized data.
#define IMAGE_SCN_MEM_EXECUTE    0x20000000 // Section is executable.
#define IMAGE_SCN_MEM_READ       0x40000000 // Section is readable.
#define IMAGE_SCN_MEM_WRITE    	  0x80000000 // Section is writable.

出处: Microsoft Platform SDK- winnt.h
  最后谈谈Name字段。Name成员不像C语言中的字符串- -样以NULL结束,并且没有“必须使用ASCII值”的限制。PE规范未明确规定节区的Name,所以可以向其中放入任何值,甚至可以填充NULL值。所以节区的Name仅供参考,不能保证其百分之百地被用作某种信息(数据节区的名称也可叫做.code )。
  下面看一下notepad.exe的节区头数组(共有3个节区),如图13-8所示。
在这里插入图片描述

图13-8 notepad.exe的IMAGE_SECTION_HEADER结构体数组

  接着看一下各结构体成员,如代码13-11所示。
代码13-11 notepad exe的IMAGE SECTION HEADER结构体数组的实际值
[IMAGE SECTION HEADER]

offsetvaluedescription
000001D82E746578Name(.text)
000001DC74000000
000001E000007748virtual size
000001E400001000RVA
000001E800007800size of raw data
000001EC0000400offset to raw data
000001F00000000offset to relocations
000001F400000000offset to line numbers
000001F80000number of relocations
000001FA0000number of line numbers
000001FC60000020characteristics
IMAGE_SCN_CNT_CODE
IMAGE_SCN_MEM_EXECUTE
IMAGE_SCN_MEM_READ
000002002E646174Name(.data)
0000020461000000
0000020800001BA8virtual size
0000020C00009000RVA
0000021000000800size of raw data
0000021400007C00offset to raw data
0000021800000000offset to relocations
0000021C00000000offset to line numbers
000002200000number of relocations
000002220000number of line numbers
00000224C0000040characteristics
IMAGE_SCN_CNT_INITIALIZED_DATA
IMAGE_SCN_MEM_READ
IMAGE_SCN_MEM_WRITE
000002282E727372Name(.rsrc)
0000022C63000000
0000023000008304virtual size
000002340000B000RVA
0000023800008400size of raw data
0000023C00008400offset to raw data
000002400000000offset to relocations
000002440000000offset to line numbers
000002480000number of relocations
000024A0000number of line numbers
0000024C4000040characteristics
IMAGE_SCN_CNT_INITIALIZED_DATA
IMAGE_SCN_MEM_READ

提示
  讲解PE文件时经常出现“映像”( Image)这一术语,希望各位牢记。PE文件加载到内存时,文件不会原封不动地加载,而要根据节区头中定义的节区起始地址、节区大小等加载。因此,磁盘文件中的PE与内存中的PE具有不同形态。将装载到内存中的形态称为“映像”以示区别,使用这一术语能够很好地区分二者。

13.4 RVA to RAW

  理解了节区头后,下面继续讲解有关PE文件从磁盘到内存映射的内容。PE文件加载到内存时,每个节区都要能准确完成内存地址与文件偏移间的映射。这种映射一般称为RVA to RAW,方法如下:
(1)查找RVA所在节区。
(2)使用简单的公式计算文件偏移( RAW )。
  根据IMAGE_ SECTION_ HEADER结构体,换算公式如下:

RAW - PointerToRawData = RVA - VirtualAddress
RAW = RVA - VirtualAddress + PointerToRawData

Quiz
  简单做个测试练习。图13-9描绘的是notepad.exe的文件与内存间的映射关系。请分别计算各个RVA (将计算器calc.exe切换到Hex模式计算会比较方便)。
在这里插入图片描述

图13-9 notepad.exe的文件与内存间的映射

Q1.RVA=5000时,File Offset=?
A1.首先查找RVA值所在节区。
  →RVA 5000位于第一个节区(.text) (假设ImageBase为01000000 )。
使用公式换算如下:
  →RAW=5000(RVA)-1000(VirtualAddress)+400(PointerToRawData)= 4400
Q2.RVA=13314时,File Offset=?
A2.查找RVA值所在节区。
  →RVA 13314位于第三个节区(.rsrc)。
使用公式换算如下:
  →RAW=13314(RVA)-B000(VA)+ 8400(PointerToRawData)=10714
Q3. RVA=ABA8时,File Offset=?
A3.查找RVA值所在节区。
  →RVA ABA8位于第二个节区(.data)。
使用公式换算如下:
  →RAW=ABA8(RVA)-9000(VA)+ 7C00(PointerToRawData)=97A8(×)
  →计算结果为RAW=97A8,但是该偏移在第三个节区( .rsrec)。RVA在第二个节区,而RAW在第三个节区,这显然是错误的。该情况表明“无法定义与RVA(ABA8)相对应的RAW值”。出现以上情况的原因在于,第二个节区的VirtualSize值(2000)要比SizeOfRawData值(800)要大很多,导致ABA8(RVA)-9000(VA)=1A18大于800超出第二个节区的范围。
提示
  RVA与RAW (文件偏移)间的相互变换是PE头的最基本的内容,各位一定要熟悉并掌握它们之间的转换关系。像Q3-样,PE文件节区中因VirtualSize与SizeOfRawData值彼此不同而引起的奇怪、有趣的
事还有很多(后面会陆续讲到)。
  以上就是对PE头基本结构体的介绍,接下来将继续学习PE头的核心内容——IAT (Import Address Table,导人地址表)与EAT(Export Address Table,导出地址表)。

13.5 IAT

  刚开始学习PE头时,最难过的一关就是IAT ( Import Address Table,导人地址表)。IAT保存的内容与Windows操作系统的核心进程、内存、DLL结构等有关。换句话说,只要理解了IAT,就掌握了Windows操作系统的根基。简言之,IAT是一种表格,用来记录程序正在使用哪些库中的哪些函数。

13.5.1 DLL

  讲解IAT前先学习一下有关DLL ( Dynamic Linked Library)的知识(知其所以然,才更易理解),它支撑起了整座Windows OS大厦。DLL翻译成中文为“动态链接库”,为何这样称呼呢?
  16位的DOS时代不存在DLL这一概念, 只有“库”( Library )一说。比如在C语言中使用printf()函数时,编译器会先从C库中读取相应函数的二进制代码,然后插入(包含到)应用程序。也就是说,可执行文件中包含着printf()函数的二进制代码。Windows OS支持多任务,若仍采用这种包含库的方式,会非常没有效率。Windows操作系统使用了数量庞大的库函数(进程、内存、窗口、消息等)来支持32位的Windows环境。同时运行多个程序时,若仍像以前一样每个程序运行时都包含相同的库,将造成严重的内存浪费(当然磁盘空间的浪费也不容小觐)。因此,WindowsOS设计者们根据需要引入了DLL这一概念,描述如下:
  不要把库包含到程序中,单独组成DLL文件,需要时调用即可。
  内存映射技术使加载后的DLL代码、资源在多个进程中实现共享。
  更新库时只要替换相关DLL文件即可,简便易行。
  加载DLL的方式实际有两种:一种是“显式链接”(Explicit Linking ),程序使用DLL时加载,使用完毕后释放内存;另一种是“隐式链接”( Implicit Linking ),程序开始时即一同加载DLL,程序终止时再释放占用的内存。IAT提供的机制即与隐式链接有关。下面使用OllyDbg打开notepad.exe来查看IAT。图13-10是调用CreateFileW)函数的代码,该函数位于kernel32.dll中。
在这里插入图片描述

图13-10调用CreateFileWO函数的代码

  调用CreateFileW)函数时并非直接调用,而是通过获取01001104地址处的值来实现(所有API调用均采用这种方式)。
  地址01001104是notepad.exe中.text节区的内存区域(更确切地说是IAT内存区域)。01001104地址的值为76BD3140,而76BD3140地址即是加载到notepad.exe进程内存中的CreateFileW()函数(位于kernel32.dll库中)的地址。此处产生一个疑问。
  “直接使用CALL 76BD3140指令调用函数不是更好、更方便吗?”
  甚至还会有人问:“编译器直接写CALL 76BD3140不是更准确、更好吗?”这是前面说过的DOS时代的方式。
  事实上,notepad.exe程序的制作者编译(生成)程序时,并不知道该notepad.exe程序要运行在哪种Windows ( 9X、2K、XP、Vista、7)、哪种语言(ENG、JPN、KOR等)、哪种服务包( ServicePack )下。上面列举出的所有环境中,kernel32.dll的版本各不相同,CreateFileW()函数的位置(地址)也不相同。为了确保在所有环境中都能正常调用CreateFileW()函数,编译器准备了要保存CreateFileWO)函数实际地址的位置(01001104),并仅记下CALL DWORD PTR DS:[1004404]形式的指令。执行文件时,PE装载器将CreateFileW()函数的地址写到01001104位置。
  编译器不使用CALL 76BD3140语句的另一个原因在于DLL重定位。DLL文件的ImageBase值一般为10000000。比如某个程序使用a.dll与b.dll时,PE装载器先把a.dl装载到内存的10000000( ImageBase)处,然后尝试把b.dll也装载到该处。但是由于该地址处已经装载了a.dll,所以PE装载器查找其他空白的内存空间( ex:3EO00000 ),然后将b.dll装载进去。
  这就是所谓的DLL重定位,它使我们无法对实际地址硬编码。另一个原因在于,PE头中表示地址时不使用VA,而是RVA。
提示
  实际操作中无法保证DLL一定会被加载到PE头内指定的ImageBase处。但是EXE文件(生成进程的主体)却能准确加载到自身的ImageBase中,因为它拥有自己的虚拟空间。
  PE头的IAT是代码逆向分析的核心内容。希望各位好好理解它。相信大家现在已经能够掌握IAT的作用了(后面讲解IAT结构为什么如此复杂时,希望各位也能很快了解)。

13.5.2 IMAGE_IMPORT_DESCRIPTOR

  IMAGE_IMPORT_DESCRIPTOR结构体中记录着PE文件要导入哪些库文件。
提示
  Import:导入,向库提供服务(函数)。
  Export:导出,从库向其他PE文件提供服务(函数)。
IMAGE_IMPORT DESCRIPTOR结构体如代码13-12所示。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;//特征
        DWORD   OriginalFirstThunk; //输入名称表(INT)的RVA
    };
DWORD   TimeDateStamp;//
DWORD   ForwarderChain;// 
    DWORD   Name;//DLL名字的指针
    DWORD   FirstThunk;//输入地址表(IAT)的 RVA
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

  执行一个普通程序时往往需要导人多个库,导人多少库就存在多少个IMAGE_IMPORT_DESCRIPTOR结构体, 这些结构体形成了数组,且结构体数组最后以NULL结构体结束。IMAGE_IMPORT_DESCRIPTOR中的重要成员如表13-5所示(拥有全部RVA值)。

表13-5 IMAGE_IMPORT_DESCRIPTOR结构体的重要成员
项目含义
OriginalFirstThunkINT的地址(RVA)
Name库名称字符串的地址(RVA)
FirstThunkIAT的地址(RVA)

提示
  ●PE头中提到的"Table” 即指数组。
  ●INT与IAT是长整型(4个字节数据类型)数组,以NULL结束(未另外明确指出大小)。
  ●INT中各元素的值为IMAGE_IMPORT_BY_NAME结构体指针(有时IAT也拥有相同的值)。
  ●INT与IAT的大小应相同。
图13-11描述了notepad.exe之kermel32 dIl的IMAGE_IMPORT_DESCRIPTOR结构。
在这里插入图片描述

图13-11 IAT

  图13-11中,INT与IAT的各元素同时指向相同地址,但也有很多情况下它们是不一致的(后面会陆续接触很多变形的PE文件,到时再逐一一讲解 )。
  下面了解一下PE装载器把导入函数输入至IAT的顺序。
代码13-13 IAT输入顺序:

1.读取IID的Name成员,获取库名称字符事(“kernel32.dll”)。
2.装载相应库。
→LoadLibrary(“kernel32.dl1”)
3.读取IID的OriginalFirstThunk成员,获取INT地址
4.逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_ NAME地址(RVA)。
5.使用IMAGE_IMPORT_BY_NAME的Hint (ordinal) 或Name项,获取相应函数的起始地址。
→GetProcAddress(“GetCurrentThreadld”)
6.读取IID的FirstThunk (IAT) 成員,获得IAT地址。
7.将上面获得的函数地址输入相应IAT数組值。
8.重复以上步骤4~7,直到INT结束(遇到NULL时)。

13.5.3 使用notepad.exe练习

  下面以notepad.exe为对象逐一查看。 先提一个问题: IMAGE_ IMPORT_DESCRIPTOR结构体数组究竟存在于PE文件的哪个部分呢?
  它不在PE头而在PE体中,但查找其位置的信息在PE头中,IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress的值即是IMAGE_IMPORT_DESCRIPTOR结构体数组的起始地址(RVA值)。IMAGE_IMPORT_DESCRIPTOR结构体数组也被称为IMPORT Directory Table(只有了解上述全部称谓,与他人交流时才能没有障碍)。
  IMAGE_OPTIONAL_HEADER32.DataDirectory[1]结构体的值如图13-12所示(第一个4字节为虚拟地址,第二个4字节为Size成员)。
在这里插入图片描述

图13-12 notepad.exe的IMAGE_OPTIONAL_HEADER32.DataDirectory[1]

  整理图13-12中的IMAGE OPTIONAL_ HEADER32.DataDirectory结 构体数组的信息以便查看,如表13-6所示(加深的部分是与导人相关的信息)。

表13-6 notepad.exe文件的DataDirectory数组-Import
偏移说明
00000015800000000RVA of EXPORT Directory
00000015C00000000size of EXPORT Directory
00000016000007604RVA ofIMPORT Directory
000000164000000C8size of IMPORT Dirctory
0000001680000B000RVA of RESOURCE Directory
00000016C00008304size of RESOURCE Directory

  像在图13-12中看到的一样,因为RVA是7604,故文件偏移为6A04。在文件中查看6A04,如图13-13所示(请使用“RVA to RAW"转换公式7604-1000+400=6A04)。
在这里插入图片描述

图13-13 notepad.exe的IMAGE_IMPORT_DESCRIPTOR结构体数组

  图13-13中,阴影部分即为全部的IMAGE_ IMPORT_ _DESCRIPTOR结构体数组,粗线框内的部分是结构体数组的第一个元素(也可以看到数组的最后是由NULL结构体组成的)。下面分别看一下粗线框中IMAGE_IMPORT_DESCRIPTOR结构体的各个成员,如表13-7所示。

表13-7 notepad.exe文件的第一个IMAGE_IMPORT_DESCRIPTOR结构体
文件偏移成员值(RVA)RVA to RAW
6A04OriginalFirstThunk(INT)0000799000006D90
6A08TimeDateStampFFFFFF
6A0CForwarderChainFFFFFFF
6A10Name00007AAC00006EAC
6A14FirstThunk(IAT)000012C4000006C4

  由于我们只是为了学习IAT,所以没有使用专业的PE Viewer,而是使用Hex Editor逐一查看(为方便起见,结构体的值(RVA)已经被转换为文件偏移。希望各位亲自转换一下)。下面依序看看吧。

1.库名称(Name)

  Name是一个字符串指针,它指向导入函数所属的库文件名称。在图13-14的文件偏移6EAC( RVA:7AAC-→RAW:6EAC )处看到字符串comdlg32.dlI了吧?
在这里插入图片描述

图13-14“comdlg32.dl1" 字符串

2.OriginalFirstThunk - INT

  INT是一个包含导人函数信息(Ordinal,Name)的结构体指针数组。只有获得了这些信息,才能在加载到进程内存的库中准确求得相应函数的起始地址(请参考后面EAT的讲解)。
  跟踪OriginalFirstThunk成员( RVA:7990-→RAW:6D90 )。图13-15是INT,由地址数组形式组成(数组尾部以NULL结束)。每个地址值分别指向IMAGE_IMPORT_BY_NAME结构体(参考图13-11)。跟踪数组的第一个值7A7A ( RVA),进入该地址,可以看到导人的API函数的名称字符串。
在这里插入图片描述

图13-15 INT

3.IMAGE_IMPORT_BY_NAME

  RVA: 7A7A即为RAW: 6E7A。文件偏移6E7A最初的2个字节值( 000F)为Ordinal,是库中函数的固有编号。Ordinal的后面为函数名称字符串PageSetupDIgW (同C语言一样,字符串末尾以Terminating NULL[’\0’]结束)。
  如图13-16所示,INT是IMAGE_ IMPORT_ BY_ NAME结构体指针数组( 参考代码13-12)。数组的第一个元素指向函数的Ordinal值000F, 函数的名称为PageSetupDlgW。
在这里插入图片描述

图13-16 IMAGE_IMPORT_BY_NAME

4.FirstThunk- IAT(Import Address Table)

  IAT的RVA:12C4即为RAW:6C4。
在这里插入图片描述

图13-17 FirstThunk- IAT

  图13-17中文件偏移6C4- -6EB区域即为IAT数组区域,对应于comdlg32.dII库。它与INT类似,由结构体指针数组组成,且以NULL结尾。
  IAT的第一个元素值被硬编码为76324906,该值无实际意义, notepad.exe文件加载到内存时,准确的地址值会取代该值。
提示
  ●其实我的系统(Windows XP SP3)中,地址76324906即是comdlg32.dll!PageSetupDlgW函数的准确地址值。但是该文件在Windows7中也能顺利运行。运行notepad.exe进程时, PE装载器会使用相应API的起始地址替换该值。
  ●微软在制作服务包过程中重建相关系统文件,此时会硬编入准确地址(普通的DLL实际地址不会被硬编码到IAT中,通常带有与INT相同的值)。
  ●另外,普通DLL文件的ImageBase为1000000,所以经常会发生DLL重定位。但是Windows系统DLL文件( kermel32/user32/gdi32等)拥有自身固有的ImageBase,不会出现DLL重定位。
  下面使用OllyDbg查看notepad.exe的IAT,在数据窗口跳转到010012C4附近的地址,右键选择长型->地址,就会把16进制的数据转换成32位地址并显示函数,如图13-18所示。
在这里插入图片描述

图13-18 notepad.exe的IAT

  notepad.exe的ImageBase值为1000000。所以comdlg32 dI!PageSetupDIgW函数的IAT地址为010012C4,其值为76324906, 它是API准确的起始地址值。
提示
  若在其他OS (2000、Vista 等)或服务包(SP1、SP2)中运行XP SP3 notepad.exe,010012C4地址中会被设置为其他值(相应OS的comdlg32.dl!PageSetupDlgW地址)。
  进人76324906地址中,如图13- 19所示,可以看到该处即为comdlg32.dI的PageSetupDIgW函数的起始位置。
  以上是对IAT的基本讲解,都是一些初学者不易理解的概念。反复阅读前面的讲解,并且实际进人相应地址查看学习,将非常有助于对概念的掌握。IAT是Windows逆向分析中的重要概念,一定要熟练把握。后面学习带有变形IAT的PE Patch文件时,会进一步学习IAT相关知识。
在这里插入图片描述

图13-19 comdlg32.PageSetupDlgW

13.6 EAT

  Windows操作系统中,“库”是为了方便其他程序调用而集中包含相关函数的文件( DLL/SYS )。Win32 API是最具代表性的库,其中的kermel32.dIl文件被 称为最核心的库文件。
  EAT是一种核心机制, 它使不同的应用程序可以调用库文件中提供的函数。也就是说,只有通过EAT才能准确求得从相应库中导出函数的起始地址。与前面讲解的IAT-样,PE文件内的特定结构体(IMAGE_EXPORT_DIRECTORY )保存着导出信息,且PE文件中仅有一个用来说明库EAT的IMAGE_EXPORT_DIRECTORY结构体。
提示
  用来说明IAT的IMAGE_ IMPORT_ DESCRIPTOR 结构体以数组形式存在,且拥有多个成员。这样是因为PE文件可以同时导入多个库。
  可以在PE文件的PE头中查找到IMAGE_EXPORT_DIRECTORY结构体的位置。IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress 值即是IMAGE_EXPORT_DIRECTORY结构体数组的起始地址(也是RVA的值)。
  图13-20显示的是kermel32.dll文件的IMAGE_ OPTIONAL_ HEADER32.DataDirectory0
在这里插入图片描述

图13-20 IMAGE_OPTIONAL_HEADER32.DataDirectory[0]

  为便于查看,将图13-20中的IMAGE_OPTIONAL_HEADER32.DataDirectory结构体数组信息整理如下表13-8(深色部分为“导出”相关信息)。

表13-8 notepad.exe文件的DataDirectory数组-Export
偏移说明
0000016000000000loader flags
0000016400000010number of directories
000001680000262CRVA of EXPORT Dirctory
0000016C00006D19size of EXPORT Directory
0000017000081898RVA of IMPORT Directory
0000017400000028size of IMPORT Directory

由于RVA值为262C,所以文件偏移为1A2C(希望各位多练习RVA与文件偏移间的转换过程)。

13.6.1 IMAGE_EXPORT_DIRECTORY

  IMAGE_EXPORT_DIRECTORY结构体如代码13-14所示。

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics; 	//标志,未用
    DWORD   TimeDateStamp; 		//时间戳 
    WORD    MajorVersion; 		//未用
    WORD    MinorVersion;		//未用
    DWORD   Name;					//指向该导出表的文件字符串
    DWORD   Base;					//导出函数的起始序号
    DWORD   NumberOfFunctions;	//所有的导出函数个数
    DWORD   NumberOfNames;		 //以函数名导出的函数个数
    DWORD   AddressOfFunctions;     // 导出函数地址表RVA
    DWORD   AddressOfNames;         // 函数名称地址表RVA
    DWORD   AddressOfNameOrdinals;  // 函数序号地扯表
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

  下面讲解其中的重要成员(全部地址均为RVA),如表13-9所示。

表13-9 IMAGE_EXPORT_DIRECTORY结构体的重要成员
项目含义
NumberOfFunctions实际Export函数的个数
NumberOfNamesExport函数中具名的函数个数
AddressOfFunctionsExport函数地址数组(数组元素个数=NumberOfFunctions)
AddressOfNames函数名称地址数组(数组元素个数=NumberOfNames)
AddressOfNameOrdinalsOrdinal地址数组(数组元素个数=NumberOfNames)

  图13-21描述的是kernel32.dll文件的IMAGE_EXPORT_DIRECTORY结构体与整个EAT结构。
在这里插入图片描述

图13-21 EAT

  从库中获得函数地址的API为GetProcAddress()函数。该AP引用EAT来获取指定API的地址。GetProcAddress()API拥有函数名称,下面讲解它如何获取函数地址。理解了这一过程,就等于征服了EAT。
GetProcAddress()操作原理

(1)利用AddressOfNames成员转到“函数名称数组”。
(2)“函数名称数组”中存储着字符串地址。通过比较(( strcmp)字符串,查找指定的函数名称(此时数组的索引称为name_index )。
(3)利用AddressOfNameOrdinals成员,转到orinal数组。
(4)在ordinal数组中通过name_index查找相应ordinal值。
(5)利用AddressOfFunctions成员转到“函数地址数组”(EAT )。
(6)在“函数地址数组”中将刚刚求得的ordinal用作数组索引,获得指定函数的起始地址。

  图13-21描述的是kernel32.dll文件的情形。kernel32.dll中所有导出函数均有相应名称,AddressOfNameOrdinals数组的值以index=ordinal的形式存在。但并不是所有的DLL文件都如此。导出函数中也有一些函数没有名称(仅通过ordinal导出),AddressOfNameOrdinals数组的值为index!=ordinal。所以只有按照上面的顺序才能获得准确的函数地址。
提示
  对于没有函数名称的导出函数,可以通过Ordinal查找到它们的地址。从Ordinal值中减去IMAGE_EXPORT_DIRECTORY.Base成员后得到一个值,使用该值作为“函数地址数组”的索引,即可查找到相应函数的地址。

13.6.2使用kernel32.dll练习

  下面看看如何实际从kernel32.dll文件的EAT中查找AddAtomW函数(参考图13-21)。由表13-8可知,kernel32.dll的IMAGE_EXPORT_DIRECTORY结构体RAW为142C。使用Hex Editor进入1A2C偏移处,如图13-22所示。
在这里插入图片描述

图13-22 kernel32.dll的IMAGE_EXPORT_DIRECTORY结构体

  图13-22深色部分就是IMAGE_EXPORT_DIRECTORY结构体区域。该IMAGE_EXPORT_DIRECTORY结构体的各个成员如表13-10所示。

表13-10 kernel32.dll文件的IMAGE_EXPORT_DIRECTORY结构体
文件偏移成员对应的RAW
1A2CCharacteristics00000000
1A30TimeDateStamp48025BE1
1A34MajorVersion0000
1A36MinorVersion0000
1A38Name00004B8E3F8E
1A3CBase00000001
1A40NumberOfFuctions000003B9
1A44NumberOfNames000003B9
1A48AddressOfFunctions000026541A54
1A4CAdderssOfNames000035382938
1A50AddressOfNameOrdinals0000441C381C

依照前面介绍的代码13-15的顺序查看。

1.函数名称数组

  AddressOfNames成员的值为RVA=3538,即RAW=2938。使用Hex Editor查看该地址,如图13-23所示。
在这里插入图片描述

图13-23 AddressOfNames

  此处为4字节RVA组成的数组。数组元素个数为NumberOfNames (3B9)。逐一跟随所有RVA值即可发现函数名称字符串。

2.查找指定函数名称

  要查找的函数名称字符串为“AddAtomW”,只要在图13-23中找到RVA数组第三个元素的值(RVA:4BB3→RAW:3FB3)即可。
  进入相应地址就会看到“AddAtomW”字符串,如图13-24所示。此时“AddAtomW”函数名即是图13-23数组的第三个元素,数组索引为2。
在这里插入图片描述

图13-24“AddAtomW”字符串

3.Ordinal数组

  下面查找“AddAtomW”函数的Ordinal值。AddressOfNameOrdinals成员的值为RVA:441C→RVA:381C,而“AddAtomW”函数的Ordinal值在数组索引为2即第三个元素的位置。
  在图13-25中可以看到,深色部分是由多个2字节的ordinal组成的数组(ordinal数组中的各元素大小为2个字节)。
在这里插入图片描述

图13-25 AddressOfNameOrdinals

4.ordinal

  将2中求得的index值(2)应用到3中的Ordinal数组即可求得Ordinal(2)。

AddressOfNameOrdinals[index]=ordinal (index=2,ordinal=2)

5.函数地址数组–EAT

  最后查找AddAtomW的实际函数地址。AddressOfFunctions成员的值为RVA:2654→RVA:1A54。图13-26深色部分即为4字节函数地址RVA数组,它就是Export函数的地址。
在这里插入图片描述

图13-26 AddressOfFunctions

6.AddAtomW函数地址

  图13-26中,为了获取“AddAtomW”函数的地址,将图13-25中求得的Ordinal用作图13-26数组的索引,得到RVA=00326D9。

AddressOfFunctions[ordinal]=RVA(ordinal=2,RVA=326D9)

  可以使用OllyDbg打开,kernel32.dll的ImageBase=7C7D0000。
在这里插入图片描述

  因此AddAtomW函数的实际地址(VA)为7C8326D9(7C800000+326D9=7C8326D9)。如图13-27所示:
在这里插入图片描述在这里插入图片描述

图13-27 AddAtomWO函数地址

  如图13-27所示,7C8026F1地址(VA)处出现的就是要查找的AddAtomW函数。以上过程是在DLL文件中查找Export函数地址的方法,与使用GetProcAddress()API获取指定函数地址的方法一致。

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值