PE结构概述
程序入口地址OEP
PE格式是 Windows下最常用的可执行文件格式

PE分节
- 节省硬盘空间
- 节省内存空间


PEloader->PE加载
- 根据SizeOfImage的大小,开辟一块缓冲区(ImageBuffer)
- 根据SizeOfHeader的大小,将头信息从FileBuffer拷贝到ImageBuffer
- 根据节表中的信息循环将FileBuffer中的节拷贝到ImageBuffer中


RVA转化成FOA
VA:英文全称是Virual Address,简称VA,中文意思是虚拟地址。指的是文件被载入虚拟空间后的地址。
ImageBase:中文意思是基址,指的是程序在虚拟空间中被装载的位置。
RVA:英文全称是Relative Virual Address,简称RVA,中文意思是相对虚拟地址。可以理解为文件被装载到虚拟空间(拉伸)后先对于基址的偏移地址。计算方式:RVA = VA(虚拟地址) - ImageBase(基址)。它的对齐方式一般是以1000h为单位在虚拟空间中对齐的(传说中的4K对齐),具体对齐需要参照IMAGE_OPTIONAL_HEADER32中的SectionAlignment成员。
FOA:英文全称是File Offset Address,简称FOA,中文意思是文件偏移地址。可以理解为文件在磁盘上存放时相对于文件开头的偏移地址。它的对齐方式一般是以200h为单位在硬盘中对齐的(512对齐),具体对齐需要参照IMAGE_OPTIONAL_HEADER32中的FileAlignment成员。
全局变量:如果全局变量没有初始化值,那么这个全局变量在PE文件中是没有保存它的位置的,如果有初始化那么就有它位置保存初始值。定位到全局变量的位置后即可修改。
FOA(文件偏移,基于PE文件开始的地址0)和RVA(虚拟地址偏移,基于ImageBase):
当SectionAlignment(内存中节区对齐大)和FileAlignment(文件中节区对齐大)相同时,这2个偏移相同,现在的大部分程序都相同.
如果不相同时:要判断是否在PE头中,如果在PE头这个节区那么他们还是相同,如果不在PE头节区,RVA先算出当前虚拟地址相对与当前节区的开始地址的偏移,然后在文件中也找到这个节区在PE文件中的偏移位置加上相同的偏移即可。
就算SectionAlignment(内存中节区对齐大)和FileAlignment(文件中节区对齐大)相同时,每个节区加载地址也可能不同这时还是要自己转换RVA和FOA
给出代码
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
|
DWORD RvaToFoa(IN LPVOID pFileBuffer, IN DWORD dwRva) {
if (pFileBuffer == NULL) {
return 0;
}
PIMAGE_DOS_HEADER pDos = NULL;
PIMAGE_NT_HEADERS pNT = NULL;
PIMAGE_FILE_HEADER pPE = NULL;
PIMAGE_SECTION_HEADER pSec = NULL;
pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE) {
return 0;
}
pNT = (PIMAGE_NT_HEADERS)((BYTE*)pFileBuffer + pDos->e_lfanew);
if (pNT->Signature != IMAGE_NT_SIGNATURE) {
return 0;
}
if (dwRva < pNT->OptionalHeader.SizeOfHeaders) {
return dwRva;
}
pPE = &pNT->FileHeader;
if (pNT->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64) {
PIMAGE_OPTIONAL_HEADER64 pOpt = (PIMAGE_OPTIONAL_HEADER64)&pNT->OptionalHeader;
pSec = (PIMAGE_SECTION_HEADER)((BYTE*)pOpt + pPE->SizeOfOptionalHeader);
} else {
PIMAGE_OPTIONAL_HEADER32 pOpt = (PIMAGE_OPTIONAL_HEADER32)&pNT->OptionalHeader;
pSec = (PIMAGE_SECTION_HEADER)((BYTE*)pOpt + pPE->SizeOfOptionalHeader);
}
WORD numSec = pPE->NumberOfSections;
for (WORD i = 0; i < numSec; i++) {
DWORD dwVA = pSec->VirtualAddress; //其中dwVA也是该节的RVA
DWORD dwVS = pSec->Misc.VirtualSize;
if (dwRva >= dwVA && dwRva < (dwVA + dwVS)) {
// 公式:FOA = RVA - 节区虚拟地址 + 节区文件偏移
DWORD dwFoa = (dwRva - dwVA) + pSec->PointerToRawData;
return dwFoa;
}
pSec++;
}
return 0;
}
|
作业见PE加载与回收+RVA转换成FOA.cpp
PE头字段说明

DOS头
PE文件开始的地方
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
1 DOS头 //滴水 //我自己
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; * //Magic number(MZ); //5A4D
WORD e_cblp; //Bytes on last page of file //0090
WORD e_cp; //Pages in file //0003
WORD e_crlc; //Relocations //0000
WORD e_cparhdr; //Size of header in paragraphs //0004
WORD e_minalloc; //Minimum extra paragraphs needed //0000
WORD e_maxalloc; //Maximum extra paragraphs needed //FFFF
WORD e_ss; //Initial (relative) SS value //0000
WORD e_sp; //Initial SP value //00B8
WORD e_csum; //Checksum //0000
WORD e_ip; //Initial IP value //0000
WORD e_cs; //Initial (relative) CS value //0000
WORD e_lfarlc; //File address of relocation table //0040
WORD e_ovno; //Overlay number //0000
WORD e_res[4]; //Reserved words //0000 0000 0000 0000
WORD e_oemid; //OEM identifier (for e_oeminfo) //0000
WORD e_oeminfo; //OEM information; e_oemid specific //0000
WORD e_res2[10]; //Reserved words //0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
DWORD e_lfanew; * //File address of new exe header //0000 00E8 -> //0000 0108
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
|
NT头/PE头
NT头 = PE标志+ 标准PE头 + 可选PE头
DOS头字段e_lfanew为NT头开始的地方
1
2
3
4
5
|
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //NT头标志 即'PE..' 00004550
IMAGE_FILE_HEADER FileHeader; //标准PE头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展PE头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
|
标准PE头/文件头
1
2
3
4
5
6
7
8
9
10
|
2 标准PE头
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; * //程序运行的CPU型号 x86CPU:0x14C x64CPU:0x8864
WORD NumberOfSections; * //PE中节的数量
DWORD TimeDateStamp; * //时间戳:文件创建的日期和时间
DWORD PointerToSymbolTable; //执行符号表,即编译好的程序生成的pdb文件,用来方便调试的
DWORD NumberOfSymbols; //符号表中的符号数量
WORD SizeOfOptionalHeader; * //可选PE头的大小 x86(32位):224字节 x64(64位):240字节
WORD Characteristics; * //文件属性,每个位有不同的含义 x86:0x3 x64:0x22
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
|
可选PE头/扩展头
标准PE头字段SizeOfOptionalHeader为可选PE头大小(32位:E0h字节 / 64位:F0h字节)
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
|
3 可选PE头
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; * //魔数 x86:0x10B x64:0x20B
BYTE MajorLinkerVersion; //链接器的版本
BYTE MinorLinkerVersion; //连接器的版本
DWORD SizeOfCode; * //所有代码区段的大小总和,必须是FileAlignment的整数倍
DWORD SizeOfInitializedData; * //已经初始化数据区段大小总和,必须是FileAlignment的整数倍
DWORD SizeOfUninitializedData; * //未初始化数据区段大小总和,必须是FileAlignment的整数倍
DWORD AddressOfEntryPoint; ** //程序入口OEP+ImageBase
DWORD BaseOfCode; * //代码段的起始RVA
DWORD BaseOfData; * //数据段的起始RVA(32+PE删除了)
DWORD ImageBase; ** //程序建议的加载地址(内存镜像基地址) x64 ULONGLONG
DWORD SectionAlignment; ** //内存中的对齐粒度 x86 0x1000 x64 0x2000
DWORD FileAlignment; ** //文件中的对齐粒度 0x200
WORD MajorOperatingSystemVersion; //操作系统版本号
WORD MinorOperatingSystemVersi on;
WORD MajorImageVersion; //PE的镜像备案本号
WORD MinorImageVersion;
WORD MajorSubsystemVersion; //子系统的版本号。
WORD MinorSubsystemVersion;
DWORD Win32VersionValue; //Win32版本(未使用)
DWORD SizeOfImage; ** //内存中整个PE镜像的尺寸,必须是SectionAlignment的整数倍
DWORD SizeOfHeaders; ** //所有头+节表按照文件对齐后的大小
DWORD CheckSum; * //校验和
WORD Subsystem; //子系统,其决定了程序入口点main函数的不同,常见的子系统有窗口,控制台等等
WORD DllCharacteristics; //DLL动态链接库特性
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;
|
最重要属性
1
2
3
4
5
6
7
8
|
DWORD AddressOfEntryPoint; ** //程序入口OEP+ImageBase
DWORD ImageBase; ** //程序建议的加载地址(内存镜像基地址)
DWORD SectionAlignment; ** //内存中的对齐粒度 x86 0x1000 x64 0x2000
DWORD FileAlignment; ** //文件中的对齐粒度 0x200
DWORD SizeOfImage; ** //内存中整个PE文件的映像(镜像)的尺寸,必须是SectionAlignment的整数倍
DWORD SizeOfHeaders; ** //所有头+节表按照文件对齐后的大小
DWORD NumberOfRvaAndSizes; * //数据目录结构的数量
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录表数组(16*8字节)
|
节表
可选PE头结束后紧跟着的地方为节表,标准PE表中字段NumberOfSections为节表个数,一个节表大小为40d字节
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]; * //每一个节都可以取一个名字,最大长度为8字节
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;
|

