什么是文件

存储数据的实体。

不同的文件是给不同的软件去使用的。不同的文件主要是格式不同。

格式就是数组的排列组织方式

。软件读取文件,按照固定的形式去解析文件的。

什么是PE文件

(Portable Executable)可执行 文件的缩写。这种类型的文件,是供windows系统解析,解析完了之后能够创建出进程去运行的文件。

PE头部信息

(DOS头,NT头,区段表)

我们学习PE文件学习的是什么呢??学习的就是PE文件的格式,学习格式就是在学习一堆结构体。很多东西需要记忆。

为了便于我们记忆,需要一些辅助性的工具。

PE头部粗略图

Mr.Hu-Image

DOS头

简介

在windows系统中的可执行文件在设计的时候,考虑到了兼容性问题。在正常的可执行文件的一开始的部分。嵌入了一个DOS可执行文件。作用就是在MS-DOS系统下能够输出一行这个程序不是运行在此系统下的。

Mr.Hu-Image

这里有两个字段是有用的:

第一个 e_magic 永远都是 0x4D 0x5A 0x5A4D 你需要知道大端和小端的知识。

最后一个 e_lfanew 它是真正的可执行文件的起始位置。

  • 实验一:假如我们修改了e_magic字段或者e_lfanew,PE文件是否还能运行?
    • 将e_magic修改为”OZ”,保存执行
    • 将e_magic改回”MZ”,e_lfanew改为0x1000,保存执行

尝试之后不行,这两个字段是重要字段,抹掉程序就无法运行了。

  • 实验二:抹掉除了e_magic和e_lfanew之外的字段,可不可以。

怎么找到DOS头

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
//lpImage是ReadFile打开文件后的写入的缓冲区(堆空间)
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
//pDos的e_magic字段,作为是否为PE文件的标志
pDos->e_magic == IMAGE_DOS_SIGNATURE
//pDos的e_lfanew字段可以指向NT头的空间
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)pDos->e_lfanew;

//例:
//1 打开文件
HANDLE hFile = CreateFile(
PATH,
GENERIC_ALL,
NULL,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
//2 获取文件大小
DWORD dwFileSize = GetFileSize(hFile, NULL);
//3 申请空间并初始化
char* buf = new char[dwFileSize] {0};
//4 读取文件
DWORD dwRealSize = 0;
ReadFile(hFile, buf, dwFileSize, &dwRealSize, NULL);

NT头

1
2
3
4
5
6
typedef struct _IMAGE_NT_HEADERS
{
DWORD Signature; // [0x00]PE标识
IMAGE_FILE_HEADER FileHeader; // [0x04]文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // [0x18]扩展头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

怎么找到Nt头?

通过DOS头的最后一个字段:e_lfanew 指定的是NT头的位置

Signature:

标识:PE00

可以和魔数配合,判断是否是PE文件。

永远都是 0x50 0x45 0x00 0x00 0x00004550

1
2
3
4
//打开PE文件后,第一个段就是DOS头部
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
//pDos的e_lfanew字段可以指向NT头的空间,使用NT头接收
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)pDos->e_lfanew;

IMAGE_FILE_HEADER:

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //[0x04] (1)运行平台
WORD NumberOfSections; //[0x06] (2)区段的数量*
DWORD TimeDateStamp; //[0x08] (3)文件创建时间
DWORD PointerToSymbolTable; //[0x0C] (4)符号表指针
DWORD NumberOfSymbols; //[0x10] (5)符号的数量
WORD SizeOfOptionalHeader;//[0x14] (6)扩展头大小*
WORD Characteristics; //[0x16] (7)文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Mr.Hu-Image

重要的:

NumberOfSection:区段的数量

SizeOfOptionalHeader:扩展头的大小。因为扩展头中数据目录表的个数是不确定的。所以这里需要一个大小

有用的:

Machine:运行平台

TimeDateStamp:时间戳 表明是在什么时候编译的

手工解析

1
2
50 45 00 00 4C 01 05 00 91 CF 44 54 00 00 00 00
00 00 00 00 E0 00 02 01

50 45 00 00 PE标识

01 4C 运行平台

00 没用

05 区段数量

91 CF 44 54 时间戳

E0 扩展头大小

0201 属性

因为大端小端,我们需要反过来看

关于镜像,映像,虚拟地址,相对虚拟地址

镜像:就是PE文件自身

映像:就是根据PE文件映射出来的,

虚拟地址:程序中的内存地址,就是虚拟地址。

相对虚拟地址:就是相对于加载基址的偏移。

文件头

IMAGE_FILE_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; //PE文件的一些属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

怎么找到文件头

1
2
3
4
5
6
//打开PE文件后,第一个段就是DOS头部
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
//pDos的e_lfanew字段可以指向NT头的空间,使用NT头接收
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)pDos->e_lfanew;
//NT头结构体的第二个参数就是指向扩展头
PIMAGE_FILE_HEADER pFile = (PIMAGE_FILE_HEADER)&pNt->FileHeader;

扩展头

IMAGE_OPTIONAL_HEADER:

找到扩展头

1
2
3
4
5
6
//打开PE文件后,第一个段就是DOS头部
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
//pDos的e_lfanew字段可以指向NT头的空间,使用NT头接收
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)pDos->e_lfanew;
//NT头结构体的第三个参数就是指向扩展头
PIMAGE_OPTIONAL_HEADER pOption = (PIMAGE_OPTIONAL_HEADER)&pNt->OptionalHeader;

