Malware Technique: DLL Injection

Ricky Severino
8 min readJan 3, 2021

DLL injection is perhaps one of the most popular techniques to inject malware into a legitimate process. DLL injection is often used by malicious actors in order to evade detection or even influence the behavior of another process, which is often the case with game hackers. This technique ensures the execution of a malicious DLL by writing its path into the address space of another process and running it by creating a new thread.

A DLL is a library that contains code and data that can be used by more than one program at the same time. For example, in Windows operating systems, the Comdlg32 DLL performs common dialog box related functions. Each program can use the functionality that is contained in this DLL to implement an Open dialog box. It helps promote code reuse and efficient memory usage. — Microsoft

A complete picture of how DLL injection works can be seen in the image below. In this post, we will go over how to perform DLL injection with C++.

DLL injection flow

First, we will create a function that will adjust the token privileges in order to acquire debug privileges on the current running process (malware process). To do this, we will call OpenProcessToken to get the access token of the current process. According to MSDN, the access token “contains the security information for a logon session. The system creates an access token when a user logs on, and every process executed on behalf of the user has a copy of the token. The token identifies the user, the user’s groups, and the user’s privileges.” Next, we call LookupPrivilegeValue to get the locally unique identifier (LUID) of the specific privilege, which in this case will be SE_DEBUG_NAME. This privilege is required to modify the memory of a process owned by another account. AdjustTokenPrivileges is then called to enable the SE_DEBUG_NAME privilege in the access token.

It is worth mentioning that adjusting the token privilege is not always required to inject a DLL. If the target process is running in the same context, i.e. same user, as the injector, then no privileges are required. In other words, SE_DEBUG_NAME will have to be enabled if the target process is running as a different user.

Below is our function that enables the debug privileges.

We will define another function to return the process ID of our target process. CreateToolhelp32Snapshot is called to get a snapshot of the all the currently running processes. The processes in this snapshot will be iterated through via a call to Process32Next and their names will be compared with the name of our target process. If the process name matches, then the function returns the process ID. If a match is not found after all process names have been checked, then the function returns NULL. The function definition is below.

Now for the fun part, DLL injection. This function will have two arguments, the process name and the DLL name. We start by enabling the SE_DEBUG_NAME privilege by calling the GrantDebugPriv function we defined above. The process ID of the target process is then retrieved by calling the FindProcessByName function we defined as well. The argument to this function is a constant wide char array containing the name of our target process. This also allows us to check if the target process actually exists, which if not, the program exits. The same check is made for the DLL with a call to GetFullPathName. This function writes the full path of the specified file name (in our case the DLL) into a buffer. If the return value is 0, we know the file does not exist. In a way, GetFullPathName serves a dual purpose for us. It gives us the full path of the DLL which will be written to the target process but it also allows to to check if the DLL file actually exists.

Using the process ID returned by FindProcessByName, we call OpenProcess which takes three arguments, the desired access, a Boolean that determines if the process inherits the handle, and a process ID. We will specify a desired access of:

PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD

This allows us to perform virtual memory operations as well as create threads on the target process. The second argument is not needed so we will provide the value FALSE. The third argument is the process ID of the target process returned by our function FindProcessByName. Our call to OpenProcess will be the following:

HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD, FALSE, dwProcessID);

Using the handle to the target process returned by OpenProcess, we can call VirtualAllocEx which takes five arguments:

LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
  • hProcess: the process handle
  • lpAddress: the desired starting address
  • dwSize: the size of the region of memory to allocate, in bytes.
  • flAllocationType: the type of memory allocation
  • flProtect: The memory protection for the region of pages to be allocated

hProcess will be the handle returned by OpenProcess, lpAddress will be NULL since we don't care for a starting address, dwSize will be MAX_PATH (260 bytes) for now, an flAllocationType of MEM_COMMIT | MEM_RESERVE, and a memory protection of PAGE_READWRITE. The call to VirtualAllocEx will be:

LPVOID lpRemoteMemory = VirtualAllocEx(
hProcess,
NULL,
dwMemSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);

The full path of the DLL will be written into the newly allocated region of memory with a call to WriteProcessMemory, whose arguments are as follows:

