Oops-re's Blog.

PE学习笔记

字数统计: 9.4k阅读时长: 35 min
2021/12/20

1PE 视频学习

1.认识PE

首先,先介绍什么是**可执行文件(executable file)**:可以由操作系统进行加载执行的文件

PE(Portable Executable) 就是windows操作系统的可执行文件结构,一种可移植的可执行文件,即可在任何windows平台上运行

另外 ELF(Executable and Linking Format)是Linux系统的可执行文件结构


1.1PE文件格式的运用

  1. 病毒与反病毒
  2. 外挂与反外挂
  3. 加壳与脱壳
  4. 无源码修改功能、软件汉化。。。等

1.2如何识别PE文件

1.2.1 文件后缀名

​ 一般的来说,DLL,EXE,OCX,SYS等这些都是可执行文件

​ 但这种判断方式很片面,PE文件可以是任何拓展名,只要它这个文件符合PE文件结构,就能确定它是PE文件,所以这里可以根据它的文件结构来判断它是否是PE文件—PE指纹

1.2.2 PE指纹

​ 使用2进制文本编辑软件查看,这里我是用的是WinHex,通过它打开文件,就可以看到文件的16进制值、对应的ascii码,还有偏移量。。。等等image-20211104122526406

这里我打开的是一个exe文件,判断其文件是否是PE文件的步骤如下:

  1. 看头两个字节是否是 4D 5A ,对应的ascii码是“MZ”,然后看偏移量为3C的位置,上图为80,然后再找到偏移量为80的位置,看到16进制值“50 45” 对应的ascII是PE,这样就能判断其文件为PE文件了

1.3 PE文件的整体结构

PE

​ 如上图,PE文件的整体结构由下到上分为:DOS部分、PE文件头、节表、节数据,其中前三个部分更像是“说明书”,由结构体组成,这些结构体可以在 WINNT.h 文件中找到。而节数据就像是一个文本里的内容,就是一些具体的数据。其中通过学习这些结构体,就是DOS部分、PE文件头和节表就能更清楚那些16进制所代表的是些什么,以及他们为什么要这样写。

1.4 认识PE文件在磁盘中的状态

1.4.1 DOS部分

​ 在一个PE文件的第一部分就是一个结构体——IMAGE_DOS_HEADER,也被称为 DOS MZ文件头

image-20211104154421150

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 {
WORD e_magic;
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew; //指向PE标识
} IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;

该结构体大小为64个字节,所以对着二进制文件看,前64个字节长度的内容都是 DOS MZ文件头image-20211104162758804

然后看结构体中的最后一个变量 LONG e_lfanew; 这是long型变量,4字节大小,同时这个变量指向PE文件头的位置,在上图指向的PE头就是 80 位置。

​ 其次就是 DOS stub,DOS块,这部分的大小是不确定的,有连接器决定,同时这部分的内容是可修改的,并不会对PE文件的运行造成修改。虽然这部分的大小是不确定的,但是我们可以根据DOS MZ文件头和PE文件头来确定————PE文件头上面,DOS MZ文件头最后一个变量的后面。如下image-20211104165115913

一般其16进制值对应的内容都是

1
This program must be run under Win32

这就是PE文件 DOS部分的大概描述,这部分在现在的windows上大概都是无关紧要的,所以可以在这部分进行相关修改,例如将其全置0image-20211104192410072

其修改前后运行结果一致。

1.4.2 PE文件头

image-20211104193145645

由结构体 IMAGE_NT_HEADERS 组成

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE文件头标志 4字节
IMAGE_FILE_HEADER FileHeader; //标准PE头 结构体IMAGE_FILE_HEADER 20字节
IMAGE_OPTIONAL_HEADER32 OptionalHeader;//拓展PE头结构体IMAGE_OPTIONAL_HEADER32 224字节
} IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;//这是32位,并不是64位的

按其大小找到二进制文件中所指定的内容

首先是 PE文件头标志 就是DOS结构体最后一个变量指向的十六进制值 “50 45 00 00” 其ascii对应的就是 ”PE “

然后就是 标准PE头 ,20字节长度image-20211104195406998

再就是 拓展PE头,其大小可由结构体 IMAGE_FILE_HEADER 中的成员SizeOfOptionalHeader 所指定

1
2
3
4
5
6
7
8
9
10
//标准PE头
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;//保存扩展PE头的大小
WORD Characteristics;
} IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;

在二进制文件中验证,其为倒数第二个变量且大小为2字节,所以在二进制文件中就是 ”E0 00“ ,由于是小端存储,所以对应大小就是224大小。

其在二进制文件中的位置为image-20211104200234028

1.4.3 节表

image-20211104200335534

由结构体 IMAGE_SECTION_HEADER 构成,其大小为40个字节

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;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;

该部分可由多个节表组成,而这部分有多少个节表,后面的 节数据 就有多少钟

比对二进制文件

image-20211105190713347

这个二进制文件有5个节表,每个节表都有ascii值所对应。

这部分后面就是由编译器放置的各种数据,而这部分后面才又是节数据

