PE Format Note

PE Format Note

Baes

PE文件不是作为单一内存映射文件被载入内存的,Windows加载器(PE加载器)遍历 PE 文件决定文件的那一部分被映射


PE文件被加载到内存后,内存中的版本被称为模块

映射文件的起始地址称为模块句柄,可以过模块句柄访问内存中的其他数据结构
这个初始内存地址也称为 基地址(ImageBase)

在 32位 Windows系统中可以直接调用 GetModuleHandle 以取得指向 DLL 的指针,通过指针访问该 DLL Module 的内容

1
HMOULDE GetModuleHandle(LPCTSTR lpModuleName);

调用该函数时会传递一个可执行文件或 DLL 文件名字符串,如果系统找到文件,则返回该可执行文件或 DLL 文件映像所加载的基地址,也可以调用 GetModuleHandle来传递 NULL参数,此时将返回调用者的可执行文件的基地址


基地址的值是由 PE文件本身设定的,默认 EXE文件的基地址是 400000h、DLL 文件的基地址是 10000000h


PE文件被映射到内存时,每个程序都有自己的虚拟空间,这个虚拟空间的内存地址称为虚拟地址(VA)


为了避免在 PE文件中出现绝对内存地址引入了 相对虚拟地址(RVA) 的概念

RVA 只是内存中的一个简单的、相对于 PE文件载入地址(基地址)的偏移位置,它是一个相对的地址(或称偏移量)

假设一个 EXE文件从 400000h处载入,而它的代码区块开始于 401000h处,代码区块的 RVA 计算如下:

1
目标地址 401000h - 载入地址 400000h = RVA 1000h

将一个 RVA 转换成真实的地址,用实际的载入地址加 RVA,得到实际的内存地址:

1
2
3
虚拟地址VA = 基地址(ImageBase) + 相对虚拟地址(RVA)

(ImageBase)400000h + (RVA)1000h = (VA)401000h

当 PE文件存储在磁盘中时,某个数据的位置与文件头的偏移量称为 文件偏移地址(File Offset)**或 **物理地址(Raw Offset); 文件偏移地址从 PE文件的第一个字符开始计数,起始值为 0; 用十六进制工具打开文件时,所显示的地址值就是文件偏移地址


Dos 头

40 bytes


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// winNT.h

typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; /* 00: MZ Header signature */
WORD e_cblp; /* 02: Bytes on last page of file */
WORD e_cp; /* 04: Pages in file */
WORD e_crlc; /* 06: Relocations */
WORD e_cparhdr; /* 08: Size of header in paragraphs */
WORD e_minalloc; /* 0a: Minimum extra paragraphs needed */
WORD e_maxalloc; /* 0c: Maximum extra paragraphs needed */
WORD e_ss; /* 0e: Initial (relative) SS value */
WORD e_sp; /* 10: Initial SP value */
WORD e_csum; /* 12: Checksum */
WORD e_ip; /* 14: Initial IP value */
WORD e_cs; /* 16: Initial (relative) CS value */
WORD e_lfarlc; /* 18: File address of relocation table */
WORD e_ovno; /* 1a: Overlay number */
WORD e_res[4]; /* 1c: Reserved words */
WORD e_oemid; /* 24: OEM identifier (for e_oeminfo) */
WORD e_oeminfo; /* 26: OEM information; e_oemid specific */
WORD e_res2[10]; /* 28: Reserved words */
DWORD e_lfanew; /* 3c: Offset to extended header */
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

xxe -l 0x40 calc.dll
xxe -l 64 calc.dll

1
2
3
4
00000000: 4d5a 9000 0300 0000 0400 0000 ffff 0000  MZ..............
00000010: b800 0000 0000 0000 4000 0000 0000 0000 ........@.......
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 d000 0000 ................

0x20 填充为 0,偏移从 0x30 开始

1
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................

PE文件头偏移量

xxd -s 0x3c -l 4 calc.dll

1
0000003c: d000 0000                                ....

e_lfanew 值为: 000000d0h


NT 头/ PE 文件头

使用 DOS头的 e_lfanew 字段得到 PE文件头的起始偏移量,用其加上基址,得到 PE 文件头的指针

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE 文件标示
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

NT 文件头 / PE 文件头

包含 PE 文件的一些基本信息,偏移量基于 PE文件头(IMAGE_NT_HEADERS)

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; // IMAGE_OPTIONAL_HEADER32 结构的大小
WORD Characteristics; // 文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

000000d0 PE文件头起始偏移量

1
xxd -s 0xd0 -l 128 calc.dll

Out:

