Motivation
This article was inspired by Robert Kuster's Three Ways to Inject Your Code into Another Process. At first, I was looking for some pieces of code that would allow me to easily inject my own DLLs into a remote running process. So I did a search on CP and found Robert Kuster's; it was really an excellent article I have to say, but unfortunately, none of those three solutions worked on my Windows 98 (Second Edition). In theory, the "first way" should work on all Windows platforms but it did not (that's why some programmers including me hate Win9x so much). The fix was actually very easy, but since Robert Kuster's code was rather a technical tutorial than an encapsulated reusable library, I thought why not write one for the ease of future uses.
Introduction
While DLL injection being extremely easy on NT platforms, Win9x users are out of luck because ::CreateRemoteThread
is unsupported on Win9x. There are of course other solutions, ::SetThreadContext
, for example, coupled with ::CreateProcess
, can handle the job for you, but it'd be a bit difficult to inject code into an already-running process because suspending a running thread on Win9x is relatively complicated.
Another simple solution is using ::SetWindowsHookEx
, as Robert Kuster explained in his article, thread-hooking (in contrast to global-hooking) a window will result the entire DLL (where the hook procedure resides) be mapped into the target window's creator process; this gives us an opportunity to do any dirty work inside the target process' virtual space, including but not limited to, calling ::LoadLibrary
or ::FreeLibrary
to map/unmap third-party DLLs into/from the target process, A.K.A., DLL injection.
The down-side of using Windows hooks is that this technique requires the remote process to have at least one valid window, which means you cannot inject your DLLs into window-less applications or services. And that's why I provided two sets of loading/unloading functions for Win9x and NT respectively; those for Win9x require the target process to have a window, whereas those for NT do not. If, however, you want to inject DLLs into running window-less processes on Win9x, this article may not be of help.
How Does It Work
The source code attached to this article is a Win32 DLL project. Compile it (or simply download the "DLL binary files" package) and you will get RemoteLib.dll. This DLL acts like an "intermedium" or "bridge" between the target process and your own DLL. It basically handles the code injection in the following way:
- RemoteLib.dll injects itself into the target process. Either by using hook or creating remote thread, depending on whether you are using Win9x or NT.
- If step 1 succeeds, RemoteLib.dll maps your DLL into the target process through a call to
::LoadLibraryA
or::LoadLibraryW
, depending on whetherUNICODE
is defined. - RemoteLib.dll unmaps itself from the target process.
To un-inject your code from the target process, step 2 is changed, RemoteLib.dll will call ::FreeLibrary
instead.
Implementation
I'm not going to discuss the ::CreateRemoteThread
approach in details since it won't work on Win9x at all, this article will explain more on Windows hooks.
First of all, since Win9x does not support ::VirtualAllocEx
and ::VirtualFreeEx
, we need to find a way to pass our DLL name string into the target process for subsequent calls to ::LoadLibrary
and ::GetModulehandle
, and the ::GetModuleFileName
call will also require a string buffer in the remote process' virtual space to temporarily store the DLL name string. Luckily, we have a linker option to do this:
#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")
Then, we need to choose the correct hook type, the most commonly used WH_CALLWNDPROC
will not work on Win9x, as MSDN states:
Windows 95/98/ME, Windows NT 3.51: The
WH_CALLWNDPROC
hook is called in the context of the thread that callsSendMessage
, not the thread that receives the message.
That's completely against the purpose of this project. So we must use another hook type instead. While there may be better choices, I personally found out that WH_CBT
is by no means a bad one.
// // 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);
Above code will map RemoteLib.dll itself into the target process, and unmap after ::SendMessageTimeoutA
returns. But what happens in between? Now we need to take a look at the hook procedure and see what it does.
/// // 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); }
So, what the hook procedure does is simple, because at that moment, the code was already executed inside the target process' virtual space. Calling ::LoadLibrary
there just maps any given DLL into the target process, and we are done.
That's the main idea of this implementation. Of course, there are a lot more factors that need to be taken into account, for example:
- Synchronization. Since we have declared shared data pieces, we must protect them from asynchronous access.
- Retrieving return values and error codes produced by remote function calls.
- Being UNICODE compliant.
- Handling KERNEL32 functions that may not exist on some Windows platforms.
- Verifying user Windows platform types (9x/NT) before invoking NT-only APIs.
I will leave all remaining details to the readers, please have a look at the project source code and see how those problems are solved.
Using the Code
To use the library, you need to:
- Copy RemoteLib.dll to your system directory or application directory.
- Add RemoteLib.lib into your project workspace.
- Include RemoteLib.h where needed.
- Call appropriate functions declared in RemoteLib.h.
For example, to inject/un-inject "c:/Tools/d2Hackit.dll" into Diablo2 game process, which always has a window whose class name is "Diablo II". Assuming that the user may be using Win9x, so this sample uses the Windows hook technique:
#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... }
Conclusion
This library encapsulates all backend work for DLL injection, and provides a very simple interface to developers who want to inject code into other running processes. Functions exported by this library are all clearly self explained by their names, so I don't think I need to provide something like an "API reference", do I? It works for both Win9x and NT platforms, but please remember that on Win9x, it requires the remote process to have at least one valid window.
History
- v1.00 Jan 03, 2005
- Initial release.
- v1.01 Jan 10, 2005
- Modified
RemoteGetModuleHandle
andRemoteGetModuleHandleNT
so you do not have to specify absolute DLL paths, instead, you now can specify relative paths or plain file names without paths, even without file extensions. - Reorganized the source code files to make them easier to understand.
- Updated source/binary/demo packages.
- Modified