而这部分长度又是怎么判断的呢?? 可以根据 拓展PE文件头 中的变量 SizeOfHeaders 来确定

1.4.4 节数据

首先要要找到它的位置,其判断方法就是:看 SizeOfHeaders 是多少,而 SizeOfHeaders 的大小就是

DOS头的大小 + PE头的大小 + 所有节表的大小 的结果再根据FileAlignment取其整数倍

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
//扩展PE头 
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment; //文件对齐量 在第36字节的后面
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders; //头文件大小总和 在FileAlignment后面20个字节
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;

在二进制文件中验证:

image-20211104210340460

FileAlignment大小为”02 00“SizeOfHeaders大小为”04 00“,符合其整数倍原则,所以,节数据的位置就可以从开始找400个字节,也可以直接看偏移量,如下就是节数据的内容

image-20211104210015735

而节数据中的各个节块的大小也满足文件对齐的整数倍的原则

==注:PE文件在运行时,其状态和其在磁盘存储中时的状态有所不同。主要区分在文件对齐位的大小,还有总大小不同==

2.头文件的属性说明

2.1 DOS头

关于DOS头部分主要是因为最初的WINDOWS系统是在DOS下运行的,而存在一部分结构,而现在的windows平台大多数不需要,所以这部分结构中的内容,很大一部分已经被抛弃,现在也只需要两部分——就是”MZ”头部和指向PE头标识的尾部,所以其余部分都可以由我们随意更改包括藏病毒,藏flag之类的,也并不会对程序的运行造成任何阻碍。

2.2 PE头

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE文件头标志 4字节
IMAGE_FILE_HEADER FileHeader; //标准PE头 结构体IMAGE_FILE_HEADER 20字节
IMAGE_OPTIONAL_HEADER32 OptionalHeader;//拓展PE头结构体IMAGE_OPTIONAL_HEADER32 224字节
} IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;//这是32位,并不是64位的

2.2.1 标准PE头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
//2字节 运行在何种CPU上;0就是任意;014C是Interl386及以后;8664则是x64也就是64位系统上
WORD NumberOfSections;
//2字节 保存节的数量
DWORD TimeDateStamp;
//4字节 由编译器填写的时间戳,与文件属性里的的(修改、创建时间)无关
//从1970年0点0时0分0秒算,和编译器所填写的时间算时间差,由编译器确定
DWORD PointerToSymbolTable;
// 调试相关
DWORD NumberOfSymbols;
//调试相关
WORD SizeOfOptionalHeader;
//2字节 可保存扩展PE头的大小 32位是0XE0(244字节),64位是0XF0(240字节)
WORD Characteristics;
//2字节 文件属性
//这部分可根据其二进制位上是否置1,来判断该PE文件的相关属性
} IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;

文件属性 Characteristics 表

