

详解 Windows DLL 远程注入的实现原理
深入剖析 Windows DLL 注入技术。本文从 DLL 的 PE 结构、导出表、导入表讲起,通过 C++ 代码演示本地加载与远程注入的完整流程,包括进程枚举、内存操作和利用 CreateRemoteThread 创建远程线程。
简单介绍DLL#
DLL (Dynamic-Link Library),即“动态链接库”,是一个编译后的代码和资源库,它允许多个可执行文件.exe
在运行时动态加载并共享其功能,类似于Linux的共享对象.so
。它的核心优势是运行时链接(Runtime linking),而非编译时将所有代码都静态链接到主程序中。
DLL文件和EXE文件一样,都使用PE文件格式。他们的主要区别在于PE头中的一个标志位,以及DLL文件一般不能独立运行,需要通过其他程序加载并运行。
导出表#
正常情况下DLL需要通过导出表来展示一个可供调用的函数列表,当一个程序想要调用DLL中的函数时,它就会通过这个导出表来找到函数的实际内存地址。
本例中方便起见不导出函数,直接通过DLL_PROCESS_ATTACH
来展示DLL被载入后的运行效果。
DLL本地注入#
这个 DLL 在被加载时,会利用其 DllMain
入口点弹出一个消息框。
#include <Windows.h>
#include <stdio.h>
BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) {
switch (dwReason) {
case DLL_PROCESS_ATTACH: {
MessageBoxA(NULL, "DLL Injected Successfully!", "Success", MB_OK);
break;
};
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
c加载器程序调用 LoadLibraryA
来加载 Dll.dll
。这个 API 会触发 Windows 加载器执行完整的 DLL 加载流程:解析 PE 头、映射段、处理导入、最后调用 DllMain。
#include <Windows.h>
#include <stdio.h>
#include <process.h>
int main() {
const char* dllPath = "mydll.dll";
printf("Attempting to load %s...\n", dllPath);
HMODULE hDll = LoadLibraryA(dllPath);
if (hDll == NULL) {
printf("Failed to load DLL. Error code: %d\n", GetLastError());
return 1;
}
printf("DLL loaded successfully. Handle: %p\n", hDll);
printf("Check for the message box!\n");
system("pause");
FreeLibrary(hDll);
printf("DLL unloaded.\n");
return 0;
}
cDLL远程注入#
远程注入的核心思想是:在目标进程的上下文中,强制其调用 LoadLibraryA 来加载我们指定的 DLL。由于进程地址空间的隔离性,这需要一系列精确的跨进程内存操作。
定位目标进程#
通过 CreateToolhelp32Snapshot ↗ API 遍历系统进程,根据进程名找到其 PID。
// 因为现代Windows默认使用Unicode编码,所以这里直接使用宽字符比较函数 _wcsicmp
DWORD FindProcessIdByName(const wchar_t* processName) {
PROCESSENTRY32W entry; // 明确使用宽字符版本
entry.dwSize = sizeof(PROCESSENTRY32W);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (Process32FirstW(snapshot, &entry)) { // 使用 Process32FirstW
do {
// 使用 _wcsicmp 进行不区分大小写的宽字符比较
if (_wcsicmp(entry.szExeFile, processName) == 0) {
CloseHandle(snapshot);
return entry.th32ProcessID;
}
} while (Process32NextW(snapshot, &entry)); // 使用 Process32NextW
}
CloseHandle(snapshot);
return 0;
}
c获取目标句柄#
调用 OpenProcess ↗获取一个拥有足够权限(PROCESS_ALL_ACCESS
)的句柄。
HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD, FALSE, pid);
if (hProcess == NULL) {
wprintf(L"Failed to open target process (Error: %d). Try running as Administrator.\n", GetLastError());
return 1;
}
c在目标进程中分配内存#
使用VirtualAllocEx ↗在目标进程的地址空间中申请一块足以保存DLL的内存空间。
LPVOID pRemoteMem = VirtualAllocEx(hProcess, NULL, dllPathSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pRemoteMem == NULL) {
wprintf(L"VirtualAllocEx failed. Error: %d\n", GetLastError());
CloseHandle(hProcess);
return 1;
}
plaintext写入DLL路径#
使用 WriteProcessMemory ↗ 将我们的 DLL 路径字符串写入到刚刚在远程进程中分配的内存里。
// 写入内存时,也要提供正确的字节大小
if (!WriteProcessMemory(hProcess, pRemoteMem, dllPath, dllPathSize, NULL)) {
wprintf(L"WriteProcessMemory failed. Error: %d\n", GetLastError());
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}
c查找 LoadLibraryW 地址#
LoadLibraryW
用于加载一个调用它的进程内部的 DLL。 由于目标是加载远程进程内部而非本地进程内部的 DLL,所以不能直接调用它。 相反,必须检索 LoadLibraryW
的地址并将其作为参数传递给进程中远程创建的线程,同时将 DLL 名称作为其参数。 这起作用是因为 LoadLibraryW
WinAPI 的地址在远程进程中和在本地进程中相同。 为了确定 WinAPI 的地址,需要使用 GetProcAddress
和 GetModuleHandle
。
// 必须使用 LoadLibraryW,而不是 LoadLibraryA
LPVOID pLoadLibraryW = (LPVOID)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW");
if (pLoadLibraryW == NULL) {
wprintf(L"GetProcAddress for LoadLibraryW failed. Error: %d\n", GetLastError());
CloseHandle(hProcess);
return 1;
}
c创建远程线程#
调用 CreateRemoteThread ↗,让它在目标进程中创建一个新线程。这个新线程的起始执行地址被设置为 LoadLibraryA 的地址,其接收的参数则被设置为我们刚刚写入的 DLL 路径的地址。
HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pLoadLibraryW, pRemoteMem, 0, NULL);
if (hRemoteThread == NULL) {
wprintf(L"CreateRemoteThread failed. Error: %d\n", GetLastError());
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}
plaintext完整代码#
#include <Windows.h>
#include <stdio.h>
#include <TlHelp32.h>
// 因为现代Windows默认使用Unicode编码,所以这里直接使用宽字符比较函数 _wcsicmp
DWORD FindProcessIdByName(const wchar_t* processName) {
PROCESSENTRY32W entry; // 明确使用宽字符版本
entry.dwSize = sizeof(PROCESSENTRY32W);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (Process32FirstW(snapshot, &entry)) { // 使用 Process32FirstW
do {
// 使用 _wcsicmp 进行不区分大小写的宽字符比较
if (_wcsicmp(entry.szExeFile, processName) == 0) {
CloseHandle(snapshot);
return entry.th32ProcessID;
}
} while (Process32NextW(snapshot, &entry)); // 使用 Process32NextW
}
CloseHandle(snapshot);
return 0;
}
int wmain() { // 使用 wmain 作为 Unicode程序的入口点
const wchar_t* targetProcessName = L"notepad.exe"; // 使用 L 前缀创建宽字符字符串
wchar_t dllPath[MAX_PATH];
// 使用 GetFullPathNameW
if (GetFullPathNameW(L"Dll.dll", MAX_PATH, dllPath, NULL) == 0) {
wprintf(L"Failed to get full path of DLL. Error: %d\n", GetLastError());
return 1;
}
wprintf(L"Targeting process: %s\n", targetProcessName);
wprintf(L"DLL to inject: %s\n", dllPath);
DWORD pid = FindProcessIdByName(targetProcessName);
if (pid == 0) {
wprintf(L"Could not find process. Is %s running?\n", targetProcessName);
return 1;
}
wprintf(L"Found PID: %d\n", pid);
HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD, FALSE, pid);
if (hProcess == NULL) {
wprintf(L"Failed to open target process (Error: %d). Try running as Administrator.\n", GetLastError());
return 1;
}
// 计算内存大小时,要乘以 sizeof(wchar_t)
size_t dllPathSize = (wcslen(dllPath) + 1) * sizeof(wchar_t);
LPVOID pRemoteMem = VirtualAllocEx(hProcess, NULL, dllPathSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pRemoteMem == NULL) {
wprintf(L"VirtualAllocEx failed. Error: %d\n", GetLastError());
CloseHandle(hProcess);
return 1;
}
// 写入内存时,也要提供正确的字节大小
if (!WriteProcessMemory(hProcess, pRemoteMem, dllPath, dllPathSize, NULL)) {
wprintf(L"WriteProcessMemory failed. Error: %d\n", GetLastError());
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}
// 必须使用 LoadLibraryW,而不是 LoadLibraryA
LPVOID pLoadLibraryW = (LPVOID)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW");
if (pLoadLibraryW == NULL) {
wprintf(L"GetProcAddress for LoadLibraryW failed. Error: %d\n", GetLastError());
CloseHandle(hProcess);
return 1;
}
HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pLoadLibraryW, pRemoteMem, 0, NULL);
if (hRemoteThread == NULL) {
wprintf(L"CreateRemoteThread failed. Error: %d\n", GetLastError());
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return 1;
}
wprintf(L"Injection successful. Waiting for remote thread to finish...\n");
WaitForSingleObject(hRemoteThread, INFINITE);
CloseHandle(hRemoteThread);
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
wprintf(L"Cleanup complete.\n");
return 0;
}
c