导入表注入

在动态链接库一章中提到 DllMain,这里再回顾一次:

  • 当 DLL 被加载进 4GB 空间时,会调用一次 DllMain(入口方法)。
  • 当程序执行完毕,将 DLL 从 4GB 空间卸载时,也会调用一次 DllMain

注入的本质是想方设法将自己的 DLL 投放到其他进程的 4GB 空间中。而在前面讲解的导入表一章中提到:

  • 只有隐式调用(静态使用)时,DLL 的名字和相关函数才会出现在调用者 PE 文件的导入表中
  • 显式调用(动态使用)时,被调用的 DLL 名和相关函数不会出现在调用者的导入表中。

换句话说:

  • 写入导入表中的 DLL 必定是被隐式调用的(静态使用)。
  • 如果将毫不相关的 DLL 写入某个 PE 文件的完整导入表中,那么打开这个 PE 文件时,该 DLL 会被直接加载(操作系统的规则)。

导入表注入的原理与过程

原理

.exe 文件被加载时,系统会根据其导入表信息加载需要的 DLL。导入表注入的原理就是修改 .exe 的导入表,将自己的 DLL 添加进去。这样,运行 .exe 时,系统会加载该 DLL 并执行其 DllMain 中的代码,从而实现注入目的。

实现步骤

  1. 移动导入表到新增节
    通常原导入表后面没有足够空间添加新表,因此需要将整个旧导入表移动到新增节中。
  2. 增加新的导入表
    在新增节中,旧导入表后增加一张新的导入表及其附表。

以下是新增内容的结构:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_IMPORT_DESCRIPTOR {	
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

新增导入表需要完整填写相关内容,例如:

  • OriginalFirstThunk 指向 DLL 的 INT 表的 RVA。
  • Name 指向 DLL 名的 RVA。
  • FirstThunk 指向 DLL 的 IAT 表的 RVA。

image-20250103224408543

INT、IAT 表的填充规则

  1. INT 表和 IAT 表 至少包含一个导入函数;
  2. 第二个函数项必须为 0;
  3. 确保表结构正确且完整,否则操作系统不会加载该 DLL。

新增节结构如下:

内容 长度 描述
IMAGE_IMPORT_BY_NAME 结构 30 字节 填写导出函数名称字符串
DLL 名称 30 字节 填写 DLL 名字符串
INT 表(OriginalFirstThunk) 8 字节 指向 IMAGE_IMPORT_BY_NAME
IAT 表(FirstThunk) 8 字节 指向 IMAGE_IMPORT_BY_NAME
原导入表 SizeOfImport 移动后的旧导入表内容
新增导入表 20 字节 包括 RVA 和其他信息

DLL 示例

为了测试注入,我们需要准备一个简单的 DLL。
以下是 MyDll.h 的头文件:

1
2
3
void Init();  
void Destroy();
extern "C" _declspec(dllexport) void ExportFunction();

对应的实现代码 MyDll.cpp 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <windows.h>

void Init() {
MessageBox(0, "Init", "Init", MB_OK);
}

void Destroy() {
MessageBox(0, "Destroy", "Destroy", MB_OK);
}

void ExportFunction() {
MessageBox(0, "ExportFunction", "ExportFunction", MB_OK);
}

DLL 的 DllMain 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "MyDll.h"

BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
Init();
break;
case DLL_PROCESS_DETACH:
Destroy();
break;
}
return TRUE;
}

导入表注入代码

以下是完成注入的关键代码及详细注释:

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
#include "Currency.h"
#include "windows.h"
#include "stdio.h"

VOID h331() {
char FilePath[] = "CrackHead.exe";
char CopyFilePath[] = "CrackHeadcopy.exe";
LPVOID pFileBuffer = NULL;
LPVOID* ppFileBuffer = &pFileBuffer;
LPVOID pNewFileBuffer = NULL;
LPVOID* ppNewFileBuffer = &pNewFileBuffer;

PIMAGE_DOS_HEADER pDos = 0;
PIMAGE_NT_HEADERS32 pNts = 0;
PIMAGE_DATA_DIRECTORY pDir = 0;
PIMAGE_IMPORT_DESCRIPTOR pImportTable = 0;
PIMAGE_SECTION_HEADER pSection = 0;
IMAGE_IMPORT_DESCRIPTOR NewImportTable = { 0 };

int NumOfImport = 0;
int SizeOfImport = 0;

DWORD SizeOfNewFileBuffer = 0;
DWORD SizeOfFileBuffer = ReadPEFile(FilePath, ppFileBuffer);

if (!SizeOfFileBuffer) {
printf("文件读取失败\n");
return;
}

pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
pNts = (PIMAGE_NT_HEADERS32)((DWORD)pFileBuffer + pDos->e_lfanew);
pDir = &pNts->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + RVA2FOA(pFileBuffer, pDir->VirtualAddress));

