PE

PE文件格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PE:
IMAGE_DOS_HEADER e_lfanew(OD)
DOS_STUB[176]
IMAGE_NT_HEADERS; NT头
DWORD Signature //Magic Number
IMAGE_FILE_HEADER; //文件头 FileHeader 描述信息
IMAGE_OPTIONAL_HEADER32 //选项头 OptionalHeader 程序加载的信息

//数据目录(表)
IMAGE_DATA_DIRECTORY []
//节表 描述区的映射
IMAGE_SECTION_HEADER[]

数据

image-20241010165339454

DOS头:

image-20241010151827112

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // Magic number
//0x00,'MZ'(0x5A4D)标识
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; // 0x3C, PE头的起始地址,默认0xB0处 PE头相对于文件的偏移,用于定位PE文件
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

magic number

魔数是文件的前几位,它唯一地标识了文件的类型。这使得编程更容易,因为不需要搜索复杂的文件结构来识别文件类型。

image-20241010152807621

例如Windows的exe:

image-20241010152752853

HEADER所占的位置及大小

image-20241010153908469

image-20241010153409932

正常32程序无法兼容16位 在16位中运行就会出现this program。。。

image-20241010154032650

指向新的文件格式

image-20241010155512755

除了magic number 以及F0 00 00 00(新文件格式头)的都可以修改 直到PE(nt的magic number)

image-20241010160030727

若修改新文件格式头的代码 会使OD无法调试 认为是无效文件格式(拒绝创建进程)

NT头:

image-20241010165939982

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

在一个有效的 PE 文件里,Signature 字段被设置为00004550h, ASCII 码字符是“PE00”。标志这 PE 文件头的开始。
PE00” 字符串是 PE 文件头的开始,DOS 头部的 e_lfanew 字段正是指向这里。

文件头(标准PE头):

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

//* Characteristics 表文件属性,EXE默认0100,DLL默认210Eh,或运算组合设置。
#define IMAGE_FILE_RELOCS_STRIPPED     0x0001 // 文件中不存在重定位信息
#define IMAGE_FILE_EXECUTABLE_IMAGE    0x0002 // 文件可执行
#define IMAGE_FILE_LINE_NUMS_STRIPPED   0x0004 // 文件中不存在行信息
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED   0x0008 // 文件中不存在符号信息
#define IMAGE_FILE_AGGRESIVE_WS_TRIM    0x0010 // 调整工作集
#define IMAGE_FILE_LARGE_ADDRESS_AWARE   0x0020 // 程序能处理大于2G的地址
#define IMAGE_FILE_BYTES_REVERSED_LO    0x0080 // 小尾方式
#define IMAGE_FILE_32BIT_MACHINE      0x0100 // 只在32位平台上运行
#define IMAGE_FILE_DEBUG_STRIPPED     0x0200 // 不包含调试信息
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // 不能从可移动盘运行
#define IMAGE_FILE_NET_RUN_FROM_SWAP    0x0800 // 不能从网络运行
#define IMAGE_FILE_SYSTEM          0x1000 // 系统文件(如驱动程序),不能直接运行
#define IMAGE_FILE_DLL            0x2000 // 是一个dll文件
#define IMAGE_FILE_UP_SYSTEM_ONLY      0x4000 // 文件不能在多处理器计算机上运行
#define IMAGE_FILE_BYTES_REVERSED_HI    0x8000 // 大尾方式

选项头:

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
//大小: 32bit(0xE0) 64bit(0xF0)

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16