数据位 为1时的含义
0 文件中不存在重定位信息
1 文件是可执行的
2 不存在行信息
3 不存在符号信息
4 调整工作集
5 应用程序可处理大于2GB的地址(64位文件
6 此标志保留
7 小尾方式
8 只在32位平台上运行
9 不包含调试信息
10 不能从可移动盘运行
11 不能从网络运行
12 系统文件(如驱动程序),不能直接运行
13 这是一个DLL文件
14 文件不能在多处理器计算机上运行
15 大尾方式

2.2.2 扩展PE头

1
首先根据平台的不同32位和64位,其结构体成员有细微的差别,先把32位的看懂,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
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
typedef struct _IMAGE_OPTIONAL_HEADER {
/**/WORD Magic;
//2字节 能准确识别该文件的位数,要是该成员位置上为 10B则该程序为32位;若为20B则是64位
BYTE MajorLinkerVersion;
//1字节 链接器版本号
BYTE MinorLinkerVersion;
//1字节 链接器版本号
//链接器 是一个将编译器产生的目标文件打包成可执行文件或者库文件或者目标文件的程序。
DWORD SizeOfCode;
//4字节 所有代码节的总和 文件对齐后的大小 由编译器填写
DWORD SizeOfInitializedData;
//4字节 包含所有已经初始化数据的节的总和 文件对齐后的大小 由编译器填写
//初始化数据 就是在编写程序时,那些被赋初值的变量;反之就是未初始化数据
DWORD SizeOfUninitializedData;
//4字节 包含所有未初始化数据的节的大小 文件对齐后的大小 由编译器填写

/**/DWORD AddressOfEntryPoint;
//4字节 程序的入口
//该成员要配合 ImageBase 成员来看
//该成员存储的值就是PE文件运行时,文件开始执行的地址(相对于ImageBase
//也就是说,文件运行时实际的执行地址是 AddressOfEntryPoint+ImageBase

DWORD BaseOfCode;
//4字节 代码开始的基址 由编译器填写
DWORD BaseOfData;
//4字节 数据开始的基址 由编译器填写

/**/DWORD ImageBase;
//4字节 内存镜像基址
//什么是内存镜像基址----
//在每个程序运行的时候都会有一个4GB的虚拟内存,而这个基址就是该PE文件的内容在这4GB的起始位置
/**/DWORD SectionAlignment;
//4字节 内存对齐
//PE文件在内存上运行时,每个节数据的大小就是其整数倍的大小;PE的DOS头+PE头+节表的大小也是其整数倍
/**/DWORD FileAlignment;
//4字节 文件对齐
//PE文件在磁盘中时,每个节数据的大小就是其整数倍的大小;PE的DOS头+PE头+节表的大小也是其整数倍

WORD MajorOperatingSystemVersion;
//2字节 标识操作系统的版本号 主版本号
WORD MinorOperatingSystemVersion;
//2字节 标识操作系统的版本号 次版本号
WORD MajorImageVersion;
//2字节 PE文件自身版本号
WORD MinorImageVersion;
//2字节 PE文件自身版本号
WORD MajorSubsystemVersion;
//2字节 运行所需子系统的版本号
WORD MinorSubsystemVersion;
//2字节 运行所需子系统的版本号
DWORD Win32VersionValue;
//4字节 子系统版本的值,必须为0

/**/DWORD SizeOfImage;
//4字节 内存中整个PE文件的映射文件的尺寸,可以大,且必须是SectionAignment的整数倍
//决定整个PE文件在4GB内存空间的大小
/**/DWORD SizeOfHeaders;
//4字节 所有头+节表按照文件对齐后的大小,否则加载会出错
//不管什么状态,取值大小的方式都一样
/**/DWORD CheckSum;
//4字节 检验和,一些系统文件有要求,用来判断文件是否被修改
//通过把PE文件以WORD为单位对数据块进行累加
//将累加和加上文件的长度
//最后比对该成员的值,如果不一样就是被修改过
WORD Subsystem;
//2字节 子系统
//如果该成员中的值为1,则是驱动程序;2就是图形界面;3就是控制台、DLL
WORD DllCharacteristics;
//2字节 文件特性
//可通过类似于标准PE头的文件属性查表来看
DWORD SizeOfStackReserve;
//4字节 初始化时保留的栈大小
DWORD SizeOfStackCommit;
//4字节 初始化时实际提交的栈大小
DWORD SizeOfHeapReserve;
//4字节 初始化时保留的堆大小
DWORD SizeOfHeapCommit;
//4字节 初始化时实际的堆大小
DWORD LoaderFlags;
//4字节 调试相关
DWORD NumberOfRvaAndSizes;
//4字节 目录项数目
//可以知道该文件使用了多少个表
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32;

文件特性–DllCharacteristics

数据位 相关特性
0 保留,必须为零
1 保留,必须为零
2 保留,必须为零
3 保留,必须为零
4 DLL 可在加载时被重定位
5 强制代码实施完整性
6 该映像兼容DEP
7 可以隔离,但并不隔离此映像
8 映像不使用SEH
9 不绑定映像
10 保留,必须为零
11 该映像为一个WDM driver
14 保留,必须为零
15 可用于终端服务器

==后续PE文件重点学习的就是:表,即扩张PE中最后两个成员的相关属性==

1
2
3
4
5
DWORD NumberOfRvaAndSizes;
//4字节 目录项数目
//可以知道该文件使用了多少个表
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//这有多少张表该数据的大小就是多少

3. 节表属性

首先要知道,在一个PE文件中节表是一个结构体数组,也就是有多个节表构成,同时,这部分数据按文件对齐的大小存储。可以通过FileAlignment看。

且该结构体中的成员大多都是标识 节数据 中的相关属性,包括在文件和在内存中的起始地址、在文件中尺寸

每个节40个字节大小

结构体**_IMAGE_SECTION_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
#define IMAGE_SIZEOF_SHORT_NAME 8

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
//节表的名字 就是ascii字符串,只截取8个字节
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
//4字节 是该节在没有对齐前的真实尺寸,可以不准确
//就是标识了在内存中,该节表会有多大
DWORD VirtualAddress;
//4字节 在内存中的偏移地址,加上imageBase才是在内存中的真正地址
DWORD SizeOfRawData;
//4字节 展示该节在文件中文件对齐后的大小
DWORD PointerToRawData;
//4字节 决定当前这个节的节数据在文件中的位置
/*
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
*/
//调试相关,对分析没有影响
DWORD Characteristics;
//节的属性
//4字节 根据二进制下标上是否置1,查表判断其相关属性
} IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;
1
2
3
4
5
6
7
8
9
10
11
关于Misc和SizeOfRawData的大小关系:
在一个程序中,往往存在一些全局变量,而这些全局变量有赋初值的和没有赋初值的
根据这两种情况,我们可以想象一个PE文件的全局变量在文件和内存中的不同
当PE文件中的一部分全局变量没有赋初值时,由于没有东西,
那么这个PE文件这部分节数据在文件的大小SizeOfRawData就可能小于Misc的值
因为当这些全局变量在内存中运行时是”会被赋初值“的,
所以就进行”展开“,也就会比文件中存在时的大小更大
当这部分节数据的全局变量被赋初值时,SizeOfRawData就可能大于Misc的值
这部分”初值“也是存在节数据中的,而在内存中就大概是这些大,
同时Misc是对齐前的大小,而SizeOfRawData是文件对齐后的大小

4.RVA和FOA的转换

首先要知道RVA和FOA是什么。

1
2
3
4
RVA是相对偏移地址,就是一个全局变量在内存中的偏移地址。
假设PE文件中的一个 全局变量 的地址为0x00425168,那么这个全局变量的RVA = 425168 - ImageBase
FOA是文件偏移. 就是文件中所在的地址.
FOA = RVA - 节.VirtualAddress + 节.PointerToRawData

然后我们可以通过这两个东西得到什么

实验:

1
2
3
address:403004
init:10
请按任意键继续. .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
假如我们得知了一个全局变量的在内存运行时的地址
address--0x403004
通过2进制文件得到该PE文件的内存镜像基址
ImageBase--0x400000
通过公式可以得知该全局变量的RVA为
RVA-- address-ImageBase = 0x3004
然后判断RVA在哪个节的位置
如果在头部,那就FOA=RVA
如果在某个节中(节.VirtualAddress ~节.内存对齐后的尺寸)
那么FOA = RVA - 节.VirtualAddress + 节.PointerToRawData
通过公式得
节.VirtualAddress--0x3000
差值--0x004
节.PointerToRawData --0x1c00
FOA-- 0x1c00 + 0x4 = 0x1c04

结果:

1
2
3
address:403004
init:7
请按任意键继续. . .

可以看到全局变量的值被我们所修改,也就是说我们可以通过计算得到FOA的值,进而在2进制文件中修改全局变量的值,来达到和CE一样的效果。

5.在PE文件中添加代码


我们已经知道了,在一个PE文件中,可以通过它的2进制文件的一些空白区域进行填充代码,从而达到修改这个PE文件的执行结果的目的,但是,如果是两三句代码还好,一旦想填充的代码过多,就会造成修改到其他的源码的后果,所以这里就可以通过 扩大节 和 新增节 的技术,来进一步增加填充的区域。

6.扩大节

而想实现扩大节这一效果,步骤如下:

1
2
这里我们只需要修改想扩大的节的节表结构体中的成员,同时对修改到的关联的头部成员进行修改就行
为了方便首选修改最后一个节的相关属性
1
2
3
4
我们需要修改的成员:
SizeOfRawData//该节在文件中的对齐后的尺寸
VirtualSize//该节在内存中的对齐前的大小
SizeOfImage//PE文件在内存中的尺寸(内存对齐)

具体步骤:

1
2
3
4
5
6
7
[1]分配一块新空间,大小为--s
[2]将最后一个节的SizeOfRawData和VirualSize改成N
N = (SizeOfRawData或VirtualSize内存对齐后的值)+ s
/*
这里的括号里的值根据 谁最大选谁 的原则来算
*/
[3]修改SizeOfImage的大小

实际操作–实验2

[1]-分配一块新空间,大小为 – 4096(0x1000)个字节

1
选中文件末尾,点击工具栏中 编辑-粘贴0字节-填入”4096“字节

image-20211123185154394

image-20211123185221081

image-20211123185239381


[2].将最后一个节的SizeOfRawData和VirualSize改成N
N = (SizeOfRawData或VirtualSize内存对齐后的值)+ s

1
2
3
4
5
6
7
这里选取的最后一个节表为---.tls
节.SizeOfRawData---0x200
节.VirualSize------0x20
内存对齐后---0x1000
根据 谁大取谁 的原则
N = 节.VirualSize(内存对齐) + s
N = 0x1000 + 0x1000 = 0x2000

image-20211123185901780

[3].修改SizeOfImage的大小

==待定==

7.新增节

前面学习了扩大填充代码区域的技术之一扩大节,然后就是另外一个技术——新增节,在我们需要填充大量代码时,不必要将这部分代码拆分成多块,穿插在多个节与节之间空白区。可以直接新增节实现。

具体步骤:

1
2
3
4
5
6
7
<1>判断是否有足够的空间,可以添加代码
<2>在节表中新增一个成员
<3>修改PE头部中节的数量
<4>修改SizeOfImage的大小
<5>在原有的数据最后,新增一个节的数据(内存对齐的整数倍)
<6>修正新增节表的属性
//可以便增加边修改

具体实践:实验3


8.合并节

一丶简介

   根据上一讲.我们为PE新增了一个节. 并且属性了各个成员中的相互配合. 例如文件头记录节个数.我们新增节就要修改这个个数.

那么现在我们要合并一个节.以上一讲我们例子讲解.

  以前我们讲过PE扩大一个节怎么做. 合并节跟扩大节类似. 只不过一个是扩大. 一个是合并了.

合并节的步骤.

  1.修改文件头节表个数

  2.修改节表中的属性

    节.sIzeofRawData 节数据对齐后的大小.

  3.修改扩展头中PE镜像大小 SizeofImage

  4.被合并的节以0填充.

二丶实战合并一个节

  **1.**修改文件头中节表个数

     为什么修改应该不用多说了. 我们既然合并. 那么节就要少一个.那么自然就进行修改了.

img

原节表有8个.我们修改为7即可.

  2.修改节.SizeofRawData 节数据对齐后的大小.

img

我们把最后的AAAA节.合并到上一个节.rsrc中.

  .rsrc.SizeofRawData = .文件对齐(rsrc.SizeofRawData + AAA.节数据的大小)

修改这个属性就按照上面的公式修改就行.原来节数据大小.加上要被合并的节的数据大小.按照文件对齐存放即可.

  例如下图:

img

原来节数据对齐后的大小是0x600. AAAA节数据对齐后的大小是0x1000.那么修改.rsrc.SizeofRawData 为 0x1600即可.

img

  最后一个节表以0填充即可.

  **3.**修改扩展头的PE镜像大小 SizeofImage

img

我们上一讲新增了一个节.所以映像大小为0x1E000. 所以现在要进行修改.合并了0x1000数据大小.那么改为0x1D000即可.

  **4.**测试程序

img

程序可以直接运行.那么内存中看看节展开位置有没有我们的合并节的节数据.

img

内存中0x41c000位置.就是节展开位置.我们没有合并之前.并没有我们的FFFF填充的数据.合并之后.出现了数据.说明已经成功合并了这个节了.

也相当于对这个节进行扩大了.


9.导入表与导出表

==注:学习这部分知识前,推荐先学习DLL文件的相关知识==

学习之前的知识,一个可执行程序被拖进winHex分析时,由一堆的二进制构成,这就是一个PE文件。但是一个可执行程序就是这么一个PE文件吗?并不是,当我们将一个可执行程序用OD分析时,在 “E”模块(当前程序的进程) 可以看到有许多的文件组成,这些都是标准的PE文件,包括DLL(动态链接库),为这个可执行程序提供函数或者其他功能。image-20211124140322372

所以一个可执行程序通常是由多个PE文件构成,而看一个这个可执行程序使用到哪些PE文件(或由哪些PE文件构成)时,就可以去查看导入表获得这些信息。

而一个PE文件提供了那些函数,这些函数怎么用,等等相关信息,同样可以查表—-导出表

也就是说,一个可执行程序有一组PE文件构成,而这些PE文件通过导入表和导出表 进行联系。同时注意,一般exe文件由于很少提供函数,所以很少存在导出表,但是并不是不能有,从根本来看,他们都是PE文件,是相同的结构,所以是可以存在的。

怎样去查看这两张表呢?

首先认识他们

​ 这两张表都在拓展PE头的最后一个属性—结构体数组IMAGE_DATA_DIRECTORY_ARRAY DataDirArray的中,由于是结构体数组,其结构都一样,每个结构体占8个字节,共16个结构体。

1
2
3
4
5
6
7
IMAGE_DIRECTORY_ENTRY_EXPORT
struct IMAGE_DATA_DIRECTORY Export{
DWORD VirtualAddress;
//导出表在哪里
DWORD Size;
//导出表的有多大
}

这就是指向导出表的结构体,位于结构体数组的第一个成员

1
2
3
4
5
6
7
IMAGE_DIRECTORY_ENTRY_IMPORT
struct IMAGE_DATA_DIRECTORY Import{
DWORD VirtualAddress;
//导入表在哪里
DWORD Size;
//导入表的有多大
}

这是指向导入表的结构体,位于结构体数组的第二个成员

9.1认识导出表

==注:图片中的DLL文件的文件对齐和内存对齐相同

1
2
3
4
5
6
7
IMAGE_DIRECTORY_ENTRY_EXPORT
struct IMAGE_DATA_DIRECTORY Export{
DWORD VirtualAddress;
//导出表在哪里
DWORD Size;
//导出表的有多大
}

image-20211128185102467

1
2
DWORD VirtualAddress----0x5A90
DWORD Size-----0x67

可以看到

导入表结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
//未使用
DWORD TimeDateStamp;
//时间戳
WORD MajorVersion;
//未使用
WORD MinorVersion;
//未使用
DWORD Name;
//指向该导出表文件名字的字符串
DWORD Base;
//导出函数起始序号
DWORD NumberOfFunctions;
//所有导出函数的个数
DWORD NumberOfNames;
//以函数名称导出的函数个数
DWORD AddressOfFunctions;
//导出函数地址表RVA
DWORD AddressOfNames;
//导出函数名称表RVA
DWORD AddressOfNameOrdinals;
//导出函数符号表RVA
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

总共40个字节

前三个不是很重要,其中第二个成员TimeDateStamp–时间戳,和标准PE头中的时间戳一样,显示的是该DLL或EXE文件的生成的时间。

1
2
3
4
5
6
7
8
DWORD Characteristics;
//未使用
DWORD TimeDateStamp;
//时间戳
WORD MajorVersion;
//未使用
WORD MinorVersion;
//未使用

重要的是后边几个成员,都是4字节的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DWORD Name;
//指向该导出表文件名字的字符串
DWORD Base;
//导出函数起始序号
DWORD NumberOfFunctions;
//所有导出函数的个数
DWORD NumberOfNames;
//以函数名称导出的函数个数
DWORD AddressOfFunctions;
//导出函数地址表RVA
DWORD AddressOfNames;
//导出函数名称表RVA
DWORD AddressOfNameOrdinals;
//导出函数序号表RVA

除Base、NumberOfFunctions、NumberOfNames 存放的都是RVA内存偏移地址,要在文件状态查看的话需要转化为FOA,来找到它指向的值。

image-20211128185545932

​ 【图一】

一个一个来看

9.1.1 Name

在该部分的第12个字节后面的那四个地址0x5ADE

image-20211128190339246

指向的字符串为DllDemo.dll这个就是这个DLL文件的名字。


==这里先跳过Base成员==

9.1.2 NumberOfFunctions 函数个数

指向该DLL文件所导出函数的个数,看【图一】,所指向的值为0x5,也就是说该DLL文件共导出5个函数,但是该文件实际编写中的只导出了4个函数

EXPORTS 函数序号 是否有函数名
Plus @12
Sub @15
Mul @13
Div @16

之所以显示的是0x5,是因为它的算法就是

1
NumberOfFunctions = 函数序号最大值 - 函数序号最小值 

9.1.3 NumberOfNames 以函数名称导出的函数个数

指向该DLL文件 以函数名称导出的函数个数 ,看【图一】,所指向的值为0x3,即有3个函数是以名字导出的。

9.1.4 AddressOfFunctions 导出函数地址表

指向了该DLL文件的函数地址表的RVA,看【图一】,其值为 0x5AB8,因为该DLL文件的内存对齐和文件对齐相同,即FOA=RVA,所以直接找到偏移量为0x5AB8进行查看

image-20211128193220921

下标 函数地址表
0 0x00 00 10 10
1 0x00 00 10 30
2 0x00 00 00 00
3 0x00 00 10 20
4 0x00 00 10 40

可以看到中间有一个是0x00000000的地址,是一个空地址,也就是“有一个函数不存在”对应了导出的只有4个函数,但是 NumberOfFunctions 的值是5.

1
2
3
拓展知识:
想知道这些地址实现的是什么功能,可以找到它指向的偏移地址,然后在OD中将这些硬编码添加进去
OD会自动生成汇编指令

9.1.5AddressOfNames 导出函数名称表

该成员指向的地址同样也是RVA,在这个DLL文件中的他的FOA=RVA.还是看【图一】,值为0x5ACC

image-20211128194952277

下标 函数名称表 指向字符串
0 0x00 00 5A EA Div
1 0x00 00 5A EE Mul
2 0x00 00 5A F2 Puls

可以看到其按开头字母的大小顺序存储。

9.1.6 AddressOfNameOrdinals 导出函数序号表

值得注意的是,函数名称表多大,序号表就有多大,在该PE文件中序号表存3个值,还是看【图一】

其RVA值为0x5AD8,由于FOA=RVA,所以FOA也等于0x5AD8

image-20211128200124601

下标 函数序号表(两字节)
0 0x00 04(4)
1 0x00 01(1)
2 0x00 00(0)

认识完导出函数的各个表后,如何去查找这些函数呢?

windowsAPI提供一个函数

1
2
3
4
5
FARPROC GetProcAddress(
HMODULE hModule, // DLL模块句柄
//就是你当前DLL文件的起始位置 ImageBase
LPCSTR lpProcName // 函数名
);

通过函数名去调用的话,像调用Div,首先在导出函数名表查找Div,然后根据其下标,(Div的下标为0)去导出函数序号表中根据下标进行查找对应的值(对应的值为4),将这个值定为导出函数地址表的下标,按对应的地址(0x00 00 10 40)找,就是该函数了。

还有一个方法通过函数序号去找的话,这个时候就要用到成员 Base

9.1.7 Base 导出函数起始序号

该成员指向的是,本文件的导出函数的起始序号,在该文件中的值为0x0C,就是12,最后完善导出函数地址表

下标 函数地址表 函数序号
0 0x00 00 10 10 12
1 0x00 00 10 30 13
2 0x00 00 00 00 14
3 0x00 00 10 20 15
4 0x00 00 10 40 16

最后假如查找的函数序号为**@15,这样就是直接找到地址0x00 00 10 20**的函数


9.2认识导入表

1
2
3
首先要知道一个进程是由一组PE文件构成的:
PE文件提供了哪些功能可由---导出表识别
PE文件运行需要哪些模块以及依赖这些模块重点哪些函数由---导入表识别

指向导入表在哪里和导入表多大的是第二的结构体。

1
2
3
4
5
6
7
IMAGE_DIRECTORY_ENTRY_IMPORT
struct IMAGE_DATA_DIRECTORY Import{
DWORD VirtualAddress;
//导入表在哪里
DWORD Size;
//导入表的有多大
}

image-20211128205437268

1
2
3
4
5
6
DWORD VirtualAddress;
//导入表在哪里
//0x0500D0
DWORD Size;
//导入表的有多大
//0x0104

这里用的EXE文件内存对齐和文件对齐一致,所以RVA=FOA。

由于一个模块对应一个导入表,所以一个PE文件里可能有多个导入表。

还是先认识导入表的基本结构体,一个导入表的宽度为20个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
__C89_NAMELESS union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //这里是RVA,指向的是_IMAGE_THUNK_DATA结构数组
} DUMMYUNIONNAME;
//
DWORD TimeDateStamp;
//时间戳
DWORD ForwarderChain;
DWORD Name;
//RVA,指向提供功能模块的DLL文件的名字,以0结尾
DWORD FirstThunk;
//这里是RVA,指向的是_IMAGE_THUNK_DATA结构数组
} IMAGE_IMPORT_DESCRIPTOR;