打印所有头和节表代码详见PE头解析.cpp,运行如下

空白区添加代码
代码节空白区添加代码
1.MessageBox函数说明
MessageBox()函数:功能是弹出一个标准的Windows对话框;它不是C函数库的标准函数,而是一个API,我们可以用C语言调用API函数。可以理解成我们在C中使用MessageBox函数就表示调用系统提供的API函数–MessageBoxA。包含在头文件windows.h;如果一个程序中包含user32.dll,则此程序就有MessageBoxAAPI函数
2.函数原型
1
|
int MessageBox( HWND hWnd,LPCTSTR lpText, LPCTSTR lpCaption = NULL, UINT nType = MB_OK );
|
- 获取MessageBox地址,构造ShellCode代码
- E8 E9计算公式,x=MessageBox地址-下一条指令在内存中的地址
- 在代码区手动添加代码
- 修改OEP,指向ShellCode
任意节空白区添加代码
新增节添加代码
SizeOfHeader - (DOS + 垃圾数据 + PE标记 + 标准PE头 + 可选PE头 + 已存在的节表) >= 2个节表的大小
需要修改的数据
1)添加一个新的节
2)在新增节后面,填充一个节大小的000
3)修改PE头中节的数量NumberOfSections
4)修改ImageBase的大小
5)在原有数据的最后,新增一个节的数据为内存对齐的整数倍
6)修正新增节表的属性
扩大节-合并节
扩大节
-
拉伸到内存
-
分配一块新的空间:SizeOfImage + Ex
-
将最后一个节的SizeOfRawData和VirtualSize改成N
其中 N = MAX(SizeOfRawData,VirtualSize)内存对齐 + Ex
-
修改SizeOfImage
合并节
-
拉伸到内存
-
将第一个节的内存大小、文件大小改成一样
-
Max = SizeOfRawData>VirtualSize?SizeOfRawData:VirtualSize
SizeOfRawData = VirtualSize = 最后一个节的VirtualAddress + Max - SizeOfHeaders内存对齐的大小
-
将第一个节的属性改为包含所有节的属性
-
修改节的数量为1
!!!
这里代码还没贴
数据目录表
可选PE头最后一个属性就是DataDirectory[16]结构体数组,该结构体中VirtualAddress属性是在内存中的偏移RVA,若要在FileBuffer中定位,则需将RVA转换成FOA
1
2
3
4
5
|
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //内存偏移
DWORD Size; //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
|
16个数据目录的大小一样,都是8字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory 导出表:动态链接库导出的函数会显示在这里
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory 导入表:写程序时调用的动态链接库会显示在这里
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory 资源表:图片,图标,字符串,嵌入的程序都在这里
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory 异常目录表:保存文件中异常处理相关的数据
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory 安全目录:存放数字签名和安全证书之类的东西
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table 基础重定位表:保存需要执行重定位的代码偏移
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory 调试表
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data 缓存信息表:有一些保留字段必须是0
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP 全局指针偏移目录
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory 线程局部存储(暂时未知)
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory 载入配置
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers 存储一些API的绑定输入信息
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table 导入地址表:导入函数的地址
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor com运行时的目录
//最后一个保留
|
导出表
当PE文件执行时 Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中函数的导出信息对可执行文件的导入表(IAT)进行修正。
导出函数的DLL文件中,导出信息被保存在导出表,导出表就是记载着动态链接库的一些导出信息。通过导出表,DLL文件可以向系统提供导出函数的名称、序号和入口地址等信息,以便Windows装载器能够通过这些信息来完成动态链接的整个过程。
导出函数存储在PE文件的导出表里,导出表的位置存放在PE文件头中的数据目录表中,与导出表对应的项目是数据目录中的首个IMAGE_DATA_DIRECTORY结构,从这个结构的VirtualAddress字段得到的就是导出表的RVA,导出表同样可以使用函数名或序号这两种方法导出函数。
数据目录项的第一个结构,就是 Export 导出表,只有一个IMAGE_EXPORT_DIRECTORY结构,结构如下:
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; //所有导出函数的个数(导出函数最大序号 - 起始序号 + 1)
DWORD NumberOfNames; //以函数名字导出的函数的个数(即有函数名的函数个数)
DWORD AddressOfFunctions; //导出函数地址表的RVA
DWORD AddressOfNames; //导出函数名称表的RVA
DWORD AddressOfNameOrdinals; //导出函数序号表的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
|

导出函数查找
名称导出:名字所在地址下标即序号地址下标,取出对应序号即函数地址下标
序号导出:导出序号 - 起始序号 = 对应函数地址下标

