导入表注入

在动态链接库一章中提到 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:支持 Execute 和 Read。

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