typedef struct _IMAGE_OPTIONAL_HEADER {

WORD Magic; //文件类型: 10bh为32位PE文件 / 20bh为64位PE文件
BYTE MajorLinkerVersion; //链接器(主)版本号 对执行没有任何影响
BYTE MinorLinkerVersion; //链接器(次)版本号 对执行没有任何影响
DWORD SizeOfCode; //包含代码的节的总大小.文件对齐后的大小.编译器填的没用
DWORD SizeOfInitializedData; //包含已初始化数据的节的总大小.文件对齐后的大小.编译器填的没用.
DWORD SizeOfUninitializedData; //包含未初始化数据的节的总大小.文件对齐后的大小.编译器填的没用.(未初始化数据,在文件中不占用空间;但在被加载到内存后,PE加载程序会为这些数据分配适当大小的虚拟地址空间).
DWORD AddressOfEntryPoint; //程序入口(RVA)
DWORD BaseOfCode; //代码的节的基址(RVA).编译器填的没用(代码节起始的RVA,表示映像被加载进内存时代码节的开头相对于ImageBase的偏移地址,节的名称通常为".text")
DWORD BaseOfData; //数据的节的基址(RVA).编译器填的没用(数据节起始的RVA,表示映像被加载进内存时数据节的开头相对于ImageBase的偏移地址,节的名称通常为".data")
DWORD ImageBase; //内存镜像基址
DWORD SectionAlignment; //内存对齐大小
DWORD FileAlignment; //文件对齐大小
WORD MajorOperatingSystemVersion; //标识操作系统主版本号
WORD MinorOperatingSystemVersion; //标识操作系统次版本号
WORD MajorImageVersion; //PE文件自身的主版本号
WORD MinorImageVersion; //PE文件自身的次版本号
WORD MajorSubsystemVersion; //运行所需子系统主版本号
WORD MinorSubsystemVersion; //运行所需子系统次版本号
DWORD Win32VersionValue; //子系统版本的值.必须为0,否则程序运行失败.
DWORD SizeOfImage; //内存中整个PE文件的映射尺寸.可比实际的值大(拉伸).必须是SectionAlignment的整数倍
DWORD SizeOfHeaders; //所有头+节表按照文件对齐后的大小.
DWORD CheckSum; //校验和.大多数PE文件该值为0.在内核模式的驱动程序和系统DLL中,该值则是必须存在且是正确的.在IMAGEHLP.DLL中函数CheckSumMappedFile就是用来计算文件头校验和的,对于整个PE文件也有一个校验函数MapFileAndCheckSum.
WORD Subsystem; //文件子系统 驱动程序(1) 图形界面(2) 控制台/DLL(3)
WORD DllCharacteristics; //文件特性.不是针对DLL文件的
DWORD SizeOfStackReserve; //初始化时保留的栈大小.该字段默认值为0x100000(1MB),如果调用API函数CreateThread时,堆栈参数大小传入NULL,则创建出来的栈大小将是1MB.
DWORD SizeOfStackCommit; //初始化时实际提交的栈大小.保证初始线程的栈实际占用内存空间的大小,它是被系统提交的.这些提交的栈不存在与交换文件里,而是在内存中.
DWORD SizeOfHeapReserve; //初始化时保留的堆大小.用来保留给初始进程堆使用的虚拟内存,这个堆的句柄可以通过调用函数GetProcessHeap获得.每一个进程至少会有一个默认的进程堆,该堆在进程启动时被创建,而且在进程的生命期中不会被删除.默认值为1MB.
DWORD SizeOfHeapCommit; //初始化时实践提交的堆大小.在进程初始化时设定的堆所占用的内存空间,默认值为PAGE_SIZE.
DWORD LoaderFlags; //调试相关
DWORD NumberOfRvaAndSizes; //目录项数目,默认为10h.
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//结构数组 数组元素个数由IMAGE_NUMBEROF_DIRECTORY_ENTRIES定义(编译器使用)
} IMAGE_OPTIONAL_HEADER32, * PIMAGE_OPTIONAL_HEADER32;

节表:

PE节表位于PE头的下面,PE节表中记录了各个节表的起始位置、大小,以及在内存中偏移位置和属性。

一个PE文件有多少个节,就有多少个节表!每一个节表的大小是确定的,40字节