9.2.1 确认导入表的数量

在全部的导入表结束后,会以20个连续的00结尾

image-20211128212136046

按照其字节数,对这个文件进行划分,最后以20个连续的00结尾,可以看到这个PE文件由12个导入表组成

怎样判断是否就是12个导入表呢?

1
通过DTdebug打开该文件,并查看,就可以看到这个PE文件所有使用到的DLL文件

image-20211128213454081

9.2.2 确认导入表的功能模块对应的DLL文件

导入表的看倒数第二个成员Name,

image-20211128212805991

按照它的RVA转化为FOA找到偏移量对应的字符串

image-20211128212716468

可以知道,这个PE文件的第一个导入表所依赖的DLL文件是KERNEL32.dll

9.2.3 通过导入表确定依赖的函数

这里就要看其他的导入表的其他两个成员OriginalFirstThunk、FirstThunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
__C89_NAMELESS union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //这里是RVA,指向的是_IMAGE_THUNK_DATA结构数组
//指向的是INT(导入函数名称表)
} DUMMYUNIONNAME;
//
DWORD TimeDateStamp;
//时间戳
DWORD ForwarderChain;
DWORD Name;
//RVA,指向提供功能模块的DLL文件的名字,以0结尾
DWORD FirstThunk;
//这里是RVA,指向的是_IMAGE_THUNK_DATA结构数组
//指向的是IAT(导入函数地址表)
} IMAGE_IMPORT_DESCRIPTOR;