1
2
3
4
5
6
7
8
000000d0: 5045 0000 4c01 0400 a00b 0d53 0000 0000  PE..L......S....
000000e0: 0000 0000 e000 0221 0b01 0c00 0002 0000 .......!........
000000f0: 000e 0000 0000 0000 3011 0000 0010 0000 ........0.......
00000100: 0020 0000 0000 0010 0010 0000 0002 0000 . ..............
00000110: 0600 0000 0000 0000 0600 0000 0000 0000 ................
00000120: 0050 0000 0004 0000 0000 0000 0200 4005 .P............@.
00000130: 0000 1000 0010 0000 0000 1000 0010 0000 ................
00000140: 0000 0000 1000 0000 0000 0000 0000 0000 ................

1
2
3
4
WORD(2)  × 9 = 18
BYTE(1) × 2 = 2
DWORD(4) × 19 = 76
= 96

可选头

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
typedef struct _IMAGE_OPTIONAL_HEADER {

/* Standard fields */

WORD Magic; /* 0x10b or 0x107 */ /* 0x00 */
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint; /* 0x10 */
DWORD BaseOfCode;
DWORD BaseOfData;

/* NT additional fields */

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

IMAGE_DATA_DIRECTORY结构:

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 指向某个具体的表结构的RVA
DWORD Size; // 这个表结构的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

Directory Entries

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Directory Entries, indices into the DataDirectory array */

#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 /* (MIPS GP) */
#define IMAGE_DIRECTORY_ENTRY_TLS 9
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11
#define IMAGE_DIRECTORY_ENTRY_IAT 12 /* Import Address Table */
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14

IMAGE_OPTIONAL_HEADER 比较重要的几个字段:

  • AddressOfEntryPoint: 持有 EP的 RVA值,指出程序最先执行的代码起始地址
  • ImageBase: 指出文件的优先装入地址(32位进程虚拟内存范围为:0~7FFFFFFF)
  • SectionAlignment: 内存中区块的对齐值
  • FileAlignment: 磁盘区块的对齐值
  • SizeOfImage: 指定了PE Image在虚拟内存中所占空间的大小
  • SizeOfHeaders: 指出整个PE头的大小
  • Subsystem: 区分系统驱动文件和普通可执行文件
  • NumberOfRvaAndSizes: 指定 DataDirectory数组的个数
  • DataDirectory: 由 IMAGE_DATA_DIRECTORY 结构体组成的数组

节区头 / 区块表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; // 区块尺寸
DWORD VirtualAddress; // 内存中区块 RAV 地址
DWORD SizeOfRawData; // 磁盘文件中区块所占大小
DWORD PointerToRawData; // 磁盘文件中区块起始位置
DWORD PointerToRelocations; // 在 OBJ 文件中使用,重定位的偏移
DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用)
WORD NumberOfRelocations; // 在 OBJ 文件中使用,重定位项数目
WORD NumberOfLinenumbers; // 行号表中行号的数目
DWORD Characteristics; // 区块属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

块名:.text.rdata.data.reloc, . 不是必须的

