DLL Injection Using LoadLibrary in C

By Brandon Arvanaghi @arvanaghi

Tutorials on Windows DLL injections in C have noticable gaps in what they explain. This blog post plus the comments on my implementation should address most questions a newcomer might have. Here’s my code on GitHub. Note that most of my code is directly taken from the Microsoft Developer Network (MSDN).

If you find this post useful, I encourage you to follow my Twitter account, where I post more tutorials and low-level explanations.

The mission

You are designing a malicious process that can “inject” a DLL into a victim process using CreateRemoteThread. There are two approaches we can take:

  1. Allocate enough space in the remote process for just the DLL’s pathname (e.g. “C:\Windows\System32\NotMalicious.dll”), and write only the pathname to that process’s memory. Have the remote process then load the DLL by calling LoadLibrary, which accepts a path to a DLL as an argument. LoadLibrary will then do the work of mapping the DLL into the process’s address space for you.

  2. Allocate enough space in the remote process for the actual contents of the DLL. Write the entire contents of the DLL into the remote process manually, bypassing the need to call LoadLibrary.

The benefit of option 2 is that it is quieter. When a process calls LoadLibrary to load a DLL, a data structure within that process gets updated to reflect that the new DLL has been loaded. Thus, anyone monitoring processes on the system can see the names of every DLL loaded into every process when done with LoadLibrary.

The drawback of option 2 is that it is more complicated. You can’t just copy and paste the bytes of the DLL into the remote process’s memory and interact with it the way you would expect. Manually dealing with the relative offsets within the DLL can be tricky when the process has no idea a DLL exists in its memory.

Tools exist to abstract some of these issues away from option 2. Since we want to implement a basic injection from scratch, we examine option 1 in this post. First, let’s examine the workflow involved in a basic DLL injection.

The workflow

  1. Allocate memory in the remote process big enough for the DLL path name.
LPVOID dllPathAddressInRemoteMemory = 
	VirtualAllocEx(hProcess, NULL, strlen(dllPath), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  1. Write the DLL path name to the space you just allocated in the remote process.
// Write DLL's path name to remote process
BOOL succeededWriting = 
	WriteProcessMemory(hProcess, dllPathAddressInRemoteMemory, dllPath, strlen(dllPath), NULL);
  1. Find the address of LoadLibrary in your own malicious process (which will be the same as the address of LoadLibrary in the victim process), and store that memory address. I explain how this works in the next section.
// Returns a pointer to the LoadLibrary address. 
// This will be the same on the remote process as in our current process.
LPVOID loadLibraryAddress = 
	(LPVOID)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
  1. Use CreateRemoteThread to create a remote thread starting at the memory address from step 3 (which means this will execute LoadLibrary in the remote process). Besides the memory address of the remote function you want to call, CreateRemoteThread also allows you to provide an argument for the function if it requires one. LoadLibrary wants the memory address of where you wrote that DLL path from earlier, so provide CreateRemoteThread that address as well.
HANDLE remoteThread = 
	CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)loadLibraryAddress, dllPathAddressInRemoteMemory, NULL, NULL);

Now that you’ve seen the general workflow, let’s break some of these steps down.

Get a Handle

There are two processes involved in this attack: your DLLInjector process (Process A), and the remote process you want to inject with a DLL (Process B). To interact with the remote process, Process A must call OpenProcess() while passing the remote process’s process ID as an argument. OpenProcess will then return to Process A a Handle to Process B.

Attacking Process Receiving a Handle to Remote Process

A Handle, defined in Windows.h, is a typedef (or alias) for a void*, or a pointer to anything. Having a Handle to the remote process allows Process A to interact with it in powerful ways. Process A can allocate memory, write memory, and create an execution thread in Process B by calling functions like VirtualAllocEx, WriteProcessMemory, and CreateRemoteThread and passing the Handle to Process B as an argument to those functions.