这两个成员各指向的一张表,但这两张表的结构相同,其结构体为

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal; //序号
DWORD AddressOfData; //RVA,指向IMAGE_IMPORT_BY_NAME
//也就指向另外一个结构体IMAGE_IMPORT_BY_NAME
//IMAGE_IMPORT_BY_NAME,这个结构体中可以看到函数在导出表的索引,和函数名称
} u1;
} IMAGE_THUNK_DATA32;
是由联合体组成的,共4字节宽度
1
2
3
4
5
6
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //可能为空,编译器决定,如果不为空,就是该函数在导出表中的索引
BYTE Name[1];//函数名称
//最后以4字节的00结尾
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

先来看OriginalFirstThunk指向的INT表,该表存储的就是导入函数名称,但是有两种情况

1
2
3
4
5
6
7
8
9
第一种:
该部分的值高位时1,即0x10 00 00 08
就去掉高位的1,去后面的值,
这部分就是函数的导出序号
只要调用函数 GetProcAddr(m,函数的名称或导出序号),就能查询到该函数
第二种:
最高位为为0,则可以判断这是一个RVA,例如:0x00 01 1D E8
指向另外一张表,由结构体 IMAGE_IMPORT_BY_NAME 构成
将其转化为FOA,就能知道该函数的名称和该函数在导出表中的索引

