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

Posted 大灬白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《逆向工程核心原理》读书笔记——第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时加载,使用完毕后释放内存;

以上是关于《逆向工程核心原理》读书笔记——第13章 PE文件格式的主要内容,如果未能解决你的问题,请参考以下文章

《逆向工程核心原理》读书笔记——第8章 Visual Basic编写的exe程序特征

《逆向工程核心原理》读书笔记——第15章 调试UPX压缩的notepad程序

《逆向工程核心原理》读书笔记——第7章 栈帧

《逆向工程核心原理》读书笔记——第10章 函数调用约定

《逆向工程核心原理》读书笔记——第5章 栈

《逆向工程核心原理》读书笔记——第6章 汇编编写的exe程序入口