Logo
blank Skip to main content

3 Effective DLL Injection Techniques for Setting API Hooks

API
C++

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.

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.

 Types of API hooking

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:

C++
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:

image 1
  1. The Some_application.exe thread is about to send a message to some window.
  2. The system checks if the WH_GETMESSAGE hook is set for this thread.
  3. 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.
  4. 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.
  5. The DllMain function of Inject.dll is called with the DLL_PROCESS_ATTACH parameter.
  6. Then a callback is called in the address space of the Some_application.exe process.
  7. 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.

Learn more
Securing Windows Solutions from DLL Injection Attacks

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:

figure 3 n

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:

C++
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:

C++
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:

C++
// 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:

C++
// 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.

C++
// 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.

Learn more

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:

Injecting DLL with remote thread context patching

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:

C++
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:

C++
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:

C++
// 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:

C++
SuspendThread(remoteThreadHandle);
  
CONTEXT context;
context.ContextFlags = CONTEXT_FULL;
  
GetThreadContext(remoteThreadHandle, &context);

Now we compile assembly code and save it in the buffer:

C++
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:

C++
context.Rip = reinterpret_cast(buffer);

Finally, we set a new context and resume the thread:

C++
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.

Learn more
How to Control Application Operations

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.

  1. We inject our DLL with the SetWindowsHookEx function:
C++
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;
}
  1. 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:

C++
typedef BOOL (*TerminateProcessType)(HANDLE hProcess, UINT uExitCode);
  
// The original function
TerminateProcessType TrueTerminateProcess = TerminateProcessType(
        GetProcAddress(GetModuleHandleW(L"kernel32"), "TerminateProcess"));
  1. 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.

C++
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;
}
  1. 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:

C++
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;
  1. 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.
C++
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.

Learn more

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:

Process Explorer utility installed

In Task Manager, we can see the SSView_immortal.exe process. Letโ€™s try to terminate it:

SSView_immortal.exe process

When we click End task, we get a message box with an error (the same error we show in our hooked function):

message box with an error

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:

ERROR_ACCESS_DENIED response

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.

Have a question?

Ask our expert!

Tell us about your project

Send us a request for proposal! Weโ€™ll get back to you with details and estimations.

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting?

Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact us