Target

When writing a DLL injector, it is helpful to have an already working DLL for a particular target. For this lesson, we will use the memory wallhack we produced in the Wallhack (Memory) lesson. While our injector will be built for the game Urban Terror, we will be able to easily modify it for other targets in the future.

Overview

In previous lessons, we used Windows’ AppInit functionality to inject DLL’s into game executables. While this approach works well for testing, it has several drawbacks:

  • AppInit_DLLs needs to be updated for each new DLL.
  • AppInit_DLLs are injected into every started process.
  • Secure Boot has to be disabled.
  • AppInit_DLLs will only be injected into processes that load user32.dll.
  • DLL’s are loaded into the process at a set time, outside of our control.

To get around these drawbacks, we will write an injector, which will manually load our DLL into the game executable.

Concepts

To load static and dynamic libraries, Windows executables can use the LoadLibraryA API function. This function takes a single argument, which is a full path to the library to load.

HMODULE LoadLibraryA(
  LPCSTR lpLibFileName
);

If we call LoadLibraryA in our injector’s code, the DLL will be loaded into our injector’s memory. Instead, we want our injector to force the game to call LoadLibraryA. To do this, we will use the CreateRemoteThread API to create a new thread in the game. This thread will then execute LoadLibraryA inside the game’s running process.

However, since the thread is running inside the game’s memory, LoadLibraryA will not be able to find the path of our DLL specified in our injector. To get around this, we have to write our DLL’s path into the game’s memory. To ensure that we do not corrupt any other memory, we will also need to allocate additional memory inside the game using VirtualAllocEx. The full breakdown of this interaction looks like:

Stack Parameters

As we know from previous lessons, we will need a process handle to interact with an external process. For example, in the External Memory Hack lesson, we used FindWindow and GetWindowThreadProcessId to retrieve a process identifier. This approach has many drawbacks and is not recommended beyond quick testing. Instead, we will use CreateToolhelp32Snapshot.

Process Identifier

To use WriteProcessMemory, we will need a handle to the Urban Terror process. Instead of using FindWindow like we did previously, we will use CreateToolhelp32Snapshot. This API takes a snapshot of all the currently running processes on the machine. Each process in this snapshot can then be examined using Process32First and Process32Next. Microsoft provides a good example of how to do this here.

While Microsoft’s example iterates all processes and dumps their loaded modules, we are only interested in finding a single process and retrieving its process identifier. Therefore, we can simplify their example code:

#include <windows.h>
#include <tlhelp32.h>

int main(int argc, char** argv) {
  HANDLE snapshot = 0;
  PROCESSENTRY32 pe32 = { 0 };

  pe32.dwSize = sizeof(PROCESSENTRY32);
  snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  Process32First(snapshot, &pe32);

  do {

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

  return 0;
}

Each process entry contains two fields that we care about: szExeFile and th32ProcessID. The former contains the name of the process, like svchost.exe or notepad.exe. The latter contains the process identifier of the process that we can pass to OpenProcess.

The process name of Urban Terror is Quake3-UrT.exe. This can be identified by viewing the process list in Task Manager while Urban Terror is running. To compare this value to the value in szExeFile, we can use the function strcmp. This function takes two strings and returns 0 if they match:

do {
  if (wcscmp(pe32.szExeFile, L"Quake3-UrT.exe") == 0) {

  }
}

Process Handle

When these strings match, we know that pe32.th32ProcessID must contain the process identifier for the running instance of Urban Terror. We can pass this value to OpenProcess just like we did in previous lessons:

HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, true, pe32.th32ProcessID);

Allocating Memory

Next, we need to allocate memory inside of Urban Terror to store the full path of our DLL. To do this, we will use VirtualAllocEx, which is defined as:

LPVOID VirtualAllocEx(
  HANDLE hProcess,
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flAllocationType,
  DWORD  flProtect
);

Going through the arguments, hProcess will be the process handle we obtained from OpenProcess. lpAddress will be NULL, since we do not care where the address is allocated. dwSize will be the length of the path to our DLL. Since we want to allocate memory and have it be usable, we will choose MEM_COMMIT as the allocation type. Finally, since we want to write to the allocated memory, we will specify the protection as PAGE_READWRITE.

VirtualAllocEx will return a void pointer containing the address that our memory is allocated at. Since we will need this value for our next call to WriteProcessMemory, we will have to create a variable for it. We will also need to create a variable for the full path of our DLL. Due to how C++ interprets backslashes, we need to use two **’s for each single backslash. With all these parameters worked out, we can add the following code:

const char *dll_path = "C:\\Users\\IEUser\\source\\repos\\wallhack\\Debug\\wallhack.dll";
...
void *lpBaseAddress = VirtualAllocEx(process, NULL, strlen(dll_path) + 1, MEM_COMMIT, PAGE_READWRITE);

Writing the DLL Name

With our memory now allocated, we can write our DLL name into Urban Terror’s memory using WriteProcessMemory. The base address for writing will be the address that we retrieved from VirtualAllocEx:

WriteProcessMemory(process, lpBaseAddress, dll_path, strlen(dll_path) + 1, NULL);

Creating the Thread

With our DLL’s path written into the game’s memory, we can create a thread to execute LoadLibraryA to load the DLL into the game. We will use CreateRemoteThread to create the thread, but first, we need to obtain the address of LoadLibraryA.

LoadLibraryA exists inside kernel32.dll. Windows takes care of loading this DLL into all processes that need any API contained inside kernel32.dll. To obtain the address of LoadLibraryA, we can use GetProcAddress. This API requires a handle to the DLL that contains the function, in this case kernel32.dll. We can get this handle using GetModuleHandle:

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

Now we can use CreateRemoteThread to load our DLL. CreateRemoteThread’s definition looks like:

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

Let’s step through each parameter required. The process will be the process handle for Urban Terror, identical to WriteProcessMemory. The next two parameters we do not need, so we can pass NULL and 0 for them. Our start address will be the address of LoadLibraryA that we retrieve through GetProcAddress. Finally, we need to pass a single parameter to LoadLibraryA, our DLL path, which we know from our call to VirtualAllocEx. For the purpose of our injector, we can ignore the last two parameters as well. With all of this down, our call ends up looking like:

HANDLE thread = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)GetProcAddress(kernel32base, "LoadLibraryA"), lpBaseAddress, 0, NULL);

We have some additional operations we need to do with our thread, so we will save a handle to the thread. Before exiting, we want our injector to wait until the thread has been created and finished executing. We can do this via WaitForSingleObject and GetExitCodeThread:

WaitForSingleObject(thread, INFINITE);
GetExitCodeThread(thread, &exitCode);

Clean Up

Finally, we can free up the memory we allocated and close the open handles we have after our DLL has been injected:

VirtualFreeEx(process, lpBaseAddress, 0, MEM_RELEASE);
CloseHandle(thread);
CloseHandle(process);
break;

The final break exits the loop that we created to scan through each process.

With all of this done, we can start Urban Terror, enter a game, and then run our injector. If everything went successfully, players will start appearing through walls, indicating that our DLL was injected. If it fails, make sure to run the injector with administrator permissions.

The full code for the injector is available on github.

 

results matching ""

    No results matching ""