扩展头结构体

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
typedef struct _IMAGE_OPTIONAL_HEADER {
// 标准域
WORD Magic; //[0x18] (1) 标志位
BYTE MajorLinkerVersion; //[0x1A] (2) 连接器主版本号
BYTE MinorLinkerVersion; //[0x1B] (3) 连接器子版本号
DWORD SizeOfCode; //[0x1C] (4) 所有代码段 的总大小
DWORD SizeOfInitializedData; //[0x20] (5) 所有初始化段总大小
DWORD SizeOfUninitializedData; //[0x24] (6) 所有未初始化段总大小
DWORD AddressOfEntryPoint; //[0x28] (7) 程序执行入口RVA*
DWORD BaseOfCode; //[0x2C] (8) 代码段起始RVA
DWORD BaseOfData; //[0x30] (9) 数据段起始RVA
// NT 附加域
DWORD ImageBase; //[0x34] (10) 程序默认载入基地址*
DWORD SectionAlignment; //[0x38] (11) 内存中的段对齐值
DWORD FileAlignment; //[0x3C] (12) 文件中的段对齐值
WORD MajorOperatingSystemVersion; //[0x40] (13) 系统主版本号
WORD MinorOperatingSystemVersion; //[0x42] (14) 系统子版本号
WORD MajorImageVersion; //[0x44] (15) 自定义的主版本号
WORD MinorImageVersion; //[0x46] (16) 自定义的子版本号
WORD MajorSubsystemVersion; //[0x48] (17) 所需子系统主版本号
WORD MinorSubsystemVersion; //[0x4A] (18) 所需子系统子版本号
DWORD Win32VersionValue; //[0x4C] (19) 保留,通常为0x00
DWORD SizeOfImage; //[0x50] (20) 内存中映像总尺寸*
DWORD SizeOfHeaders; //[0x54] (21) 各个文件头的总尺寸*
DWORD CheckSum; //[0x58] (22) 映像文件校验和
WORD Subsystem; //[0x5C] (23) 文件子系统
WORD DllCharacteristics; //[0x5E] (24) DLL标志位
DWORD SizeOfStackReserve; //[0x60] (25) 初始化栈大小
DWORD SizeOfStackCommit; //[0x64] (26) 初始化实际提交栈大小
DWORD SizeOfHeapReserve; //[0x68] (27) 初始化保留栈大小
DWORD SizeOfHeapCommit; //[0x6C] (28) 初始化实际保留栈大小
DWORD LoaderFlags; //[0x70] (29) 调试相关,默认0x00
DWORD NumberOfRvaAndSizes; //[0x74] (30) 数据目录表的数量*
IMAGE_DATA_DIRECTORY DataDirectory[0x10]; //[0x78] (31) 数据目录表*
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Mr.Hu-Image

非常重要的:

ImageBase:程序的默认加载基址。

AddressOfEntryPoint: 程序的入口点(EP)。

比较重要的:

SectionAlignment: 内存对齐 0x1000(因为一页内存是4KB)

FileAlignment: 文件对齐 0x200

SizeOfImage: 映像大小(我这个PE文件被加载到内存,占用空间应该是多大)

SIzeOfHeader: 头部大小 DOS头+NT头+区块表的大小。

NumberOfRvaAndSizes: 数据目录表的元素个数

DllCharacteristics: PE的一组属性。。。。

极为重要的:

数据目录表

DataDirectory

1
2
3
4
5
6
7
8
//打开PE文件后,第一个段就是DOS头部
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
//pDos的e_lfanew字段可以指向NT头的空间,使用NT头接收
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)pDos->e_lfanew;
//NT头结构体的第三个参数就是指向扩展头
PIMAGE_OPTIONAL_HEADER pOption = (PIMAGE_OPTIONAL_HEADER)&pNt->OptionalHeader;
//扩展头最后一个参数就是数据目录表
PIMAGE_DATA_DIRECTORY pDataDir = pOption->DataDirectory;

描述了PE文件中16个非常重要的数据块的大小和位置。

Mr.Hu-Image

导出表

什么是导出

提供函数给其他模块使用的一种行为

怎么导出

方法1:声明导出

1
2
3
#pragma once
extern "C" _declspec(dllexport) void Fun1();
extern "C" _declspec(dllexport) void Fun2();

方法2:def文件导出

1
2
3
EXPORTS 
Fun3 @1
Fun4 @2 NONAME

注意:NONAME 是只导出序号,没有名字

导出表的作用是什么? 没有它exe能运行吗?

用以记录本模块能够给其他模块提供的函数的信息。

函数名 函数地址 函数的序号

程序运行的时候,会检查主模块的导入表,看用了哪些其他模块,就会将此模块加载到进程空间中。加载进来之后,分析INT(IAT)得到函数名称,用这个名称去导出表中找到函数地址的RVA,RVA+模块基址,就是真正的函数地址,将此函数地址填充到IAT中,从而完成加载后的IAT功能。

一个PE文件,可以没有导出表的,比如exe文件,一般都没有。

已知一个dll名,和一个dll导出函数的名字, 如何得到这个函数名的地址?

得到DLL的导出表,然后在函数名称表中找函数名,如果找到了,由于序号表和名称表位置是一一对应的,就会得到序号表的下标,然后就将序号当成地址表的下标,从而得到地址。

GetProcAddress();

怎么才能知道一个exe都使用了哪些API?

分析exe的导入表即可,其中记录了模块名和函数名。

如何判断导入函数是以序号导入或是以名称导入?

IMAGE_THUNK_DATA32这个结构体,他的最高位是1的话,那么就只有序号,如果最高位是0的话,那么久有序号,也有名称。

怎么才知道导出函数是仅以序号导出还是以名称导出?

对于一个导出函数而言,他的地址表的下标,就是此函数的序号,如果这个序号,在序号表中,没有记录,那么他就是一个虚序号,也就是没有名称,只有序号。

怎么找到一个PE文件的导出信息

通过数据目录表的第0项:

手工分析过程:

Mr.Hu-Image
获得信息:

导出表的RVA:00018D90 ——>FOA: 7590 (FOA= RVA - RVA区段+ FOA区段)

备注:

​ FOA:文件的偏移位置

​ RVA:相对虚拟地址

​ RVA区段:该段段首地址

​ FOA区段:文件偏移的段首地址

SIZE:5F21

Mr.Hu-Image

模块名称在12个字节后的四个字节中,也就是0x0001DBEA

Base:00 00 00 01 索引基数

NumberOfFunctions:00 00 13 88 导出地址表中成员个数

NumberOfNames:00 00 00 03

AddressOfFunctions(rva):00 01 8D B8 导出地址表(EAT)

AddressOfNames(rva):00 01 DB D8 导出名称表(ENT)

AddressOfNameOrdinals;:0x1DBE4 指向导出序号表

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // (1) 保留,恒为0x00000000
DWORD TimeDateStamp; // (2) 时间戳
WORD MajorVersion; // (3) 主版本号,一般不赋值
WORD MinorVersion; // (4) 子版本号,一般不赋值
DWORD Name; // (5) 模块名称*
DWORD Base; // (6) 索引基数*
DWORD NumberOfFunctions; // (7) 导出地址表中成员个数*
DWORD NumberOfNames; // (8) 导出名称表中成员个数*
DWORD AddressOfFunctions; // (9) 导出地址表(EAT)*
DWORD AddressOfNames; // (10) 导出名称表(ENT)*
DWORD AddressOfNameOrdinals; // (11) 指向导出序号表*
}IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

模块名称0x0001DBEA 转换RVA —FOA: C3EA

Mr.Hu-Image

找到dll.dll

地址表成员个数为1388转换成字节:1388h=5000d 十六进制转换为十进制

