PE结构详解

First Post:

Last Update:

本篇笔记以我个人能够理解为基础,尽可能将其写成其他人也能明白的笔记。如果发现其中存在错误,请务必指正。

范本与工具:010Editor & Notepad.exe & kernel32.dll

PE文件种类:

种类

主拓展名

可执行

EXE / SCR

驱动程序

SYS / VXD

DLL / OCX / CPL / DRV

对象文件

OBJ (但这并不是可执行的,在逆向分析中不怎么需要关心)

正经的PE结构头包括:

DOS头(DOS header) & DOS存根(Dos Stub) & 节区头(Section header) & NT头(NT header)

    其中,NT头包括了 文件头 与 可选头 。而节区头包括了 .text / .bss / .rdata / .data / .rsrc / .edata / .idata / .pdata / .debug 这九个预定义段,其分别规定了不同区块的访问权限、特性等内容。但并不是说每个应用程序都一定要规规矩矩的保留这些义段,对于那些用不到的区段是在程序中没有的,这一点可以自行打开程序确认。

(比如:Notepad.exe只有 .text / .data / .rsrc 这三个义段和节区)

(节区头的作用:PE文件包含多个节区,其包括了 Code节区 / Data节区 / Resource节区 等诸多节区,正因为节区之间相互区分,所以需要规定好程序可以对 一个节区做些什么 ,因此需要在节区头中去规定。所以这些义段和节区是一一对应的关系。)

    PE头的详细内容将在下面写出,但在此之前,我觉得有必要先介绍一下VA,RVA等内容。以下也是些一概而论的东西,细节都将在之后解释。

    VA(Virtual Address):虚拟地址。

    RVA(Relative Virtual Address):相对虚拟地址

    FOA(File Offset Address):文件偏移地址。但是在很多地方并不这么称呼,他们会用FA,RAW来称呼FOA,实际上是一个东西。

    Image Base:模块地址。指可执行文件加载到内存的时候所在的位置。

    虚拟地址间的关系:

RVA+Image Base = VA

    在很多时候,将一个程序加载到内存的时候,他的实际物理地址是不确定的。但文件总不会自己去寻址,必须要有人事先告诉他将要调用的函数在什么地方,如果用实际地址去描述的话,将会变得十分困难。为解决这个问题,人们构造出了“虚拟地址”的概念。将一个文件载入内存的时候,不管他被载入到了什么地方,都将其头地址映射到一块规定大小的虚拟地址空间(虚拟内存空间的大小可能比实际加载进内存所用的大小还大),之后在调用任何一个函数的时候,都只需要访问虚拟地址即可。

    但实际在访问的时候,也不是直接访问虚拟地址(特别是对DLL等动态链接库),而是利用RVA来访问。比方说初始位置在0x1000,而某个函数在0x1400,则在访问该函数的时候通过0x1000+0x400来访问(RVA即是指0x400)。之所以这样,还是因为PE文件加载进内存的时候,也可能发生“当前位置已经被占用”的问题,但加载必然是按顺序进行的,所以相对位置不会发生变化。

    (注:我觉得这样解释还是有些晦涩,所以再换了一种说法————将一个文件加载进内存,但现在我们无法知道其实际地址被放到了哪里。但我们一定清楚,我们想要调用的函数在文件开头往下找0x400的地方,那么程序在访问的时候将虚拟地址基址加上这个RVA就能找到实际的虚拟地址,然后再映射回去就能到达实际的物理地址。)

    接下来将详细对PE头的内容进行介绍,这里用Notepad.exe来示范。将其用010Editor打开(用Hex Editor也行,但010的自动识别功能会在这里提供很大的方便,对我来说减少了很多不必要的烦恼……)

    如图,010会将上述的PE结构头全都识别出来,并标好位置等。这将为接下来的介绍减少很多不必要的检索操作。

DOS头

    对应IMAGE_DOS_HEADER。在Microsoft Platform SSDK-winnt.h中可以找到他的成员,实际上就是一个C语言中的结构体。(通常是64字节的大小,但一些可以为缩减而设计的PE文件惊人的小,整个PE文件都只有97字节。但那都是特例,在学习过程中,我们可以权且将PE头每个部分都当作固定长度的结构体理解,不需要在意那些特例)