while (pImportTable->OriginalFirstThunk && pImportTable->Name) {
NumOfImport++;
pImportTable++;
}
SizeOfImport = NumOfImport * sizeof(IMAGE_IMPORT_DESCRIPTOR);

SizeOfNewFileBuffer = AddNewSection(pFileBuffer, SizeOfFileBuffer, SizeOfImport + 256, ppNewFileBuffer);
free(pFileBuffer);

pDos = (PIMAGE_DOS_HEADER)pNewFileBuffer;
pNts = (PIMAGE_NT_HEADERS32)((DWORD)pNewFileBuffer + pDos->e_lfanew);
pDir = &pNts->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pNewFileBuffer + RVA2FOA(pNewFileBuffer, pDir->VirtualAddress));
pSection = IMAGE_FIRST_SECTION(pNts);

for (int i = 1; i < pNts->FileHeader.NumberOfSections; i++) {
pSection++;
}

memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 2), "ExportFunction", sizeof("ExportFunction"));
memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30), "InjectDll.dll", sizeof("InjectDll.dll"));

memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30), &pSection->VirtualAddress, 4);
memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30 + 8), &pSection->VirtualAddress, 4);

memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30 + 8 + 8), pImportTable, SizeOfImport);
pDir->VirtualAddress = pSection->VirtualAddress + 30 + 30 + 8 + 8;
pDir->Size += sizeof(IMAGE_IMPORT_DESCRIPTOR);

NewImportTable.OriginalFirstThunk = pSection->VirtualAddress + 30 + 30;
NewImportTable.FirstThunk = pSection->VirtualAddress + 30 + 30 + 8;
NewImportTable.Name = pSection->VirtualAddress + 30;
memcpy((PVOID)((DWORD)pNewFileBuffer + pSection->PointerToRawData + 30 + 30 + 8 + 8 + SizeOfImport), &NewImportTable, sizeof(IMAGE_IMPORT_DESCRIPTOR));

MemeryToFile(pNewFileBuffer, SizeOfNewFileBuffer, CopyFilePath);
free(pNewFileBuffer);
}

注意事项

  • 新增节的属性
    新增节的 Characteristics 必须包含 C0000040

  • C0000000:支持 ExecuteRead

  • 0040:支持 Write

  • 填充对齐
    新增节的大小必须是文件对齐的倍数。

  • 多次测试
    确保注入的 DLL 能正常运行,避免修改后 PE 文件崩溃。

Work

示例代码

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
#include "PE.h"