​ 1388*4=4E20 十六进制数乘4

​ 5000*4=20000 十进制数乘4

我们从地址表:0x 18DB8开始找,由于18DB8是RVA,这里需要转换FOA:75B8

Mr.Hu-Image

AddressOfNameOrdinals;:1DBE4 指向导出序号表 转换FOA:C3E4

Mr.Hu-Image Mr.Hu-Image

测试代码

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


#include <iostream>
#include <Windows.h>
#define PATH L"C:\\Users\\SouLinker\\Desktop\\dll.dll"
// 判断是不是PE文件


DWORD RvaToFoa(char* lpImage, DWORD dwRva);
BOOL IsPE_File(char* lpImage);
void AnalyzeExportsTabel(char* lpImage);
int main()
{
//1 打开文件
HANDLE hFile = CreateFile(
PATH,
GENERIC_ALL,
NULL,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
//2 获取文件大小
DWORD dwFileSize = GetFileSize(hFile, NULL);
//3 申请空间并初始化
char* buf = new char[dwFileSize] {0};
//4 读取文件
DWORD dwRealSize = 0;
ReadFile(hFile, buf, dwFileSize, &dwRealSize, NULL);
//5 判断是不是PE文件
if (IsPE_File(buf) == TRUE)
{
//printf("这是一个PE文件");
AnalyzeExportsTabel(buf);
}
else
{
printf("这不是一个PE文件");
}
delete[]buf;
buf = nullptr;
return 0;
}

DWORD RvaToFoa(char* lpImage, DWORD dwRva)
{
//1 获取区段表的起始位置
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);
PIMAGE_SECTION_HEADER pHeader = IMAGE_FIRST_SECTION(pNt);

if (dwRva < pNt->OptionalHeader.SizeOfHeaders)
{
return dwRva;
}
//2 循环判断RVA落在了哪个区段中
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
DWORD dwSectionRva = pHeader[i].VirtualAddress;
DWORD dwSectionEndRva = dwSectionRva + pHeader[i].SizeOfRawData;
DWORD dwSectionFOA = pHeader[i].PointerToRawData;
if (dwRva >= dwSectionRva && dwRva <= dwSectionEndRva)
{
pHeader[i].VirtualAddress;
DWORD dwFOA = dwRva - dwSectionRva + dwSectionFOA;
return dwFOA;
}
}
return -1;
}
BOOL IsPE_File(char* lpImage)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE)
{
return FALSE;
}
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);

if (pNt->Signature != IMAGE_NT_SIGNATURE)
{
return FALSE;
}
return TRUE;
}

void AnalyzeExportsTabel(char* lpImage)
{
//1 获取到导出表的结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);
//1 获取到导出表的数据目录结构
PIMAGE_DATA_DIRECTORY pExportDir = &pNt->OptionalHeader.DataDirectory[0];
//1 导出表的数据目录结构中,有导出表的RVA,咱们需要将其转换为FOA,才能在文件中使用
DWORD dwExportFOA = RvaToFoa(lpImage, pExportDir->VirtualAddress);
//1 已经得到了FOA,直接就能够找到导出表的结构
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(lpImage + dwExportFOA);
//typedef struct _IMAGE_EXPORT_DIRECTORY {
// DWORD Characteristics;
// DWORD TimeDateStamp;
// WORD MajorVersion;
// WORD MinorVersion;
// DWORD Name;
// DWORD Base;
// DWORD NumberOfFunctions;
// DWORD NumberOfNames;
// DWORD AddressOfFunctions; // RVA from base of image
// DWORD AddressOfNames; // RVA from base of image
// DWORD AddressOfNameOrdinals; // RVA from base of image
//} IMAGE_EXPORT_DIRECTORY, * PIMAGE_EXPORT_DIRECTORY;
DWORD dwBase = pExport->Base;
//2 得到地址表,名称表,序号表的 FOA
DWORD EatFoa = RvaToFoa(lpImage,pExport->AddressOfFunctions);
DWORD EntFoa = RvaToFoa(lpImage, pExport->AddressOfNames);
DWORD EotFoa = RvaToFoa(lpImage, pExport->AddressOfNameOrdinals);
//3 得到地址表,名称表,序号表在文件中的位置
PDWORD pEat= (PDWORD)(lpImage + EatFoa);
PDWORD pEnt = (PDWORD)(lpImage + EntFoa);
PWORD pEot = (PWORD)(lpImage + EotFoa);
//4 开始解析
for (int i = 0; i < pExport->NumberOfFunctions; i++)
{
//4.1 无效地址
if (pEat[i] == 0)
{
continue;
}
//4.2 有效地址将下标放到序号表中去寻找
int j = 0;
int nSign = FALSE;
for (; j < pExport->NumberOfNames; j++)
{
if (i == pEot[j])
{
nSign = TRUE;
break;
}
}
//4.2.1 找到了,就是有名字的函数
if (nSign== TRUE)
{
//名称表中,存储的是RVA,需要转为FOA
DWORD dwFunNameFOA = RvaToFoa(lpImage, pEnt[j]);
char* pFunName = lpImage + dwFunNameFOA;
printf("序号:%4x 地址:%x 名称:%s\n", i+ dwBase, pEat[i], pFunName);
}
//4.2.2 没有找到,就是没有名字的函数,虚序号
else
{
printf("序号:%4x 地址:%x 名称:NULL\n", i + dwBase, pEat[i]);
}
}
}

导入表

什么是导入

当一个可执行文件使用到了其他模块中的函数的时候,就是导入行为。在PE文件中,有一个位置记录了此可执行文件 使用的所有其他模块的函数信息。这个位置就是导入表。

导入表的作用是什么?

在加载之前,导入表里面记录本模块所使用的哪些DLL中的哪些函数的名称信息。

在加载之后,导入表能够记录所使用的函数的地址。供程序运行期间,找到所使用的函数。

导入表结构体

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;//(1) 指向导入名称表(INT)的RAV*
};
DWORD TimeDateStamp; // (2) 时间标识
DWORD ForwarderChain; // (3) 转发链,如果不转发则此值为0
DWORD Name; // (4) 指向导入映像文件的名字*
DWORD FirstThunk; // (5) 指向导入地址表(IAT)的RAV*
} IMAGE_IMPORT_DESCRIPTOR;

OriginalFirstThunk与FirstThunk得到位置,也是一个结构体数组,定义如下:

1
2
3
4
5
6
7
8
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString; // (1) 转发字符串的RAV
PDWORD Function; // (2) 被导入函数的地址
DWORD Ordinal; // (3) 被导入函数的序号
PIMAGE_IMPORT_BY_NAME AddressOfData; // (4) 指向输入名称表
} u1;
} IMAGE_THUNK_DATA32;
  1. 当此结构体最高位为0的时候,且此时存储的是导入名称信息,PIMAGE_IMPORT_BY_NAME起作用
  2. 当此结构体最高位为1的时候,且此时存储的是导入名称信息, DWORD Ordinal起作用
  3. 当此结构体存储的是导入地址信息的时候,PDWORD Function起作用

手工解析

数据目录表的第1项,就是导入表信息:

Mr.Hu-Image

RVA:19CE90 ———》 转为FOA: B890

SIZE:0168

Mr.Hu-Image
1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;//(1) 指向导入名称表(INT)的RAV*
};
DWORD TimeDateStamp; // (2) 时间标识
DWORD ForwarderChain; // (3) 转发链,如果不转发则此值为0
DWORD Name; // (4) 指向导入映像文件的名字*
DWORD FirstThunk; // (5) 指向导入地址表(IAT)的RAV*
} IMAGE_IMPORT_DESCRIPTOR;

Name: 0019E460 —–> 19CE60

Mr.Hu-Image

OriginalFirstThunk: 0019D1CC —–>19BBCC (INT)

Mr.Hu-Image

FirstThunk: 001504D4 —–> 14EED4(IAT)

Mr.Hu-Image

加载之后,INT中的内容还是原来的内容

在OD中,加载之后,IAT里面,存储的已经是各个API的地址了。

测试代码

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
// 02_解析导入表.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>
#define PATH L"C:\\Users\\hugan\\Desktop\\FileCleaner2.0.exe"
// 判断是不是PE文件


DWORD RvaToFoa(char* lpImage, DWORD dwRva);
BOOL IsPE_File(char* lpImage);
void AnalyzeImportTabel(char* lpImage, bool bAnalyzeInt);
int main()
{
//1 打开文件
HANDLE hFile = CreateFile(
PATH,
GENERIC_ALL,
NULL,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
//2 获取文件大小
DWORD dwFileSize = GetFileSize(hFile, NULL);
//3 申请空间并初始化
char* buf = new char[dwFileSize] {0};
//4 读取文件
DWORD dwRealSize = 0;
ReadFile(hFile, buf, dwFileSize, &dwRealSize, NULL);
//5 判断是不是PE文件
if (IsPE_File(buf) == TRUE)
{
//printf("这是一个PE文件");
AnalyzeImportTabel(buf,true);
}
else
{
printf("这不是一个PE文件");
}
delete[]buf;
buf = nullptr;
return 0;
}

DWORD RvaToFoa(char* lpImage, DWORD dwRva)
{
//1 获取区段表的起始位置
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);
PIMAGE_SECTION_HEADER pHeader = IMAGE_FIRST_SECTION(pNt);

if (dwRva < pNt->OptionalHeader.SizeOfHeaders)
{
return dwRva;
}
//2 循环判断RVA落在了哪个区段中
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
DWORD dwSectionRva = pHeader[i].VirtualAddress;
DWORD dwSectionEndRva = dwSectionRva + pHeader[i].SizeOfRawData;
DWORD dwSectionFOA = pHeader[i].PointerToRawData;
if (dwRva >= dwSectionRva && dwRva <= dwSectionEndRva)
{
pHeader[i].VirtualAddress;
DWORD dwFOA = dwRva - dwSectionRva + dwSectionFOA;
return dwFOA;
}
}
return -1;
}
BOOL IsPE_File(char* lpImage)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE)
{
return FALSE;
}
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);

if (pNt->Signature != IMAGE_NT_SIGNATURE)
{
return FALSE;
}
return TRUE;
}

void AnalyzeImportTabel(char* lpImage,bool bAnalyzeInt)
{
//1 获取到导入表的结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);
//1 获取到导入表的数据目录结构
PIMAGE_DATA_DIRECTORY dwImportDir = &pNt->OptionalHeader.DataDirectory[1];
//1 导入表的数据目录结构中,有导出表的RVA,咱们需要将其转换为FOA,才能在文件中使用
DWORD dwImportFOA = RvaToFoa(lpImage, dwImportDir->VirtualAddress);
//1 已经得到了FOA,直接就能够找到导出表的结构
PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)(lpImage + dwImportFOA);

//2 开始解析
while (pImportTable->Name!=0)
{
//2.1 先解析DLL的名字
DWORD dwNameFoa = RvaToFoa(lpImage,pImportTable->Name);
char* pDllName = (char*)(dwNameFoa + lpImage);
printf("DllName:%s\n", pDllName);

//2.2 解析函数名字,选择用什么去解析
PIMAGE_THUNK_DATA32 pNameTable = NULL;
if (bAnalyzeInt == true)
{
DWORD Foa = RvaToFoa(lpImage, pImportTable->OriginalFirstThunk);
pNameTable = (PIMAGE_THUNK_DATA32)(lpImage + Foa);
}
else
{
DWORD Foa = RvaToFoa(lpImage, pImportTable->FirstThunk);
pNameTable = (PIMAGE_THUNK_DATA32)(lpImage + Foa);
}
//2.3 开始解析名字
while (pNameTable->u1.Ordinal!=0)
{
//2.3.1 判断最高位是不是1
if (IMAGE_SNAP_BY_ORDINAL32(pNameTable->u1.Ordinal)==1)
{
//只有序号
printf(" 序号:%x,名称:NULL\n", pNameTable->u1.Ordinal&0x7FFFFFFF);
}
else
{
//既有名字,又有序号
DWORD dwNameFoa = RvaToFoa(lpImage, pNameTable->u1.AddressOfData);
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(dwNameFoa + lpImage);
printf(" 序号:%x,名称:%s\n", pName->Hint,pName->Name);
}
pNameTable++;
}
printf("----------------------------\n");
pImportTable++;
}
}

重定位表

什么叫做重定位

一般情况下,exe的默认加载基址是0x0040 0000

会有一些代码,是这样的:

lea eax, ds:[0x0040 1100]

push eax

call printf

但是目前来看,几乎都不会加载到默认基址上。

比如说,exe加载到0x0050 0000上,那么以上的代码还对么??

不对了,怎么才能对呢??

lea eax, ds:[0x0050 1100]

push eax

call printf

这样就对了。

由于一个模块加载到什么位置,几乎是不确定的,所以每次程序运行,都需要将使用了VA的地方进行一次修改。这个修改的过程就称之为重定位。

有一个区域就记录着,程序中的哪些位置使用了VA。这个区域就叫做重定位表。