image-20241010163419240

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//*[1]:节名
union {
DWORD PhysicalAddress;
DWORD VirtualSize; //*[2]:对齐前节的大小,内存中节的大小 值可以不准确(值可以被修改)
} Misc;//联合体(匿名类型变量Misc)
DWORD VirtualAddress; //*[3]:内存中偏移 加上ImageBase才是内存中的真正地址
DWORD SizeOfRawData; //*[4]:文件对齐后节的大小(对齐提高效率)
DWORD PointerToRawData; //*[5]:文件中节位置 节区在文件中的偏移(距0的大小) 值一定是文件对齐的整数倍
DWORD PointerToRelocations;//在obj文件中使用 对exe无意义 4字节
DWORD PointerToLinenumbers;//行号表的位置 调试的时候使用 4字节
WORD NumberOfRelocations; //在obj文件中使用 对exe无意义 2字节
WORD NumberOfLinenumbers; //行号表中行号的数量 调试的时候使用 2字节
DWORD Characteristics; //*[6]:节属性的标志(可读,可写,可执行)
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

image-20241012203518887

定位节表位置

一个PE文件从哪里开始是节表(硬盘上的地址):DOS头大小 + 垃圾空位 + PE签名大小 + 标准PE头大小 + 可选PE头大小(需要查);

DOS头大小固定为64字节;

PE签名大小为4字节;

标准PE头大小固定为20字节;

可选PE头大小可以通过标准PE头中的SizeOfOptionalHeader字段的值来确定

1
e_lfanew + 4 + 20 + SizeOfOptionalHeader = 节表开始地址

Name

8个字节一般是以“\0”结尾的ASCII码字符串来标识的名称,内容可以自定义,该名称并不遵守必须以“\0”结尾的规律,如果不是以“\0”结尾,系统会截取8个字节的长度进行处理,所以不能以名称作为定位的标准和依据,正确的方法是按照IMAGE_OPTIONAL_HEADER32结构中的数据目录字段结合进行定位(当按照char *来定位时若结尾无“\0”则会持续一直读取)

FileBuffer->ImageBuffer

一个文件在硬盘要运行的时候是要经过拉伸的,如果简单拉伸后,程序就能够运行吗?

答案是否定的 ImageBuffer已经离运行非常接近了

image-20241018154828721

Misc.VirtualSize (在内存中拉伸后的实际大小)和 SizeOfRawData谁更大?

因为可能会存在一些未初始化数据,而这些数据在文件中是不会体现的,但是放在内存的时候,会被计算出来,会在内存里会留出空间。

因此有可能Misc.VirtualSize 比 SizeOfRawData大
VirtualSize是加载到内存对齐前的大小,SizeOfRawData是磁盘文件中对齐后的大小。

FileBuffer拷贝到ImageBuffer

将FileBuffer放到ImageBuffer 得先读取sizeofimage大小 然后开辟相应大小的空间 SIzeofheaders大小在FileBuffer和ImageBuffer中是一致的 可以直接拷贝

ImageBuffer空间申请完后需要全部初始化为0

将节的内容拷贝到ImageBuffer中 PointerToRawData决定从什么地方拷贝 VirtualAddress决定拷贝到什么地方

拷贝的时候应该按照那个较小的数值(或者选择SizeOfRawData来确定需要复制的节的大小)

选择SizeOfRawData的原因:在极端情况下,当节中存在足够大未初始化的数据时,按照Misc.VirtualSize值将FileBuffer中的数据复制到ImageBuffer中,很可能会把FileBuffer中下一个节的数据也复制过去,这样就会造成复制错误。

计算内存中节存储的某个地址在文件中的地址:

在某个节中地址为0x501234 基地址为0x500000

节表的信息:
节1,PointerToRawData 400: VirtualAddress 1000

节2:PointerToRawData 600: VirtualAddress 2000

节3,PointerToRawData 800: VirtualAddress 3000

0x501234-0x500000=0x1234

0x1234<VirtualAddress 1000 && 0x1234>VirtualAddress 2000

所以可以判断位于第一个节中

判断:循环判断条件使得偏移>VirtualAddress同时<VirtualAddress+Misc.VirtualSize

计算位于节中的偏移:
0x1234-0x1000=0x234

所以在文件中的地址就是在文件中第一个节表的地址+0x234

VA,虚拟地址,也就是程序被加载到内存中的地址

RVA,相对虚拟地址