9.2.4通过导入表确定导入函数的地址

这里我们是通过IAT表来看的,也就是导入表的最后一个成员FirstThunk

注意,IAT表有两种状态,根据PE文件的状态来看,也就是文件中和内存中的状态。

1
2
3
4
5
在文件中,
IAT表是多个RVA来指向其所对应的导入函数的名字或序号
然后再通过 GetProcAddr() 函数 获得其地址
在内存中
IAT表存储的就是导入函数的地址

1
2
3
4
5
补充知识:
这里有一个问题,就是IAT和INT表意义都一样,为什么要存在两个?
这是因为,当我们在修改时程序要是把IAT改坏了
我们就可以根据INT表来获得正确的函数地址
并将正确的地址填入到IAT表中,以便程序正常运行

10.重定位表

想要认识重定位表,就要先要有这么一个概念:

1
2
3
4
5
6
7
一个进程由一堆PE文件构成,包括EXE、DLL,一个进程的运行依赖于这些个PE文件提供各种功能模块。
由之前学习的知识可以知道,每个PE文件中都有自己的IMageBase 和 Size
那么一个进程在内存中时,其PE文件展开顺序就是——》
该文件名.exe
{
一堆dll文件
}

通过DTDUG调试展示:

image-20211220102616935

1
2
3
	1.而这个进程中的很多的PE文件的ImageBase是相同的,这就需要操作系统进行更改,这是一部分