练习:
-
编写程序打印导出表信息
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
|
VOID DisplayExportTable(){
DWORD dwSize = 0;
DWORD dwFOA = 0;
DWORD dwSizeOfDirectory = 0;
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
PIMAGE_EXPORT_DIRECTORY pExportDirectory = NULL;
dwSize = ReadPEFile(path, &pFileBuffer);
if(dwSize == 0 || !pFileBuffer){
printf("Fail to read file\n");
return ;
}
if(!isPE(pFileBuffer)){
printf("Is not PE file!\n");
return ;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
pNTHeader = (PIMAGE_NT_HEADERS)((BYTE*)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = &pNTHeader->FileHeader;
if (pNTHeader->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64) {
PIMAGE_OPTIONAL_HEADER64 pOptionHeader = (PIMAGE_OPTIONAL_HEADER64)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
} else {
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
}
dwFOA = RvaToFoa(pFileBuffer, pDataDirectory[0].VirtualAddress); //定位导出表
pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)pFileBuffer + dwFOA);
if(pExportDirectory->Name == NULL || pExportDirectory->NumberOfFunctions == 0){
printf("ExportDirectory is NULL\n");
return ;
}
printf("***************ExportDirectory***************\n");
printf("导出表RVA:0x%x\n", pDataDirectory[0].VirtualAddress);
printf("导出表大小:0x%x字节\n", pDataDirectory[0].Size);
printf("导出表FOA:0x%x\n", dwFOA);
printf("TimeDataStamp(经加密):0x%x\n", pExportDirectory->TimeDateStamp);
printf("Name(导出表文件名字符串):%s\n", (BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, pExportDirectory->Name));
printf("Base(函数起始序号):%d\n", pExportDirectory->Base);
printf("NumberOfFunction(导出函数总数):0x%x\n", pExportDirectory->NumberOfFunctions);
printf("NumberOfNames(以名称导出函数的总数):0x%x\n", pExportDirectory->NumberOfNames);
printf("AddressOfFunctions(导出函数地址表RVA):0x%x\n", pExportDirectory->AddressOfFunctions);
printf("AddressOfNames(导出函数名称表RVA):0x%x\n", pExportDirectory->AddressOfNames);
printf("AddressOfNameOrdinals(导出函数序号表RVA):0x%x\n", pExportDirectory->AddressOfNameOrdinals);
printf("-------AddressOfFunctions-------\n");
int i = 0;
PDWORD AddressOfFunction = (PDWORD)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, pExportDirectory->AddressOfFunctions));
for(i = 0; i < pExportDirectory->NumberOfFunctions; i++){
dwFOA = (DWORD)(RvaToFoa(pFileBuffer, *AddressOfFunction));
printf("下标:%d\t函数地址RVA:0x%x\tFOA:0x%x\n", i, *(AddressOfFunction), dwFOA);
AddressOfFunction++;
}
printf("-------NameOfFunctions-------\n");
PDWORD AddressOfNames = (PDWORD)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, pExportDirectory->AddressOfNames));
for(i = 0; i < pExportDirectory->NumberOfNames; i++){
char* name = (char*)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, *AddressOfNames));
printf("下标:%d\t函数名称:%s\t\t名称RVA:0x%x\n", i, name, *AddressOfNames);
AddressOfNames++;
}
printf("-------OrdinalsOfFunctions-------\n");
PWORD AddressOfNameOrdinals = (PWORD)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, pExportDirectory->AddressOfNameOrdinals));
for(i = 0; i < pExportDirectory->NumberOfNames; i++){
printf("下标:%d\t函数序号(加Base):%d\n", i, *AddressOfNameOrdinals + pExportDirectory->Base);
AddressOfNameOrdinals++;
}
}
|
-
按函数名获取函数地址GetFunctionAddrByName(函数名指针)
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
|
DWORD GetFunctionAddrByName(IN char* pFunctionName){
DWORD dwSize = 0;
DWORD dwFOA = 0;
DWORD dwSizeOfDirectory = 0;
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
PIMAGE_EXPORT_DIRECTORY pExportDirectory = NULL;
dwSize = ReadPEFile(path, &pFileBuffer);
if(dwSize == 0 || !pFileBuffer){
printf("Fail to read file\n");
return -1;
}
if(!isPE(pFileBuffer)){
printf("Is not PE file!\n");
return -1;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
pNTHeader = (PIMAGE_NT_HEADERS)((BYTE*)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = &pNTHeader->FileHeader;
if (pNTHeader->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64) {
PIMAGE_OPTIONAL_HEADER64 pOptionHeader = (PIMAGE_OPTIONAL_HEADER64)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
} else {
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
}
dwFOA = RvaToFoa(pFileBuffer, pDataDirectory[0].VirtualAddress); //定位导出表
pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)pFileBuffer + dwFOA);
if(pExportDirectory->Name == NULL || pExportDirectory->NumberOfFunctions == 0){
printf("ExportDirectory is NULL\n");
return -1;
}
DWORD i, idx, f = 1;
PDWORD AddressOfNames = (PDWORD)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, pExportDirectory->AddressOfNames));
for(i = 0; i < pExportDirectory->NumberOfNames; i++){
char* name = (char*)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, *AddressOfNames));
if(!strcmp(name, pFunctionName)){
f = 0; break;
}
AddressOfNames++;
}
if(f){
printf("Notfound addr by name\n");
return -1;
}
PWORD AddressOfNameOrdinals = (PWORD)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, pExportDirectory->AddressOfNameOrdinals));
idx = AddressOfNameOrdinals[i]; //名称下标对应函数序号(真正导出序号 = idx + Base)
PDWORD AddressOfFunction = (PDWORD)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, pExportDirectory->AddressOfFunctions));
return AddressOfFunction[idx];
}
|
-
按函数导出序号获取函数地址GetFunctionAddrByOrdinals(函数导出序号)
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
|
DWORD GetFunctionAddrByOrdinals(IN DWORD pFunctionOrdinals){
DWORD dwSize = 0;
DWORD dwFOA = 0;
DWORD dwSizeOfDirectory = 0;
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
PIMAGE_EXPORT_DIRECTORY pExportDirectory = NULL;
dwSize = ReadPEFile(path, &pFileBuffer);
if(dwSize == 0 || !pFileBuffer){
printf("Fail to read file\n");
return -1;
}
if(!isPE(pFileBuffer)){
printf("Is not PE file!\n");
return -1;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
pNTHeader = (PIMAGE_NT_HEADERS)((BYTE*)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = &pNTHeader->FileHeader;
if (pNTHeader->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64) {
PIMAGE_OPTIONAL_HEADER64 pOptionHeader = (PIMAGE_OPTIONAL_HEADER64)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
} else {
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
}
dwFOA = RvaToFoa(pFileBuffer, pDataDirectory[0].VirtualAddress); //定位导出表
pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)pFileBuffer + dwFOA);
if(pExportDirectory->Name == NULL || pExportDirectory->NumberOfFunctions == 0){
printf("ExportDirectory is NULL\n");
return -1;
}
PDWORD AddressOfFunction = (PDWORD)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, pExportDirectory->AddressOfFunctions));
if(pFunctionOrdinals - pExportDirectory->Base >= pExportDirectory->NumberOfFunctions){
printf("The ordinals is wrong");
return -1;
}
return AddressOfFunction[pFunctionOrdinals - pExportDirectory->Base];
}
|
相当于GetProAddress()
重定位表
1.程序加载过程
一般情况下,exe可按照自身的ImageBase加载,有自己的4GB虚拟空间,而dll需加载到相应exe进程空间
模块对齐:10000h 即64K

2.为什么要用重定位表
全局变量编译时生成的地址 = ImageBase + RVA,这个地址直接写入文件中
若没有加载到预定位置,则直接按文件地址会出错
需要加载的dll所对应的ImageBase有可能相同,会被加载到其他地址
因此,dll大多都有重定位表,而exe不一定有
一旦某个模块没有按照自身ImageBase进行加载,那么类似上述地址都需要修正
3.重定位表的结构分析
数据目录项第六个结构,就是重定位表,记录了需要重定位的函数地址的RVA
1
2
3
4
5
6
|
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; //RVA
DWORD SizeOfBlock; //重定位块大小
//WORD TypeOffset[1]; //虽然被注释,但仍存在,其16位大小,前四位为Type,后十二位为Offest
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
|
- 通过
IMAGE_DATA_DIRECTORY结构的VirtualAddress属性找到第一个IMAGE_BASE_RELOCATION
- 判断一共有几块数据:最后一个结构的VirtualAddress与SizeOfBlock均为0
- 具体项 宽度:2字节
内存中的页大小是1000h,也就是2的12次方就可以表示一个页内所有的偏移地址,具体项的宽度是16位,高四位代表类型:值为3代表需要修改的数据的RVA;值为0代表用于数据对齐的数据,可以不用修改
当前这一块的数据,每一个低12位的值 + VirtualAddress才是真正需要修复地址的RVA
当前块总大小,具体项数目 = (SizeOfBlock - 8) / 2

4.打印重定位表
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
|
VOID DisplayRelocTable(){
DWORD dwSize = 0;
DWORD dwFOA = 0;
DWORD dwSizeOfDirectory = 0;
LPVOID pFileBuffer = NULL;
PWORD pRelocData = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
PIMAGE_BASE_RELOCATION pRelocTable = NULL;
dwSize = ReadPEFile(path, &pFileBuffer);
if(dwSize == 0 || !pFileBuffer){
printf("Fail to read file\n");
return ;
}
if(!isPE(pFileBuffer)){
printf("Is not PE file!\n");
return ;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
pNTHeader = (PIMAGE_NT_HEADERS)((BYTE*)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = &pNTHeader->FileHeader;
if (pNTHeader->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64) {
PIMAGE_OPTIONAL_HEADER64 pOptionHeader = (PIMAGE_OPTIONAL_HEADER64)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
} else {
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
}
dwFOA = RvaToFoa(pFileBuffer, pDataDirectory[5].VirtualAddress); //定位重定位表
pRelocTable = (PIMAGE_BASE_RELOCATION)((BYTE*)pFileBuffer + dwFOA);
printf("***************BaseRelocation***************\n");
int i=1;
while (pRelocTable->VirtualAddress != 0 && pRelocTable->SizeOfBlock != 0){ //遍历重定位表
printf("RelocTable[%d]\tRVA:%0X, Size:%0X\n", i, pRelocTable->VirtualAddress, pRelocTable->SizeOfBlock);
DWORD dwItems = ((pRelocTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2);
printf("项目数:%X h / %d d\r\n", dwItems, dwItems);
pRelocData = (PWORD)((BYTE*)pRelocTable + 0x8);
for (int i = 0; i < dwItems; i++ ){
if (*(pRelocData + i) >> 12 == IMAGE_REL_BASED_HIGHLOW){ //IMAGE_REL_BASED_HIGHLOW = 3
DWORD dwRva = ((*(pRelocData + i) & 0x0fff) + pRelocTable->VirtualAddress);
DWORD dwFoa = RvaToFoa(pFileBuffer, dwRva);
DWORD dwFarCall = *(DWORD*)(pFileBuffer + dwFoa);
printf("RVA:0x%X\tFOA:0x%X\tFarCall:0x%X\t(HIGHLOW)\n", dwRva, dwFoa, dwFarCall); //需要重定位的函数
}else{
printf("-(ABSLUTE)\r\n"); //类型非3的提示
}
}
pRelocTable = (PIMAGE_BASE_RELOCATION)((BYTE*)pRelocTable + pRelocTable->SizeOfBlock);
i++;
}
}
|
移动导出表 - 重定位表
为什么要移动各种表
1.这些表是编译器生成的,里面存储了非常重要的信息
2.在程序启动的时候,系统会根据这些表做初始化的工作:比如,将用到的dll中的函数地址存储到IAT表中
3.为了保护程序,可以对.exe的二进制代码进行加密操作,但若各种表的信息与客户的代码和数据混在一起,进行加密,系统初始化会出问题
4.学会移动各种表,是对程序加密/破解的基础
移动导出表
- 在dll中新增节,并返回FOA
- 依次复制AddressOfFunctions、AddressOfNameOrdinals、AddressOfNames
- 复制所有函数名,复制时修复AddressOfNames,同时复制dll名,并修改导出表Name字段
- 复制IMAGE_EXPORT_DIRECTORY,并修复AddressOfFunctions、AddressOfNameOrdinals、AddressOfNames
- 修复目录项中的值,指向新的IMAGE_EXPORT_DIRECTORY
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
|
// 移动导出表函数
BOOL MoveExportTable(LPVOID* ppFileBuffer, DWORD* pFileSize) {
IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)*ppFileBuffer;
IMAGE_NT_HEADERS* pNtHeaders = (IMAGE_NT_HEADERS*)((BYTE*)*ppFileBuffer + pDosHeader->e_lfanew);
// 获取导出表目录项
IMAGE_DATA_DIRECTORY* exportDir = &pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (exportDir->VirtualAddress == 0 || exportDir->Size == 0) {
printf("No export table found\n");
return FALSE;
}
// 获取导出表结构
DWORD exportFoa = RvaToFoa(*ppFileBuffer, exportDir->VirtualAddress);
IMAGE_EXPORT_DIRECTORY* pExportDir = (IMAGE_EXPORT_DIRECTORY*)((BYTE*)*ppFileBuffer + exportFoa);
// 获取DLL名称
DWORD dllNameFoa = RvaToFoa(*ppFileBuffer, pExportDir->Name);
char* dllName = (char*)((BYTE*)*ppFileBuffer + dllNameFoa);
DWORD dllNameLen = strlen(dllName) + 1; // 包括null终止符
// 计算导出表总大小
DWORD totalSize = sizeof(IMAGE_EXPORT_DIRECTORY);
totalSize += pExportDir->NumberOfFunctions * sizeof(DWORD); // AddressOfFunctions
totalSize += pExportDir->NumberOfNames * sizeof(DWORD); // AddressOfNames
totalSize += pExportDir->NumberOfNames * sizeof(WORD); // AddressOfNameOrdinals
// 计算函数名字符串总长度
DWORD* nameRVAs = (DWORD*)((BYTE*)*ppFileBuffer + RvaToFoa(*ppFileBuffer, pExportDir->AddressOfNames));
for (DWORD i = 0; i < pExportDir->NumberOfNames; i++) {
DWORD nameFoa = RvaToFoa(*ppFileBuffer, nameRVAs[i]);
totalSize += strlen((char*)*ppFileBuffer + nameFoa) + 1; // 字符串+null终止符
}
// 添加DLL名称长度
totalSize += dllNameLen;
// 添加新节
DWORD newSectionFoa = AddNewSection(ppFileBuffer, pFileSize, ".edata", totalSize,
IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ);
if (!newSectionFoa) {
printf("Failed to add new section for export table\n");
return FALSE;
}
// 获取新节RVA
DWORD newSectionRva = FoaToRva(*ppFileBuffer, newSectionFoa);
// 准备复制指针
BYTE* newSectionStart = (BYTE*)*ppFileBuffer + newSectionFoa;
BYTE* currentPos = newSectionStart;
// 1. 复制AddressOfFunctions数组
DWORD* srcFunctions = (DWORD*)((BYTE*)*ppFileBuffer + RvaToFoa(*ppFileBuffer, pExportDir->AddressOfFunctions));
DWORD* destFunctions = (DWORD*)currentPos;
memcpy(destFunctions, srcFunctions, pExportDir->NumberOfFunctions * sizeof(DWORD));
DWORD newFunctionsRva = newSectionRva + (DWORD)((BYTE*)destFunctions - newSectionStart);
currentPos += pExportDir->NumberOfFunctions * sizeof(DWORD);
// 2. 复制AddressOfNameOrdinals数组
WORD* srcOrdinals = (WORD*)((BYTE*)*ppFileBuffer + RvaToFoa(*ppFileBuffer, pExportDir->AddressOfNameOrdinals));
WORD* destOrdinals = (WORD*)currentPos;
memcpy(destOrdinals, srcOrdinals, pExportDir->NumberOfNames * sizeof(WORD));
DWORD newOrdinalsRva = newSectionRva + (DWORD)((BYTE*)destOrdinals - newSectionStart);
currentPos += pExportDir->NumberOfNames * sizeof(WORD);
// 3. 复制AddressOfNames数组和函数名字符串
DWORD* srcNames = (DWORD*)((BYTE*)*ppFileBuffer + RvaToFoa(*ppFileBuffer, pExportDir->AddressOfNames));
DWORD* destNames = (DWORD*)currentPos;
currentPos += pExportDir->NumberOfNames * sizeof(DWORD);
for (DWORD i = 0; i < pExportDir->NumberOfNames; i++) {
// 复制字符串
DWORD nameFoa = RvaToFoa(*ppFileBuffer, srcNames[i]);
char* srcName = (char*)*ppFileBuffer + nameFoa;
char* destName = (char*)currentPos;
DWORD nameLen = strlen(srcName) + 1;
memcpy(destName, srcName, nameLen);
// 更新AddressOfNames数组
destNames[i] = newSectionRva + (DWORD)(currentPos - newSectionStart);
currentPos += nameLen;
}
DWORD newNamesRva = newSectionRva + (DWORD)((BYTE*)destNames - newSectionStart);
// 4. 复制DLL名称字符串
char* newDllName = (char*)currentPos;
memcpy(newDllName, dllName, dllNameLen);
DWORD newDllNameRva = newSectionRva + (DWORD)(currentPos - newSectionStart);
currentPos += dllNameLen;
// 5. 复制IMAGE_EXPORT_DIRECTORY结构
IMAGE_EXPORT_DIRECTORY* destExportDir = (IMAGE_EXPORT_DIRECTORY*)currentPos;
memcpy(destExportDir, pExportDir, sizeof(IMAGE_EXPORT_DIRECTORY));
// 修复导出表指针
destExportDir->AddressOfFunctions = newFunctionsRva;
destExportDir->AddressOfNames = newNamesRva;
destExportDir->AddressOfNameOrdinals = newOrdinalsRva;
destExportDir->Name = newDllNameRva; // 更新DLL名称的RVA
// 6. 更新数据目录
DWORD newExportDirRva = newSectionRva + (DWORD)(currentPos - newSectionStart);
pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress = newExportDirRva;
return TRUE;
}
|
移动重定位表
循环遍历重定位表的大小,复制到新增节,然后修改目录项中VirtualAddress的值
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
|
// 移动重定位表函数
BOOL MoveRelocationTable(LPVOID* ppFileBuffer, DWORD* pFileSize) {
IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)*ppFileBuffer;
IMAGE_NT_HEADERS* pNTHeader = (IMAGE_NT_HEADERS*)((BYTE*)*ppFileBuffer + pDosHeader->e_lfanew);
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
if (pNTHeader->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64) {
PIMAGE_OPTIONAL_HEADER64 pOptionHeader = (PIMAGE_OPTIONAL_HEADER64)&pNTHeader->OptionalHeader;
pDataDirectory = pOptionHeader->DataDirectory;
} else {
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)&pNTHeader->OptionalHeader;
pDataDirectory = pOptionHeader->DataDirectory;
}
// 获取导出表目录项
IMAGE_DATA_DIRECTORY* relocDir = &pDataDirectory[5];
if (relocDir->VirtualAddress == 0 || relocDir->Size == 0) {
printf("No relocation table found, %xh--%xh\n", relocDir->VirtualAddress, relocDir->Size);
return FALSE;
}
// 添加新节
DWORD newSectionFoa = AddNewSection(ppFileBuffer, pFileSize, ".reloc", relocDir->Size,
IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ);
if (!newSectionFoa) {
printf("Failed to add new section for relocation table\n");
return FALSE;
}
// 获取重定位表FOA
DWORD relocFoa = RvaToFoa(*ppFileBuffer, relocDir->VirtualAddress);
// 复制重定位表数据
memcpy((BYTE*)*ppFileBuffer + newSectionFoa,
(BYTE*)*ppFileBuffer + relocFoa,
relocDir->Size);
// 更新数据目录
DWORD newRelocRva = FoaToRva(*ppFileBuffer, newSectionFoa);
relocDir->VirtualAddress = newRelocRva;
return TRUE;
}
|
附加:
修改dll的ImageBase,根据重定位表修正,然后存盘
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
|
// 修改ImageBase并应用重定位修正
BOOL RebasePE(LPVOID pFileBuffer, DWORD newImageBase) {
IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)pFileBuffer;
IMAGE_NT_HEADERS* pNtHeaders = (IMAGE_NT_HEADERS*)((BYTE*)pFileBuffer + pDosHeader->e_lfanew);
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
DWORD oldImageBase = 0;
if (pNtHeaders->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64) {
PIMAGE_OPTIONAL_HEADER64 pOptionHeader = (PIMAGE_OPTIONAL_HEADER64)&pNtHeaders->OptionalHeader;
oldImageBase = pOptionHeader->ImageBase;
pOptionHeader->ImageBase = newImageBase; // 更新ImageBase
pDataDirectory = pOptionHeader->DataDirectory;
} else {
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)&pNtHeaders->OptionalHeader;
oldImageBase = pOptionHeader->ImageBase;
pOptionHeader->ImageBase = newImageBase;
pDataDirectory = pOptionHeader->DataDirectory;
}
// 如果新旧基址相同,不需要重定位
if (oldImageBase == newImageBase) {
printf("ImageBase unchanged (0x%08X), no rebasing needed\n", newImageBase);
return TRUE;
}
printf("ImageBase changed from 0x%08X to 0x%08X\n", oldImageBase, newImageBase);
// 获取重定位表
IMAGE_DATA_DIRECTORY* relocDir = &pDataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
if (relocDir->VirtualAddress == 0 || relocDir->Size == 0) {
printf("No relocation table found, skipping rebasing\n");
return TRUE;
}
// 计算基址差值
DWORD_PTR delta = (DWORD_PTR)newImageBase - (DWORD_PTR)oldImageBase;
// 获取重定位表起始位置
DWORD relocFoa = RvaToFoa(pFileBuffer, relocDir->VirtualAddress);
IMAGE_BASE_RELOCATION* pRelocTable = (IMAGE_BASE_RELOCATION*)((BYTE*)pFileBuffer + relocFoa);
// 遍历重定位块
int i=1;
while (pRelocTable->VirtualAddress != 0 && pRelocTable->SizeOfBlock != 0){ //遍历重定位表
DWORD dwItems = ((pRelocTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2);
PWORD pRelocData = (PWORD)((BYTE*)pRelocTable + 0x8);
for (int i = 0; i < dwItems; i++ ){
if (*(pRelocData + i) >> 12 == IMAGE_REL_BASED_HIGHLOW){ //IMAGE_REL_BASED_HIGHLOW = 3
DWORD dwRva = ((*(pRelocData + i) & 0x0fff) + pRelocTable->VirtualAddress);
DWORD dwFoa = RvaToFoa(pFileBuffer, dwRva);
DWORD* pFuncall = (DWORD*)(pFileBuffer + dwFoa);
*pFuncall += delta; //重定位
}
}
pRelocTable = (PIMAGE_BASE_RELOCATION)((BYTE*)pRelocTable + pRelocTable->SizeOfBlock);
i++;
}
return TRUE;
}
|
IAT表
重定位表(Relocation Table)和导入地址表(IAT - Import Address Table)在 Windows PE 文件格式中都扮演着关键角色,但它们处理的是不同阶段、不同类型的地址问题。它们之间的关系可以概括为:重定位表负责修正模块(DLL/EXE)内部代码和数据对自身地址的引用,而 IAT 负责存储模块调用其他模块(DLLs)中函数的实际地址。更重要的是,IAT 表项本身所在的内存地址,在模块被加载到非首选基址时,就需要通过重定位表来修正。
理解重定位表如何帮助程序找到调用函数的地址,需要结合导入地址表一起来看。核心思想是:重定位表修正指向IAT项的指针,IAT项本身存储着最终的函数地址。
以下是详细步骤:
- 编译与链接时的假设:
- 编译器在生成调用外部DLL函数(如
MessageBoxA)的代码时,并不知道这个函数最终会加载到内存的哪个地址。
- 它生成一条间接调用指令,例如:
call dword ptr [0x00403000](32位示例)。
- 这里的
0x00403000是一个硬编码的地址,是编译器/链接器假设的DLL加载基址(称为ImageBase)加上一个固定偏移量(.idata节或IAT的RVA)。在这个例子中,0x00403000指向导入地址表中的一个位置。
- 导入地址表的角色:
- IAT是PE文件中一个专门的数据结构(通常在
.idata节)。
- 在程序加载完成之前,IAT中存储的并不是函数的真实地址,而是:
- 函数名的指针(用于按名称导入)。
- 或者函数的序号(用于按序号导入)。
- 它的目的是在运行时被Windows加载器填充。
- 加载时的挑战:
- 程序启动时,Windows加载器尝试将DLL加载到它的
ImageBase(例如0x10000000)。
- 如果这个地址没有被其他模块占用,加载成功。DLL的代码和数据就位于预期的地址。
- 但是,如果
ImageBase地址已被占用(很常见),加载器必须将DLL重定位到另一个空闲的基地址(例如0x15000000)。
- 重定位意味着DLL中所有基于
ImageBase的绝对地址引用(代码和数据中的指针)都不再正确,它们需要被调整一个差值 Delta = ActualBase - ImageBase(例如 0x15000000 - 0x10000000 = 0x05000000)。
- 重定位表的作用:
- 重定位表(
.reloc节)就是告诉加载器:“我的DLL代码和数据中,哪些位置存储的是需要根据加载基址调整的绝对指针”。
- 它包含一系列条目,每个条目指定一个内存页(4KB对齐)和该页内所有需要修正的偏移量列表。
- 关键点: 指向IAT项的指针(比如例子中
call dword ptr [0x00403000]里的0x00403000)就是需要被修正的绝对地址之一!它是在编译时基于ImageBase计算出来的,如果DLL被重定位了,这个值就错了。
- 加载器如何修正调用函数的地址:
- 解析IAT (填充函数真实地址): 无论DLL是否被重定位,加载器首先都会解析所有导入函数。它找到目标DLL(可能也需要重定位),加载它,找到
MessageBoxA的实际内存地址(例如0x15012345),然后将这个真实地址写回主程序的IAT中对应的位置(假设这个位置在IAT中的偏移是固定的,由链接器确定)。
- 应用重定位 (修正指向IAT的指针):
- 加载器检查主程序是否需要重定位(如果主程序的
ImageBase被占用)。
- 如果需要,加载器读取主程序的重定位表。
- 在重定位表中,它会找到像
0x00403000这样的地址(位于某个.reloc条目指定的页内)。
- 加载器计算
Delta(实际加载基址 - ImageBase)。
- 加载器将这个
Delta值加到存储在0x00403000这个内存位置的值上吗?不对!
- 正确的操作是: 加载器将
Delta值加到指向IAT项的指针本身,也就是加到0x00403000这个地址上(更准确地说,是加到包含0x00403000这个地址值的那个内存位置的内容上)。0x00403000这个地址值本身是基于ImageBase计算出来的,它指向IAT中的一个槽位。重定位后,主程序的实际基址变了,这个指向IAT槽位的指针0x00403000也必须加上Delta才能指向正确的内存位置(即新的IAT槽位地址)。
- 间接调用生效:
- 假设主程序被重定位到
0x00450000(原ImageBase是0x00400000,Delta = 0x00050000)。
- 指向IAT项的原指针是
0x00403000。
- 加载器通过重定位表找到这个指针的位置(假设是主程序代码段中的
0x00401023处存储着值0x00403000,即0x00401023 = [0x00403000],重定位表中存的是0x0 + 0x1023这个RVA)。
- 加载器将存储在
0x00001023 + 0x00450000处的值修改为 0x00403000 + 0x00050000 = 0x00453000。
- 现在,调用指令
call dword ptr [0x00453000] 就能正确执行了:
- 它去
0x00453000(这是重定位后IAT中MessageBoxA条目所在的新地址)读取内容。
- 这个新地址
0x00453000处的内容,在步骤1中已经被加载器填充为MessageBoxA的真实地址0x15012345。
- 因此,CPU执行
call [0x00453000](中间有个jmp dword ptr [0x15012345])最终会跳转到0x15012345,即MessageBoxA函数的入口点。
具体关系如下图

