Windows 中的三种常用 DLL 注入技术
目录
本文中代码仓库地址:
https://gitee.com/langshanglibie/dll-injection
一、前言——DLL 注入技术的用途
在 Windows 中,每个进程都有自己私有的虚拟地址空间。所以,一个进程没法访问另一个进程的内存。
但是,很多时候我们还是需要跨越进程的边界,来访问另一个进程的地址空间,比如:
- 获取其他进程的信息,如加载了哪些 DLL
- 对其他进程的某些操作进行拦截
- 从另一个进程创建的窗口来派生子类窗口,比如附着在资源管理器上的一些小插件等
- 藏身于别的进程,以达到隐藏自己的目的
- 假借其他进程之名做某些事情
二、DLL 注入基础
2.1 进程虚拟地址空间
32位进程:4GB,0x00000000~0xFFFFFFFF
64位进程:16EB,0x00000000'00000000~0xFFFFFFFF'FFFFFFFF
因为每个进程都有自己专有的虚拟地址空间,当进程中的各线程运行时,他们只能访问自己进程的内存。
线程既看不到属于其他进程的内存,也无法访问他们,更不能修改他们。
进程 A 可以在位于 0x12345678 地址处存储一个数据,进程 B 也可以在自己的地址空间中相同地址 0x12345678 处存储一个数据。
当进程 A 中的线程访问位于地址 0x12345678 处的内存时,它访问的是进程 A 的数据。
因为每个进程都有自己的地址空间,一个应用程序破坏另外一个应用程序的可能性就非常小,从而使得整个系统更加稳固。
2.2 读写其他进程的内存
Windows 提供了一组 API 读写其他进程的内存,甚至在其他进程的内存中分配内存。
OpenProcess | 打开一个已存在的进程对象,并返回进程的句柄。 |
VirtualAllocEx | 在指定进程的虚拟地址空间中分配内存。 |
WriteProcessMemory | 将当前进程中的地址空间中的数据,拷贝至指定进程的地址空间中。 |
ReadProcessMemory | 将指定进程的指定范围内的地址空间内的数据,拷贝至当前进程的地址空间中。 |
HANDLE OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId);
欲获取的权限,一般传递 PROCESS_ALL_ACCESS 获取所有权限。
LPVOID VirtualAllocEx(HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);
MEM_RESERVE:预订进程的虚拟地址空间,而不调拨任何物理存储器。
MEM_COMMIT:预订进程的虚拟地址空间,并且从物理内存或磁盘上的页交换文件中调拨物理存储器。
经常是两个一起组合使用 MEM_RESERVE | MEM_COMMIT,在一步中完成预订地址空间和调拨物理存储器。
BOOL WriteProcessMemory(HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T * lpNumberOfBytesWritten);
欲向其中写入数据的的目标进程的句柄,一般为 OpenProcess 函数的返回值。
BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T * lpNumberOfBytesRead);
欲从其中读取数据的的目标进程的句柄,一般为 OpenProcess 函数的返回值。
2.2.1 实践
向其他进程写入"Hello, world!",再修改掉,再读取修改后的内容。
2.2.2 主要代码
const DWORD dwProcessId = GetProcessIdByName(processName);
const char buffer[] = "Hello, world!"; // 准备向其他进程中写入的字符串
const SIZE_T bufferSize = sizeof(buffer);
pAddress = WriteProcessMemory(dwProcessId, (const LPVOID)(buffer), bufferSize);
std::cout<< pAddress << std::endl;
SIZE_T readBbufferSize = sizeof(readBbuffer);
const bool bSuccess = ReadProcessMemory(dwProcessId, pAddress,
LPVOID WriteProcessMemory(DWORD dwProcessId, const LPVOID pBuffer, SIZE_T bufferSize)
if (pBuffer == nullptr || bufferSize == 0)
const HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
const LPVOID pAddress = ::VirtualAllocEx(hProcess,
SIZE_T numberOfBytesWritten = 0;
const BOOL bSuccess = ::WriteProcessMemory(hProcess,
if (!bSuccess || numberOfBytesWritten != bufferSize)
::VirtualFreeEx(hProcess, pAddress, 0, MEM_RELEASE);
bool ReadProcessMemory(DWORD dwProcessId, const LPVOID pAddress, LPVOID pBuffer, SIZE_T bufferSize)
if (pBuffer == nullptr || bufferSize == 0)
const HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
const BOOL bSuccess = ::ReadProcessMemory(hProcess,
if (!bSuccess || numberOfBytesRead != bufferSize)
2.3 LoadLibraryW 函数
进行 APC 注入、远程线程注入时,都需要传递一个函数地址到目标进程中,分别为 APC 函数和远程线程函数。
2. 调用 VirtualAllocEx 在目标进程中分配一段能够容纳得下这个函数代码的内存,假如返回地址为 0x1234567。
3. 然后调用 WriteProcessMemory 把这个函数代码拷贝到目标进程中以 0x1234567 为起始的内存中 。
0x1234567 这个地址就是后面进行 APC 注入、远程线程注入时传递的函数地址。
幸运的是,Windows 系统 kernel32.dll 中有 2 个导出函数 LoadLibraryA、LoadLibraryW。
HMODULE WINAPI LoadLibraryW(LPCWSTR lpLibFileName);
typedef VOID (NTAPI *PAPCFUNC)(ULONG_PTR Parameter);
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(LPVOID lpThreadParameter);
我们发现,这两个函数原型都可以兼容、接受 LoadLibraryW 函数。
并且,LoadLibraryW 在所有相同位数(32位、64位)的进程中地址一致。
所以,综上所述,我们不用另外实现 APC 函数和远程线程函数,直接将在本进程中通过 GetProcAddress 获取到的 LoadLibraryW 函数地址传递过去即可,省了不少工夫。
三、APC 注入
3.1 何为 APC
一个 APC(asynchronous procedure call,异步过程调用)是指一个在线程的上下文中异步运行的函数。
当一个用户模式的 APC 被插入到线程的队列时,该线程不会立即调用该 APC 函数,直到线程处于警戒状态(alertable state)。
当一个线程调用以下等待类函数时(如果是前四个函数, 需要传递 TRUE 给 bAlertable 参数),会进入该状态。
- DWORD SleepEx( [in] DWORD dwMilliseconds, [in] BOOL bAlertable);
- DWORD SignalObjectAndWait( [in] HANDLE hObjectToSignal, [in] HANDLE hObjectToWaitOn, [in] DWORD dwMilliseconds, [in] BOOL bAlertable);
- DWORD WaitForMultipleObjectsEx( [in] DWORD nCount, [in] const HANDLE *lpHandles, [in] BOOL bWaitAll, [in] DWORD dwMilliseconds, [in] BOOL bAlertable);
- DWORD WaitForSingleObjectEx( [in] HANDLE hHandle, [in] DWORD dwMilliseconds, [in] BOOL bAlertable);
- DWORD MsgWaitForMultipleObjectsEx( [in] DWORD nCount, [in] const HANDLE *pHandles, [in] DWORD dwMilliseconds, [in] DWORD dwWakeMask, [in] DWORD dwFlags);
进入警戒状态后,线程会按照先进先出的顺序,调用 APC 函数。
之前调用的等待类函数会返回 WAIT_IO_COMPLETION。
应用程序通过调用 QueueUserAPC 函数将 APC 添加到线程的队列。
调用 QueueUserAPC 时,需要传递 APC 函数的地址。
3.2 关键 API
DWORD QueueUserAPC(PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData);
3.3 实践
使用 APC 向指定进程的线程注入 TestDll.dll,然后显示被注入进程加载的所有模块及其基地址。
3.4 进程被注入 DLL 判断
3.5 主要代码
bool ApcInjectDll(const TCHAR *pszProcessName, const TCHAR* pszDllFileName)
const DWORD dwProcessId = GetProcessIdByName(pszProcessName);
LOG("GetProcessIdByName failed");
DWORD* pThreadIdArray = nullptr;
#define FREE_THREAD_ID_ARRAY if (pThreadIdArray != nullptr) delete[] pThreadIdArray;
if (!GetAllThreadIdByProcessId(dwProcessId, &pThreadIdArray, &dwThreadIdLength))
LOG("GetAllThreadIdByProcessId failed");
// 在注入进程中申请内存、向申请的内存中写入欲注入的 DLL 的全路径
const SIZE_T dwDllPathLen = (::lstrlen(pszDllFileName) + 1) * sizeof(pszDllFileName[0]);
const LPVOID pDllPathAddress = WriteProcessMemory(dwProcessId,
if (pDllPathAddress == nullptr)
LOG("WriteProcessMemory failed");
// 获取 kernel32.dll 中 LoadLibraryW 函数的地址,注意不是 LoadLibrary !
PVOID pLoadLibraryAFunc = ::GetProcAddress(::GetModuleHandle(_T("kernel32.dll")), "LoadLibraryW");
if (pLoadLibraryAFunc == nullptr)
LOG("GetProcessAddress failed");
for (DWORD i = 0; i < dwThreadIdLength; ++i)
const HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdArray[i]);
::QueueUserAPC((PAPCFUNC)pLoadLibraryAFunc, hThread, (ULONG_PTR)pDllPathAddress);
四、远程线程注入
4.1 何为远程线程
远程线程是指某个进程在其他进程的虚拟地址空间中创建并运行的线程。
4.2 关键 API
LPSECURITY_ATTRIBUTES lpThreadAttributes,
LPTHREAD_START_ROUTINE lpStartAddress,
除了多了第一个参数 hProcess 外,其余参数和常用的在本进程创建线程的函数 CreateThread 一模一样。
4.3 实践
在指定进程中创建远程线程,注入 TestDll.dll,然后显示被注入进程加载的所有模块及其基地址。
4.4 主要代码
bool CreateRemoteThreadInjectDll(const TCHAR* pszProcessName, const TCHAR* pszDllFileName)
const DWORD dwProcessId = GetProcessIdByName(pszProcessName);
LOG("GetProcessIdByNamefailed");
const HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
const SIZE_T dwDllPathLen = (::lstrlen(pszDllFileName) + 1) * sizeof(pszDllFileName[0]);
const LPVOID pDllPathAddress = WriteProcessMemory(hProcess,
if (pDllPathAddress == nullptr)
LOG("WriteProcessMemoryfailed");
const FARPROC pFuncProcAddr = ::GetProcAddress(::GetModuleHandle(_T("kernel32.dll")), "LoadLibraryW");
LOG("GetProcAddressLoadLibraryW failed");
// 使用 CreateRemoteThread 创建远程线程,创建完立即执行,实现 DLL 注入
const HANDLE hRemoteThread = ::CreateRemoteThread(hProcess,
(LPTHREAD_START_ROUTINE)pFuncProcAddr,
LOG("CreateRemoteThreadfailed");
五、Windows 钩子
5.1 何为钩子
钩子(Hook)是操作系统提供的一种机制,应用程序通过它可以监视各种事件,例如鼠标操作、键盘操作、窗口的创建、关闭等。
钩子往往会减慢系统的速度,因为它们延长了系统对每个事件的处理流程。所以应该只在必要时安装挂钩,用完尽快将其移除。
5.2 钩子链
Windows 系统支持很多不同类型的钩子。每种类型监视系统消息处理机制的不同方面。例如,应用程序可以使用 WH_MOUSE 钩子来监视鼠标消息。
钩子链是一个包含应用程序定义的回调函数的列表,这个回调函数就是钩子过程。
当出现与某种类型的钩子相关联的消息时,系统会将该消息一个接一个地传递给该类型的钩子链中的每一个钩子过程。
5.3 钩子过程
为了使用钩子,开发人员需要提供了一个钩子过程,并使用 SetWindowsHookEx 函数将其安装到该类型的钩子链中。钩子过程必须具有以下函数原型:
LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam)
return CallNextHookEx(NULL, nCode, wParam, lParam);
nCode 参数是钩子过程用来确定要执行的操作的钩子代码,它的值取决于钩子的类型。每种类型的钩子都有自己的钩子代码集合。
wParam 和 lParam 参数的值取决于 nCode,但它们通常包含与被发送的消息相关的信息。
SetWindowsHookEx 函数始终在钩子链的开头安装钩子过程。
当发生某种类型的钩子监视的事件时,系统先调用该类型钩子链中开头的钩子过程。
链中的每个钩子过程决定是否将事件传递给下一个过程。钩子过程通过调用 CallNextHookEx 函数将事件传递给下一个钩子过程。
请注意,某些类型的钩子的钩子过程只能监视消息,系统会将消息传递给每个钩子过程,而不管上一个钩子过程是否调用了 CallNextHookEx。
某些类型的钩子的钩子过程只能监视消息,不能修改消息,或阻止系统将它们传到下一个钩子过程或目标窗口,比如 WH_CALLWNDPROC。
全局钩子的钩子过程可以在任何应用程序中调用,因此该钩子过程必须位于独立的 DLL 中。
线程钩子的钩子过程仅在所监视的线程的中调用。如果应用程序为自己的一个线程安装了钩子过程,钩子过程可以与应用程序的其余代码位于同一模块中,也可以位于独立的 DLL 中。但是,如果应用程序为其它应用程序的某个线程安装了钩子过程,则该过程必须位于独立 DLL 中。
5.4 钩子类型
每种类型的钩子都使应用程序能够监视系统消息处理机制的不同方面。
- WH_CALLWNDPROC 和 WH_CALLWNDPROCRET
- WH_CBT
- WH_DEBUG
- WH_FOREGROUNDIDLE
- WH_GETMESSAGE
- WH_JOURNALPLAYBACK
- WH_JOURNALRECORD
- WH_KEYBOARD_LL
- WH_KEYBOARD
- WH_MOUSE_LL
- WH_MOUSE
- WH_MSGFILTER 和 WH_SYSMSGFILTER
- WH_SHELL
WH_CALLWNDPROC & WH_CALLWNDPROCRET
系统在将消息传递给接收窗口的窗口过程之前调用 WH_CALLWNDPROC 钩子过程,并在窗口过程处理完消息之后调用 WH_CALLWNDPROCRET 钩子过程。
比如:SendMessage(hWnd, WM_CLOSE, 0, 0);
当其它线程使用 GetMessage 或 PeekMessage 函数取回线程消息队列中的消息之后,但是在开始处理之前,系统会将这个消息传给 WH_GETMESSAGE 钩子。
比如:PostMessage(hWnd, WM_CLOSE, 0, 0)
VS 中的 Spy++ 工具能够监视窗口消息,就是安装了上面三个钩子。
罗技项目中使用了这两个钩子,只要用户操作了鼠标或键盘,就停止语音识别。
在系统调用任何其他类型钩子的钩子过程之前,系统都会先调用 WH_DEBUG 类型的钩子过程。
可以使用这个钩子来决定是否允许系统调用其他类型钩子的钩子过程。
六、全局钩子注入
Windows 中很多线程都有消息队列,都需要调用 GetMessage 或 PeekMessage 从消息队列中取消息,
所以我们选用 WH_GETMESSAGE 钩子进行 DLL 注入。
6.1 关键 API
安装某种类型的钩子 | |
HHOOK SetWindowsHookEx(int idHook, HOOKPROC lpfn, HINSTANCE hmod, DWORD dwThreadId);
指向了一个动态链接的句柄,该动态连接库包含了参数 lpfn 所指向的钩子过程。
若参数 dwThreadId 指向的线程由当前进程创建,并且相应的钩子过程定义于当前进程中,则参数 hMod 必须被设置为 NULL。
如果指向了一个线程 ID,则安装的钩子只监视此线程。若此参数值为 0,则监视系统中所有线程。
BOOL UnhookWindowsHookEx(HHOOK hhk);
欲卸载的钩子的句柄,即之前调用 SetWindowsHookEx 函数的返回值。
LRESULT CallNextHookEx(HHOOK hhk, int nCode, WPARAM wParam, LPARAM lParam);
6.2 实践
安装 WH_GETMESSAGE 钩子监控指定进程,在钩子过程第一次被调用时,显示被注入进程加载的所有模块及其基地址。
6.3 主要代码
HMODULE g_hDllModule = nullptr;
#pragma data_seg("shared_data")
HHOOK g_hHook = nullptr; // 安装的钩子的句柄
DWORD g_hookProcessId = 0; // Hook 目标进程 ID
#pragma comment(linker, "/SECTION:shared_data,RWS")
BOOL APIENTRY DllMain( HMODULE hModule,
// 若不是目标进程,则返回 FALSE,使得其不用加载注入 DLL,消除对其它进程的干扰。
if (g_hookProcessId != 0&& ::GetCurrentProcessId() != g_hookProcessId)
static LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
if (::GetCurrentProcessId() == g_hookProcessId)
return ::CallNextHookEx(g_hHook, code, wParam, lParam);
bool InstallGlobalHook(DWORD hookProcessId)
g_hookProcessId = hookProcessId;
g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0);
::UnhookWindowsHookEx(g_hHook);
七、总结
通过上述三种方式,我们均可实现将一个 DLL 注入到另一个进程的地址空间。
一旦 DLL 代码进入另一个地址空间,那么我们就可以在那个进程中随心所欲,肆意妄为了。这听上去够吓人的,因此在真的打算这样做之前,请务必慎重考虑。