1
2
3
4
5
6
7
8
9
10
11
12
000001c0: 0000 0000 0000 0000 2e74 6578 7400 0000  .........text...
000001d0: 5201 0000 0010 0000 0002 0000 0004 0000 R...............
000001e0: 0000 0000 0000 0000 0000 0000 2000 0060 ............ ..`
000001f0: 2e72 6461 7461 0000 0c01 0000 0020 0000 .rdata....... ..
00000200: 0002 0000 0006 0000 0000 0000 0000 0000 ................
00000210: 0000 0000 4000 0040 2e64 6174 6100 0000 ....@..@.data...
00000220: 0d08 0000 0030 0000 000a 0000 0008 0000 .....0..........
00000230: 0000 0000 0000 0000 0000 0000 4000 00c0 ............@...
00000240: 2e72 656c 6f63 0000 2000 0000 0040 0000 .reloc.. ....@..
00000250: 0002 0000 0012 0000 0000 0000 0000 0000 ................
00000260: 0000 0000 4000 0042 0000 0000 0000 0000 ....@..B........
00000270: 0000 0000 0000 0000 0000 0000 0000 0000 ................

常见区块:

  • .text : 默认代码区块
  • .data : 默认读/写数据区块,全局变量、静态变量一般放在这里
  • .rdata : 默认只读数据区块,程序很少用到该块中的数据
  • .idata : 包含其他外来 DLL 的函数及数据信息,及输入表,经常合并到另一个区块,如: .rdata 区块
  • .edata : 输出表
  • .bss : 未初始化数据,很少使用
  • .tls : 线程局部存储器
  • .reloc : 可执行文件的基址重定位,一般只是 DLL 需要

创建和重命名自己的区块

1
#pragma data_seg("MY_DATA")

此时,所以被 Visual C++处理的数据都将放在一个叫 MY_DATA 的区块内,而不是默认的 .data 区块


objdump -M intel -h calc.dll

1
2
3
4
5
6
7
8
9
10
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000152 10001000 10001000 00000400 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rdata 0000010c 10002000 10002000 00000600 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 0000080d 10003000 10003000 00000800 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .reloc 00000020 10004000 10004000 00001200 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA

NOTE: 为加载到内存中的 PE 文件为 PE 文件,加载到内存中的形态被称为 映像(image)


区块对齐值

有两种对其值,一种用于磁盘文件内,另一种用于内存中

在 PE文件头里,FileAlignment定义了磁盘区块的对齐值。每个区块从对齐值的倍数的偏移位置开始,区块的实际代码或数据不一定刚好这么多,所以在不足的地方一般以 00h 来填充,这就是区块的 间隙


例如,一个 PE文件的对其值是 200h,这样每个区块从 200h的倍数的文件偏移位置开始。

假如区块的第一个节在 400h 处,长度为 90h,那么 400h~490h为这一区块的内容,而文件对其值为 200h,那么 490~600h会被 0 填充,这段空间称为区块间隙,下一个区块的开始地址为 600h


objdump -M intel -x calc.dll

1
2
SectionAlignment	00001000
FileAlignment 00000200

在 PE文件头里,SectionAlignment定义了内存中区块的对齐值,当 PE文件被映射到内存中时,区块总是至少从一个页边界处开始。也就是说,当一个 PE文件被映射到内存中时,每个区块的第 1 个字节对应于某个内存页

在 x86 系列CPU中,内存页是按 4KB(1000h)排列的
在 x64 中,内存页是按照 8KB(2000h)排列的

所以,在 x86系统中,PE文件区块的内存对齐值一般为 1000h,每个区块从 1000h的倍数的内存偏移开始


例如,.text区块在磁盘文件中的偏移为 400h,在内存中将其载入 1000h字节处;.rdata 区块在磁盘文件偏移的 600h处,在内存中将被载入地址之上的 2000h字节处


文件偏移与虚拟地址转换

在同一区块中,各地址偏移量是相等的,可用下面的公式对区块中的任意 File Offset 与 VA 进行转换。注意,不同区块在磁盘与内存中的差值不同

文件被映射到内存时,MS-DOS头、PE文件头和块表的偏移位置与大小均不会变,而各地区块被映射到内存后,其偏移位置会发生变化

1
2
3
4
Offset = RVA - ∆k
FIle Offset = VA - ImageBase - ∆k

FIle Offset = VA - ImageBase - (RVA - Section.Raw Offset)

区块 虚拟偏移量(RVA) 文件偏移量(Raw Offset) 差值(∆k)
.text 1000h 400h 0C00h
.rdata 2000h 600h 1A00h
.data 3000h 800h 2800h

∆k = 虚拟偏移量 - 文件偏移量

1
∆k = RVA - Section.Raw Offset

例如,某一虚拟地址(VA) 为 401112h(基地址: 400000h), 要计算它的文件偏移地址。

401112h 在 .text块中,此时 ∆k = 0C00h,故

File Offset = VA - ImageBase - ∆k = 401112h - 40000h - C00h = 521h

若虚拟内存地址为 4020D2h

File Offset = VA -ImageBase - ∆k = 4020D2h - 40000h - 1A00h = 6D2h


输入表/IAT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
There is an import table in .rdata at 0x10002024

The Import Tables (interpreted .rdata section contents)
vma: Hint Time Forward DLL First
Table Stamp Chain Name Thunk
00002024 0000204c 00000000 00000000 000020fe 00002000

DLL Name: KERNEL32.dll
vma: Hint/Ord Member-Name Bound-To
2070 127 CloseHandle
207e 338 ExitThread
208c 1191 ResumeThread
209c 215 CreateProcessA
20ae 740 GetThreadContext
20c2 1322 SetThreadContext
20d6 1434 VirtualAllocEx
20e8 1512 WriteProcessMemory

00002038 00000000 00000000 00000000 00000000 00000000

可执行文件使用来自其他 DLL的代码或数据的动作称为输入

输入函数:被程序调用但其执行代码不再程序中的函数

在 PE文件内有一组数据结构,他们分别对应于被输入的 DLL。每一个这样的结构都给出了被输入的 DLL 的名称并指向一组函数指针。这组函数指针被称为 输入地址表。每一个被引入的 API 在 IAT里有都保留的位置,在哪里它将被 Windows加载器写入输入函数的地址。一旦模块被载入,IAT中将包含所要调用输入函数的地址

导出函数修饰符

1
__declspec(dllimport) void func(void);

输入表的结构

PE文件头的可选映像头,数据目录表的第二个成员指向输入表,以一个 IMAGE_IMPORT_DESCRIPTOR(IID)数组开始。每个被 PE文件隐式链接的 DLL都有一个 IID

IID用来描述被引入的 DLL文件


1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
  • OriginalFirstThunk: 包含指向 **输入名称表(INT)**的地址(RVA),以 0/NULL 结束
  • Name: 库名称字符串的地址(RVA),该字符串包含输入 DLL 的名称(例如:USER32.DLL)
  • FirstThunk: 包含指向 **输入地址表(IAT)**的地址(RVA),是一个 IMAGE_THUNK_DATA结构的数组,以 0/NULL 结束

INT是一个指向 IMAGE_THUNK_DATA 结构的数组,数组中的每个 IMAGE_THUNK_DATA结构都指向 IMAGE_IMPORT_BY_NAME 结构


1
2
3
4
5
6
7
8
9
10
                                                        INT     IMAGE_IMPORT_BY_NAME    IAT

-> IMAGE_THUNK_DATA -> 01[函数1] <- IMAGE_THUNK_DATA <-|
|-> OriginalFirstThunk -> IMAGE_THUNK_DATA -> n[函数n] <- IMAGE_THUNK_DATA <-|
| -> 0 (结束) |
IMAGE_IMPORT_DESCRIPTOR |-> TimeDateStamp |
|-> ForwarderChain |
|-> Name -> "USER32.DLL" |
|-> FirstThunk ---------------------------------------------------------------|


IMAGE_THUNK_DATA结构:

1
2
3
4
5
6
7
8
IMAGE_THUNK_DATA STRUCT
union u1
ForwarderString DWORD // 指向一个转向者字符串的RVA
Function DWORD // 被输入的函数的内存地址
Ordinal DWORD // 被输入的 API 的序数值
AddressOdData DWORD // 指向 IMAGE_IMPORT_BY_NAME
ends
IMAGE_THUNK_DATA ENDS

IMAGE_IMPORT_BY_NAME结构:

1
2
3
4
IMAGE_IMPORT_BY_NAME STRUCT
Hint WORD // 本函数在其所驻留 DLL的输出表中的序号
Name BYTE // 输入函数的函数名, NULL 结尾
IMAGE_IMPORT_BY_NAME ENDS

输入地址表

Q: 为什么会有两个并行的指针数组指向 IMAGE_IMPORT_BY_NAME结构?

A: 因为 OriginalFirstThunk 是单独的一项,不可改写,称为 INT;FirstThunk是有 PE装载器重写的。PE装载器先搜索 OriginalFirstThunk ,如果找到,加载器就迭代搜索数组中的每个指针,找出每个 IMAGE_IMPORT_BY_NAME结构所指向的输入函数的地址(Function)。然后,加载器用函数真正的入口地址来替代由 FirstThunk 指向的 IMAGE_THUNK_DATA 数组里元素的值

1
2
3
4
5
6
7
8
9
10
                                                        INT     IMAGE_IMPORT_BY_NAME    IAT

-> IMAGE_THUNK_DATA -> 01[函数1] <- [函数 1 的地址] <-|
|-> OriginalFirstThunk -> IMAGE_THUNK_DATA -> n[函数n] <- [函数 n 的地址] <-|
| -> 0 (结束) |
IMAGE_IMPORT_DESCRIPTOR |-> TimeDateStamp |
|-> ForwarderChain |
|-> Name -> "USER32.DLL" |
|-> FirstThunk ---------------------------------------------------------------|

在某些情况下,一些函数仅由序号引出


确定数据目录表 (DataDirectory) 的第二个成员的位置(指向输入表)

该指针在 PE文件头的 80h 偏移处,需要找到 PE文件头的起始位置

PE文件头的起始位置在 DOS 头的 e_lfanew 处得到

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SectionAlignment	00001000
FileAlignment 00000200


Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000152 10001000 10001000 00000400 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rdata 0000010c 10002000 10002000 00000600 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 0000080d 10003000 10003000 00000800 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .reloc 00000020 10004000 10004000 00001200 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA

PE头起始位置:
e_lfanew: 000000d0

数据目录表的第二个成员位置(0x01):
D0h + 80h = 150h

输入表的 RVA:
00000150: 2420 0000 : 0x2024 (RVA)

磁盘文件偏移:
2024h 位于 .rdata
∆k = 2000h - 600h = 1A00h

File Offset = RVA - ∆k = 2024h - 1A00h = 624h

输入表(IID)位置:624h


xxd -s 0x624 -l 0x20 calc.dll

1
2
00000624: 4c20 0000 0000 0000 0000 0000 fe20 0000  L ........... ..
00000634: 0020 0000 0000 0000 0000 0000 0000 0000 . ..............

objdump -x calc.dll

1
2
3
vma:            Hint    Time      Forward  DLL       First
Table Stamp Chain Name Thunk
00002024 0000204c 00000000 00000000 000020fe 00002000

第四个字段(DLL Name) 是 20FEh(RVA),将它减去 1A00h = 6FE
查看此处数据:

xxd -s 0x6fe -l 0x10 calc.dll

1
000006fe: 4b45 524e 454c 3332 2e64 6c6c 0000 0000  KERNEL32.dll....

查看 IMAGE_THUNK_DATA:
OriginalFirstThunk (204C RVA) 处的数据 204Ch - 1A00h = 64C

xxd -s 0x64C -l 0x20 calc.dll

1
2
0000064c: 7020 0000 7e20 0000 8c20 0000 9c20 0000  p ..~ ... ... ..
0000065c: ae20 0000 c220 0000 d620 0000 e820 0000 . ... ... ... ..

一共有 8 个 IMAGE_THUNK_DATA 结构,也就是 8 个导入函数
第一个函数: 2070h - 1A00h = 670h

1
00000670: 7f00 436c 6f73 6548 616e 646c 6500 5201  ..CloseHandle.R.

第二个函数: 207Eh - 1A00 = 67Eh

1
0000067e: 5201 4578 6974 5468 7265 6164 0000 a704  R.ExitThread....

第三个函数: 208Ch - 1A00h = 68Ch

1
0000068c: a704 5265 7375 6d65 5468 7265 6164 0000  ..ResumeThread..

前面两字节的空缺是函数名引用(Hint)


查看导入函数:
= (IMAGE_THUNK_DATA)DWORD * 4 + (IMAGE_IMPORT_BY_NAME[Hint])4 = 20

到达 IMAGE_IMPORT_BY_NAME 的 Name 字段
64Ch + 20h(0ffset) = 66Ch

查看数据(导入函数名):
xxd -s 0x66C -l 0x90 calc.dll

1
2
3
4
5
6
7
8
9
0000066c: 0000 0000 7f00 436c 6f73 6548 616e 646c  ......CloseHandl
0000067c: 6500 5201 4578 6974 5468 7265 6164 0000 e.R.ExitThread..
0000068c: a704 5265 7375 6d65 5468 7265 6164 0000 ..ResumeThread..
0000069c: d700 4372 6561 7465 5072 6f63 6573 7341 ..CreateProcessA
000006ac: 0000 e402 4765 7454 6872 6561 6443 6f6e ....GetThreadCon
000006bc: 7465 7874 0000 2a05 5365 7454 6872 6561 text..*.SetThrea
000006cc: 6443 6f6e 7465 7874 0000 9a05 5669 7274 dContext....Virt
000006dc: 7561 6c41 6c6c 6f63 4578 0000 e805 5772 ualAllocEx....Wr
000006ec: 6974 6550 726f 6365 7373 4d65 6d6f 7279 iteProcessMemory

然后再看看 FirstThunk(2000h),减去 1A00h = 600h
同样指向 IMAGE_THUNK_DATA结构

1
2
3
4
5
6
7
8
9
10
00000600: 7020 0000 7e20 0000 8c20 0000 9c20 0000  p ..~ ... ... ..
00000610: ae20 0000 c220 0000 d620 0000 e820 0000 . ... ... ... ..
00000620: 0000 0000 4c20 0000 0000 0000 0000 0000 ....L ..........
00000630: fe20 0000 0020 0000 0000 0000 0000 0000 . ... ..........

00000640: 0000 0000 0000 0000 0000 0000 7020 0000 ............p ..
00000650: 7e20 0000 8c20 0000 9c20 0000 ae20 0000 ~ ... ... ... ..
00000660: c220 0000 d620 0000 e820 0000 0000 0000 . ... ... ......
00000670: 7f00 436c 6f73 6548 616e 646c 6500 5201 ..CloseHandle.R.
00000680: 4578 6974 5468 7265 6164 0000 a704 5265 ExitThread....Re

FirstThunk 在程序运行时被初始化。

FirstThunk 字段值指向的地址与 INT重复,系统在程序初始化的时候根据 OriginalFirstThunk找到函数名,调用 GetProcAddress函数(或功能类似的函数)并根据函数名取得函数的入口地址,然后用函数入口地址取代 FirstThunk 指向地址中对应的值(IAT)

1
2
3
    call 00401164
00401164
jmp dword ptr [00402010] ; 这个 IAT 地址内的数据指向导入函数真实地址

输出表

输出表时数据目录表(DataDirectory)的第一个成员(0x00),指向 IMAGE_EXPORT_DIRECTORY(IED)结构
DLL 文件提供输出表向系统提供输出函数名、序号和入口地址等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // 模块的真实名称
DWORD Base; // 函数的起始序号
DWORD NumberOfFunctions; // AddressOfFunctions 阵列中元素个数(导出函数的总数)
DWORD NumberOfNames; // AddressOfNames 阵列中的元素个数(以名称方式导出的函数的总数)
DWORD AddressOfFunctions; // 指向函数地址数组 RVA
DWORD AddressOfNames; // 函数名字的指针地址 RAV
DWORD AddressOfNameOrdinals; // 指向输出序列号数组 RVA
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

AddressOfFunctions : 指向保存所有输出函数的地址数组

NumberOfFunctions : 保存数组元素数目

AddressOfNames : 指向保存通过名字引出的函数的地址数组 RVA

NumberOfNames : 名字数目

此时有两个模块,分别是地址数组和名字数组,两个数组以 AddressOfNameOrdinals 作为连接枢纽;使用指向地址数组的索引最为链接,PE装载器在名字数组中找到匹配名字的同时,也获取了指向地址指标中对应元素的索引

1
2
3
4
5
6
7
8
9
                       |-> ...                               AddressOfFunctions
|-> NumberOfFunctions=3 0x40042 0x400197
|-> NumberOfNames=2 |-> [Func1][0x40085][Func3] (其中 2个以名称输出)
IMAGE_EXPORT_DIRECTORY |-> AddressOfFunctions --------|
|
|-> AddressOfNames ------------|-> RVA of Name 1 <---> [Index of Name 1] <-|
| |-> RVA of Name 3 <---> [Index of Name 2] <-|
|-> AddressOfNameOrdinals ---------------------------------------------------|
AddressOfNames AddressOfNameOrdinals

例子:

PE 头起始:f8h
输出表:f8h + 78h(offset) = 170h

1
00000030: 0000 0000 0000 0000 0000 0000 f800 0000  ................

输出表: 18E50h

1
00000170: 508e 0100                                P...

计算文件偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Sections:
Idx Name Size VMA LMA File off Algn
0 .textbss 00010000 10001000 10001000 00000000 2**2
ALLOC, LOAD, CODE
1 .text 000051a2 10011000 10011000 00000400 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
2 .rdata 00001f9c 10017000 10017000 00005600 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .data 00000200 10019000 10019000 00007600 2**2
CONTENTS, ALLOC, LOAD, DATA
4 .idata 00000973 1001a000 1001a000 00007800 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .msvcjmc 0000010f 1001b000 1001b000 00008200 2**2
CONTENTS, ALLOC, LOAD, DATA
6 .00cfg 00000109 1001c000 1001c000 00008400 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .rsrc 00000326 1001d000 1001d000 00008600 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .reloc 0000057c 1001e000 1001e000 00008a00 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA

18E50h 位于 .rdata 段

∆k = 17000h - 5600h = 11A00h

File Offset = RVA - ∆k = 18E50h - 11A00 = 7450h

文件偏移到 2450h 就是输出表内容:

1
2
3
4
5
00007450: 0000 0000 ffff ffff 0000 0000 828e 0100  ................
00007460: 0100 0000 0100 0000 0100 0000 788e 0100 ............x...
00007470: 7c8e 0100 808e 0100 0e11 0100 8c8e 0100 |...............
00007480: 0000 6d79 646c 6c2e 646c 6c00 4d73 6700 ..mydll.dll.Msg.
00007490: 0000 0000 0000 0000 0000 0000 0000 0000 ................

这个 DLL 只有一个函数 Msg


DLL Name : 18E82h - 11A00h = 7482h

AddressOfFunctions : 18E78h - 11A00h = 7478h

AddressOfNames : 18E7Ch - 11A00h = 747Ch

AddressOfNameOrdinals : 18E80h - 11A00h = 7480h

1
2
3
4
5
6
7
8
9
00007482: 6d79 646c 6c2e 646c 6c00 4d73 6700 0000  mydll.dll.Msg...

00007478: 0e11 0100 8c8e 0100 0000 6d79 646c 6c2e ..........mydll.
00007488: 646c 6c00 4d73 6700 0000 0000 0000 0000 dll.Msg.........

0000747c: 8c8e 0100 0000 6d79 646c 6c2e 646c 6c00 ......mydll.dll.
0000748c: 4d73 6700 0000 0000 0000 0000 0000 0000 Msg.............

00007480: 0000 6d79 646c 6c2e 646c 6c00 4d73 6700 ..mydll.dll.Msg.

PE 装载器调用 GetProcAddress 来查找 mydll.dll 里的 API 函数 Msg ,系统通过定位 mydll.dll 的 IMAGE_EXPORT_DIRECTORY结构开始工作

PE 装载器获得输出函数名表(ENT)的起始地址,知道这个数组只有一个条目,然后对名字进行二进制查找,直到发现字符串 “Msg” 为止。PE 装载器发现 Msg是数组的第一个条目后,加载器从输出序数表中读取相应的第一个值,这个值是 Msg的输出序数。使用输出序数作为进入 ETA 的索引(也要考虑 Base域值),得到 Msg的 RVA,用这个 RVA地址加 mydll.dll 的载入地址,得到 Msg的实际地址


基址重定位

在 PE 文件中,重定位表往往单独作为一块,用 .reloc 表示

将文件中的所有可能需要修改的地址放在一个数组里,如果 PE 文件不在首选的地址(基地址)载入,那么文件中的每一个定位都需要被修正


EXE文件总是使用独立的虚拟地址空间,所以 EXE 文件不需要重定位信息


基址重定位表结构

基址重定位表通过数据目录表(0x05)的 IMAGE_DIRECTORY_ENTRY_BASERELOC 条目查找;基址重定位数据采用类似按页分割的方法组织,是由许多重定位块串接成的,每个块中存放 4KB(1000h) 的重定位信息,每个重定位数据块的大小必须以 **DWORD(4字节)**对齐


1
2
3
4
5
6
typedef struct _IMAGE_BASE_RELOCATION
{
DWORD VirtualAddress; // 重定位数据的开始 RVA 地址
DWORD SizeOfBlock; // 重定位块的长度
/* WORD TypeOffset[1]; */ // 重定位项数组
} IMAGE_BASE_RELOCATION,*PIMAGE_BASE_RELOCATION;
  • VirtualAddress : 这组重定位数据的开始 RVA地址。各重定向的地址加上这个值才是该重定位项的完整 RVA 地址
  • SizeOfBlock : 当前重定位结构的大小。因为 VirtualAddress 和 SizeOfBlock 的大小都是固定的 4 字节,所以这个值减去 8 就是 TypeOffset 数组的大小
  • TypeOffset : 一个数组,数据每项大小为 2字节,共 16位。这 16位分为高 4 位和低 12位。高 4 位代表重定向类型;低 12位是重定向地址,它与 VirtualAddress 相加就是指向 PE映像中需要修改的地址数据的指针

重定位类型:

  • 0 : IMAGE_REL_BSAED_ABSOLUTE : 没有具体含义,只是为了让每个段 4字节对齐
  • 3 : IMAGE_REL_BSAED_HIGHLOW : 重定位指向整个地址都需要修正,实际上大部分情况下都是这样的
  • 10 : IMAGE_REL_BSAED_DIR64 : 出现在 64位 PE文件中,对指向的整个地址进行修正

在 x86可执行文件中,所有基址重定向类型都是 3 ,在一组重定位结束的地方会出现 0,这些重定位什么都不做,只是用于填充,以便下一个 IMAGE_BASE_RELOCATION 按 4 字节对齐

对于 IA-64 可执行文件,重定向类型似乎总是 10,尽管 IA-64 的 EXE页大小是 8KB,但基址重定位仍是 4KB的块


例子:

xxd -s 0x198 -l 0x10 mydll.dll

1
00000198: 00e0 0100 9403 0000 b082 0100 3800 0000  ............8...

重定位表: 1E000h,位于 .reloc , ∆k = 1E000h - 8A00h = 15600h

重定位表一直位于 .reloc 段,可以直接找 .reloc 段的地址

File Offset = RVA - ∆k = 1E000h - 15600h = 8A00h

Sections:

1
2
3
4
7 .rsrc         00000326  1001d000  1001d000  00008600  2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .reloc 0000057c 1001e000 1001e000 00008a00 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA

IMAGE_BASE_RELOCATION 结构:

1
2
3
4
5
6
7
8
9
10
11
00008a00: 0010 0100 8400 0000 0f36 3736 4036 4536  .........676@6E6

00008a00: 0010 0100 8400 0000 0f36 3736 4036 4536 .........676@6E6
00008a10: 4d36 5f36 6436 6c36 7e36 8336 8b36 9d36 M6_6d6l6~6.6.6.6
00008a20: a236 aa36 d036 d436 d836 dc36 3f37 4d37 .6.6.6.6.6.6?7M7
00008a30: 5237 5a37 a737 cb37 d437 da37 013a 113a R7Z7.7.7.7.7.:.:
00008a40: 223a 353a 983a ec3a f03a f43a f83a 263b ":5:.:.:.:.:.:&;
00008a50: 2b3b 3d3b 7d3b 8d3b b33b b83b d93b de3b +;=;};.;.;.;.;.;
00008a60: ec3b 603c 693c 723c f63c fb3c 0d3d 223d .;`<i<r<.<.<.="=
00008a70: 313d 393d 5c3d 7b3d 363e 3b3e 4d3e 6b3e 1=9=\={=6>;>M>k>
00008a80: 763f 0000 0020 0100 a800 0000 2830 3730 v?... ......(070
  • VirtualAddress : 11000h
  • SizeOfBlock : 84h (84h - 8h) / 2h(WORD) = 3Eh (62)
  • 重定位数据1 : 360f
  • 重定位数据2 : 3637
  • 重定位数据61 : 763f
  • 结束 : 0000

(重定位数据)61 + 1(结束) = 62


查看重定位数据:
原始数据 : 0F36h
TypeOffset : 360Fh
TypeOffset 高4 : 3h
TypeOffset 低12: 60Fh
低 12加 VirtualAddress : 11000h + 60Fh = 1160Fh
转换为文件偏移地址: 1160Fh ,

∆k = 11000h - 400h = 10C00h

File Offset = 1160Fh - 10C00h = A0Fh

重定位所需要的数据1 指向的数据
xxd -s 0xA0F -l 0x4 mydll.dll

1
00000a0f: 0cb0 0110                                ....

执行 PE 文件前,加载程序在进行重定位的时候,会用 PE 文件在内存中的实际映像地址减 PE 文件所要求的映像地址,根据重定位类型的不同将差值添加到相应的地址数据中


Easy Code

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
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <Windows.h>


IMAGE_DOS_HEADER image_dos;
IMAGE_FILE_HEADER image_file;
IMAGE_OPTIONAL_HEADER image_option;


void read_pe(void)
{
FILE* read_pe;
errno_t* err;
if ((err = fopen_s(&read_pe, "C:\\Users\\0x20C\\Desktop\\dll hijacking\\Invoke-Dll.exe", "r")) != 0)
{
printf("File Open Fail!\n");
exit(0);
}

printf("File Open Done!\n");

fread(&image_dos, sizeof(IMAGE_DOS_HEADER), 1, read_pe);
printf("e_magic : %04X\n", image_dos.e_magic);
printf("PE Header Start(e_lfanew) : %08X\n", image_dos.e_lfanew);
DWORD pe_header = image_dos.e_lfanew;

// IMAGE_FILE_HEADER offset
DWORD file_offset = pe_header + sizeof(DWORD);
fseek(read_pe, file_offset, SEEK_SET);
fread(&image_file, sizeof(IMAGE_FILE_HEADER), 1, read_pe);
printf("SizeOfOptionalHeader : %04X\n", image_file.SizeOfOptionalHeader);

// IMAGE_OPTIONAL_HEADER offset
DWORD option_offset = file_offset + sizeof(IMAGE_FILE_HEADER);
fseek(read_pe, option_offset, SEEK_SET);
fread(&image_option, sizeof(IMAGE_OPTIONAL_HEADER), 1, read_pe);

printf("SectionAlignment : %08X\n", image_option.SectionAlignment);
printf("FileAlignment : %08X\n", image_option.FileAlignment);
printf("SizeOfImage : %08X\n", image_option.SizeOfImage);
printf("IMPORT : %08X\n", image_option.DataDirectory[1]);
printf("BASERELOC : %08X\n", image_option.DataDirectory[5]);

fclose(read_pe);
}

int main(int argc, char* argv[])
{
//
read_pe();
return 0;
}

Out

1
2
3
4
5
6
7
8
9
File Open Done!
e_magic : 5A4D
PE Header Start(e_lfanew) : 000000E8
SizeOfOptionalHeader : 00E0
SectionAlignment : 00001000
FileAlignment : 00000200
SizeOfImage : 00020000
IMPORT : 0001B1D0
BASERELOC : 0001F000


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!