0、 前言
0.1 什么是DLL注入
DLL(动态链接库)注入是一种技术,通过将外部的 DLL 文件强行加载到目标进程的地址空间中,使得外部代码可以执行。这种技术常用于修改或扩展应用程序的行为,甚至用于恶意攻击。
0.2 DLL注入的应用场景
通过DLL注入可以改变程序原来的一些行为,比如安全方面可以阻止某些程序打开,游戏方面通过外挂进行游戏作弊等。通常应用在以下场景:
0.2.1. 调试和逆向工程
- 逆向工程:开发者可以使用 DLL 注入来注入自定义的 DLL,修改目标程序的行为。这常用于破解软件、分析恶意软件行为等。
- 调试工具:调试人员可以利用 DLL 注入在目标应用程序中添加日志记录、修改代码执行流程等,帮助分析和调试。
0.2.2 性能监控和分析
- 一些性能分析工具(如性能监视器、反病毒软件等)通过 DLL 注入来捕获目标进程的运行时数据,记录函数调用、内存分配等,从而提供有关程序性能的反馈。
0.2.3. 功能扩展和修改
- 功能扩展:一些第三方工具或游戏作弊程序利用 DLL 注入技术来修改应用程序的行为。例如,修改游戏中的某些参数、提高程序的功能、或者添加新功能。
- 破解保护机制:一些商业软件使用 DLL 注入来绕过版权保护和反作弊系统。
0.2.4. 恶意软件(病毒、木马)
- 恶意软件和病毒经常利用 DLL 注入来获取对目标程序的控制,执行不需要用户授权的恶意行为,如窃取密码、监视用户输入、截获敏感信息等。
0.2.5. 游戏作弊
- 游戏作弊程序常通过 DLL 注入修改游戏中的行为,例如修改玩家的血量、金钱,或者直接修改游戏逻辑,以达到不正当的游戏优势。
1、DLL注入的基本原理和过程
1.1 基本原理
DLL 注入的核心原理是让目标进程加载外部的 DLL 文件,通常通过一些系统函数来实现。注入后的 DLL 会被执行,其中的代码可以影响目标进程的行为。
我们编写程序时可以通过LoadLibrary这个函数来加载DLL,DLL注入也是基于此。想要在目标进程中强制加载待注入的DLL,核心就是在目标进程中调用LoadLibrary来加载待注入的DLL。
1.2 基本步骤
-
获取目标进程的进程 ID:首先,我们需要知道目标进程的进程 ID(PID)。这可以通过查找目标进程的名称或直接从操作系统的进程列表中获得。
-
打开目标进程:使用 Windows API 中的
OpenProcess
函数以适当的权限打开目标进程,通常需要PROCESS_ALL_ACCESS
权限。 -
分配内存:使用
VirtualAllocEx
在目标进程的地址空间中分配一块内存空间来存放 DLL 的路径。 -
写入 DLL 路径:使用
WriteProcessMemory
将 DLL 的文件路径写入到目标进程的内存中。 -
获取并调用
LoadLibrary
地址:通过GetProcAddress
获取LoadLibraryA
或LoadLibraryW
函数的地址,LoadLibrary
是用来加载 DLL 的标准函数。 -
创建远程线程:通过
CreateRemoteThread
在目标进程中创建一个线程来执行LoadLibrary
,并传入之前写入的 DLL 路径。这样,目标进程就会加载外部 DLL。 -
DLL 被加载并执行:一旦目标进程加载了 DLL,DLL 中的代码就会执行,从而实现对目标进程行为的修改或扩展。
2、DLL注入示例
要将DLL注入到目标进程,需要完成两件事:
- 编写待注入的DLL
- 编写注入程序
2.1 编写待注入DLL
编写待注入的DLL与实际需要完成的功能有关,这里只是为了演示注入DLL过程,编写一个最简单的DLL,被注入到进程时,弹出一个messagebox。
通过visual studio创建一个DLL项目,代码内容如下:
代码内容非常简单,就是待注入DLL被目标进程加载时,会有一个messagebox弹窗提时。
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"BOOL APIENTRY DllMain( HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:MessageBoxW(NULL,L"有DLL注入!",L"注入 提示",MB_OK | MB_ICONINFORMATION);case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:case DLL_PROCESS_DETACH:break;}return TRUE;
}
2.2 编写注入程序
要将待注入DLL注入的目标进程,需要按照1.2中的过程进行编码,完成每一个步骤即可。
注入程序的所有代码如下:
以注入到windows记事本程序(notepad.exe)为例,先通过FindNotepadPID()函数查找notepad进程的PID,然后进行注入。
需要注意的时,程序中待注入的DLL路径需要替换为自己实际的DLL路径。
// InjectDllMain.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//#include <windows.h>
#include <tlhelp32.h>
#include <iostream>DWORD FindNotepadPID() {HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);if (snapshot == INVALID_HANDLE_VALUE) return 0;PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };DWORD pid = 0;if (Process32First(snapshot, &pe)) {do {if (_wcsicmp(pe.szExeFile, L"notepad.exe") == 0) {pid = pe.th32ProcessID;break;}} while (Process32Next(snapshot, &pe));}CloseHandle(snapshot);return pid;
}bool InjectDLL(DWORD pid, const char* dllPath) {HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);if (!hProcess) return false;LPVOID pRemoteMem = VirtualAllocEx(hProcess, NULL, strlen(dllPath) + 1, MEM_COMMIT, PAGE_READWRITE);if (!pRemoteMem) {CloseHandle(hProcess);return false;}WriteProcessMemory(hProcess, pRemoteMem, dllPath, strlen(dllPath) + 1, NULL);HMODULE hKernel32 = GetModuleHandleA("Kernel32");LPTHREAD_START_ROUTINE loadLib = (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, "LoadLibraryA");HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, loadLib, pRemoteMem, 0, NULL);if (!hThread) {VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);CloseHandle(hProcess);return false;}WaitForSingleObject(hThread, INFINITE);VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);CloseHandle(hThread);CloseHandle(hProcess);return true;
}int main() {DWORD pid = FindNotepadPID();if (!pid) {std::cerr << "Notepad not running!" << std::endl;return 1;}char dllPath[MAX_PATH];GetCurrentDirectoryA(MAX_PATH, dllPath);strcat_s(dllPath, "\\InjectDll.dll");// 这里需要替换为实际的DLL路径if (InjectDLL(pid, "C:\\InjectMessagebox\\x64\\Release\\InjectMessagebox.dll")) {std::cout << "Injection successful!" << std::endl;}else {std::cerr << "Injection failed!" << std::endl;}return 0;
}
2.3 演示效果
编译完待注入DLL和注入程序之后,打开windows记事本,然后运行注入程序,演示效果如下:
2.4 结合程序的一些原理分析
注入程序中,最核心的核心函数是 CreateRemoteThread。这个函数干了两件事:
- 在目标进程中创建一个新线程
- 新线程调用LoardLibrary函数去加载待注入的DLL
问题1:为什么要创建一个新的线程来调用LoardLibrary函数,而不是直接调用LoardLibrary?
我个人理解是为了不影响目标进程原来的一些行为和功能。
问题2:CreateRemoteThread中参数为什么要传入pRemoteMem,而不是直接传入dllPath?
首先来看一下微软官方文档对这个函数CreateRemoteThread参数的解释:
HANDLE CreateRemoteThread(
[in] HANDLE hProcess,
[in] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in] LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out] LPDWORD lpThreadId
);
[in] hProcess
要在其中创建线程的进程句柄。 句柄必须具有 PROCESS_CREATE_THREAD、PROCESS_QUERY_INFORMATION、PROCESS_VM_OPERATION、PROCESS_VM_WRITE和 PROCESS_VM_READ 访问权限,并且在某些平台上没有这些权限可能会失败。 有关详细信息,请参阅 进程安全性和访问权限。
[in] lpThreadAttributes
指向 SECURITY_ATTRIBUTES 结构的指针,该结构指定新线程的安全描述符,并确定子进程是否可以继承返回的句柄。 如果 lpThreadAttributes 为 NULL,则线程将获取默认的安全描述符,并且无法继承句柄。 线程的默认安全描述符中的访问控制列表(ACL)来自创建者的主要令牌。
Windows XP:线程的默认安全描述符中的 ACL 来自创建者的主令牌或模拟令牌。 此行为随 SP2 和 Windows Server 2003 的 Windows XP 发生更改。
[in] dwStackSize
堆栈的初始大小(以字节为单位)。 系统将此值舍入到最近的页面。 如果此参数为 0(零),则新线程使用可执行文件的默认大小。 有关详细信息,请参阅 线程堆栈大小。
[in] lpStartAddress
指向由线程执行的 LPTHREAD_START_ROUTINE 类型的应用程序定义的函数的指针,表示远程进程中线程的起始地址。 函数必须存在于远程进程中。 有关详细信息,请参阅 ThreadProc。
[in] lpParameter
指向要传递给线程函数的变量的指针。
[in] dwCreationFlags
控制线程创建的标志。
【参考链接】
CreateRemoteThread 函数 (processthreadsapi.h) - Win32 apps | Microsoft Learn
我们重点关注 lpStartAddress以及lpParameter
这两个参数, lpStartAddress
是线程要调用的函数(即LoadLibraryA)的起始地址,lpParameter
是调用函数的参数(即待加载的库的路径)。
而dllPath不就是动态库的路径吗?为什么参数是pRemoteMem?
这是因为dllPath是动态库的路径,只是在注入程序中(即InjectDllMain),而目标程序(notepad)根本不认识dllPath。我们的目的是要目标进程调用LoadLibraryA, 所以我们需要在目标进程中开辟一块内存空间pRemoteMem,在这块内存空间写入要注入的DLL路径。
问题3:怎么知道目标进程中LoadLibraryA?
注意到,我们是在注入程序中或取的LoadLibraryA的地址,但是实际上我们应该获取目标进程中LoadLibraryA的地址才对。
LoadLibraryA这个函数是在Kernel32.dll这个核心DLL里的,而这个DLL很特殊,不管对于哪个进程,Windows总是把它加载到相同的地址上去。因此你的进程中LoadLibraryA的地址和目标进程中LoadLibraryA的地址是相同的(其实,这个DLL里的所有函数都是如此)。
【参考文章】
DLL注入