bool InsertDllByImportTab(
IN LPVOID pe,
IN DWORD index = -1
) {
PIMAGE_DOS_HEADER pDosHeader = nullptr;
PIMAGE_FILE_HEADER pFileHeader = nullptr;
PIMAGE_OPTIONAL_HEADER pOptionalHeader = nullptr;
PIMAGE_SECTION_HEADER* pSectionHeaderArr = nullptr;
AnlyzePE(pe, pDosHeader, pFileHeader, pOptionalHeader, pSectionHeaderArr);

if (index == -1)
{
index = pFileHeader->NumberOfSections - 1;
}

//获取导入表
PIMAGE_IMPORT_DESCRIPTOR importTable = (PIMAGE_IMPORT_DESCRIPTOR)((PCHAR)pe + RvaToFoa(pe, pOptionalHeader->DataDirectory[1].VirtualAddress));
DWORD importTableNumber = (pOptionalHeader->DataDirectory[1].Size - 0x14) / 0x14; //导入dll的数量
/*while (!StructIsNull(importTable, sizeof(*importTable)))
{
importTableNumber++;
importTable = (PIMAGE_IMPORT_DESCRIPTOR)((PCHAR)importTable + 0x14);
}
importTable = (PIMAGE_IMPORT_DESCRIPTOR)((PCHAR)pe + RvaToFoa(pe, pOptionalHeader->DataDirectory[1].VirtualAddress));*/

//获取导入表 + 新加结构的大小(这里没有写入计算字符串的大小,可以在里面自行添加判断,因为我是新加了一个节区,所以就不判断了)
size_t importTableSize = (sizeof(*importTable) * importTableNumber) + 0x10 + sizeof(PIMAGE_IMPORT_BY_NAME);
if (pSectionHeaderArr[index]->SizeOfRawData - pSectionHeaderArr[index]->Misc.VirtualSize >= importTableSize)
{
//拷贝导入表
memcpy_s(
(PCHAR)pe + pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize,
(sizeof(*importTable) * importTableNumber),
(PCHAR)importTable,
(sizeof(*importTable) * importTableNumber)
);
//修改导入表的偏移
pOptionalHeader->DataDirectory[1].VirtualAddress = FoaToRva(pe, pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize);
//修改节区的真实数据大小
pSectionHeaderArr[index]->Misc.VirtualSize += sizeof(*importTable) * importTableNumber;

//设置注入DLL的结构
memset((PCHAR)pe + pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize, 0, sizeof(IMAGE_IMPORT_DESCRIPTOR));
//指针指向节区中注入DLL结构的开始地址
PIMAGE_IMPORT_DESCRIPTOR insertDllTab = (PIMAGE_IMPORT_DESCRIPTOR)((PCHAR)pe + pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize);
//修改节区的真实数据大小
pSectionHeaderArr[index]->Misc.VirtualSize += sizeof(IMAGE_IMPORT_DESCRIPTOR) * 2;

//注入DLL的名称
PCHAR insertDllName = (PCHAR)"Inject.dll";
//将字符串拷贝到节区中
memcpy_s(
(PCHAR)pe + pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize,
strlen(insertDllName) + 1,
insertDllName,
strlen(insertDllName) + 1
);
//设置注入DLL结构中的DLL名称在内存中的偏移
insertDllTab->Name = FoaToRva(pe, pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize);
//修改节区的真实数据大小
pSectionHeaderArr[index]->Misc.VirtualSize += strlen(insertDllName) + 1;

//设置INT表
memset((PCHAR)pe + pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize, 0, 8);
//指针指向节区中INT表数据的开始地址
PDWORD intTab = (PDWORD)((PCHAR)pe + pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize);
//设置注入DLL结构的INT表在内存中的偏移
insertDllTab->OriginalFirstThunk = FoaToRva(pe, pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize);
//修改节区的真实数据大小
pSectionHeaderArr[index]->Misc.VirtualSize += 8;

//设置INT表
memset((PCHAR)pe + pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize, 0, 8);
//指针指向节区中IAT表数据的开始地址
PDWORD iatTab = (PDWORD)((PCHAR)pe + pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize);
//设置注入DLL结构中IAT表在内存中的偏移
insertDllTab->FirstThunk = FoaToRva(pe, pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize);
//修改节区的真实数据大小
pSectionHeaderArr[index]->Misc.VirtualSize += 8;

//设置函数名称结构
memset((PCHAR)pe + pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize, 0, sizeof(IMAGE_IMPORT_BY_NAME));
//指向指向节区中PIMAGE_IMPORT_BY_NAME结构的开始地址
PIMAGE_IMPORT_BY_NAME ibn = (PIMAGE_IMPORT_BY_NAME)((PCHAR)pe + pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize);
//仅供参考数据,写什么无所谓
ibn->Hint = 0;
//函数名称
const char* ibnName = "Load";
//将函数名称拷贝到节区中(注意:这里写入的起始地址就是PIMAGE_IMPORT_BY_NAME->Name在节区中的起始地址)
memcpy_s(
(PCHAR)pe + pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize + 0x2,
strlen(ibnName) + 1,
ibnName,
strlen(ibnName) + 1
);
//设置INT表中数据
*intTab = FoaToRva(pe, pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize);
//设置IAT表中数据
*iatTab = FoaToRva(pe, pSectionHeaderArr[index]->PointerToRawData + pSectionHeaderArr[index]->Misc.VirtualSize);
//修改节区真实数据大小(+2 = Hint的大小)
pSectionHeaderArr[index]->Misc.VirtualSize += strlen(ibnName) + 1 + 2;

//修改导入表的大小
pOptionalHeader->DataDirectory[1].Size += 0x14;
}
free(pSectionHeaderArr);
return true;
}



int main()
{
LPVOID pe = ReadPE(R"(D:\source\repos\00\Debug\00.exe)");
if (pe && CreateSection(pe, 0x1000))
{
InsertDllByImportTab(pe);
WriteMyFilePeData(pe, R"(D:\source\repos\00\Debug\00.exe)");
free(pe);
}
return 0;
}