《逆向工程核心原理》读书笔记——第13章 PE文件格式
Posted 大灬白
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《逆向工程核心原理》读书笔记——第13章 PE文件格式相关的知识,希望对你有一定的参考价值。
第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所示。
种类 | 主扩展名 |
---|---|
可执行系列 | 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.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.2 VA&RVA
VA指的是进程虚拟内存的绝对地址,RVA(Relative Virtual Address,相对虚拟地址)指从某个基准位置(ImageBase)开始的相对地址。VA与RVA满足下面的换算关系。
PE头内部信息大多以RVA形式存在。原因在于,PE文件(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他PE文件(DLL)。此时必须通过重定位(Relocation)将其加载到其他空白的位置,若PE头信息使用的是VA,则无法正常访问。因此使用RVA来定位信息,即使发生了重定位,只要相对于基准位置的相对地址没有变化,就能正常访问到指定信息,不会出现任何问题。
提示
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所示。
根据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中,文件偏移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所示。
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,以结构体成员的形式表示如下。
Offset | Value | Description |
---|---|---|
000000E4 | 014C | machine |
000000E6 | 0003 | number of sections |
000000E8 | 48025287 | time date stamp (Mon Apr 14 03:35:51 2008) |
000000EC | 00000000 | offset to symbol table |
000000F0 | 00000000 | number of symbols |
000000F4 | 00E0 | size of optional header |
000000F6 | 010F | characteristics 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所示。
值 | 含义 | 备注 |
---|---|---|
1 | Driver文件 | 系统驱动(如:nfs.sys) |
2 | GUI文件 | 窗口应用程序(如:notepad.exe) |
3 | CUI文件 | 控制台应用程序(如: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-8所示。
代码13-8 notepad.exe文 件的IMAGE OPTIONAL HEADER
[ IMAGE_ OPTIONAL_ HEADER ] - notepad . exe
offset | value | description |
---|---|---|
000000F8 | 010B | magic |
000000FA | 07 | major linker version |
000000FB | 0A | minor linker version |
000000FC | 00007800 | size of code |
00000100 | 0000800 | size of initialized data |
00000104 | 0000000 | size of uninitialized data |
00000108 | 0000739D | address of entry point |
0000010C | 00001000 | base of code |
00000110 | 00009000 | base of data |
00000114 | 01000000 | image base |
00000118 | 00001000 | section alignment |
0000001C | 0000200 | file alignment |
00000120 | 0005 | major 0S version |
00000122 | 0001 | minor 0S version |
00000124 0005 | major image version | |
00000126 | 0001 | minor image version |
00000128 | 0004 | major subsystem version |
0000012A | 0000 | minor subsystem version |
0000012C | 00000000 | win32 version value |
00000130 | 00014000 | size of image |
00000134 | 00000400 | size of headers |
00000138 | 000126CE | Checksum |
0000013C | 0002 | subsystem |
0000013E | 8000 DLL | characteristics |
00000140 | 00040000 | size of stack reserve |
00000144 | 00011000 | size of stack commit |
00000148 | 00000000 | size of heap reserve |
0000014C | 00001000 | size of heap commit |
00000150 | 00000000 | loader flags |
00000154 | 00000010 | number of di rectories |
00000158 | 00000000 | RVA of EXPORT Directory |
0000015C | 00000000 | size of EXPORT Directory |
00000160 | 00007604 | RVA of IMPORT Directory |
00000164 | 000000C8 | size of IMPORT Directory |
00000168 | 00008000 | RVA of RESOURCE Directory |
0000016C | 00008304 | size of RESOURCE Di rectory |
00000170 | 00000000 | RVA of EXCEPTION Directory |
00000174 | 00000000 | size of EXCEPTION Directory |
00000178 | 00000000 | RVA of SECURITY Directory |
0000017C | 00000000 | size of SECURITY Directory |
00000180 | 00000000 | RVA of BASERELOC Directory |
00000184 | 00000000 | size of BASERELOC Directory |
00000188 | 00013050 | RVA of DEBUG Directory |
0000018C | 0000001C | size of DEBUG Directory |
00000190 | 00000000 | RVA of COPYRIGHT Directory |
00000194 | 00000000 | size of COPYRIGHT Directory |
00000198 | 00000000 | RVA of GLOBALPTR Directory |
0000019C | 00000000 | size of GLOBALPTR Directory |
000001A0 | 00000000 | RVA of TLS Directory |
00000144 | 00000000 | size of TLS Directory |
000001A8 | 000018A8 | RVA of LOAD CONFIG Directory |
000001AC | 0000040 | size of LOAD CONFIG Directory |
000001B0 | 00000250 | RVA of BOUND IMPORT Directory |
000001B4 | 00000D0 | size of BOUND_ IMPORT Di rectory |
000001B8 | 00001000 | RVA of IAT Directory |
000001BC | 0000348 | size of IAT Directory |
000001C0 | 0000000 | RVA of DELAY_ IMPORT Directory |
000001C4 | 0000000 | size of DELAY IMPORT Directory |
000001C8 | 0000000 | RVA of COM DESCRIPTOR Di rectory |
000001CC | 0000000 | size of COM DESCRIPTOR Directory |
000001D0 | 0000000 | RVA of Reserved Directory |
000001D4 | 0000000 | size of Reserved Directory |
13.3.6 节区头
节区头中定义了各节区属性。看节区头之前先思考- -下:前面提到过,PE文件中的code(代码)、data(数据)、resource(资源)等按照属性分类存储在不同节区,设计PE文件格式的工程师们之所以这样做,一定有着某些好处。
我认为把PE文件创建成多个节区结构的好处是,这样可以保证程序的安全性。若把code与data放在一个节区中相互纠缠(实际上完全可以这样做)很容易引发安全问题,即使忽略过程的烦琐。
假如向字符串data写数据时,由于某个原因导致溢出(输入超过缓冲区大小时),那么其下的code(指令)就会被覆盖,应用程序就会崩溃。因此,PE文件格式的设计者们决定把具有相似属性的数据统一保存在一个被称为“节区”的地方,然后需要把各节区属性记录在节区头中(节区属性中有文件/内存的起始位置、大小、访问权限等)。
换言之,需要为每个code/data/resource分别设置不同的特性、访问权限等,如表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结构体中要了解的重要成员(不使用其他成员)。
项目 | 含义 |
---|---|
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-11所示。
代码13-11 notepad exe的IMAGE SECTION HEADER结构体数组的实际值
[IMAGE SECTION HEADER]
offset | value | description |
---|---|---|
000001D8 | 2E746578 | Name(.text) |
000001DC | 74000000 | |
000001E0 | 00007748 | virtual size |
000001E4 | 00001000 | RVA |
000001E8 | 00007800 | size of raw data |
000001EC | 0000400 | offset to raw data |
000001F0 | 0000000 | offset to relocations |
000001F4 | 00000000 | offset to line numbers |
000001F8 | 0000 | number of relocations |
000001FA | 0000 | number of line numbers |
000001FC | 60000020 | characteristics IMAGE_SCN_CNT_CODE IMAGE_SCN_MEM_EXECUTE IMAGE_SCN_MEM_READ |
00000200 | 2E646174 | Name(.data) |
00000204 | 61000000 | |
00000208 | 00001BA8 | virtual size |
0000020C | 00009000 | RVA |
00000210 | 00000800 | size of raw data |
00000214 | 00007C00 | offset to raw data |
00000218 | 00000000 | offset to relocations |
0000021C | 00000000 | offset to line numbers |
00000220 | 0000 | number of relocations |
00000222 | 0000 | number of line numbers |
00000224 | C0000040 | characteristics IMAGE_SCN_CNT_INITIALIZED_DATA IMAGE_SCN_MEM_READ IMAGE_SCN_MEM_WRITE |
00000228 | 2E727372 | Name(.rsrc) |
0000022C | 63000000 | |
00000230 | 00008304 | virtual size |
00000234 | 0000B000 | RVA |
00000238 | 00008400 | size of raw data |
0000023C | 00008400 | offset to raw data |
00000240 | 0000000 | offset to relocations |
00000244 | 0000000 | offset to line numbers |
00000248 | 0000 | number of relocations |
000024A | 0000 | number of line numbers |
0000024C | 4000040 | characteristics 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结构体,换算公式如下:
Quiz
简单做个测试练习。图13-9描绘的是notepad.exe的文件与内存间的映射关系。请分别计算各个RVA (将计算器calc.exe切换到Hex模式计算会比较方便)。
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程序特征