为什么模块不加载到默认基址上??

  1. 对于dll来说,他默认基址上经常已经被别的模块使用了。
  2. 对于exe来说,每次都加载到默认基址,是一个危险的行为。数据和代码的地址每次都是固定的,就很危险。对于exe来说,就有一个随机基址的功能,使得每次运行,基址都不一样。

手工解析重定位

1
2
3
4
5
6
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // (1) 需重定位数据的起始RVA
DWORD SizeOfBlock; // (2) 本结构与TypeOffset总大小
// WORD TypeOffset[1]; // (3) 原则上不属于本结构
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED IMAGE_BASE_RELOCATION;
Mr.Hu-Image

VirtualAddress:0A9000 ——->FOA:76E00

size:3ACC

Mr.Hu-Image

VirtualAddress:031000

SizeofBlock:30

数据 偏移 重定位位置 FOA 要重定位的数据
3A7F A7F 031000+A7F 2E7F 004A4840
3A85 A85 031000+A85 2E85 0048FC88
3A92 A92 031000+A92 2E92 004A4844

咱们再OD中,去看RVA是031000+A7F的地址,发现确实是被重定位过的了。

Mr.Hu-Image

怎么重定位的呢??

原始VA - 默认基址 = 新VA - 新基址

验证:

0x00D6 0000 - 0x0040 0000 + 004A4840 得到就是0xE04840

测试代码

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
#include <iostream>
#include <Windows.h>
#define PATH L"C:\\Users\\hugan\\Desktop\\FileCleaner2.0.exe"



DWORD RvaToFoa(char* lpImage, DWORD dwRva);
BOOL IsPE_File(char* lpImage);
void AnalyzeReloc(char* lpImage);
int main()
{
setlocale(LC_ALL, "chs");
//1 打开文件
HANDLE hFile = CreateFile(
PATH,
GENERIC_ALL,
NULL,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
//2 获取文件大小
DWORD dwFileSize = GetFileSize(hFile, NULL);
//3 申请空间并初始化
char* buf = new char[dwFileSize] {0};
//4 读取文件
DWORD dwRealSize = 0;
ReadFile(hFile, buf, dwFileSize, &dwRealSize, NULL);
//5 判断是不是PE文件
if (IsPE_File(buf) == TRUE)
{
//printf("这是一个PE文件");
AnalyzeReloc(buf);
}
else
{
printf("这不是一个PE文件");
}
delete[]buf;
buf = nullptr;
return 0;
}

DWORD RvaToFoa(char* lpImage, DWORD dwRva)
{
//1 获取区段表的起始位置
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);
PIMAGE_SECTION_HEADER pHeader = IMAGE_FIRST_SECTION(pNt);

if (dwRva < pNt->OptionalHeader.SizeOfHeaders)
{
return dwRva;
}
//2 循环判断RVA落在了哪个区段中
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
DWORD dwSectionRva = pHeader[i].VirtualAddress;
DWORD dwSectionEndRva = dwSectionRva + pHeader[i].SizeOfRawData;
DWORD dwSectionFOA = pHeader[i].PointerToRawData;
if (dwRva >= dwSectionRva && dwRva < dwSectionEndRva)
{
pHeader[i].VirtualAddress;
DWORD dwFOA = dwRva - dwSectionRva + dwSectionFOA;
return dwFOA;
}
}
return -1;
}
BOOL IsPE_File(char* lpImage)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE)
{
return FALSE;
}
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);

if (pNt->Signature != IMAGE_NT_SIGNATURE)
{
return FALSE;
}
return TRUE;
}
struct TYPEOFFSET {
WORD OFFSET : 12;
WORD TYPE : 4;
};
void AnalyzeReloc(char* lpImage)
{
//1. 找到重定位的结构体
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);
PIMAGE_OPTIONAL_HEADER pOption = &pNt->OptionalHeader;
//1. 得到了重定位的数据目录
PIMAGE_DATA_DIRECTORY pRelocDir = &pOption->DataDirectory[5];

//2. 得到重定位表
PIMAGE_BASE_RELOCATION pReloc =
(PIMAGE_BASE_RELOCATION)
(RvaToFoa(lpImage, pRelocDir->VirtualAddress)+ lpImage);
//3. 开始解析重定位
while (pReloc->SizeOfBlock!=0)
{
//得到描述每一个位置偏移的数组
TYPEOFFSET* typeoffset = (TYPEOFFSET*)(pReloc + 1);
DWORD dwCount = (pReloc->SizeOfBlock - 8) / 2;
DWORD dwBeginRva = pReloc->VirtualAddress;
printf("----------------------------------\n");
for (int i = 0; i < dwCount; i++)
{
if (typeoffset[i].TYPE==3)
{
DWORD dwRelocRva = (dwBeginRva + typeoffset[i].OFFSET);
printf("要重定位的位置RVA:%p\n", dwRelocRva);
PDWORD pRelocData = (PDWORD)(RvaToFoa(lpImage, dwRelocRva) + lpImage);
printf("要重定位的数据:%p\n", *pRelocData);
}
else
{
printf("类型是%d", typeoffset[i].TYPE);
}

}
printf("----------------------------------\n");
//找到下一个0x1000字节重定位信息
pReloc = (PIMAGE_BASE_RELOCATION)((char*)pReloc + pReloc->SizeOfBlock);
}

}

资源表

菜单,对话框,图标,光标,位图,工具栏…..他们称之为资源,资源就是PE文件在运行的时候需要使用到的一些通用数据。编译的时候,将他们独立保存在一个区域中。

记录这些区域的一个结构,就是资源表了。

理解资源表

当咱们通过数据目录表找到资源表的时候,这个资源表分成了3层结构:

第一层:一共有多少种资源

第二层:这种资源有多少个

第三层:这个资源的位置

手工解析PE文件

找到资源表

找到资源表的数据目录:

resourcebin

RVA:1AF000———–> FOA:1A5400

SIZE:153058

分析第一层
Mr.Hu-Image

NumberOfNamedEntries:0x2

NumberOfIdEntries:0xC

代表着后面有14个下面的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset :31; // (1) 资源名偏移
DWORD NameIsString:1; // (2) 资源名为字符串
};
DWORD Name; // (3) 资源/语言类型
WORD Id; // (4) 资源数字ID
};
union {
DWORD OffsetToData; // (5) 数据偏移地址
struct {
DWORD OffsetToDirectory:31;// (6) 子目录偏移地址
DWORD DataIsDirectory :1;// (7) 数据为目录
};
};
}IMAGE_RESOURCE_DIRECTORY_ENTRY,*PIMAGE_RESOURCE_DIRECTORY_ENTRY;