将VA减去MODULE的BASE就是RVA的值。

FOA:在文件中的对应的地址

代码节空白区添加代码

在exe中添加messagebox 并获取其地址

image-20241024201346771

在messagebox处下一个断点 然后在断点中查看

image-20241024201408926

image-20241024201420509

image-20241024201436273

messagebox地址为0x772AAC60

硬编码:E8 E9(call jmp)

硬编码:

硬编码是指将具体的数值、路径、参数等直接写入程序代码中,而不通过变量或配置文件来表示。这样的做法使得程序中的这些数值和参数变得固定,不容易修改,且缺乏灵活性。硬编码的值通常被称为”魔法数”(Magic Numbers)或”魔法字符串”,因为它们没有直观的含义,只能通过查看代码来了解。

举例

image-20241024203421389

分析可以发现,无论是E8还是E9,后面的地址貌似都不是直接小端序转化过来的地址

真正要跳转的地址=E8这条指令的下一条指令的地址 + X
X=真正要跳转的地址 - E8要要跳转的地址的下一条指令的地址

X为E8后面的地址

例如(上图中jmp):

1
2
772AAD77   /E9 A9000000     jmp user32.772AAE25
772AAD7C 8B5C24 14 mov ebx,dword ptr ss:[esp+0x14]

772AAE25为真正要跳转的地址

772AAD7C E9这条指令的下一条指令的地址

X=772AAE25-772AAD7C=00 00 00 A9

转为小端序即是E9 A9 00 00 00

也可以这样计算X = 要跳转的地址 - (E9的地址 +5)

E9的地址为拉伸后的地址

将代码添加到空白区不需要修改节的属性

在任意代码空白区添加代码

image-20241106201731234

计算NewBuffer的大小

通过最后一个节表的信息知道最后一个节表的偏移地址+ImageBase+节的大小,就是NewBuffer的大小

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
#include <iostream>
#include <windows.h>


//变量声明
PIMAGE_DOS_HEADER dosHeader = nullptr; //DOS结构
PIMAGE_FILE_HEADER fileHeader = nullptr; //FILE结构
PIMAGE_OPTIONAL_HEADER32 optionalHeader = nullptr; //OPTIONAL结构
PIMAGE_SECTION_HEADER* sectionArr = nullptr; //SECTION数组
LPVOID MemoryData = nullptr; //拉伸后的pe结构

// 读取程序数据
LPVOID ReadProgramData(LPCSTR programPath)
{
FILE* program = nullptr;
size_t size = NULL;
LPVOID data = nullptr;

//打开程序
fopen_s(&program, programPath, "rb");
if (program == nullptr)
{
printf("failed to open by program!\n");
goto END;
}

//获取程序字节大小
if (fseek(program, 0, SEEK_END) == 0)
{
size = ftell(program);
if (fseek(program, 0, SEEK_SET) != 0)
{
printf("failed to move the pointer to begin!\n");
goto END;
}
}
else
{
printf("failed to move the pointer to end!\n");
goto END;
}

//申请内存空间存储数据
data = malloc(size);
if (data != nullptr)
{
memset(data, '\0', size);
fread_s(data, size, 1, size, program);
}
else
{
printf("failed to apply by memory\n");
goto END;
}

END:
if (program)
fclose(program);

return data;
}

//解析pe结构
void AnalyzePeStruct(PCHAR fileData, PIMAGE_DOS_HEADER& dos, PIMAGE_FILE_HEADER& file, PIMAGE_OPTIONAL_HEADER32& optional, PIMAGE_SECTION_HEADER*& section)
{
//解析DOS结构
dos = (PIMAGE_DOS_HEADER)fileData;
fileData = &fileData[dos->e_lfanew + 4];

//解析FILE结构
file = (PIMAGE_FILE_HEADER)fileData;
fileData = &fileData[20];

//解析OPTIONAL结构
optional = (PIMAGE_OPTIONAL_HEADER32)fileData;
fileData = &fileData[file->SizeOfOptionalHeader];

//解析节表
section = (PIMAGE_SECTION_HEADER*)malloc(file->NumberOfSections * sizeof(IMAGE_SECTION_HEADER));
if (section != nullptr)
{
for (size_t index = 0; index < file->NumberOfSections; index++)
{
section[index] = (PIMAGE_SECTION_HEADER)fileData;
fileData = &fileData[40];
}
}
}

