RemoteLib - Win9x & NT 平台的 DLL 注入






4.91/5 (33投票s)
2005年1月5日
5分钟阅读

172863

7419
一个在 Win9x 和 NT 平台下都可用的 DLL 注入库。
动机
本文的灵感来自于 Robert Kuster 的《三种将代码注入其他进程的方法》。起初,我正在寻找一些代码片段,以便能够轻松地将我自己的 DLL 注入到远程运行的进程中。于是我在 CP 上搜索,找到了 Robert Kuster 的文章;我不得不说,那确实是一篇非常出色的文章,但不幸的是,这三种解决方案都无法在我的 Windows 98(第二版)上运行。理论上,“第一种方法”应该适用于所有 Windows 平台,但它没有(这就是为什么包括我在内的一些程序员如此讨厌 Win9x)。修复其实非常简单,但由于 Robert Kuster 的代码更像是一个技术教程,而不是一个封装好的可重用库,所以我想为什么不为未来的方便使用而写一个呢。
引言
虽然 DLL 注入在 NT 平台上极其容易,但 Win9x 用户就没那么幸运了,因为 Win9x 不支持 ::CreateRemoteThread
。当然还有其他解决方案,例如 ::SetThreadContext
结合 ::CreateProcess
就可以完成这项工作,但将代码注入到已经运行的进程中会有点困难,因为在 Win9x 上暂停运行中的线程相对复杂。
另一个简单的解决方案是使用 ::SetWindowsHookEx
,正如 Robert Kuster 在他的文章中解释的,线程挂钩(与全局挂钩相反)一个窗口会导致整个 DLL(挂钩过程所在的位置)被映射到目标窗口的创建者进程中;这给了我们一个机会在目标进程的虚拟空间内做任何“脏活”,包括但不限于调用 ::LoadLibrary
或 ::FreeLibrary
来将第三方 DLL 映射/取消映射到/从目标进程,即 DLL 注入。
使用 Windows 钩子的缺点是这种技术要求远程进程至少有一个有效窗口,这意味着您不能将 DLL 注入到无窗口应用程序或服务中。这就是为什么我为 Win9x 和 NT 分别提供了两组加载/卸载函数;Win9x 的函数要求目标进程有一个窗口,而 NT 的则不需要。但是,如果您想在 Win9x 上将 DLL 注入到正在运行的无窗口进程中,本文可能无法提供帮助。
它是如何工作的
本文附带的源代码是一个 Win32 DLL 项目。编译它(或直接下载“DLL 二进制文件”包),您将获得 RemoteLib.dll。此 DLL 充当目标进程和您自己的 DLL 之间的“中介”或“桥梁”。它基本上以以下方式处理代码注入:
- RemoteLib.dll 将自己注入到目标进程中。根据您使用的是 Win9x 还是 NT,通过使用钩子或创建远程线程。
- 如果步骤 1 成功,RemoteLib.dll 会通过调用
::LoadLibraryA
或::LoadLibraryW
将您的 DLL 映射到目标进程中,具体取决于是否定义了UNICODE
。 - RemoteLib.dll 将自己从目标进程中解除映射。
要从目标进程中取消注入您的代码,步骤 2 会改变,RemoteLib.dll 将调用 ::FreeLibrary
。
实现
我不会详细讨论 ::CreateRemoteThread
方法,因为它根本无法在 Win9x 上工作,本文将更多地解释 Windows 钩子。
首先,由于 Win9x 不支持 ::VirtualAllocEx
和 ::VirtualFreeEx
,我们需要找到一种方法将 DLL 名称字符串传递给目标进程,以便后续调用 ::LoadLibrary
和 ::GetModuleHandle
,并且 ::GetModuleFileName
调用也需要在远程进程的虚拟空间中有一个字符串缓冲区来临时存储 DLL 名称字符串。幸运的是,我们有一个链接器选项可以实现这一点。
#pragma data_seg ("SHARED") static HHOOK g_hHook = NULL; // Hook handle. static wchar_t g_szDllPath[MAX_PATH + 1] = { 0 }; // DLL path. // other shared data... #pragma data_seg () #pragma comment(linker, "/section:SHARED,RWS")
然后,我们需要选择正确的钩子类型,最常用的 WH_CALLWNDPROC
在 Win9x 上将不起作用,正如 MSDN 所述:
Windows 95/98/ME,Windows NT 3.51:
WH_CALLWNDPROC
钩子在调用SendMessage
的线程上下文中被调用,而不是在接收消息的线程上下文中。
这完全与本项目目的相悖。所以我们必须使用另一种钩子类型。虽然可能有更好的选择,但我个人发现 WH_CBT
绝不是一个糟糕的选择。
////////////////////////////////////////////////////////////////// // Pseudo-code for injecting a DLL into a remote running process. ////////////////////////////////////////////////////////////////// // First we save the DLL name in our shared string buffer. ::strncpy((LPSTR)g_szDllPath, "c:\\test\\MyTest.dll", MAX_PATH); // Then RemoteLib.dll maps itself into the remote process through windows hook g_hHook = ::SetWindowsHookEx(WH_CBT, (HOOKPROC)HookProcA, g_hModInstance, dwTargetWndThreadID); if (g_hHook == NULL) { // Error handling... } // Send a special system message to the target window if (!::SendMessageTimeoutA(hTargetWnd, WM_SYSCOMMAND, 0, REMOTE_LOADLIBRARY, SMTO_ABORTIFHUNG | SMTO_BLOCK, 2000, NULL)) { // Error handling... } if (g_hHook) { // Make sure we remove the hook, in case it wasn't removed by "HookProc" ::UnhookWindowsHookEx(g_hHook); g_hHook = NULL; } // Send a dummy system message // to the target window to force unloading RemoteLib.dll ::SendMessageTimeoutA(hTargetWnd, WM_SYSCOMMAND, 0, 0, SMTO_ABORTIFHUNG | SMTO_BLOCK, 2000, NULL);
上述代码将 RemoteLib.dll 自身映射到目标进程中,并在 ::SendMessageTimeoutA
返回后解除映射。但在其间发生了什么?现在我们需要查看钩子过程,看看它做了什么。
/////////////////////////////////////////////////////////////////////// // HookProcA (Pseudo-code) /////////////////////////////////////////////////////////////////////// LRESULT CALLBACK HookProcA(int code, WPARAM wParam, LPARAM lParam) { if (code == HCBT_SYSCOMMAND && lParam == REMOTE_LOADLIBRARY) { if (g_hHook) { // Remove the hook ASAP ::UnhookWindowsHookEx(g_hHook); g_hHook = NULL; } // Since we are now inside // the target process's virtual space already... g_dwProcResult = (DWORD)::LoadLibraryA((LPCSTR)g_szDllPath); g_dwProcError = ::GetLastError(); return 1; // Returns 1 so the window won't receive this "meaningless" message } return ::CallNextHookEx(g_hHook, code, wParam, lParam); }
因此,钩子过程所做的事情很简单,因为在那一刻,代码已经在目标进程的虚拟空间内执行了。在那里调用 ::LoadLibrary
只需将任何给定的 DLL 映射到目标进程中,我们就完成了。
这就是此实现的主要思想。当然,还有许多其他因素需要考虑,例如:
- 同步。由于我们声明了共享数据,因此必须保护它们免受异步访问。
- 检索远程函数调用产生的返回值和错误代码。
- 兼容 UNICODE。
- 处理在某些 Windows 平台上可能不存在的 KERNEL32 函数。
- 在调用仅限 NT 的 API 之前验证用户 Windows 平台类型 (9x/NT)。
我将所有剩余的细节留给读者,请查看项目源代码,了解这些问题是如何解决的。
Using the Code
要使用该库,您需要:
- 将 RemoteLib.dll 复制到您的系统目录或应用程序目录。
- 将 RemoteLib.lib 添加到您的项目工作区。
- 在需要的地方包含 RemoteLib.h。
- 调用 RemoteLib.h 中声明的适当函数。
例如,要将“c:\Tools\d2Hackit.dll”注入/取消注入到暗黑破坏神2游戏进程中,该进程始终有一个类名为“Diablo II”的窗口。假设用户可能正在使用 Win9x,因此此示例使用 Windows 钩子技术:
#include "RemoteLib.h" #include <stdio.h> void DisplayError(DWORD dwErrorCode) { char szMsg[256] = ""; sprintf(szMsg, "Function failed. Error code: %d", dwErrorCode); ::MessageBoxA(NULL, szMsg, "RemoteLib Error", MBOK); } int main() { // Find the Diablo2 game window HWND hWnd = ::FindWindow("Diablo II", NULL); if (hWnd == NULL) return 1; // Game window not found. // Inject our DLL HMODULE hModule = RemoteLoadLibrary(hWnd, "c:\\Tools\\D2Hackit.dll"); if (hModule == NULL) { DisplayError(::GetLastError()); // Why did the function fail? return -1; // Injection failure. } /* DLL injected successfully. DO some stuff here... ... ... No we are going to un-inject the DLL */ // If for some reason you // lost the previous hModule, you can have it back: // hModule = RemoteGetModuleHandle(hWnd, "c:\\Tools\\D2Hackit.dll"); // Un-inject our DLL BOOL bOK = RemoteFreeLibrary(hWnd, hModule); if (!bOK) { DisplayError(::GetLastError()); // Why did the function fail? return -2; // Un-injection failure. } return 0; // Every thing went fine... }
结论
此库封装了 DLL 注入的所有后端工作,并为希望将代码注入其他运行进程的开发人员提供了一个非常简单的接口。此库导出的函数名称都非常清晰地自解释,因此我认为我不需要提供像“API 参考”之类的东西,是吗?它适用于 Win9x 和 NT 平台,但请记住,在 Win9x 上,它要求远程进程至少有一个有效窗口。
历史
- v1.00 2005 年 1 月 3 日
- 初始发布。
- v1.01 2005 年 1 月 10 日
- 修改了
RemoteGetModuleHandle
和RemoteGetModuleHandleNT
,您无需指定绝对 DLL 路径,现在可以指定相对路径或不带路径的纯文件名,甚至不带文件扩展名。 - 重新组织了源代码文件,使其更易于理解。
- 更新了源/二进制/演示包。
- 修改了