导入表
实现:
1.使用OD打开一个发布版的exe程序,定位到某个dll的API
2.在没有加载的exe中找到这个位置,观察加载前后的区别
数据目录项的第二个结构,就是 Import 导入表,从IMAGE_DATA_DIRECTORY字段得到的是导入表的RVA值,如果在内存中查找导入表,那么将RVA值加上PE文件装入的基址就是实际的地址。
找到了数据目录结构,既能够找到导入表,导入表由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成,结构的数量取决于程序需要使用的DLL文件数量,每个结构对应一个DLL文件,在所有结构的最后,由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构作为结束标志,表结构定义如下:
(使用到几个dll就有几个导入表)
1
2
3
4
5
6
7
8
9
10
11
|
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; * // 包含指向IMAGE_THUNK_DATA(导入名称表INT)结构数组的RVA
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 时间戳:当可执行文件不与被输入的DLL进行绑定时,此字段为0 若绑定,则为-1(0xffffffff)
DWORD ForwarderChain; // 第一个被转向的API的索引
DWORD Name; // 指向被输入的DLL的ASCII字符串的RVA
DWORD FirstThunk; * // 指向导入地址表(IAT)的RVA
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED * IMAGE_IMPORT_DESCRIPTOR;
|
OriginalFirstThunk和FirstThunk字段是相同的,他们都指向一个包含IMAGE_THUNK_DATA结构的数组,数组中每个IMAGE_THUNK_DATA结构定义了一个导入函数的具体信息,数组的最后以一个内容全为0的IMAGE_THUNK_DATA结构作为结束,该结构的定义如下:
判断最高位是否为1 如果是,那么去除最高位的值就是函数的导出序号;如果不是,那么这个值是一个指向IMAGE_IMPORT_BY_NAME的RVA
1
2
3
4
5
6
7
8
9
|
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal; // 导出序号
PIMAGE_IMPORT_BY_NAME AddressOfData; // 指向IMAGE_IMPORT_BY_NAME的RVA(指针都是4字节)
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
|
从上方的结构定义不难看出,这是一个双字共用体结构,当结构的最高位为1时,表示函数是以序号的方式导入的,这时双字的低位就是函数的序号,当双字最高位为0时,表示函数以函数名方式导入,这时双字的值是一个RVA,指向一个用来定义导入函数名称的IMAGE_IMPORT_BY_NAME结构,此结构定义如下:
1
2
3
4
|
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // 函数序号
CHAR Name[]; // 导入函数的名称(指针4字节)
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
|
PE加载前