第一个资源的信息:

Id:0x80008CC8 最高位是1,所以是以字符串为标识所以NameOffset:8CC8 这是一个相对于资源表起始位置的偏移

他不是RVA

​ 1A5400+8CC8 = 1AE0C8

Mr.Hu-Image

OffsetToData:0x80 00 00 80 最高位为1,所以下一层是目录

OffsetToDirectory:80 这是一个相对于资源表起始位置的偏移

​ 他不是RVA

1A5400+80 = 1A5480

这个就是第二层的位置
Mr.Hu-Image

Id:0x800095E0

NameOffset:95E0

1A5400+95E0 = 1AE9E0

Mr.Hu-Image

OffsetToData:0x80001890

OffsetToDirectory:1890

1A5400+1890 = 1A6C90

这个就是第三层的位置了
Mr.Hu-Image

OffsetToData:5F20

​ 1A5400+5E20 =1AB220

和LordPE种解析出来的是一样的

Mr.Hu-Image

代码解析

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

#include <iostream>
#include <Windows.h>
#define PATH L"C:\\Users\\SouLinker\\Desktop\\FileCleaner2.0.exe"
// 判断是不是PE文件
#include <map>
using std::map;

map<int, const char*> g_mapResourceInfo;
map<const char*, const char*> abc;
void InitResourceInfo()
{
g_mapResourceInfo[0x1] = "Cursor";
g_mapResourceInfo[0x2] = "BitMap";
g_mapResourceInfo[0x3] = "Icon";
g_mapResourceInfo[0x4] = "Menu";
g_mapResourceInfo[0x5] = "Dialog";
g_mapResourceInfo[0x6] = "String Table";
g_mapResourceInfo[0x7] = "Font Directory";
g_mapResourceInfo[0x8] = "Font";
g_mapResourceInfo[0x9] = "Accelerators";
g_mapResourceInfo[0xA] = "UnFormatted";
g_mapResourceInfo[0xB] = "Message Table";
g_mapResourceInfo[0xC] = "Group Cursor";
g_mapResourceInfo[0xE] = "Group Icon";
g_mapResourceInfo[0x10] = "Version Information";
}



DWORD RvaToFoa(char* lpImage, DWORD dwRva);
BOOL IsPE_File(char* lpImage);
void AnalyzeResource(char* lpImage);
int main()
{
setlocale(LC_ALL, "chs");
InitResourceInfo();
//1 打开文件
HANDLE hFile = CreateFile(
PATH,
GENERIC_ALL,
NULL,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
//2 获取文件大小
DWORD dwFileSize = GetFileSize(hFile, NULL);
//3 申请空间并初始化
char* buf = new char[dwFileSize] {0};
//4 读取文件
DWORD dwRealSize = 0;
ReadFile(hFile, buf, dwFileSize, &dwRealSize, NULL);
//5 判断是不是PE文件
if (IsPE_File(buf) == TRUE)
{
//printf("这是一个PE文件");
AnalyzeResource(buf);
}
else
{
printf("这不是一个PE文件");
}
delete[]buf;
buf = nullptr;
return 0;
}

DWORD RvaToFoa(char* lpImage, DWORD dwRva)
{
//1 获取区段表的起始位置
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);
PIMAGE_SECTION_HEADER pHeader = IMAGE_FIRST_SECTION(pNt);

if (dwRva < pNt->OptionalHeader.SizeOfHeaders)
{
return dwRva;
}
//2 循环判断RVA落在了哪个区段中
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
DWORD dwSectionRva = pHeader[i].VirtualAddress;
DWORD dwSectionEndRva = dwSectionRva + pHeader[i].SizeOfRawData;
DWORD dwSectionFOA = pHeader[i].PointerToRawData;
if (dwRva >= dwSectionRva && dwRva < dwSectionEndRva)
{
pHeader[i].VirtualAddress;
DWORD dwFOA = dwRva - dwSectionRva + dwSectionFOA;
return dwFOA;
}
}
return -1;
}
BOOL IsPE_File(char* lpImage)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE)
{
return FALSE;
}
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);

if (pNt->Signature != IMAGE_NT_SIGNATURE)
{
return FALSE;
}
return TRUE;
}

void AnalyzeResource(char* lpImage)
{
//1. 找到资源表的结构体
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);
PIMAGE_OPTIONAL_HEADER pOption = &pNt->OptionalHeader;
//1. 得到了资源表的数据目录
PIMAGE_DATA_DIRECTORY pResourceDir= &pOption->DataDirectory[2];