Passing the Handle to Process B as an Arguments

Kernel32.dll and LoadLibrary

Kernel32.dll is loaded into every Windows process, and within it is a useful function called LoadLibrary. When LoadLibrary is called in a certain process, it maps a DLL into that process. LoadLibrary needs to know what DLL to load, so you need to provide it the path to the DLL on your system. LoadLibrary will then find the DLL at that path and load that DLL into memory for you.

We can call any function we want in Process B by calling CreateRemoteThread in Process A and passing the Handle to Process B as an argument. CreateRemoteThread needs to know what function to execute in Process B; in our case, it needs the address of the LoadLibrary function in Process B.

Finding the location of LoadLibrary in Process B is easy. 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.

GetModuleHandle will return the base address of a DLL that is loaded into your process. By passing kernell32.dll as an argument to GetModuleHandle in Process A, we now know where kernell32.dll resides in memory for both Process A. Since kernell32.dll is a core DLL, it will reside at the same virtual address in Process B. We can then use GetProcAddress to find the LoadLibrary function inside kernell32.dll within Process A. Again, this function will have the same virtual address in Process B.

GetProcAddress Note: LoadLibraryA is the function name. “A” means you provide the DLL path as an ASCII string.

Allocating Memory for the DLL Path

Why do we write the DLL path to Process B using VirtualAllocEx and then WriteRemoteMemory? Because, as you just saw, LoadLibrary needs to know what DLL you want to inject. The string it accepts as a parameter needs to be present in Process B’s memory so it somewhere so it can actually use it.

This is why, from the workflow above, we first allocate memory in Process B large enough for that string, and then write that string to that block of memory.

CreateToolhelp32Snapshot

This function creates a snapshot of every process currently running on the system, and it requires you to #include "tlhelp32.h". Documentation and examples here.

You can iterate through the list of processes returned from the snapshot and compare it against a certain process name you’re looking for, like so:

// assume processName was a command-line argument "Receiver.exe"
// assume pe32 declared earlier as PROCESSENTRY struct
do {
	// wcscmp evaluates to 0 if the strings match, hence the !
	if (!wcscmp(pe32.szExeFile, processName)) { 
		wprintf(L"[+] The process %s was found in memory.\n", pe32.szExeFile);
		hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe32.th32ProcessID);
		if (hProcess != NULL) {
			return hProcess;
		} else {
			printf("[---] Failed to open process %s.\n", pe32.szExeFile);
			return NULL;
		}
	}
} while (Process32Next(hProcessSnap, &pe32));

WCHAR and char discrepancies

Let me save you some Googling. pe32, declared just above this code block, is a PROCESSENTRY struct. Its member szExeFile is an array of WCHAR, which means each index in the array refers to a UTF-16 value. If you design your program (like mine) to accept a process name from the command line, and your IDE interprets command-line arguments as UTF-8 strings, you will need to first convert that command-line argument into an array of WCHAR. From here, you can use use wcscmp, which compares two UTF-16 values. Alternatively, you could convert the pe32.szExeFile value to a const char[] array and use strcmp.

I stuck with the WCHAR data type, which is why I used wmain instead of main to start my program. wmain interprets the command-line arguments passed in as WCHAR arrays rather than char arrays.

int wmain(int argc, wchar_t* argv[]) {
	// Everything in argv[] will be in Unicode
}

My program accepts two arguments, as in DLL_Injector.exe <Executable_Name> <Path_to_DLL_to_Inject>. I had no reason for the DLL path to be Unicode, so I converted it back to a const char[] like so:

// Convert WCHAR to char
const char dllPath[MAX_PATH];
wcstombs(dllPath, argv[2], MAX_PATH);

Confirming it worked

Since we mapped the DLL to the remote process using LoadLibrary, we will see the DLL registered in the victim process when viewed in ProcessExplorer.

Have fun with this – I know I did. Once again, here’s my code on GitHub.