2.同时,一个程序会有很多的全局变量和编译器填充的类似全局变量的数据,这部分数据在编写的时候,编译器是直接填写的是在内存中的地址(类似于0x4014F80),而一个当PE文件中的一个数据在内存中的地址和另一个PE文件中的数据内存地址冲突时,操作系统同样需要对冲突的数据进行一个更改。
对于这两种情况,都是通过 重定位表 来实现更改的。
1
2
也就是说
重定位表 是 用来处理一个进程中 不同PE文件的数据 在内存中 地址冲突 的。

10.1如何找到重定位表

和导入表和导出表一样,在扩展PE头的最后一个结构体成员去找,找到第6张表,这张表就是存储重定位表的地址和大小的表。

结构如下:

1
2
3
4
5
6
7
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
//重定位表的位置
DWORD SizeOfBlock;
//重定位表的大小
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED *PIMAGE_BASE_RELOCATION;

要注意的是,重定位表并不只有一块,因为,每个PE文件中,有 地址冲突 的不止一个数据,所以他就存在多个重定位块。如何去判断重定位表的结束呢?

这里就要介绍**_IMAGE_BASE_RELOCATION**这个结构体中的两个成员了

首先是virtualAddress,就是一个重定位表的基址,即需要修改的数据的地址的一个基址,实际地址是这个基址加上SizeOfBlock后边的两个字节,两个字节的加(virtualAddress+两字节)。