//2. 得到资源表第一层的位置
DWORD dwResourceFOA = RvaToFoa(lpImage, pResourceDir->VirtualAddress);
PIMAGE_RESOURCE_DIRECTORY pFirstDir =(PIMAGE_RESOURCE_DIRECTORY)(dwResourceFOA + lpImage);
PIMAGE_RESOURCE_DIRECTORY_ENTRY pFirstRes = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pFirstDir + 1);
//3. 解析资源表
DWORD dwFirstCount = pFirstDir->NumberOfIdEntries + pFirstDir->NumberOfNamedEntries;
//3.1 解析第一层
for (int i = 0; i < dwFirstCount; i++)
{
//解析第一层的名称信息
if (pFirstRes[i].NameIsString == TRUE)
{
//以字符串作为标识
PIMAGE_RESOURCE_DIR_STRING_U pNameInfo =
(PIMAGE_RESOURCE_DIR_STRING_U)(pFirstRes[i].NameOffset + (DWORD)pFirstDir);
WCHAR* pName = new WCHAR[pNameInfo->Length + 1]{0};
//wcscpy_s(pName, pNameInfo->Length, pNameInfo->NameString);
for (int m = 0; m < pNameInfo->Length; m++)
{
pName[m] = pNameInfo->NameString[m];
}
wprintf(L"资源种类标识:%s\n", pName);
}
//以数字作为标识
else
{
if (g_mapResourceInfo.count(pFirstRes[i].Id) == 1)
{
printf("资源种类标识%s\n", g_mapResourceInfo[pFirstRes[i].Id]);
}
else
{
printf("资源种类标识%d\n", pFirstRes[i].Id);
}

}
//解析第一层的位置信息,从而得到第二层

if (pFirstRes[i].DataIsDirectory == 1)
{
//根据偏移得到第二层的位置,以及数组
PIMAGE_RESOURCE_DIRECTORY pSecondDir =
(PIMAGE_RESOURCE_DIRECTORY)(pFirstRes[i].OffsetToDirectory + (DWORD)pFirstDir);
PIMAGE_RESOURCE_DIRECTORY_ENTRY pSecondRes =
PIMAGE_RESOURCE_DIRECTORY_ENTRY(pSecondDir + 1);


//第二层的资源个数
DWORD dwSecondCount =
pSecondDir->NumberOfIdEntries + pSecondDir->NumberOfNamedEntries;
for (int j = 0; j < dwSecondCount; j++)
{
//解析第二层的名称信息
if (pSecondRes[j].NameIsString == TRUE)
{
//以字符串作为标识
PIMAGE_RESOURCE_DIR_STRING_U pNameInfo =
(PIMAGE_RESOURCE_DIR_STRING_U)(pSecondRes[j].NameOffset + (DWORD)pFirstDir);
WCHAR* pName = new WCHAR[pNameInfo->Length + 1]{ 0 };
//wcscpy_s(pName, pNameInfo->Length + 1, pNameInfo->NameString);
for (int m = 0; m < pNameInfo->Length; m++)
{
pName[m] = pNameInfo->NameString[m];
}
wprintf(L" 资源标识:%s\n", pName);
}
else
{
wprintf(L" 资源标识:%d\n", pSecondRes[j].Id);
}
//解析第二层的位置信息
if (pSecondRes[j].DataIsDirectory == TRUE)
{
//根据偏移得到第三层的位置,以及数组
PIMAGE_RESOURCE_DIRECTORY pThirdDir =
(PIMAGE_RESOURCE_DIRECTORY)
(pSecondRes[j].OffsetToDirectory+ (DWORD)pFirstDir);
PIMAGE_RESOURCE_DIRECTORY_ENTRY pThirdRes =
(PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pThirdDir + 1);

//解析第三层,但是不需要循环了
PIMAGE_RESOURCE_DATA_ENTRY pData =
(PIMAGE_RESOURCE_DATA_ENTRY)(pThirdRes->OffsetToData + (DWORD)pFirstDir);
//pData->OffsetToData 资源起始位置的RVA
//pData->Size 资源的大小
printf(" 资源的起始RVA:%x 资源的大小:%x\n", pData->OffsetToData, pData->Size);
//获取到资源的数据了,输出10个字节,用于对比
unsigned char* pRes = (unsigned char *)
(RvaToFoa(lpImage, pData->OffsetToData) + lpImage);
printf(" ");
for (size_t m= 0; m < 10; m++)
{
printf("%x ", pRes[m]);
}
printf("\n");
}
}
}
}
}

TLS表

简介

线程局部存储:可以将TLS全局变量,在每一个线程中都创建一份,从而解决一定的线程同步问题。

测试代码

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
// 03_TLS.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>
#pragma comment(linker, "/INCLUDE:__tls_used")
int g_nNum1 = 100;
_declspec(thread) int g_nNum2 = 200;//TLS的全局变量

// TLS回调函数A
void NTAPI t_TlsCallBack_A(PVOID DllHandle, DWORD Reason, PVOID Red) {
if (DLL_PROCESS_ATTACH == Reason)
{
printf("i am a mimidaima!\r\n");
}
if (DLL_THREAD_DETACH == Reason) // 如果线程退出则打印信息
printf("t_TlsCallBack_A -> ThreadDetach!\r\n");
return;
}
// TLS回调函数B
void NTAPI t_TlsCallBack_B(PVOID DllHandle, DWORD Reason, PVOID Red) {
if (DLL_THREAD_DETACH == Reason) // 如果线程退出则打印信息
printf("t_TlsCallBack_B -> ThreadDetach!\r\n");
return;
}

/*
* 注册TLS回调函数,".CRT$XLB"的含义是:
* CRT表明使用C RunTime机制
* X表示标识名随机
* L表示TLS callback section
* B其实也可以为B-Y的任意一个字母
*/
#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK p_thread_callback[] = {
t_TlsCallBack_A,
t_TlsCallBack_B,
NULL };
#pragma data_seg()

DWORD WINAPI t_ThreadFun(PVOID pParam) {

printf("%p ", &g_nNum1);
printf("%p\n", &g_nNum2);
return 0;
}

int main()
{
printf("%p ", &g_nNum1);
printf("%p\n", &g_nNum2);
HANDLE hThread = CreateThread(NULL, 0, t_ThreadFun, NULL, 0, 0);
WaitForSingleObject(hThread, -1);
return 0;
}

区段表

结构体数组

数组的元素个数由文件头中的NumberOfSection决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[0x8]; // (1) 区段名
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // (2) *区段大小
} Misc;
DWORD VirtualAddress; // (3)区段的RVA地址*
DWORD SizeOfRawData; // (4) 文件中的区段对齐大小*
DWORD PointerToRawData; // (5) 区段在文件中的偏移*
DWORD PointerToRelocations; // (6) 重定位的偏移(OBJ)
DWORD PointerToLinenumbers; // (7) 行号表的偏移(调试)
WORD NumberOfRelocations; // (8) 重定位项数量(OBJ)
WORD NumberOfLinenumbers; // (9) 行号表项数量
DWORD Characteristics; // (10) 区段的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

怎么找到区段表

宏:IMAGE_FIRST_SECTION(NT头的地址)

计算:NT头的地址+0x4+0x14+0xE0

1
2
3
4
5
6
//打开PE文件后,第一个段就是DOS头部
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
//pDos的e_lfanew字段可以指向NT头的空间,使用NT头接收
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)pDos->e_lfanew;
//使用IMAGE_FIRST_SECTION这个宏,传图NT头来获取区段表
PIMAGE_SECTION_HEADER pHeader = IMAGE_FIRST_SECTION(pNt);

区段表中的一个元素描述的就是一个区段的信息

1 Name:区段名

2 PointerToRawData 在文件中的位置 FOA

3 SizeOfRawData在文件中的大小

4 VirtualAddress在内存中的位置 RVA

5 Misc.VirtualSize:在内存中的大小

6 Characteristics:区段的属性:可读 可写 可执行。。。。

区段表中的以下四个字段保存的是什么?

VirtualAddress: 区段起始位置的RVA

PointerToRawData: 区段在文件中的起始偏移

VirtualSize: 区段在内存中的大小(没有对齐)

SizeOfRawData: 区段在文件中的大小(对齐过的)

是否VirtualSize 一定会小于SizeOfRawData?

不是的,有可能是这个区段在文件中没有数据,运行起来之后,才有数据。