//缩小pe结构
LPVOID ShrinkData(LPVOID memoryData)
{
LPVOID fileData = nullptr;
LPVOID tempMemoryDataPointer = nullptr;
LPVOID tempFileDataPointer = nullptr;
int copyCharNumber = NULL;

//计算硬盘pe结构大小
size_t fileDataSize = optionalHeader->SizeOfHeaders;
for (size_t index = 0; index < fileHeader->NumberOfSections; index++)
{
fileDataSize += sectionArr[index]->SizeOfRawData;
}

//申请内存空间
fileData = malloc(fileDataSize);
if (fileData != nullptr)
{
memset(fileData, '\0', fileDataSize);
tempMemoryDataPointer = memoryData;
tempFileDataPointer = fileData;
copyCharNumber = optionalHeader->FileAlignment;

while (copyCharNumber < optionalHeader->SizeOfHeaders)
{
copyCharNumber *= 2;
}
//写入所有头+节表
memcpy_s(tempFileDataPointer, copyCharNumber, tempMemoryDataPointer, optionalHeader->SizeOfHeaders);
//写入节区
for (size_t index = 0; index < fileHeader->NumberOfSections; index++)
{
copyCharNumber = optionalHeader->FileAlignment;
tempFileDataPointer = (LPVOID)((UINT_PTR)fileData + sectionArr[index]->PointerToRawData);
tempMemoryDataPointer = (LPVOID)((UINT_PTR)memoryData + sectionArr[index]->VirtualAddress);
while (copyCharNumber < sectionArr[index]->SizeOfRawData)
{
copyCharNumber *= 2;
}
memcpy_s(tempFileDataPointer, copyCharNumber, tempMemoryDataPointer, sectionArr[index]->SizeOfRawData);
}
}
return fileData;
}

//拉伸pe结构
LPVOID StretchData(PCHAR fileData)
{
LPVOID tempMemoryDataPointer = nullptr; //拉伸后的pe结构指针(用于指向写入的位置)
PCHAR tempFileDataPointer = nullptr; //拉伸前的pe结构指针(用于指向读取的位置)
int copyCharNumber = NULL; //内存对其

//读取pe结构
AnalyzePeStruct(fileData, dosHeader, fileHeader, optionalHeader, sectionArr);
//申请内存pe结构空间
MemoryData = malloc(optionalHeader->SizeOfImage);
if (MemoryData != nullptr)
{
memset(MemoryData, '\0', optionalHeader->SizeOfImage);
tempMemoryDataPointer = MemoryData;
tempFileDataPointer = fileData;
copyCharNumber = optionalHeader->SectionAlignment;

//写入所有头+节表
while (copyCharNumber < optionalHeader->SizeOfHeaders)
{
copyCharNumber *= 0x2;
}
memcpy_s(tempMemoryDataPointer, copyCharNumber, tempFileDataPointer, optionalHeader->SizeOfHeaders);

//写入节区
for (size_t index = 0; index < fileHeader->NumberOfSections; index++)
{
copyCharNumber = optionalHeader->SectionAlignment;
tempMemoryDataPointer = (LPVOID)((UINT_PTR)MemoryData + sectionArr[index]->VirtualAddress);
tempFileDataPointer = (PCHAR)((UINT_PTR)fileData + sectionArr[index]->PointerToRawData);
while (copyCharNumber < sectionArr[index]->SizeOfRawData)
{
copyCharNumber *= 2;
}
memcpy_s(tempMemoryDataPointer, copyCharNumber, tempFileDataPointer, sectionArr[index]->SizeOfRawData);
}
}

return MemoryData;
}