然后是SizeOfBlock,表示这张重定位表的大小,即可通过这个值判断重定位表的边界。

结束的标志 就是:当SizeOfBlockvirtualAddress都为0时,重定位表块也就结束了。

10.2重定位表的重定位操作

之前提到了重定位表中的两个成员,SizeOfBlockvirtualAddress,假设他们现在的存储就是下图

image-20211220170047156

X代表的就是中存储的virtualAddress,Y代表的就是SizeOfBlock,一个就是1个字节,现在找到第一张表,其要修改的值就是 X的值 + 后面两字节的值,然后这里要注意,这两个字节的高4位

1
2
3
4
5
高位前4位无效 只需要12

只有高位为0011 = 3 才需要将VirtualAddress+低12

高位不是3是用来填位置的没用的数据,存在的价值是为了内存对齐

在对重定位表进行修改的时候,就需要判断该地址(高四位是否为3)指向的数据是否需要修改


本次初学PE的笔记到此结束!后序还PE文件的一些实验将更新。。。

CATALOG
  1. 1. 1PE 视频学习
    1. 1.1. 1.认识PE
      1. 1.1.1. 1.1PE文件格式的运用
      2. 1.1.2. 1.2如何识别PE文件
        1. 1.1.2.1. 1.2.1 文件后缀名
        2. 1.1.2.2. 1.2.2 PE指纹
      3. 1.1.3. 1.3 PE文件的整体结构
      4. 1.1.4. 1.4 认识PE文件在磁盘中的状态
        1. 1.1.4.1. 1.4.1 DOS部分
        2. 1.1.4.2. 1.4.2 PE文件头
        3. 1.1.4.3. 1.4.3 节表
        4. 1.1.4.4. 1.4.4 节数据
    2. 1.2. 2.头文件的属性说明
      1. 1.2.1. 2.1 DOS头
      2. 1.2.2. 2.2 PE头
        1. 1.2.2.1. 2.2.1 标准PE头
        2. 1.2.2.2. 2.2.2 扩展PE头
    3. 1.3. 3. 节表属性
    4. 1.4. 4.RVA和FOA的转换
    5. 1.5. 5.在PE文件中添加代码
    6. 1.6. 6.扩大节
    7. 1.7. 7.新增节
    8. 1.8. 8.合并节
    9. 1.9. 9.导入表与导出表
      1. 1.9.1. 9.1认识导出表
        1. 1.9.1.1. 9.1.1 Name
        2. 1.9.1.2. 9.1.2 NumberOfFunctions 函数个数
        3. 1.9.1.3. 9.1.3 NumberOfNames 以函数名称导出的函数个数
        4. 1.9.1.4. 9.1.4 AddressOfFunctions 导出函数地址表
        5. 1.9.1.5. 9.1.5AddressOfNames 导出函数名称表
        6. 1.9.1.6. 9.1.6 AddressOfNameOrdinals 导出函数序号表
        7. 1.9.1.7. 9.1.7 Base 导出函数起始序号
      2. 1.9.2. 9.2认识导入表
        1. 1.9.2.1. 9.2.1 确认导入表的数量
        2. 1.9.2.2. 9.2.2 确认导入表的功能模块对应的DLL文件
        3. 1.9.2.3. 9.2.3 通过导入表确定依赖的函数
        4. 1.9.2.4. 9.2.4通过导入表确定导入函数的地址
    10. 1.10. 10.重定位表
    11. 1.11. 10.1如何找到重定位表
      1. 1.11.1. 10.2重定位表的重定位操作