测试代码

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
#include <Windows.h>
void AnalyzeNTHeader(char* lpImage)
{
//1 找到NT头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);
PIMAGE_FILE_HEADER pFileHeader = &pNt->FileHeader;
PIMAGE_OPTIONAL_HEADER pOption = &pNt->OptionalHeader;
//2 开始解析文件头
printf("运行平台:%x\n",pFileHeader->Machine);
printf("区段数量:%x\n", pFileHeader->NumberOfSections);
printf("扩展头大小:%x\n", pFileHeader->SizeOfOptionalHeader);
printf("时间戳:%x\n", pFileHeader->TimeDateStamp);
printf("属性:%x\n", pFileHeader->Characteristics);
//3 解析扩展头
//。。。
}
void AnalyzeSectionTable(char* lpImage)
{
//1 获取区段表的起始位置
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);
PIMAGE_SECTION_HEADER pHeader = IMAGE_FIRST_SECTION(pNt);
//2 解析
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
printf("区段名:%s\t", pHeader[i].Name);
printf("内存中大小:%X\t", pHeader[i].Misc.VirtualSize);
printf("内存中RVA:%X\t", pHeader[i].VirtualAddress);
printf("文件中大小:%X\t", pHeader[i].SizeOfRawData);
printf("文件中FOA:%X\t", pHeader[i].PointerToRawData);
printf("属性:%X\t", pHeader[i].Characteristics);
printf("\n");
}
}

RVA和FOA之间的转换

文件偏移(FOA或者Offset): 某一个数据距离文件开头的偏移

RVA怎么转换成FOA

RVA - RVA区段 = FOA -FOA区段

FOA = RVA - RVA区段+FOA区段

一般情况下,PE文件的头部在文件中是多大?

0x400

PE文件的头部在内存中是多大

0x1000

RVA为0x600的时候,转为FOA 转不了,在文件中没有对应的位置

虚拟地址(VA):程序在运行的时候,是将PE文件加载到进程的内存空间中。进程的这块内存空间就称之为 虚拟内存空间 32位程序虚拟内存空间是以字节为单位的,每一个字节都有一个编号从0x0000 0000到0xFFFFFFFF之间。这些编号就是虚拟地址

相对虚拟地址(RVA):PE文件不会占满整个虚拟内存空间,而是会占用一部分。那么就会有一个起始位置,这个起始位置也成为加载基址。PE文件中的数据相对于加载基址的偏移就是相对虚拟地址。

如果系统加载PE文件的时候,是将PE文件原封不动的复制到了内存中,那么某一个数据的FOA和RVA就是相等的。

但是现实情况并非如此,系统加载PE文件到内存的之后,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
DWORD RvaToFoa(char* lpImage,DWORD dwRva)
{
//1 获取区段表的起始位置
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpImage);
PIMAGE_SECTION_HEADER pHeader = IMAGE_FIRST_SECTION(pNt);

if (dwRva< pNt->OptionalHeader.SizeOfHeaders)
{
return dwRva;
}
//2 循环判断RVA落在了哪个区段中
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
DWORD dwSectionRva = pHeader[i].VirtualAddress;
DWORD dwSectionEndRva = dwSectionRva + pHeader[i].SizeOfRawData;
DWORD dwSectionFOA = pHeader[i].PointerToRawData;
if (dwRva>= dwSectionRva &&dwRva<= dwSectionEndRva)
{
pHeader[i].VirtualAddress;
DWORD dwFOA = dwRva - dwSectionRva+ dwSectionFOA;
return dwFOA;
}
}
return -1;
}

小功能的实现

位置计算

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

//位置计算器按钮
void CPEDlg::OnBnClickedButton2()
{
UpdateData(TRUE);
if (m_strEditTest1 != L"")
{
CMGpsCalcDlg* pPrcPopDg = new CMGpsCalcDlg;

pPrcPopDg->Create(IDD_DIALOG1, this);
pPrcPopDg->ShowWindow(SW_SHOW);
pPrcPopDg->GetPath(buf);
}

}

void CMGpsCalcDlg::RvaToFoa()
{
UpdateData(TRUE);

CString svPid = m_strEditRva.GetString();
DWORD dwRva = _tcstoul(svPid, NULL, 16);



//1 获取区段表的起始位置
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buf;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buf);
PIMAGE_SECTION_HEADER pHeader = IMAGE_FIRST_SECTION(pNt);
PIMAGE_OPTIONAL_HEADER pOption = &pNt->OptionalHeader;

if (dwRva < pNt->OptionalHeader.SizeOfHeaders)
{
CString cd;
cd.Format(L"%x", dwRva);
m_strEditFoa.SetString(cd);
UpdateData(FALSE);
return;
}
//2 循环判断RVA落在了哪个区段中
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
DWORD dwSectionRva = pHeader[i].VirtualAddress;
DWORD dwSectionEndRva = dwSectionRva + pHeader[i].SizeOfRawData;
DWORD dwSectionFOA = pHeader[i].PointerToRawData;
if (dwRva >= dwSectionRva && dwRva <= dwSectionEndRva)
{
pHeader[i].VirtualAddress;

//FOA = RVA - RVA区段 + FOA区段
DWORD dwFOA = dwRva - dwSectionRva + dwSectionFOA;
CString foa;
foa.Format(L"%x",dwFOA);
m_strEditFoa.SetString(foa);

//va虚拟地址= InmageBase(加载基址) + RVA(相对虚拟地址)
DWORD dwVA = pOption->ImageBase + dwRva;
CString va;
va.Format(L"%x", dwVA);
m_strEditVa.SetString(va);

//区段名
CString name;
name.Format(L"%S", pHeader[i].Name);
m_strEditName.SetString(name);

UpdateData(FALSE);
return;
}
}
MessageBox(L"不存在该地址");
}

响应拖拽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//响应文件拖拽
void CPEDlg::OnDropFiles(HDROP hDropInfo)
{

// 存放文件路径
wchar_t filePath[MAX_PATH] = {};

// 获取拖拽文件的路径
DragQueryFile(hDropInfo, 0, filePath, MAX_PATH);

// 更新变量
m_strEditPath = filePath;
UpdateData(FALSE);

CDialogEx::OnDropFiles(hDropInfo);
}

时间换算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//时间转换
void CPEDlg::OnBnClickedButton3()
{

PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buf;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buf);
PIMAGE_FILE_HEADER pFileHeader = &pNt->FileHeader;//NT头

struct tm* p;

time_t t =pFileHeader->TimeDateStamp;
p = localtime(&t);
CString Time;
Time.Format(L"%d年%d月%d日%d时%d分%d秒", 1900 + p->tm_year, 1 + p->tm_mon, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec);
MessageBox(Time);
}

PE文件结构图

Mr.Hu-Image