BOOL WriteProcessMemory(
HANDLE hProcess,
LPVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T* lpNumberOfBytesWritten
);

hProcess will be our handle to the target process, lpBaseAddress will be the memory address returned by our call to VirtualAllocEx. lpBuffer is the buffer containing the full path of our DLL written by GetFullPathName, dwMemSize is 260 bytes like before, and lpNumberOfBytesWritten will be NULL. Here is our call:

WriteProcessMemory(
hProcess,
lpRemoteMemory,
(LPCVOID)dllFullPath,
dwMemSize,
NULL
);

Next, we will have to get the address of LoadLibraryW which will be used to load the our DLL into the address space of our target process. The LoadLibraryW function resides within Kernel32.dll so we will have to get a handle to this with:

HMODULE hKernel32 = GetModuleHandle(L"kernel32.dll");

Kernel32.dll is the 32-bit dynamic link library found in the Windows operating system kernel. It handles memory management, input/output operations, and interrupts. When Windows boots up, kernel32.dll is loaded into a protected memory space so other applications do not take that space over. — webopedia

The address of LoadLibraryW is then received with the call:

LPVOID lpLoadLibrary = GetProcAddress(hKernel32, "LoadLibraryW");

GetModuleHandle returns a handle to the specified module and GetProcAddress returns the address of the specified function in the module.

At this point we have written the path of our DLL into the virtual memory space of the target process, all we have to do now is call CreateRemoteThread which according to Microsoft, “creates a thread that runs in the virtual address space of another process.” The syntax for this function is:

HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);

What matters to us are the arguments hProcess, lpStartAddress, and lpParameter, which will be the handle to our target process, the address of LoadLibraryW returned by GetProcAddress, and the location of newly allocated memory returned by WriteProcessMemory, respectively. The call to CreateRemoteThread is as follows:

HANDLE hRemoteThread = CreateRemoteThread(
hProcess,
NULL,
NULL,
(LPTHREAD_START_ROUTINE)lpLoadLibrary,
lpRemoteMemory,
NULL,
NULL
);

We will then wait for the thread to finish executing with a call to WaitForSingleObject. This is necessary as we do not want to release the allocated memory containing the DLL path before the thread finishes executing.

WaitForSingleObject(hRemoteThread, INFINITE);

Once WaitForSingleObject returns, we can be nice and release the allocated memory with VirtualFreeEx and close the handle to the target process with CloseHandle.

VirtualFreeEx(hProcess, (LPVOID)lpRemoteMemory, 0, MEM_RELEASE);
CloseHandle(hProcess);

The entire DLL injection process mentioned above can be seen in the code snippet below. Please note that error handling was written in the code but I did not cover these above for the sake of brevity.

For our DLL, we can write a simple “Hello World” program with a pop up window.

We can start the DLL injection by calling our InjectDLL function with the name of the target process and DLL as arguments.

We can actually see the DLL path being written into the target process if we debug our executable using x64dbg. All we have to do is set a breakpoint after VirtualAllocEx to get the address of the allocated memory, then a breakpoint after WriteProcessMemory. We open another instance of x64dbg and attach the target process to see if it was written.

As you can see above, the breakpoints are set after both calls, indicated by the red lines. Let us run the binary to the first breakpoint.

The RAX register contains the return value of VirtualAllocEx, i.e. the base address of the newly allocated region of memory. This address is 0x0000018B97700000. Since the target process is notepad.exe, we can attach this process in another instance of x64dbg and go to this address.

The memory contents at address 0x0000018B97700000 is empty which is expected since it was just allocated. Let us now run our executable to the second breakpoint, after the WriteProcessMemory call.

We know the call to WriteProcessMemory was successful because according to Microsoft, “if the function succeeds, the return value is nonzero.”; the RAX register has a value of 1. Let’s go back to x64bdg with notepad.exe.

YES! We can see in memory dump 1 that the path of our DLL has been written into the allocated memory region. When we let our executable run we should see a pop up window with our “Hello World” message.

Well there you have it, we successfully injected a DLL into a legitimate process and got code execution!

The entire code base for the DLL injection and DLL along with their Visual Studio solution files can be found on my GitHub HERE.

If you liked what you read, please click ‘Follow’ for more content!

--

--