//写入缩小后的pe结构
bool WriteMyFilePeData(LPVOID fileData, LPCSTR newProgramPath)
{
FILE* newProgram = nullptr;
bool result = true;
size_t size = NULL;

//打开文件
fopen_s(&newProgram, newProgramPath, "wb");
if (newProgram == nullptr)
{
printf("failed to create by program!\n");
result = false;
goto END;
}

size = _msize(fileData);
fwrite(fileData, 1, size, newProgram);



END:
if (newProgram)
fclose(newProgram);

return result;
}

//内存地址转文件地址
UINT_PTR RvaToFoa(UINT_PTR address)
{
//节区开始地址
UINT_PTR thisSectionBegin = NULL;
//节区结束地址
UINT_PTR thisSectionEnd = NULL;
//在硬盘pe结构中的位置
UINT_PTR offset = NULL;

//开始查找
for (size_t index = 0; index < fileHeader->NumberOfSections; index++)
{
//获取当前节区开始和结束地址
thisSectionBegin = (UINT_PTR)MemoryData + sectionArr[index]->VirtualAddress;
thisSectionEnd = thisSectionBegin + sectionArr[index]->Misc.VirtualSize;
//如果转换地址是这个节区的地址
if (thisSectionBegin <= address && thisSectionEnd >= address)
{
//转换为硬盘pe地址
offset = address - (UINT_PTR)MemoryData; //减去ImageBase得到在内存中的偏移
offset = offset - sectionArr[index]->VirtualAddress; //减去VirtualAddress得到地址距离节区开始的偏移
offset = offset + sectionArr[index]->PointerToRawData; //加上当前节区在硬盘PE中的偏移得到最终结果
break;
}
}
return offset;
}

bool WriteNewDataToSection(char* memoryData)
{
//写入的字节
char data[] = {
0x6A, 0x00,0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, //push 0 push 0 push 0 push 0
0xE8,0x00,0x00,0x00,0x00, //call xxxx
0xE9,0x00,0x00,0x00,0x00 //jmp xxxx
};
//指向要写入的地方
char* pointer = nullptr;
//指向要写入data数据的地方(E8 + 1,E9 + 1)
char* tempDataPointer = nullptr;
//数值类型的pointer
UINT_PTR entryPointer = NULL;
//E8后面四个字节的值
UINT_PTR E8_X = NULL;
//E9后面四个字节的值
UINT_PTR E9_X = NULL;
//E8真正要跳转的地址
UINT_PTR message = (UINT_PTR)&MessageBoxA;
//新OEP在内存中的偏移(也是写入OEP的值)
UINT_PTR newEntryPointer = NULL;
//开始循环所有节区
for (DWORD index = 0; index < fileHeader->NumberOfSections; index++)
{
//判断节区是否可执行(这里可以使用二进制运算判断,偷懒了就不写了)
if (sectionArr[index]->Characteristics != 0x60000020)
{
continue; //如果不是可执行节区就开始下一次循环
}
//如果节区空白字节大于写入字节
if (sectionArr[index]->SizeOfRawData - (sectionArr[index]->Misc.VirtualSize + sizeof(data)) >= 0)
{
//指针指向空白节区地址
pointer = (char*)((UINT_PTR)memoryData + sectionArr[index]->VirtualAddress + sectionArr[index]->Misc.VirtualSize);
entryPointer = (UINT_PTR)pointer;

//指针指向data中的E8后面一个的位置(也就是X(视频中运算公式中的X)的最高位)
tempDataPointer = &data[9];
/* 通过运算获取X
* 公式 = 真正要跳转的地址 - 当前指令的下一行地址(在真是运行中内存的地址,不是现在代码中的地址)
* 1.message = 真正要跳转的地址
* 2.optionalHeader->ImageBase + sectionArr[index]->VirtualAddress = 真是运行中内存中节区的开始地址
* 3.entryPointer - sectionArr[index]->Misc.VirtualSize = 现在代码内存中节区的开始地址(!!!不是真实运行中的节区开始地址)
* 4.(UINT_PTR)&pointer[13] = 现在代码内存中E8指令下一行的地址
* 5. 4 - 3 = 地址在节区中的偏移(现在这个偏移跟在内存中是一致的)
* 6. 2 + 5 = 当前指令下一行的地址
*/
E8_X = message - (optionalHeader->ImageBase + sectionArr[index]->VirtualAddress + ((UINT_PTR)&pointer[13] - (entryPointer - sectionArr[index]->Misc.VirtualSize)));
//通过右移分别获取X的四个二进制八位写入到E8后面的四个字节
tempDataPointer[0] = E8_X;
tempDataPointer[1] = E8_X >> 8;
tempDataPointer[2] = E8_X >> 16;
tempDataPointer[3] = E8_X >> 24;

//以下同上
tempDataPointer = &data[14];
E9_X = (optionalHeader->ImageBase + optionalHeader->AddressOfEntryPoint) - (optionalHeader->ImageBase + sectionArr[index]->VirtualAddress + ((UINT_PTR)&pointer[18] - (entryPointer - sectionArr[index]->Misc.VirtualSize)));;
tempDataPointer[0] = E9_X;
tempDataPointer[1] = E9_X >> 8;
tempDataPointer[2] = E9_X >> 16;
tempDataPointer[3] = E9_X >> 24;

//将字节写入到拉伸后的PE结构中
memcpy_s(pointer, sizeof(data), data, sizeof(data));
/* 计算写入字节处的地址在真实运行内存中的偏移
* 公式 = 地址在节区中的偏移 + 节区开始地址
* 1.entryPointer = 写入的开始地址
* 2.entryPointer - sectionArr[index]->Misc.VirtualSize = 节区开始地址(!!!不是真实运行的内存地址)
* 3.1 - 2 = 地址在节区中的偏移(也是在真实运行内存节区的偏移)
*/
newEntryPointer = (entryPointer - (entryPointer - sectionArr[index]->Misc.VirtualSize)) + sectionArr[index]->VirtualAddress;
//修改OEP
optionalHeader->AddressOfEntryPoint = newEntryPointer;
//指针指向PE结构的OPTIONAL开始地址
pointer = &memoryData[dosHeader->e_lfanew + 4 + sizeof(*fileHeader)];
//将新的OPTIONAL结构覆盖原本的OPTIONAL结构
memcpy_s(pointer, sizeof(*optionalHeader), optionalHeader, sizeof(*optionalHeader));
return true;
}
}
return false;
}

