0%

PE 结构学习

前言

PE 是Windows 平台上最主流的可执行文件格式。

可以在 winnt.h 中找到 PE 结构的定义。

EXE 与 DLL 其实使用的是一样的 PE 结构,只是在一个字段的内容不同,同时 DLL 拥有一些自己的扩展。

64位的 Windows 可执行程序使用的格式叫做 PE32+ , 没有新的结构加入。对很多字段进行了长度扩展。

当 PE 文件被装载入内存后,内存中的版本就叫 模块(Module)。映射文件的起始地址叫做模块句柄(hModule)。这个初始地址也叫基地址 ImageBase。

按照默认配置 VC++ 建立的 EXE 基地址 40 0000H , DLL 为 1000 0000H。

虚拟地址 VA = 基地址 ImageBase + 相对虚拟地址 RVA

PE 文件在磁盘中时,某个数据的位置相对文件开头的偏移量叫做 文件偏移地址 File Offset,或物理地址 RAW Offset。使用十六进制编辑器打开文件时显示的地址就是文件偏移地址。

如果编写解析 PE 的程序,读取其内部相关内容(输入表、输出表等),不能将区块名称做为参考。正确的方法是按照数据目录表中的字段进行定位。

INT 和 IAT 在磁盘中是一样的。在加载入内存后,INT 中的内容不变, IAT 中的每一项被改写为该函数的地址。

参考资料:

  • 《加密与解密》

正文

PE format

MS-DOS 头

包括 MZ-header 和 DOS stub,后者就是一个 DOS 程序,通常用来显示这个程序不能在 DOS 模式下跑。

开头的两个字节必须是 5A4D,即 ASCII 值 “MZ”。是MS-DOS 创建者之一的名字缩写。文件开始偏移 3CH 处是 e_lfanew 是真正 PE 文件头的 RVA,大小 4 个字节。可以看到,我使用 PE View 打开的这个文件的 PE 文件头的偏移是 F8H。

image-20200310215547454

PE 文件头

PE 文件头是 “IMAGE_NT_HEADERS” 的简称。位于 DOS stub 后面。可以通过前面的 e_lfanew 来找到 PE 文件头。其内部结构可参见这张我从 winnt.h 中截取的截图。

IMAGE_NT_HEADER

Signature

在有效的 PE 文件中,该字段值为 00004550H,即 ASCII码 PE\0\0 。

IMAGE_FILE_HEADER

映射文件头,包含一些 PE 文件的基本信息。指出 IMAGE OPTIONAL HEADER 的大小。

image-20200310220803060

Machine

表示了目标 CPU 类型。 表格参考了 《加密与解密》。

机器 标志
Intel i386 14CH
MIPS R3000 162H
MIPS R4000 166H
Alpha AXP 184H
Power PC 1F0H

TimeDataStamp

文件创建时间,自 1970.1.1 GMT 以来的秒数。

PointerToSymbolTable、NumberOfSymbols

COFF 函数表在文件中的偏移位置以及其中符号数目。由于较为少见,不做赘述。

SizeOfOptionalHeader

就是 IMAGE_OPTIONAL_HEADER 的大小。32位通常值为 00E0H , 64 位通常为 00F0H。也有可能出现较大值。

Characteristics

文件属性。具体定义参照下图,同样来自 winnt.h。

image-20200310222648306

IMAGE_OPTIONAL_HEADER

由于截图太长了就不放了,直接扔代码,这里以 64位 为例。

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
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;

#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107

导入表(import table)

在运行前,磁盘上的PE文件是不知道他要调用的DLL中的函数的地址的。

导入可以这样划分:一般情况下我们编写程序调用DLL都是隐式调用,而如果我们在程序中使用 LoadLibrary 函数和 GetProcAddress 函数,这种方法叫显式调用。

要导入函数,我们可以在PE文件中看见其调入表。导入表的每个元素(IID, Image Import descriptor)都是一个 DLL 。 每个元素的结构体定义如下。IID数组以一个 NULL 结束(即全0填充)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)

DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

挑上面的重要的说,Original First Thunk 指向的是 INT(Import Name Table)。First Thunk 指向的是 IAT(Import Address Table)。Name 则指向DLL 的名称字符串。

INT 和 IAT 中的元素都是 IMAGE THUNK DATA。其定义如下。在 32 位情况下是一个 双字。

1
2
3
4
5
6
7
8
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;

对于这个数据结构,如果最高位是1,表示后面的位数代表一个函数序号,最高位是0,表示此时的双字的值代表一个 RVA,指向一个 IMAGE_IMPORT_BY_NAME 结构。

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

实际分析一个 Import Table。

import table example

从图中可以看到每一个 IID 代表了一个 DLL。该数组最终以一个全部 为 0 的 IID 结尾。

然后我们根据每个 IID 中的 Original First Thunk 就可以找到 INT 的地址。INT内容如下图所示。

INT Example

可以看到 INT 中的内容是根据不同的DLL进行分割,每个DLL的函数结束之后都会有一个 全 0 的 INT 元素标志结束。每一个 INT 元素代表了一个函数。

而 IAT 在装载入内存前其实和 INT 的内容是完全一致的,在装载入内存后,由装载器负责将对应函数地址装入 IAT 每个元素的对应位置。

RVA 与 FOA 的转换

文件偏移地址(File Offset Address,FOA)和内存无关,它是指某个位置距离文件头的偏移。

杂项

为什么我拿 Ollydbg 或 WinDbg 打开程序以后,没有装载到 IMAGE_BASE ?

很有可能是链接的时候开启了基地址随机化。以 VS 2019 为例,关闭地址随机化的方法为:项目属性、链接器、高级、随机基址。

img

把随机基址改成否就可以看到打开后被正常的装载到了 400000H 了。

思考题

File Alignment 和Section Alignment为什么不同?

文件对齐参考了磁盘扇区大小,因为磁盘扇区大小一般为 512 字节,故一般文件对齐为 200H。而节对齐考虑了内存对齐,通常为 4 K 对齐,即 1000 H。

程序会加载到内存的什么位置? 如何确保它从哪里开始执行?

AddressofEntryPoint+ImageBase。

ImageBase被占了,如何处理?

哪些代码或数据需要重定向?

为什么PE结构中有许多RVA?PE结构自身有哪些RVA?

16个数据表,IDT表,重定位表,AddressofEntryPoint, 节表

什么是RVA?谁把RVA变化为VA?

rva - SectionTables[i].VirtualAddress + SectionTables[i].PointerToRawData;

VRk = RVA - File_Offset

对于我们编写的程序而言,哪些地方(变量)需要重定位?

IAT和EAT的作用?


施工中