Being able to control and manipulate system behavior and API calls is a useful skill for any Windows developer. It allows you to investigate internal processes and detect suspicious and malicious code. Previously, we described an easy way to set a global API hook by manipulating the AppInit_DLLs registry key and make the calc.exe process invisible in the list of running processes.
This time, we dive even deeper into dynamic-link library (DLL) injection techniques. We demonstrate how to make any Windows process immortal so that no other process can terminate it. This DLL injection tutorial will be useful for Windows developers who want to know more about different ways of modifying the flow and behavior of API calls in Windows applications.
Contents:
API hooking basics
Before we dive into the depths of code manipulations, letโs go over some of the basics of API hooking.
What is API hooking? API hooking is a technique that developers use for manipulating the behavior of a system or an application. With the help of API hooking, you can intercept calls in a Windows application or capture information related to API calls. Additionally, API hooking is one of the techniques that antivirus and Endpoint Detection and Response solutions use for identifying malicious code.
There are many ways you can implement API hooking. The three most popular methods are:
- DLL injection โ Allows you to run your code inside a Windows process to perform different tasks
- Code injection โ Implemented via the WriteProcessMemory API used for pasting custom code into another process
- Win32 Debug API toolset โ Provides you with full control over a debugged application, making it easy to manipulate the memory of a debugged process
In this article, we focus on the DLL injection method as itโs the most flexible, best-known, and most studied approach to manipulating system behavior through API calls. But what is DLL injection to begin with? In short, itโs the process of running custom code within the address space of a different process. DLL injection is also the most universal API hooking method and has fewer limitations than other API hooking techniques.
There are three widely used DLL injection methods based on the use of:
- the SetWindowsHookEx function. This method is only applicable to applications that use a graphical user interface (GUI).
- the CreateRemoteThread function. This method can be used for hooking any process but requires a lot of coding.
- remote thread context patching. This method is efficient but rather complex, so itโs better to use it only if the other two methods donโt work out for some reason.
Further in this article, we explain how to implement each of these methods and provide a practical example of setting API hooks with one of them. Our journey begins with overviewing the first technique on this list โ using the SetWindowsHookEx function.
Looking for ways to change the behavior of your Windows software?
Choose and implement relevant behavioral changes that will help you meet your project needs with the help of experienced Apriorit’s expert developers.
DLL injection with the SetWindowsHookEx function
The first DLL injection technique we overview in this post is based on the SetWindowsHookEx function. Using the WH_GETMESSAGE hook, we set a process that will watch for messages processed by system windows. To set the hook, we call the SetWindowsHookEx function:
SetWindowsHookExW(WH_GETMESSAGE, functionAddress, dllToBeInjected, 0);
The WH_GETMESSAGE argument determines the type of hook, and the functionAddress parameter determines the address of the function (in the address space of your process) that the system should call whenever a window is about to process a message.
The dllToBeInjected parameter identifies the DLL containing the functionAddress function. The last argument, 0, indicates the thread for which the hook is intended. Passing 0, we tell the system that weโre setting a hook for all GUI threads that exist in it. So this method can be applied to hook a specific process or all processes in the system.
Letโs see how all this works:
- The Some_application.exe thread is about to send a message to some window.
- The system checks if the WH_GETMESSAGE hook is set for this thread.
- Then the system finds out whether Inject.dll, the DLL containing the callback for the message, is mapped to the address space of the Some_application.exe process.
- If Inject.dll isnโt mapped yet, the system maps it to the address space of the Some_application.exe process and increments the lock count of the DLL in that process.
- The DllMain function of Inject.dll is called with the DLL_PROCESS_ATTACH parameter.
- Then a callback is called in the address space of the Some_application.exe process.
- After returning from the callback, the DLL lock counter in the address space of the process is reduced by 1.
Now letโs see how we can inject DLL with a second method โ using the CreateRemoteThread function.
Read also
Securing Your Windows Solutions from DLL Injection Attacks [With Examples]
Explore how to protect your software from unwanted DLL injections with three practical examples.
Injecting DLL with the CreateRemoteThread function
Now weโre going to look at the most flexible way of injecting DLL โ using the CreateRemoteThread function. The overall flow looks like this:
Injecting a DLL involves invoking the LoadLibrary function within the thread of the target process to load the desired DLL. Since managing threads of another process is extremely complicated, itโs better to create your own thread in it. Fortunately, the CreateRemoteThread function makes this easy:
HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
This function is very similar to the CreateThread function but has an additional hProcess parameter that identifies the process to which the new thread will belong.
We start with getting the handle of the process weโre going to hook:
HANDLE processHandle = OpenProcess(
PROCESS_CREATE_THREAD | // For CreateRemoteThread
PROCESS_VM_OPERATION | // For VirtualAllocEx/VirtualFreeEx
PROCESS_VM_WRITE, // For WriteProcessMemory
FALSE, // Don't inherit handles
processPid); // PID of our target process
Then, we should allocate some memory in the target process in order to pass the DLL path, as the target process can access only its private memory:
// How many bytes we need to hold the whole DLL path
int bytesToAlloc = (1 + lstrlenW(injectLibraryPath)) * sizeof(WCHAR);
// Allocate memory in the remote process for the DLL path
LPWSTR remoteBufferForLibraryPath = LPWSTR(VirtualAllocEx(
processHandle, NULL, bytesToAlloc, MEM_COMMIT, PAGE_READWRITE));
Using the WriteProcessMemory function, we can place the DLL path into the address space of our target process:
// Put the DLL path into the address space of the target process
WriteProcessMemory(processHandle, remoteBufferForLibraryPath,
injectLibraryPath, bytesToAlloc, NULL);
Then we can start a new thread. With the help of this thread, our DLL will be loaded into the target process.
// Get the real address of LoadLibraryW in Kernel32.dll
PTHREAD_START_ROUTINE loadLibraryFunction = PTHREAD_START_ROUTINE>(
GetProcAddress(GetModuleHandleW(L"Kernel32"), "LoadLibraryW"));
// Create remote thread that calls LoadLibraryW
HANDLE remoteThreadHandle = CreateRemoteThread(processHandle,
NULL, 0, loadLibraryFunction, remoteBufferForLibraryPath, 0, NULL);
Finally, we can move to the third DLL injection method thatโs based on thread context patching.
Read also
A Comprehensive Guide to Hooking Windows APIs with Python
Find out why Python can be more convenient for hooking Windows APIs than C/C++ and discover the most popular Python libraries for hooking that you can use in your project.
Injecting DLL with remote thread context patching
This method of DLL injection isnโt easy to detect, as it mostly looks like a regular thread activity. To succeed, we need to manipulate the context of an existing remote thread and make sure the thread doesnโt know about these manipulations. The instruction pointer of the target thread is first set to a custom piece of code. When the code is executed, the pointer is redirected to its original location.
This is what the whole process looks like:
Letโs see how we can implement this DLL injection method in an x64 system.
First, we need to locate the target process and pick a thread within it. Itโs better to choose a thread thatโs already running or is likely to run so that our DLL can be loaded as early as possible. Selecting a waiting thread isnโt the best idea, as such a thread wonโt run the code unless itโs ready to run.
First, we use the OpenThread function to open the handle of the remote thread:
HANDLE remoteThreadHandle = OpenThread(
THREAD_SET_CONTEXT | // For SetThreadContext
THREAD_SUSPEND_RESUME | // For SuspendThread and ResumeThread
THREAD_GET_CONTEXT, // For GetThreadContext
FALSE, // Don't inherit handles
remoteThreadId); // TID of our target thread
Then we need to allocate memory in the remote process to store our injected code and the DLL path in it:
SYSTEM_INFO systemInformation;
GetSystemInfo(&systemInformation);
// Allocate systemInformation.dwPageSize bytes in the remote process
LPBYTE buffer = LPBYTE(VirtualAllocEx(remoteProcessHandle, NULL, systemInformation.dwPageSize,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
Next we write the DLL path in the middle of the remote allocated buffer:
// Calculate how many bytes to write into the remote buffer
int libraryPathSizeBytes = (wcslen(injectLibraryPath) + 1) * sizeof(WCHAR);
WriteProcessMemory(remoteProcessHandle,
buffer + systemInformation.dwPageSize / 2, injectLibraryPath, libraryPathSizeBytes, NULL);
Then we suspend the remote thread and retrieve its context:
SuspendThread(remoteThreadHandle);
CONTEXT context;
context.ContextFlags = CONTEXT_FULL;
GetThreadContext(remoteThreadHandle, &context);
Now we compile assembly code and save it in the buffer:
BYTE codeToBeInjected[] = {
// sub rsp, 28h
0x48, 0x83, 0xec, 0x28,
// mov [rsp + 18h], rax
0x48, 0x89, 0x44, 0x24, 0x18,
// mov [rsp + 10h], rcx
0x48, 0x89, 0x4c, 0x24, 0x10,
// mov rcx, 11111111111111111h; placeholder for DLL path
0x48, 0xb9, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
// mov rax, 22222222222222222h; placeholder for โLoadLibraryWโ address
0x48, 0xb8, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
// call rax
0xff, 0xd0,
// mov rcx, [rsp + 10h]
0x48, 0x8b, 0x4c, 0x24, 0x10,
// mov rax, [rsp + 18h]
0x48, 0x8b, 0x44, 0x24, 0x18,
// add rsp, 28h
0x48, 0x83, 0xc4, 0x28,
// mov r11, 333333333333333333h; placeholder for the original RIP
0x49, 0xbb, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
// jmp r11
0x41, 0xff, 0xe3
};
// Set the DLL path
*reinterpret_cast<PVOID*>(codeToBeInjected + 0x10) = static_cast<void*>(buffer + systemInformation.dwPageSize / 2);
// Set LoadLibraryW address
*reinterpret_cast<PVOID*>(codeToBeInjected + 0x1a) = static_cast<void*>(GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW"));
// Jump address (back to the original code)
*reinterpret_cast<unsigned long="">(codeToBeInjected + 0x34) = context.Rip;</unsigned>
We set the remote IP (RIP) register of our remote thread to the buffer:
context.Rip = reinterpret_cast(buffer);
Finally, we set a new context and resume the thread:
SetThreadContext(remoteThreadHandle, &context);
ResumeThread(remoteThreadHandle);
Now that youโve got a better understanding of different DLL injection techniques, itโs time to see how these techniques work in practice.
Read also
How to Control Application Operations: Reverse Engineering an API Call and Creating Custom Hooks on Windows
Explore how to combine custom hooking and reverse engineering skills to improve your control over an application and its overall cybersecurity posture.
Setting API hooks with DLL injection in practice
While using the CreateRemoteThread function is the most universal way of setting API hooks with DLL injection, this method requires an extensive amount of preliminary coding. Thatโs why weโll illustrate how to set API hooks with DLL injection using the SetWindowsHookEx function, which is a less time-consuming method.
This example is based on a basic user-mode DLL written in C++. To be able to follow your trail, make sure to add the latest version of the Mhook sources to your project.
Our main goal here is to create an immortal process thatโs impossible for any other process in the system to terminate. We begin with setting a global API hook.
- We inject our DLL with the SetWindowsHookEx function:
int main(int argc, char* argv[])
{
HMODULE dllToBeInjected = LoadLibraryExW(L"dllWithMhook.dll", NULL, DONT_RESOLVE_DLL_REFERENCES)
// Get the address of the function to be called in a message
HOOKPROC functionAddress = HOOKPROC(GetProcAddress(dllToBeInjected, "MessageHookFunction"));
// Set the hook in the hook chain
HHOOK hookHandle = SetWindowsHookExW(WH_GETMESSAGE, functionAddress, dllToBeInjected, 0);
// Trigger the hook (our DLL is being loaded to the target process)
PostThreadMessage(threadId, WM_NULL, NULL, NULL);
system("pause");
UnhookWindowsHookEx(hookHandle);
return 0;
}
- To make sure we can restore the original function after removing our hook, we need to store its address.
To terminate a process, we need to call the TerminateProcess function from kernel32.dll. Thanks to the creation and initialization of a global variable, we can now store the original functionโs address:
typedef BOOL (*TerminateProcessType)(HANDLE hProcess, UINT uExitCode);
// The original function
TerminateProcessType TrueTerminateProcess = TerminateProcessType(
GetProcAddress(GetModuleHandleW(L"kernel32"), "TerminateProcess"));
- Weโve hooked the HookedTerminateProcess function instead of the original TerminateProcess function. The hooked function first calls the QueryFullProcessImageNameW function from kernel32.dll and gets the full name of the executable image for the process.
Now we need to check the process name. If it has the โ_immortalโ suffix, itโs the process we should not allow to be terminated.
Note: Both functions, the original and the hooked, must have identical signatures.
BOOL HookedTerminateProcess(HANDLE hProcess, UINT uExitCode)
{
WCHAR processExecutablePath[MAX_PATH + 1] = { 0 };
DWORD processExecutablePathSize = MAX_PATH;
if (!QueryFullProcessImageNameW(hProcess, PROCESS_NAME_NATIVE,
processExecutablePath, &processExecutablePathSize))
{
return TrueTerminateProcess(hProcess, uExitCode);
}
// It's not a process of interest; just call the original function
if (!wcsstr(processExecutablePath, L"_immortal.exe"))
{
return TrueTerminateProcess(hProcess, uExitCode);
}
MessageBoxW(0, L"The process can't be terminated!",
L"Injected Dll", MB_OK | MB_ICONERROR);
// Return error as if the original 'TerminateProcess' failed
SetLastError(ERROR_ACCESS_DENIED);
return 0;
}
- Here, we can finally inject our DLL into the code of the target process to set our hook.
Once loaded in the target process, the DllMain function will receive the DLL_PROCESS_ATTACH parameter. Now we can manipulate this process and hook the chosen function with the help of the Mhook library:
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
WCHAR libraryPath[MAX_PATH + 1] = { 0 };
DWORD libraryPathSize = MAX_PATH;
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
if (!GetModuleFileNameW(hinstDLL, libraryPath, libraryPathSize))
{
return TRUE;
}
// Increment load library link count
LoadLibraryW(libraryPath);
Mhook_SetHook((PVOID*)&TrueTerminateProcess, HookedTerminateProcess);
break;
- Once the DLL is unloaded from the target processโs address space, the DllMain function receives the DLL_PROCESS_DETACH parameter. After that, we remove the hook and restore the original function.
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
WCHAR libraryPath[MAX_PATH + 1] = { 0 };
DWORD libraryPathSize = MAX_PATH;
switch (fdwReason)
{
..................
case DLL_PROCESS_DETACH:
Mhook_Unhook((PVOID*)&TrueTerminateProcess);
break;
}
return TRUE;
}
We now have all the code needed for setting API hooks with Windows DLL injection. Itโs time to check if this code is actually working.
Read also
Controlling and Monitoring a Network with User Mode and Driver Mode Techniques: Overview, Pros and Cons, WFP Implementation
Manage network traffic of your application with hooking and other user- and kernel mode techniques. Discover how to manage user access, detect security threats, and prevent data leaks by controlling traffic.
Executing our API hooking sample code
For a practical illustration, we used the Structured Storage Viewer utility and turned it into an immortal process by injecting a DLL with the SetWindowsHookEx function. As a result of this process, we got an executable with the name SSView_immortal.exe. Letโs launch this executable and look at it in Task Manager. Weโll also need the Process Explorer utility installed to check if our DLL is, in fact, injected in the Taskmgr.exe process:
In Task Manager, we can see the SSView_immortal.exe process. Letโs try to terminate it:
When we click End task, we get a message box with an error (the same error we show in our hooked function):
Then we also receive a message saying โAccess is denied.โ This is the ERROR_ACCESS_DENIED response we set earlier with the help of the SetLastError function when implementing our hooked function:
As you can see, we successfully hooked a system process and made it impossible for any other Windows process to terminate it, which is exactly what we intended to do.
Conclusion
There are many methods to hook an API call. DLL injection is one of the most flexible, effective, and well-studied methods for injecting custom code into a system process. When performing DLL injection, itโs important to insert code into a running process, as DLLs are meant to be loaded as needed at runtime.
There are many ways you can hook a function with DLL injection โ by setting hooks in specific functions or manipulating the context of a remote thread. From our experience, we can say that setting hooks with the CreateRemoteThread function is the most effective approach. As this function is supported by the Windows operating system, thereโs no need to use any additional tricks, complicated executable file structures, or operating system internals when working with it. However, if youโre working with a GUI application, you can use the most effortless option โ the SetWindowsHookEx function.
At Apriorit, weโve already set thousands of hooks and know how to find our way around different operating systems and processes. Get a step closer to realizing your dream project โ contact us and tell us all about it!
Need to add complex hooks to your software?
Let us figure out the best way to hook needed functions and help you implement agreed changes.