int main()
{
char* filePe = (char*)ReadProgramData("C:\\Users\\64107\\source\\repos\\PE\\PE\\Release\\PE.exe");
char* memoryPe = (char*)StretchData(filePe);
if (WriteNewDataToSection(memoryPe) != false)
{
LPVOID meFilePe = ShrinkData(memoryPe);
WriteMyFilePeData(meFilePe, "C:\\Users\\64107\\source\\repos\\PE\\PE\\Release\\PE.exe");
}
else
{
printf("程序可执行节区没有空白区域可供写入!\n");
}
system("pause");
free(filePe);
free(memoryPe);
return 0;
}

新增节-添加代码

新增节需要满足的条件:

确保新增节后,已存在的节表区域仍有剩余空间以容纳一个节表(遵循Windows规则,若不遵守规则,可能导致程序异常。)。计算公式如下:

1
SizeOfHeader - DOS文件头 - 可选PE头 - 节表数量 * 0x28(40) > 80

新增节步骤:

  1. 添加新的节表
    • 在现有节表末尾新增一个节,可以直接复制已有节表数据进行初始化。
  2. 填充新增节后的空间
    • 在新增节后,填充一个节大小的 0x00,确保数据完整。
  3. 修改PE头中的节表数量
    • 找到PE头中的NumberOfSections字段,增加1,更新节表数量。
  4. 更新SizeOfImage字段
    • 修改PE头中的SizeOfImage字段,增加内存对齐后的新增节大小。
  5. 在文件末尾新增节数据
    • 在原有数据末尾,添加一个内存对齐后的新增节数据区域。
  6. 修正新增节的属性
    • 确保新增节的属性字段设置正确(如只读、可执行等)。根据需要调整其Characteristics字段。
  7. 填充新增节数据
    • 在新增节区域内写入有效数据,避免数据为空导致程序运行失败。