(注:结构体代码放在结尾,其成员在下图可见)

    MZSignature:DOS签名(4D5A经过ASCII值转换会为“MZ”,但图中写的是5A4D,这与Intel系列的CPU储存方式有关,该方法被称为“小端序标识法”,具体内容可自行搜索了解,在汇编的学习过程中,教科书上通常也会有介绍)。在一些书中,作者将把这一栏称之为e_magic**(原因出自于结构体定义的时候写下的名称,但几经迭代后可能就变得不一样了)。另外,MZ取自DOS可执行文件设计者的名字首字母**。

    AddressOfNewExeHeader:指示NT头的偏移(不同文件可能有不同的值,也被称之为e_lfanew),但注意,其数值应为000000E0(小端序)。

DOS存根:

    比较特殊的一项,即便没有这个结构体,程序也能在Windows下运行。但在DOS环境下,将会执行DOS存根中保留的代码。在本例中,将其在DOS环境下将会输出“This program cannot  be run in DOS mode”后退出(具体的执行方式可以查看其汇编代码)。(所用用这个特性也能做很多乱七八糟的事情,比如在EXE文件中创建另一个文件,然后支持DOS和Windows两个环境等)

NT头:(大小为F8)

    Signature:签名。(同DOS签名相似,其数值经ASCII转换后为”PE”)

IMAGE_FILE_HEADER文件头:(FileHeader)

    Machine:每个CPU都有唯一的Machine码,算是一种规定。

1
#define IMAGE_FILE_MACHINE_I386 0x14c // Intel 386.

    诸如这样的定义,其表示兼容32位的Intel x86芯片。Notepad中的Machine码即位14C。类似的定义还有很多很多,细节可自查。

    NumberOfSections:用于指出文件中存在的节区数量。(如果实际的节区数和这里记录的不一样,运行的时候会出错)

    SizeOfOptionalHeader:用于指出IMAGE_OPTIONAL_HEADER32结构体的长度。(其实这一项是给PE装载器看的,结构体的长度都是固定好了的,不会因为这一项数值改变而改变)

    Characteristics:用于标识文件的属性。这一栏的属性比较不好逐个说明,详细的内容放在最后的附录里面,可自行对照每一栏的用处。

    TimeDataStamp:标识文件被编译器创建的时间。(应该是没太大用处的一项)

IMAGE_OPTIONAL_HEADER32可选头:(OptionalHeader)

    这一栏太大了,以至于我没办法一张屏幕把全部都包括进图里……

    Magic:标识32位与64位的标记(10B——32位,20B——64位)。

    AddressOfEntryPoint:EP(EntryPoint)的RVA值。指出最先执行的代码的位置。

    ImageBase:指出文件的优先装入地址(32位的虚拟内存的范围在0~FFFFFFFF,不同类型的文件回被写入不同的值。在执行的时候,PE装载器创建进程后,将会把EIP寄存器的值设定为ImageBase+AddressOfEntryPoint)

    SectionAlignment / FileAliganment:前者指定了节区在内存中的最小单位,后者指定了节区在磁盘中的最小单位。(磁盘文件或内存的节区大小一定和这二者成整数倍)

    SizeOfImage:指定PE Image在虚拟内存中所占的空间的大小。

    SizeOfHeaders:用于指出整个PE头的大小。

    Subsystem:标识文件的类型。

含义

备注

1

Driver

系统驱动(如:ntfs.sys)

2

GUI

窗口应用程序(如:notepad.exe)

3

GUI

控制台应用程序(如:cmd.exe)

    NumberOfRvaAndSize:指定DataDirectory数组(本例中也叫DataDirArray)的个数。

    **DataDirectory(DataDirArray)**:这些数组里只有两个元素,VirtualAddress和Size。这些内容能够用于计算RAW的实际地址。

IMAGE_SECTION_HEADER节区头:

    能够规定不同节区的特性、访问权限等内容。同样按照数组的方式排列。一个单元对应一个节区。

    VirtualAddress:内存中节区的起始地址

    VirtualSize:内存中节区的大小

    SizeOfRawData:磁盘文件中节区所占的大小

    PointerToRawData:磁盘文件中节区的起始位置

    Characteristics:节区属性

    其中,VA和PTRD(都是简写)不带任何值,由SectionAlignment和FileAlignment决定。

RVA to RAW:

RAW - PointerToRawData = RVA - VirtualAddress

    公式如上。在了解了以上信息后,即可通过该公式计算出RAW的值了。

    范例:以Notepad.exe为例。在节区头的第一个单元中可找到VA=1000h,以及PointerToRawData=400h。

    而RVA在DataDirArray中IMAGE_DATA_DIRECTORY Import中可见。其值为7604h。最后得出RAW=6A04h

    (可能会有人和我一样开始疑惑为什么RVA是这个值。事实上这个值是随意规定的,这个公式的目的是“我知道RVA,现在想计算RAW”,所以其实可以随意设定RVA值。但有必要说明的是,不同的RVA值会处在不同的节区中,例如RVA=5000就在.text节区中,所以才到节区头中的第一个单元找VirtualAddress和PointerToRawData)

    (如果你直接在010中转到6A04这个位置,你会发现它确实对应了了comdlg32.dll的数据块起始位置)

动态链接库DLL

    加载DLL的方式主要有两种——“显示链接”(用到时加载,用完就释放)和“隐式链接”(程序开始时加载,程序结束时释放)。而IAT提供的机制与隐式链接有关。如果使用OD或者x64dbg等反汇编软件打开范例,将在其调用函数的时候发现其写法套用了两层(call 1001104,而1001104处的值为7C8107F0,然后才是7C8107F0地址处存放的函数)。其中,1001104是一个固定的值,但7C8107F0则根据操作系统的不同而出现差异,于是在加载程序的时候,PE装载器会将正确的地址装入1001104处,以保证程序在各种环境下都能够正常使用(这样做的理由很多,除了让其能在多平台兼容外,也有因为实际地址可能出现不同的原因存在)。

    以该链接库为例。

    库名称Name:在注释里就有标出。通过7990算处RAW后直接查找过去,也能找到comdlg32.dll的字符串。

    **OriginalFirstThunk(INT)**:包含函数导入信息的结构体指针。通过相同的方法到达6D90可见多个指针。(这实际上是一个数组,以NULL结尾,所以到00000000的时候就算结束了)

    自7A7A开始,每4个字节代表了一个指针。如果跟入7A7A(算出的RAW为6E7A),就能找到函数的名称。(名称也是数组,同样用\0结尾。而000F为库内的函数的编号)

    **导入地址表FirstThunk(IAT——Import Address Table)**:将12C4换为RAW=6C4,跟入。

    标蓝的区段即为IAT数组区域,对应了comdlg32.dll库。与INT类似,也用NULL结尾,以结构体指针为成员。

    但76344906这个指针没有实际意义,当程序加载的内存的时候,准确的地址值会取代这个数值(这其中大概是PE装载器做了很多,但我不太了解这个东西)。

EAT:

IMAGE_EXPORT_DIRECTORY:

    NumberOfFunction:实际Export函数的个数

    NumberOfNames:Export函数中有名字的函数个数、

    AddressOfFunctions:Export函数地址数组

    AddressOfNames:函数名称地址数组

    AddressOfNameOrdinals:Ordinal地址数组

    实际上,从库中获取函数需要调用GetProcAddress()函数。以下为该过程的流程。

    首先,利用AddressOfNames成员转到函数名称位置。通过比较字符串的方法,查找到我们所想要的函数名称(这时候该数组的索引是name_index)。(可以假设我们在AddressOfNames[2]的位置找到了目标的名称,那么index=2)

    再利用AddressOfNameOrdinals数组找到对应的Ordinal值。(上一步找到了Index=2,AddressOfNameOrdinals[Index]=Ordinal,所以Ordinal=2)

    通过AddressOfFunctions和刚才获得的Ordinal值即可在AddressOfFunctions数组中获取目标函数的地址。(AddressOfFunctions[Ordinal]=目标函数的RVA)

最后是一些定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//

WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;

//
// NT additional fields.
//

DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_TARGET_HOST 0x0001 // Useful for indicating we want to interact with the host and not a WoW guest.
#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
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP 0x01a3
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2 // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT 0x01c4 // ARM Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_AM33 0x01d3
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon
#define IMAGE_FILE_MACHINE_CEF 0x0CEF
#define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian
#define IMAGE_FILE_MACHINE_ARM64 0xAA64 // ARM64 Little-Endian
#define IMAGE_FILE_MACHINE_CEE 0xC0EE