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:
-
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. -
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
- Allocate memory in the remote process big enough for the DLL path name.
- Write the DLL path name to the space you just allocated in the remote process.
- Find the address of
LoadLibrary
in your own malicious process (which will be the same as the address ofLoadLibrary
in the victim process), and store that memory address. I explain how this works in the next section.
- Use
CreateRemoteThread
to create a remote thread starting at the memory address from step 3 (which means this will executeLoadLibrary
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 provideCreateRemoteThread
that address as well.
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.
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.
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.
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:
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.
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:
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.