当节表区空间不足时的处理方法:

  • 方法:前移NT头
    • 将NT头整体前移 0x20 个字节,覆盖DOS头后面的字符串部分,为节表区腾出额外空间。
  • 注意事项:
    • 确保调整后,PE结构完整且头部的偏移量正确。
    • 修改后及时校验文件是否能够正常运行。

例:

找到节表数量为5

image-20241127204016101

如图为已存在的节表 往下开始全是空白区(空间足够大 可以添加新的节)

image-20241127204150254

复制40个字节即第一个节表到空白区,创建新的节表,同时将节数量改为6

image-20241127204636604

假设新增大小为1000字节的节表,找到sizeofimage加上1000

image-20241127205129099

image-20241127205101675

在文件结尾插入1000h大小的字节

image-20241128190018596

image-20241128190037757

填写新节表信息

image-20241201193157063

Name即节表的名字。可以随便改但不要超过限制大小;

Virtual Size即内存中的大小(对齐前的长度),这里直接填1000即可;

Virtual Offset即内存中的偏移(Virtual Address),根据前一个节表的信息可知这里应填00000E00+00005000=00005E00;

Raw Size即文件中大小(对齐后的长度),这里和Virtual Size填一样即可(1000),因为这个文件对齐前后没有拉伸;

同理Raw Offset即文件中偏移,和Virtual Offset填一样即可(00005E00);

Characteristics即块属性(标志),看需要填写,这里保持和第一个节表相同即可

image-20241201193256065

扩大节-合并节-数据目录

扩大节

注意事项:只能扩大最后一个节。

操作步骤:

  1. 拉伸到内存

    • 将节加载到内存后处理。
  2. 分配新的空间

    • 增加Ex的空间,修改 SizeOfImage

      1
      SizeOfImage = SizeOfImage + Ex

    image-20250112164309830

  3. 修改最后一个节的大小

    • 更新最后一个节的 SizeOfRawDataVirtualSize

      1
      2
      3
      SizeOfRawData = VirtualSize = N

      N = (max(SizeOfRawData, VirtualSize) 内存对齐后的值) + Ex
  4. 更新SizeOfImage大小

    • 修改 PE 头中的 SizeOfImage值,增加新增空间的大小:

      1
      SizeOfImage = SizeOfImage + Ex

合并节

  1. 拉伸到内存

    • 将要合并的节加载到内存,便于处理。
  2. 计算合并后的大小

    • 合并后的大小取决于所有节的大小:

      1
      2
      Max = max(VirtualSize, SizeOfRawData) 内存对齐后的大小
      合并节大小 = VirtualAddress + Max - SizeOfHeader 内存对齐后的值
  3. 修改第一个节的大小

    • 更新第一个节的 VirtualSizeSizeOfRawData,两者设为合并后的大小。
  4. 调整节的属性

    • 修改第一个节的属性,使其包含所有节的属性(如读、写、执行权限)。
  5. 修改节表数量

    • 更新 PE 头中的 NumberOfSections 字段,将其设为 1

数据目录

基础概念

  1. 数据目录位置:
    • 数据目录位于可选 PE 头的最后一项,包含 PE 文件运行时需要的重要信息。
  2. 数据目录的作用:
    • 定位程序的资源信息,例如:
      • 程序图标位置。
      • 使用的系统函数。
      • 提供给其他程序的函数。

数据目录结构:

1
2
3
4
5
6
7
c
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 数据目录的内存偏移(经过内存拉伸)
DWORD Size; // 数据目录的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16

数据目录的 16 项:

  • 与程序运行相关的主要目录项:
    1. 导出表 (Export Table)
    2. 导入表 (Import Table)
    3. 重定位表 (Relocation Table)
    4. IAT 表 (Import Address Table)
  • 其他目录项:资源表、异常信息表、安全证书表、调试信息表、版权所以表、全局指针表、TLS表、加载配置表、绑定导入表、延迟导入表、COM信息表、最后一个表保留未使用。