PE加载后,调用GetProcAddr()

打印导入表
1.定位导入表
2.输出dll名字
3.遍历OriginalFirstThunk
4.遍历FirstThunk
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
|
VOID DisplayImportTable(){
DWORD dwSize = 0;
DWORD dwFOA = 0;
DWORD dwSizeOfDirectory = 0;
LPVOID pFileBuffer = NULL;
PWORD pRelocData = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = NULL;
dwSize = ReadPEFile(path, &pFileBuffer);
if(dwSize == 0 || !pFileBuffer){
printf("Fail to read file\n");
return ;
}
if(!isPE(pFileBuffer)){
printf("Is not PE file!\n");
return ;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
pNTHeader = (PIMAGE_NT_HEADERS)((BYTE*)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = &pNTHeader->FileHeader;
if (pNTHeader->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64) {
PIMAGE_OPTIONAL_HEADER64 pOptionHeader = (PIMAGE_OPTIONAL_HEADER64)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
} else {
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
}
dwFOA = RvaToFoa(pFileBuffer, pDataDirectory[1].VirtualAddress); //定位导入表
pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)pFileBuffer + dwFOA);
printf("***************ImportDescriptor***************\n");
while(!(pImportDescriptor->FirstThunk == 0 && pImportDescriptor->OriginalFirstThunk == 0)){ //遍历导入表
printf("***%s***\n", (PBYTE)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, pImportDescriptor->Name)));
printf("-------OriginalFirstThunk-------\n");
PDWORD pOriginalFirstThunk = (PDWORD)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, pImportDescriptor->OriginalFirstThunk));
printf("%x - %x\n", pOriginalFirstThunk, *pOriginalFirstThunk);
printf("TimeDateStamp: %x\n", pImportDescriptor->TimeDateStamp);
while(*pOriginalFirstThunk){ //遍历INT表
if(*pOriginalFirstThunk & IMAGE_ORDINAL_FLAG32){ //IMAGE_ORDINAL_FLAG32 = 0x80000000
printf("按序号导入:%x\n", (*pOriginalFirstThunk)&0x0FFFF);
}else{
PIMAGE_IMPORT_BY_NAME pImageByName = (PIMAGE_IMPORT_BY_NAME)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, *pOriginalFirstThunk));
printf("按名称导入Hint-Name: %x-%s\n", pImageByName->Hint, pImageByName->Name);
}
//pOriginalFirstThunk = (PDWORD)((BYTE*)pOriginalFirstThunk + sizeof(IMAGE_THUNK_DATA32));
pOriginalFirstThunk++;
}
printf("-------FirstThunk-------\n");
PDWORD pFirstThunk = (PDWORD)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, pImportDescriptor->FirstThunk));
printf("%x - %x\n", pFirstThunk, *pFirstThunk);
while(*pFirstThunk){
if(*pFirstThunk & IMAGE_ORDINAL_FLAG32){ //IMAGE_ORDINAL_FLAG32 = 0x80000000
printf("按序号导入:%x\n", (*pFirstThunk)&0x0FFFF);
}else{
PIMAGE_IMPORT_BY_NAME pImageByName = (PIMAGE_IMPORT_BY_NAME)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, *pFirstThunk));
printf("按名称导入Hint-Name: %x-%s\n", pImageByName->Hint, pImageByName->Name);
}
//pFirstThunk = (PDWORD)((BYTE*)pFirstThunk + sizeof(IMAGE_THUNK_DATA32));
pFirstThunk++;
}
pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)pImportDescriptor + sizeof(IMAGE_IMPORT_DESCRIPTOR));
}
free(pFileBuffer);
}
|
绑定导入表
PE加载exe相关dll时,首先会根据IMAGE_IMPORT_DESCRIPTOR结构中的TimeDateStamp来判断是否要重新计算IAT
TimeDateStamp为0则IAT未绑定;若TimeDateStamp为-1则IAT已绑定 而真正绑定时间为IMAGE_BOUND_IMPORT_DESCRIPTOR的TimeDateStamp
绑定导入表位于数据目录项的第12项
1
2
3
4
5
|
struct _IMAGE_BOUND_IMPORT_DESCRIPTOR{
DWORD TimeDateStamp; //时间戳
WORD OffsetModuleName; //DLL的名字RVA(加第一个结构中RVA才是字符串真正RVA,详见下面)
WORD NumberOfModuleForwarderRefs; //这个绑定导入表结构后面还有几个_IMAGE_BOUND_FORWARDER_REF这种结构
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR; //绑定导入表有很多这种结构或者_IMAGE_BOUND_FORWARDER_REF这种结构,最后如果有sizeof(_IMAGE_BOUND_IMPORT_DESCRIPTOR)个0,表示绑定导入表结束
|
1)TimeDateStamp
绑定导入表的时间戳:表示程序使用到的DLL中的函数绝对地址真正绑定到IAT表中的时间
作用:系统通过程序使用到的DLL对应的绑定导入表结构中TimeDateStamp和该DLL的标准PE头中的TimeDateStamp对比
如果两个时间戳一样:表明DLL的创建时间和把DLL中的函数绝对地址绑定到IAT表的时间是一样的,则在绑定以后,DLL没有更新或者修改
如果两个时间戳不一样:表明在绑定以后,此DLL又被更新或者修改了,那么绑定到IAT表中的地址可能不准确了!(因为DLL中的函数地址可能变了,但绑定到IAT中的数据可能是以前的函数地址)
2)OffsetModuleName
对应DLL的名字:因为一个程序的导入表结构对应一个使用到的DLL;一个程序的绑定导入表结构也对应一个程序使用到的DLL,这个绑定导入表结构记录了该DLL中函数绝对地址绑定到IAT表的时间戳、该DLL的名字、还有该DLL使用到别的DLL的个数
注意:不管是IMAGE_BOUND_IMPORT_DESCRIPTOR结构中的OffsetModuleName、还是后面要讲的IMAGE_BOUND_FORWARDER_REF 结构中的OffsetModuleName,都必须加上绑定导入表起始RVA值,才是这个结构对应DLL名字的真正RVA
3)NumberOfModuleForwarderRefs
因为一个DLL可能还会使用到别的DLL中的函数,所以NumberOfModuleForwarderRefs字段的值是多少,就表明当前绑定导入表对应的DLL还使用了多少个别的DLL,同样表示这个绑定导入表结构后面跟了多少个IMAGE_BOUND_FORWARDER_REF这种结构
IMAGE_BOUND_FORWARDER_REF结构
1
2
3
4
5
|
struct _IMAGE_BOUND_FORWARDER_REF {
DWORD TimeDateStamp; //时间戳
WORD OffsetModuleName; //对应DLL的名字
WORD Reserved; //保留(未使用)
} IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;
|
打印绑定导入表
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
|
VOID DisplayBoundImportTable(){
DWORD dwSize = 0;
DWORD dwFOA = 0;
DWORD dwSizeOfDirectory = 0;
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pPEHeader = NULL;
PIMAGE_DATA_DIRECTORY pDataDirectory = NULL;
PIMAGE_BOUND_IMPORT_DESCRIPTOR pBoundImportDescriptor = NULL;
PIMAGE_BOUND_FORWARDER_REF pBoundImportRef = NULL;
dwSize = ReadPEFile(path, &pFileBuffer);
if(dwSize == 0 || !pFileBuffer){
printf("Fail to read file\n");
return ;
}
if(!isPE(pFileBuffer)){
printf("Is not PE file!\n");
return ;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
pNTHeader = (PIMAGE_NT_HEADERS)((BYTE*)pFileBuffer + pDosHeader->e_lfanew);
pPEHeader = &pNTHeader->FileHeader;
if (pNTHeader->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64) {
PIMAGE_OPTIONAL_HEADER64 pOptionHeader = (PIMAGE_OPTIONAL_HEADER64)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
} else {
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)&pNTHeader->OptionalHeader;
dwSizeOfDirectory = pOptionHeader->NumberOfRvaAndSizes;
pDataDirectory = pOptionHeader->DataDirectory;
}
dwFOA = RvaToFoa(pFileBuffer, pDataDirectory[11].VirtualAddress); //定位绑定导入表
pBoundImportDescriptor = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)((BYTE*)pFileBuffer + dwFOA);
auto dwNameBase = (DWORD_PTR)pBoundImportDescriptor;
printf("***************BoundImportDescriptor***************\n");
while(pBoundImportDescriptor->OffsetModuleName != 0){
printf("TimeDateStamp: %x\n", pBoundImportDescriptor->TimeDateStamp);
printf("OffsetModuleName: %s\n", (char*)((BYTE*)pFileBuffer + RvaToFoa(pFileBuffer, dwNameBase + pBoundImportDescriptor->OffsetModuleName)));
printf("NumberOfModuleForwarderRefs: %x\n", pBoundImportDescriptor->NumberOfModuleForwarderRefs);
DWORD temp = pBoundImportDescriptor->NumberOfModuleForwarderRefs;
while(temp--){
printf("--------------Ref--------------\n");
pBoundImportRef = (PIMAGE_BOUND_FORWARDER_REF)((BYTE*)pBoundImportDescriptor + 8);
pBoundImportDescriptor = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)((BYTE*)pBoundImportDescriptor + 8);
printf(" TimeDateStamp: %x\n", pBoundImportDescriptor->TimeDateStamp);
printf(" OffsetModuleName: %s\n", dwNameBase + pBoundImportDescriptor->OffsetModuleName);
}
pBoundImportDescriptor = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)((BYTE*)pBoundImportDescriptor + 8);
}
free(pFileBuffer);
}
|
导入表注入
导入表注入原理:
当exe被加载时,系统会根据exe导入表信息来加载需要用到的dll,导入表注入的原理就是修改exe导入表,将自己的dll添加到exe的导入表中,这样exe运行时可以将自己的dll加载到exe的进程空间
导入表注入的实现步骤:
1.根据目录项得到导入表信息:VirtualAddress和Size
2.A:导入表(20B),B:INT表、IAT表(最少8+8B),C:DLL名字符串(dll名称长度+1),D:序号函数名(函数名+1+2B)
判断哪一个节区空白 > Size(原表大小)+ 20 + A + B + C + D(若空间不够可将C/D存储在其他空白区)
即空白区 > Size + 0x20
3.将原导入表Copy到空白区
4.在新的导入表后面,追加一个空导入表
5.追加8BINT表、8BIAT表
6.追加一个IMAGE_IMPORT_BY_NAME结构,前两个字节为0,后面是函数名字符串
7.将IMAGE_IMPORT_BY_NAME结构的RVA赋值给INT表和IAT表第一项
8.分配空间存储dll名称字符串,并将该RVA赋值给Name属性
9.修正IMAGE_DATA_DIRECTORY结构的VirtualAddress和Size
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
|
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#define ALIGN_UP(x, align) (((x) + (align) - 1) & ~((align) - 1))
BOOL InjectImportTable(LPCSTR pszFilePath, LPCSTR pszDllName, LPCSTR pszFuncName) {
HANDLE hFile = CreateFileA(pszFilePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) return FALSE;
HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL);
if (!hMapping) {
CloseHandle(hFile);
return FALSE;
}
LPVOID pBase = MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, 0);
if (!pBase) {
CloseHandle(hMapping);
CloseHandle(hFile);
return FALSE;
}
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBase;
if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
UnmapViewOfFile(pBase);
CloseHandle(hMapping);
CloseHandle(hFile);
return FALSE;
}
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)pBase + pDosHeader->e_lfanew);
if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
UnmapViewOfFile(pBase);
CloseHandle(hMapping);
CloseHandle(hFile);
return FALSE;
}
// 获取导入表信息
DWORD importRVA = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
DWORD importSize = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size;
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
// 查找导入表所在节
PIMAGE_SECTION_HEADER pImportSection = NULL;
for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++) {
if (importRVA >= pSection[i].VirtualAddress &&
importRVA < pSection[i].VirtualAddress + pSection[i].Misc.VirtualSize) {
pImportSection = &pSection[i];
break;
}
}
if (!pImportSection) {
UnmapViewOfFile(pBase);
CloseHandle(hMapping);
CloseHandle(hFile);
return FALSE;
}
// 计算所需空间
DWORD newImportSize = importSize + sizeof(IMAGE_IMPORT_DESCRIPTOR); // 增加新描述符
DWORD dllNameLen = (DWORD)strlen(pszDllName) + 1;
DWORD funcNameLen = (DWORD)strlen(pszFuncName) + 1 + sizeof(WORD); // Hint + 函数名
// 计算总需求空间 (描述符+INT+IAT+DLL名+函数名)
DWORD totalSpace = newImportSize + 2 * sizeof(DWORD_PTR) * 2 + dllNameLen + funcNameLen;
// 寻找空白区域 (从各节末尾查找)
PIMAGE_SECTION_HEADER pTargetSection = NULL;
DWORD blankOffset = 0;
for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++) {
DWORD sectionEnd = pSection[i].PointerToRawData + pSection[i].SizeOfRawData;
DWORD blankSize = sectionEnd - (pSection[i].PointerToRawData + pSection[i].Misc.VirtualSize);
if (blankSize >= totalSpace) {
pTargetSection = &pSection[i];
blankOffset = pSection[i].PointerToRawData + pSection[i].Misc.VirtualSize;
break;
}
}
if (!pTargetSection) {
// 没有足够空间,使用文件末尾
pTargetSection = &pSection[pNtHeaders->FileHeader.NumberOfSections - 1];
blankOffset = pTargetSection->PointerToRawData + pTargetSection->SizeOfRawData;
// 扩展文件大小
DWORD newFileSize = blankOffset + totalSpace;
UnmapViewOfFile(pBase);
CloseHandle(hMapping);
CloseHandle(hFile);
// 重新打开文件并设置大小
hFile = CreateFileA(pszFilePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
SetFilePointer(hFile, newFileSize, NULL, FILE_BEGIN);
SetEndOfFile(hFile);
// 重新映射
hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, newFileSize, NULL);
pBase = MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, newFileSize);
if (!pBase) return FALSE;
// 更新指针
pDosHeader = (PIMAGE_DOS_HEADER)pBase;
pNtHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)pBase + pDosHeader->e_lfanew);
pSection = IMAGE_FIRST_SECTION(pNtHeaders);
pTargetSection = &pSection[pNtHeaders->FileHeader.NumberOfSections - 1];
}
// 计算新导入表RVA
DWORD newImportRVA = pTargetSection->VirtualAddress + (blankOffset - pTargetSection->PointerToRawData);
DWORD newImportOffset = blankOffset;
// 1. 复制原导入表
DWORD oldImportOffset = importRVA - pImportSection->VirtualAddress + pImportSection->PointerToRawData;
memcpy((BYTE*)pBase + newImportOffset, (BYTE*)pBase + oldImportOffset, importSize);
// 2. 追加新的导入描述符
PIMAGE_IMPORT_DESCRIPTOR pNewImport = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)pBase + newImportOffset + importSize);
memset(pNewImport, 0, sizeof(IMAGE_IMPORT_DESCRIPTOR));
// 3. 在描述符后追加结束标记
PIMAGE_IMPORT_DESCRIPTOR pEnd = pNewImport + 1;
memset(pEnd, 0, sizeof(IMAGE_IMPORT_DESCRIPTOR));
// 4. 计算INT/IAT位置
DWORD thunkOffset = newImportOffset + newImportSize;
PDWORD_PTR pINT = (PDWORD_PTR)((BYTE*)pBase + thunkOffset);
PDWORD_PTR pIAT = pINT + 2; // INT后紧跟IAT
// 5. 设置INT/IAT内容
*pINT = 0; // 初始化为0
*(pINT + 1) = 0; // 结束标记
*pIAT = 0;
*(pIAT + 1) = 0;
// 6. 写入DLL名称
DWORD dllNameOffset = thunkOffset + 4 * sizeof(DWORD_PTR); // INT(8)+IAT(8)=16字节
strcpy((char*)pBase + dllNameOffset, pszDllName);
// 7. 写入函数名结构
DWORD funcNameOffset = dllNameOffset + dllNameLen;
*(WORD*)((BYTE*)pBase + funcNameOffset) = 0; // Hint=0
strcpy((char*)pBase + funcNameOffset + sizeof(WORD), pszFuncName);
// 8. 设置描述符字段
pNewImport->OriginalFirstThunk = newImportRVA + importSize; // INT的RVA
pNewImport->Name = newImportRVA + newImportSize; // DLL名称RVA
pNewImport->FirstThunk = newImportRVA + newImportSize + 8; // IAT的RVA (INT后8字节)
// 9. 设置函数地址指针
*pINT = newImportRVA + newImportSize + 16; // 函数名结构RVA (INT后16字节)
*pIAT = *pINT; // 初始时相同
// 10. 更新数据目录
pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress = newImportRVA;
pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size = newImportSize;
// 11. 更新节属性
pTargetSection->Characteristics |= IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE;
pTargetSection->Misc.VirtualSize = ALIGN_UP(pTargetSection->Misc.VirtualSize + totalSpace,
pNtHeaders->OptionalHeader.SectionAlignment);
// 清理
UnmapViewOfFile(pBase);
CloseHandle(hMapping);
CloseHandle(hFile);
return TRUE;
}
int main() {
if (InjectImportTable("D:\\code\\c++\\1.exe", "D:\\code\\c++\\TestDll.dll", "mySub")) {
printf("Import table injected successfully!\n");
} else {
printf("Injection failed!\n");
}
return 0;
}
|

运行上述代码成功,查看一下导入表

-
文件映射与PE头验证:
- 使用内存映射操作PE文件
- 验证DOS头和NT头有效性
-
导入表定位:
- 通过数据目录表获取原导入表RVA和大小
- 定位导入表所在节区
-
空间计算:
- 新导入表所需空间 = 原大小 + 20字节(新描述符)
- 总空间 = 新导入表大小 + 16字节(INT/IAT) + DLL名长度 + 函数名长度+3(Hint+字符串)
-
空白区域查找:
-
新导入表构建:
- 复制原导入表
- 追加新描述符和结束标记
- 构建INT/IAT双字指针数组
- 写入DLL名称和函数名结构
-
地址设置:
1
2
3
4
|
pNewImport->OriginalFirstThunk = INT_RVA;
pNewImport->Name = DLL_NAME_RVA;
pNewImport->FirstThunk = IAT_RVA;
*pINT = FUNC_NAME_RVA; // 指向IMAGE_IMPORT_BY_NAME
|
-
目录项更新:
- 更新导入表VirtualAddress指向新位置
- 设置新Size(原Size+20字节)
注意事项:
- 需要管理员权限操作文件
- 目标文件需有足够扩展空间
- 函数名结构包含2字节Hint+函数名字符串
- INT/IAT结构以NULL指针结束
- 添加节区可写属性(IMAGE_SCN_MEM_WRITE)
- 对齐处理使用PE文件对齐值
参考
手写PE结构解析工具